Initial commit

This commit is contained in:
sanyi
2025-12-30 11:02:17 +08:00
commit 6524fabd80
841 changed files with 293919 additions and 0 deletions
+133
View File
@@ -0,0 +1,133 @@
# Copyright 2025-present the zvec project
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
from __future__ import annotations
import sys
from typing import TYPE_CHECKING
if TYPE_CHECKING:
from importlib.metadata import PackageNotFoundError
# ==============================
# Public API — grouped by category
# ==============================
from . import model as model
# —— Extensions & typing ——
from .extension import DenseEmbeddingFunction, ReRanker, RrfReRanker, WeightedReRanker
from .model import param as param
from .model import schema as schema
# —— Core data structures ——
from .model.collection import Collection
from .model.doc import Doc
# —— Query & index parameters ——
from .model.param import (
AddColumnOption,
AlterColumnOption,
CollectionOption,
FlatIndexParam,
HnswIndexParam,
HnswQueryParam,
IndexOption,
InvertIndexParam,
IVFIndexParam,
IVFQueryParam,
OptimizeOption,
)
from .model.param.vector_query import VectorQuery
# —— Schema & field definitions ——
from .model.schema import CollectionSchema, CollectionStats, FieldSchema, VectorSchema
# —— tools ——
from .tool import require_module
from .typing import (
DataType,
IndexType,
MetricType,
QuantizeType,
Status,
StatusCode,
)
from .typing.enum import LogLevel, LogType
# —— lifecycle ——
from .zvec import create_and_open, init, open
# ==============================
# Public interface declaration
# ==============================
__all__ = [
# Zvec functions
"create_and_open",
"init",
"open",
# Core classes
"Collection",
"Doc",
# Schema
"CollectionSchema",
"FieldSchema",
"VectorSchema",
"CollectionStats",
# Parameters
"VectorQuery",
"InvertIndexParam",
"HnswIndexParam",
"FlatIndexParam",
"IVFIndexParam",
"CollectionOption",
"IndexOption",
"OptimizeOption",
"AddColumnOption",
"AlterColumnOption",
"HnswQueryParam",
"IVFQueryParam",
# Extensions
"ReRanker",
"DenseEmbeddingFunction",
"RrfReRanker",
"WeightedReRanker",
# Typing
"DataType",
"MetricType",
"QuantizeType",
"IndexType",
"LogLevel",
"LogType",
"Status",
"StatusCode",
# Tools
"require_module",
]
# ==============================
# Version handling
# ==============================
__version__: str
try:
from importlib.metadata import version
except ImportError:
from importlib_metadata import version # Python < 3.8
try:
__version__ = version("zvec")
except Exception:
__version__ = "unknown"
+172
View File
@@ -0,0 +1,172 @@
"""
Zvec core module
"""
from __future__ import annotations
import collections
from . import typing
from .extension import ReRanker, RrfReRanker, WeightedReRanker
from .extension.embedding import DenseEmbeddingFunction
from .model import param, schema
from .model.collection import Collection
from .model.doc import Doc
from .model.param import (
AddColumnOption,
AlterColumnOption,
CollectionOption,
FlatIndexParam,
HnswIndexParam,
HnswQueryParam,
IndexOption,
InvertIndexParam,
IVFIndexParam,
IVFQueryParam,
OptimizeOption,
)
from .model.param.vector_query import VectorQuery
from .model.schema import CollectionSchema, CollectionStats, FieldSchema, VectorSchema
from .tool import require_module
from .typing import (
DataType,
IndexType,
MetricType,
QuantizeType,
Status,
StatusCode,
)
from .typing.enum import LogLevel, LogType
from .zvec import create_and_open, init, open
__all__: list = [
"AddColumnOption",
"AlterColumnOption",
"Collection",
"CollectionOption",
"CollectionSchema",
"CollectionStats",
"DataType",
"DenseEmbeddingFunction",
"DenseEmbeddingFunction",
"Doc",
"FieldSchema",
"FlatIndexParam",
"HnswIndexParam",
"HnswQueryParam",
"IVFIndexParam",
"IVFQueryParam",
"IndexOption",
"IndexType",
"InvertIndexParam",
"LogLevel",
"LogType",
"MetricType",
"OptimizeOption",
"QuantizeType",
"ReRanker",
"ReRanker",
"RrfReRanker",
"Status",
"StatusCode",
"VectorQuery",
"VectorSchema",
"WeightedReRanker",
"create_and_open",
"init",
"open",
"require_module",
]
class _Collection:
@staticmethod
def CreateAndOpen(
arg0: str, arg1: schema._CollectionSchema, arg2: param.CollectionOption
) -> _Collection: ...
@staticmethod
def Open(arg0: str, arg1: param.CollectionOption) -> _Collection: ...
def AddColumn(
self,
arg0: str,
arg1: schema._FieldSchema,
arg2: str,
arg3: param.AddColumnOption,
) -> None: ...
def AlterColumn(
self,
arg0: str,
arg1: str,
arg2: schema._FieldSchema,
arg3: param.AlterColumnOption,
) -> None: ...
def CreateIndex(
self, arg0: str, arg1: param.IndexParam, arg2: param.IndexOption
) -> None: ...
def Delete(self, arg0: collections.abc.Sequence[str]) -> list[typing.Status]: ...
def DeleteByFilter(self, arg0: str) -> None: ...
def Destroy(self) -> None: ...
def DropColumn(self, arg0: str) -> None: ...
def DropIndex(self, arg0: str) -> None: ...
def Fetch(self, arg0: collections.abc.Sequence[str]) -> dict[str, _Doc]: ...
def Flush(self) -> None: ...
def GroupByQuery(self, arg0: ...) -> list[...]: ...
def Insert(self, arg0: collections.abc.Sequence[_Doc]) -> list[typing.Status]: ...
def Optimize(self, arg0: param.OptimizeOption) -> None: ...
def Options(self) -> param.CollectionOption: ...
def Path(self) -> str: ...
def Query(self, arg0: param._VectorQuery) -> list[_Doc]: ...
def Schema(self) -> schema._CollectionSchema: ...
def Stats(self) -> schema.CollectionStats: ...
def Update(self, arg0: collections.abc.Sequence[_Doc]) -> list[typing.Status]: ...
def Upsert(self, arg0: collections.abc.Sequence[_Doc]) -> list[typing.Status]: ...
def __getstate__(self) -> tuple: ...
def __setstate__(self, arg0: tuple) -> None: ...
class _Doc:
def __getstate__(self) -> bytes: ...
def __init__(self) -> None: ...
def __setstate__(self, arg0: bytes) -> None: ...
def field_names(self) -> list[str]: ...
def get_any(self, arg0: str, arg1: typing.DataType) -> typing.Any: ...
def has_field(self, arg0: str) -> bool: ...
def pk(self) -> str: ...
def score(self) -> float: ...
def set_any(self, arg0: str, arg1: typing.DataType, arg2: typing.Any) -> bool: ...
def set_pk(self, arg0: str) -> None: ...
def set_score(self, arg0: typing.SupportsFloat) -> None: ...
class _DocOp:
"""
Members:
INSERT
UPDATE
DELETE
UPSERT
"""
DELETE: typing.ClassVar[_DocOp] # value = <_DocOp.DELETE: 3>
INSERT: typing.ClassVar[_DocOp] # value = <_DocOp.INSERT: 0>
UPDATE: typing.ClassVar[_DocOp] # value = <_DocOp.UPDATE: 2>
UPSERT: typing.ClassVar[_DocOp] # value = <_DocOp.UPSERT: 1>
__members__: typing.ClassVar[
dict[str, _DocOp]
] # value = {'INSERT': <_DocOp.INSERT: 0>, 'UPDATE': <_DocOp.UPDATE: 2>, 'DELETE': <_DocOp.DELETE: 3>, 'UPSERT': <_DocOp.UPSERT: 1>}
def __eq__(self, other: typing.Any) -> bool: ...
def __getstate__(self) -> int: ...
def __hash__(self) -> int: ...
def __index__(self) -> int: ...
def __init__(self, value: typing.SupportsInt) -> None: ...
def __int__(self) -> int: ...
def __ne__(self, other: typing.Any) -> bool: ...
def __repr__(self) -> str: ...
def __setstate__(self, state: typing.SupportsInt) -> None: ...
def __str__(self) -> str: ...
@property
def name(self) -> str: ...
@property
def value(self) -> int: ...
+18
View File
@@ -0,0 +1,18 @@
# Copyright 2025-present the zvec project
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
from __future__ import annotations
from .constants import DenseVectorType, SparseVectorType, VectorType
__all__ = ["DenseVectorType", "SparseVectorType", "VectorType"]
+22
View File
@@ -0,0 +1,22 @@
# Copyright 2025-present the zvec project
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
from __future__ import annotations
from typing import Optional, Union
import numpy as np
DenseVectorType = Union[list[float], list[int], np.ndarray]
SparseVectorType = dict[int, float]
VectorType = Optional[Union[DenseVectorType, SparseVectorType]]
+26
View File
@@ -0,0 +1,26 @@
# Copyright 2025-present the zvec project
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
from __future__ import annotations
from .query_executor import (
QueryContext,
QueryExecutor,
QueryExecutorFactory,
)
__all__ = [
"QueryContext",
"QueryExecutor",
"QueryExecutorFactory",
]
+307
View File
@@ -0,0 +1,307 @@
# Copyright 2025-present the zvec project
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
from __future__ import annotations
import os
from abc import ABC, abstractmethod
from concurrent.futures import ThreadPoolExecutor, as_completed
from typing import Optional, Union, final
import numpy as np
from _zvec import _Collection
from _zvec.param import _VectorQuery
from ..extension import ReRanker, RrfReRanker, WeightedReRanker
from ..model.convert import convert_to_py_doc
from ..model.doc import Doc
from ..model.param.vector_query import VectorQuery
from ..model.schema import CollectionSchema
from ..typing import DataType
__all__ = [
"QueryContext",
"QueryExecutor",
"QueryExecutorFactory",
]
DTYPE_MAP = {
DataType.VECTOR_FP16.value: np.float16,
DataType.VECTOR_FP32.value: np.float32,
DataType.VECTOR_FP64.value: np.float64,
DataType.VECTOR_INT8.value: np.int8,
}
def convert_to_numpy(vec: Union[list, np.ndarray], dtype: np.dtype) -> np.ndarray:
if isinstance(vec, np.ndarray):
if vec.dtype == dtype and vec.ndim == 1:
return vec
return np.asarray(vec, dtype=dtype).flatten()
try:
arr = np.asarray(vec, dtype=dtype)
if arr.ndim != 1:
arr = arr.flatten()
return arr
except (ValueError, TypeError) as e:
raise TypeError(
f"Cannot convert input to 1D numpy array with dtype={dtype}: {type(vec)}"
) from e
class QueryContext:
def __init__(
self,
topk: int,
filter: Optional[str] = None,
include_vector: bool = False,
queries: Optional[list[VectorQuery]] = None,
output_fields: Optional[list[str]] = None,
reranker: Optional[ReRanker] = None,
):
# query param
self._filter = filter
self._queries = queries or []
self._topk = topk
self._include_vector = include_vector
self._output_fields = output_fields
# reranker
self._reranker = reranker
# core vectors
self._core_vectors = []
@property
def topk(self):
return self._topk
@property
def queries(self):
return self._queries
@property
def filter(self):
return self._filter
@property
def reranker(self):
return self._reranker
@property
def output_fields(self):
return self._output_fields
@property
def include_vector(self):
return self._include_vector
@property
def core_vectors(self):
return self._core_vectors
@core_vectors.setter
def core_vectors(self, core_vectors: list[_VectorQuery]):
self._core_vectors = core_vectors
class QueryExecutor(ABC):
def __init__(self, schema: CollectionSchema):
self._schema = schema
self._concurrency = max(1, int(os.getenv("ZVEC_QUERY_CONCURRENCY", "1")))
@abstractmethod
def _do_validate(self, ctx: QueryContext) -> None:
pass
@abstractmethod
def _do_build(
self, ctx: QueryContext, collection: _Collection
) -> list[_VectorQuery]:
pass
def _do_build_query_wo_vector(self, ctx: QueryContext) -> _VectorQuery:
core_vector = _VectorQuery()
core_vector.topk = ctx.topk
core_vector.include_vector = ctx.include_vector
if ctx.filter:
core_vector.filter = ctx.filter
if ctx.output_fields:
core_vector.output_fields = ctx.output_fields
return core_vector
def _do_build_query_with_vector(
self, ctx: QueryContext, query: VectorQuery, collection: _Collection
) -> _VectorQuery:
core_vector = self._do_build_query_wo_vector(ctx)
core_vector.field_name = query.field_name
if query.param:
core_vector.query_params = query.param
vector_schema = (
self._schema.vector(query.field_name) if query else self._schema.vectors[0]
)
if vector_schema is None:
raise ValueError("No vector field found")
# set output_fields
core_vector.output_fields = ctx.output_fields
# set vector
if query.has_vector():
vec_data = query.vector
else:
fetched = collection.Fetch([query.id])
doc = next(iter(fetched.values()))
if not doc:
return core_vector
vec_data = doc.get_any(vector_schema.name, vector_schema.data_type)
target_dtype = DTYPE_MAP.get(vector_schema.data_type.value)
core_vector.set_vector(
vector_schema._get_object(),
convert_to_numpy(vec_data, target_dtype) if target_dtype else vec_data,
)
return core_vector
def _do_execute(
self, vectors: list[_VectorQuery], collection: _Collection
) -> dict[str, list[Doc]]:
query_cnt = len(vectors)
if query_cnt == 0:
raise ValueError("No query to execute")
if len(vectors) == 1 or self._concurrency == 1:
results = {}
for query in vectors:
docs = collection.Query(query)
results[query.field_name] = [
convert_to_py_doc(doc, self._schema) for doc in docs
]
return results
results = {}
with ThreadPoolExecutor(max_workers=self._concurrency) as executor:
future_to_query = {
executor.submit(collection.Query, query): query.field_name
for query in vectors
}
for future in as_completed(future_to_query):
field_name = future_to_query[future]
try:
docs = future.result()
results[field_name] = [
convert_to_py_doc(doc, self._schema) for doc in docs
]
except Exception as e:
raise e
return results
def _do_merge_rerank_results(
self, ctx: QueryContext, docs_map: dict[str, list[Doc]]
) -> list[Doc]:
query_result_cnt = len(docs_map) if docs_map else 0
if query_result_cnt == 0:
raise ValueError("Query results is none and dost not to rerank")
if query_result_cnt == 1:
if not ctx.reranker or isinstance(
ctx.reranker, (RrfReRanker, WeightedReRanker)
):
return next(iter(docs_map.values()))
return ctx.reranker.rerank(docs_map)
return ctx.reranker.rerank(docs_map)
@final
def execute(self, ctx: QueryContext, collection: _Collection) -> list[Doc]:
# 1. validate query
self._do_validate(ctx)
# 2. build query vector
query_vectors = self._do_build(ctx, collection)
if not query_vectors:
raise ValueError("No query to execute")
# 3. execute query
docs = self._do_execute(query_vectors, collection)
# 4. merge and rerank result
return self._do_merge_rerank_results(ctx, docs)
class NoVectorQueryExecutor(QueryExecutor):
def __init__(self, schema: CollectionSchema):
super().__init__(schema)
def _do_validate(self, ctx: QueryContext) -> None:
if len(ctx.queries) > 0:
raise ValueError("Collection does not support query with vector or id")
def _do_build(
self, ctx: QueryContext, _collection: _Collection
) -> list[_VectorQuery]:
return [self._do_build_query_wo_vector(ctx)]
class SingleVectorQueryExecutor(NoVectorQueryExecutor):
def __init__(self, schema: CollectionSchema) -> None:
super().__init__(schema)
def _do_validate(self, ctx: QueryContext) -> None:
if len(ctx.queries) > 1:
raise ValueError(
"Collection has only one vector field, cannot query with multiple vectors"
)
for query in ctx.queries:
query._validate()
def _do_build(
self, ctx: QueryContext, collection: _Collection
) -> list[_VectorQuery]:
if len(ctx.queries) == 0:
return [self._do_build_query_wo_vector(ctx)]
vectors = []
for query in ctx.queries:
vectors.append(self._do_build_query_with_vector(ctx, query, collection))
return vectors
class MultiVectorQueryExecutor(SingleVectorQueryExecutor):
def __init__(self, schema: CollectionSchema) -> None:
super().__init__(schema)
def _do_validate(self, ctx: QueryContext) -> None:
if len(ctx.queries) > 1 and ctx.reranker is None:
raise ValueError("Reranker is required for multi-vector query")
seen_fields = set()
for query in ctx.queries:
query._validate()
field = query.field_name
if field in seen_fields:
raise ValueError(f"Query field name '{field}' appears more than once")
seen_fields.add(field)
def _do_execute(
self, vectors: list[_VectorQuery], collection: _Collection
) -> dict[str, list[Doc]]:
return super()._do_execute(vectors, collection)
class QueryExecutorFactory:
@staticmethod
def create(schema: CollectionSchema) -> QueryExecutor:
vectors = schema.vectors
if len(vectors) == 0:
return NoVectorQueryExecutor(schema)
if len(vectors) == 1:
return SingleVectorQueryExecutor(schema)
return MultiVectorQueryExecutor(schema)
+26
View File
@@ -0,0 +1,26 @@
# Copyright 2025-present the zvec project
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
from __future__ import annotations
from .embedding import DenseEmbeddingFunction, QwenEmbeddingFunction
from .rerank import QwenReRanker, ReRanker, RrfReRanker, WeightedReRanker
__all__ = [
"DenseEmbeddingFunction",
"QwenEmbeddingFunction",
"QwenReRanker",
"ReRanker",
"RrfReRanker",
"WeightedReRanker",
]
+188
View File
@@ -0,0 +1,188 @@
# Copyright 2025-present the zvec project
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
from __future__ import annotations
import os
from abc import ABC, abstractmethod
from functools import lru_cache
from http import HTTPStatus
from typing import Optional, Union
from ..tool import require_module
from ..typing import DataType
class DenseEmbeddingFunction(ABC):
"""Abstract base class for dense vector embedding functions.
Dense embedding functions map text to fixed-length real-valued vectors.
Subclasses must implement the ``embed()`` method.
Args:
dimension (int): Dimensionality of the output embedding vector.
data_type (DataType, optional): Numeric type of the embedding.
Defaults to ``DataType.VECTOR_FP32``.
Note:
This class is callable: ``embedding_func("text")`` is equivalent to
``embedding_func.embed("text")``.
"""
def __init__(self, dimension: int, data_type: DataType = DataType.VECTOR_FP32):
self._dimension = dimension
self._data_type = data_type
@property
def dimension(self) -> int:
"""int: The expected dimensionality of the embedding vector."""
return self._dimension
@property
def data_type(self) -> DataType:
"""DataType: The numeric data type of the embedding (e.g., VECTOR_FP32)."""
return self._data_type
@abstractmethod
def embed(self, text: str) -> list[Union[int, float]]:
"""Generate a dense embedding vector for the input text.
Args:
text (str): Input text to embed.
Returns:
list[Union[int, float]]: A list of numbers representing the embedding.
Length must equal ``self.dimension``.
"""
raise NotImplementedError
def __call__(self, text: str) -> list[Union[int, float]]:
return self.embed(text)
class SparseEmbeddingFunction(ABC):
"""Abstract base class for sparse vector embedding functions.
Sparse embedding functions map text to a dictionary of {index: weight},
where only non-zero dimensions are stored.
Note:
Subclasses must implement the ``embed()`` method.
"""
@abstractmethod
def embed(self, text: str) -> dict[int, float]:
"""Generate a sparse embedding for the input text.
Args:
text (str): Input text to embed.
Returns:
dict[int, float]: Mapping from dimension index to non-zero weight.
"""
raise NotImplementedError
class QwenEmbeddingFunction(DenseEmbeddingFunction):
"""Dense embedding function using Qwen (DashScope) Text Embedding API.
This implementation uses the DashScope service to generate embeddings
via Qwen's text embedding models (e.g., ``text-embedding-v4``).
Args:
dimension (int): Desired embedding dimension (e.g., 1024).
model (str, optional): DashScope embedding model name.
Defaults to ``"text-embedding-v4"``.
api_key (Optional[str], optional): DashScope API key. If not provided,
reads from ``DASHSCOPE_API_KEY`` environment variable.
Raises:
ValueError: If API key is missing or input text is invalid.
Note:
Requires the ``dashscope`` Python package.
Embedding results are cached using ``functools.lru_cache`` (maxsize=10).
"""
def __init__(
self,
dimension: int,
model: str = "text-embedding-v4",
api_key: Optional[str] = None,
):
super().__init__(dimension, DataType.VECTOR_FP32)
self._model = model
self._api_key = api_key or os.environ.get("DASHSCOPE_API_KEY")
if not self._api_key:
raise ValueError("DashScope API key is required")
@property
def model(self) -> str:
"""str: The DashScope embedding model name in use."""
return self._model
def _connection(self):
dashscope = require_module("dashscope")
dashscope.api_key = self._api_key
return dashscope
@lru_cache(maxsize=10)
def embed(self, text: str) -> list[Union[int, float]]:
"""
Generate embedding for a given text using Qwen (via DashScope).
Args:
text (str): Input text to embed. Must be non-empty and valid string.
Returns:
list[Union[int, float]]: The dense embedding vector.
Raises:
ValueError: If input is invalid or API response is malformed.
RuntimeError: If network or internal error occurs during API call.
"""
if not isinstance(text, str):
raise TypeError(f"Expected 'text' to be str, got {type(text).__name__}")
text = text.strip()
if not text:
raise ValueError("Input text cannot be empty or whitespace only")
resp = self._connection().TextEmbedding.call(
model=self.model, input=text, dimension=self.dimension, output_type="dense"
)
if resp.status_code != HTTPStatus.OK:
error_msg = getattr(resp, "message", "Unknown error")
error_detail = f"Status={resp.status_code}, Message={error_msg}"
raise ValueError(f"QwenEmbedding failed: {error_detail}")
output = getattr(resp, "output", None)
if not isinstance(output, dict):
raise ValueError("Invalid response: missing or malformed 'output' field")
embeddings = output.get("embeddings")
if not isinstance(embeddings, list):
raise ValueError(
"Invalid response: 'embeddings' field is missing or not a list"
)
if len(embeddings) != 1:
raise ValueError(
f"Expected 1 embedding, got {len(embeddings)}. Response: {resp}"
)
first_emb = embeddings[0]
if not isinstance(first_emb, dict):
raise ValueError("Invalid response: embedding item is not a dictionary")
return list(first_emb.get("embedding"))
+343
View File
@@ -0,0 +1,343 @@
# Copyright 2025-present the zvec project
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
from __future__ import annotations
import heapq
import math
import os
from abc import ABC, abstractmethod
from collections import defaultdict
from http import HTTPStatus
from typing import Optional
from ..model.doc import Doc
from ..tool import require_module
from ..typing import MetricType
class ReRanker(ABC):
"""Abstract base class for re-ranking search results.
Re-rankers refine the output of one or more vector queries by applying
a secondary scoring strategy. They are used in the ``query()`` method of
``Collection`` via the ``reranker`` parameter.
Args:
query (Optional[str], optional): Query text used for re-ranking.
Required for LLM-based re-rankers. Defaults to None.
topn (int, optional): Number of top documents to return after re-ranking.
Defaults to 10.
rerank_field (Optional[str], optional): Field name used as input for
re-ranking (e.g., document title or body). Defaults to None.
Note:
Subclasses must implement the ``rerank()`` method.
"""
def __init__(
self,
query: Optional[str] = None,
topn: int = 10,
rerank_field: Optional[str] = None,
):
self._query = query
self._topn = topn
self._rerank_field = rerank_field
@property
def topn(self) -> int:
"""int: Number of top documents to return after re-ranking."""
return self._topn
@property
def query(self) -> str:
"""str: Query text used for re-ranking."""
return self._query
@property
def rerank_field(self) -> Optional[str]:
"""Optional[str]: Field name used as re-ranking input."""
return self._rerank_field
@abstractmethod
def rerank(self, query_results: dict[str, list[Doc]]) -> list[Doc]:
"""Re-rank documents from one or more vector queries.
Args:
query_results (dict[str, list[Doc]]): Mapping from vector field name
to list of retrieved documents (sorted by relevance).
Returns:
list[Doc]: Re-ranked list of documents (length ≤ ``topn``),
with updated ``score`` fields.
"""
raise NotImplementedError
class RrfReRanker(ReRanker):
"""Re-ranker using Reciprocal Rank Fusion (RRF).
RRF combines results from multiple queries without requiring relevance scores.
It assigns higher weight to documents that appear early in multiple result lists.
The RRF score for a document at rank ``r`` is: ``1 / (k + r + 1)``,
where ``k`` is the rank constant.
Args:
query (Optional[str], optional): Ignored by RRF. Defaults to None.
topn (int, optional): Number of top documents to return. Defaults to 10.
rerank_field (Optional[str], optional): Ignored by RRF. Defaults to None.
rank_constant (int, optional): Smoothing constant ``k`` in RRF formula.
Larger values reduce the impact of early ranks. Defaults to 60.
"""
def __init__(
self,
query: Optional[str] = None,
topn: int = 10,
rerank_field: Optional[str] = None,
rank_constant: int = 60,
):
super().__init__(query, topn, rerank_field)
self._rank_constant = rank_constant
@property
def rank_constant(self) -> int:
return self._rank_constant
def _rrf_score(self, rank: int):
return 1.0 / (self._rank_constant + rank + 1)
def rerank(self, query_results: dict[str, list[Doc]]) -> list[Doc]:
"""Apply Reciprocal Rank Fusion to combine multiple query results.
Args:
query_results (dict[str, list[Doc]]): Results from one or more vector queries.
Returns:
list[Doc]: Re-ranked documents with RRF scores in the ``score`` field.
"""
rrf_scores: dict[str, float] = defaultdict(float)
id_to_doc: dict[str, Doc] = {}
for _, query_result in query_results.items():
for rank, doc in enumerate(query_result):
doc_id = doc.id
rrf_score = self._rrf_score(rank)
rrf_scores[doc_id] += rrf_score
if doc_id not in id_to_doc:
id_to_doc[doc_id] = doc
top_docs = heapq.nlargest(self.topn, rrf_scores.items(), key=lambda x: x[1])
results = []
for doc_id, rrf_score in top_docs:
doc = id_to_doc[doc_id]
new_doc = doc._replace(score=rrf_score)
results.append(new_doc)
return results
class WeightedReRanker(ReRanker):
"""Re-ranker that combines scores from multiple vector fields using weights.
Each vector field's relevance score is normalized based on its metric type,
then scaled by a user-provided weight. Final scores are summed across fields.
Args:
query (Optional[str], optional): Ignored. Defaults to None.
topn (int, optional): Number of top documents to return. Defaults to 10.
rerank_field (Optional[str], optional): Ignored. Defaults to None.
metric (MetricType, optional): Distance metric used for score normalization.
Defaults to ``MetricType.L2``.
weights (Optional[dict[str, float]], optional): Weight per vector field.
Fields not listed use weight 1.0. Defaults to None.
Note:
Supported metrics: L2, IP, COSINE. Scores are normalized to [0, 1].
"""
def __init__(
self,
query: Optional[str] = None,
topn: int = 10,
rerank_field: Optional[str] = None,
metric: MetricType = MetricType.L2,
weights: Optional[dict[str, float]] = None,
):
super().__init__(query, topn, rerank_field)
self._weights = weights
self._metric = metric
@property
def weights(self) -> dict[str, float]:
"""dict[str, float]: Weight mapping for vector fields."""
return self._weights
@property
def metric(self) -> MetricType:
"""MetricType: Distance metric used for score normalization."""
return self._metric
def rerank(self, query_results: dict[str, list[Doc]]) -> list[Doc]:
"""Combine scores from multiple vector fields using weighted sum.
Args:
query_results (dict[str, list[Doc]]): Results per vector field.
Returns:
list[Doc]: Re-ranked documents with combined scores in ``score`` field.
"""
weighted_scores: dict[str, float] = defaultdict(float)
id_to_doc: dict[str, Doc] = {}
for vector_name, query_result in query_results.items():
for _, doc in enumerate(query_result):
doc_id = doc.id
weighted_score = self._normalize_score(
doc.score, self.metric
) * self.weights.get(vector_name, 1.0)
weighted_scores[doc_id] += weighted_score
if doc_id not in id_to_doc:
id_to_doc[doc_id] = doc
top_docs = heapq.nlargest(
self.topn, weighted_scores.items(), key=lambda x: x[1]
)
results = []
for doc_id, weighted_score in top_docs:
doc = id_to_doc[doc_id]
new_doc = doc._replace(score=weighted_score)
results.append(new_doc)
return results
def _normalize_score(self, score: float, metric: MetricType) -> float:
if metric == MetricType.L2:
return 1.0 - 2 * math.atan(score) / math.pi
if metric == MetricType.IP:
return 0.5 + math.atan(score) / math.pi
if metric == MetricType.COSINE:
return 1.0 - score / 2.0
raise ValueError("Unsupported metric type")
class QwenReRanker(ReRanker):
"""Re-ranker using Qwen (DashScope) LLM-based re-ranking API.
This re-ranker sends documents to the DashScope TextReRank service for
cross-encoder style re-ranking based on semantic relevance to the query.
Args:
query (str): Query text for semantic re-ranking. **Required**.
topn (int, optional): Number of top documents to return. Defaults to 10.
rerank_field (str): Field name containing document text for re-ranking.
**Required**.
model (str, optional): DashScope re-ranking model name.
Defaults to ``"gte-rerank-v2"``.
api_key (Optional[str], optional): DashScope API key. If not provided,
reads from ``DASHSCOPE_API_KEY`` environment variable.
Raises:
ValueError: If ``query`` is missing, ``rerank_field`` is missing,
or API key is not provided.
Note:
Requires the ``dashscope`` Python package.
Documents without content in ``rerank_field`` are skipped.
"""
def __init__(
self,
query: Optional[str] = None,
topn: int = 10,
rerank_field: Optional[str] = None,
model: str = "gte-rerank-v2",
api_key: Optional[str] = None,
):
super().__init__(query, topn, rerank_field)
if not query:
raise ValueError("Query is required for reranking")
self._model = model
self._api_key = api_key or os.environ.get("DASHSCOPE_API_KEY")
if not self._api_key:
raise ValueError("DashScope API key is required")
@property
def model(self) -> str:
"""str: DashScope re-ranking model name."""
return self._model
def _connection(self):
dashscope = require_module("dashscope")
dashscope.api_key = self._api_key
return dashscope
def rerank(self, query_results: dict[str, list[Doc]]) -> list[Doc]:
"""Re-rank documents using Qwen's TextReRank API.
Args:
query_results (dict[str, list[Doc]]): Results from vector search.
Returns:
list[Doc]: Re-ranked documents with relevance scores from Qwen.
Raises:
ValueError: If API call fails or no valid documents are found.
"""
if not query_results:
return []
id_to_doc: dict[str, Doc] = {}
doc_ids = []
contents = []
for _, query_result in query_results.items():
for doc in query_result:
doc_id = doc.id
if doc_id in id_to_doc:
continue
field_value = doc.field(self.rerank_field)
rank_content = str(field_value).strip() if field_value else ""
if not rank_content:
continue
id_to_doc[doc_id] = doc
doc_ids.append(doc_id)
contents.append(rank_content)
if not contents:
raise ValueError("No documents to rerank")
resp = self._connection().TextReRank.call(
model=self.model,
query=self.query,
documents=list(contents),
top_n=self.topn,
return_documents=False,
)
if resp.status_code != HTTPStatus.OK:
raise ValueError(
f"QwenReranker failed with status {resp.status_code}: {resp.message}"
)
results = []
for item in resp.output.results:
idx = item.index
doc_id = doc_ids[idx]
doc = id_to_doc[doc_id]
new_doc = doc._replace(score=item.relevance_score)
results.append(new_doc)
return results
+22
View File
@@ -0,0 +1,22 @@
# Copyright 2025-present the zvec project
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
from __future__ import annotations
from .collection import Collection
from .doc import Doc
from .param.vector_query import VectorQuery
from .schema.collection_schema import CollectionSchema
from .schema.field_schema import FieldSchema
__all__ = ["Collection", "CollectionSchema", "Doc", "FieldSchema", "VectorQuery"]
+381
View File
@@ -0,0 +1,381 @@
# Copyright 2025-present the zvec project
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
from __future__ import annotations
from typing import Optional, Union
from _zvec import _Collection
from ..executor import QueryContext, QueryExecutorFactory
from ..extension import ReRanker
from ..typing import Status
from .convert import convert_to_cpp_doc, convert_to_py_doc
from .doc import Doc
from .param import (
AddColumnOption,
AlterColumnOption,
CollectionOption,
FlatIndexParam,
HnswIndexParam,
IndexOption,
InvertIndexParam,
IVFIndexParam,
OptimizeOption,
)
from .param.vector_query import VectorQuery
from .schema import CollectionSchema, CollectionStats, FieldSchema
__all__ = ["Collection"]
_VECTOR_INDEX_TYPES = (HnswIndexParam, IVFIndexParam, FlatIndexParam)
class Collection:
"""Represents an opened collection in Zvec.
A `Collection` provides methods for data definition (DDL), data manipulation (DML),
and querying (DQL). It is obtained via `create_and_open()` or `open()`.
This class is not meant to be instantiated directly; use factory functions instead.
"""
def __init__(self, obj: _Collection):
self._obj = obj
self._schema = None
self._querier = None
@classmethod
def _from_core(cls, core_collection: _Collection) -> Collection:
if not core_collection:
raise ValueError("Collection is None")
inst = cls.__new__(cls)
inst._obj = core_collection
schema = CollectionSchema._from_core(core_collection.Schema())
inst._schema = schema
inst._querier = QueryExecutorFactory.create(schema)
return inst
@property
def path(self) -> str:
"""str: The filesystem path of the collection."""
return self._obj.Path()
@property
def option(self) -> CollectionOption:
"""CollectionOption: The options used to open the collection."""
return self._obj.Options()
@property
def schema(self) -> CollectionSchema:
"""CollectionSchema: The schema defining the structure of the collection."""
return self._schema
@property
def stats(self) -> CollectionStats:
"""CollectionStats: Runtime statistics about the collection (e.g., doc count, size)."""
return self._obj.Stats()
# ========== Collection DDL Methods ==========
def destroy(self) -> None:
"""Permanently delete the collection from disk.
Warning:
This operation is irreversible. All data will be lost.
"""
self._obj.Destroy()
def flush(self) -> None:
"""Force all pending writes to disk.
Ensures durability of recent inserts/updates.
"""
self._obj.Flush()
# ========== Index DDL Methods ==========
def create_index(
self,
field_name: str,
index_param: Union[
HnswIndexParam, IVFIndexParam, FlatIndexParam, InvertIndexParam
],
option: IndexOption = IndexOption(),
) -> None:
"""Create an index on a field.
Vector index types (HNSW, IVF, FLAT) can only be applied to vector fields.
Inverted index (`InvertIndexParam`) is for scalar fields.
Args:
field_name (str): Name of the field to index.
index_param (Union[HnswIndexParam, IVFIndexParam, FlatIndexParam, InvertIndexParam]):
Index configuration.
option (Optional[IndexOption], optional): Index creation options.
Defaults to ``IndexOption()``.
Raises:
ValueError: If a vector index is applied to a non-vector field.
"""
if index_param in _VECTOR_INDEX_TYPES and not self.schema.vector(field_name):
supported_types = ", ".join(cls.__name__ for cls in _VECTOR_INDEX_TYPES)
raise ValueError(
f"Cannot apply vector index to non-vector field '{field_name}'. "
f"The field must be of vector type to use index types like {supported_types}."
)
self._obj.CreateIndex(field_name, index_param, option)
self._schema = CollectionSchema._from_core(self._obj.Schema())
def drop_index(self, field_name: str) -> None:
"""Remove the index from a field.
Args:
field_name (str): Name of the indexed field.
"""
self._obj.DropIndex(field_name)
self._schema = CollectionSchema._from_core(self._obj.Schema())
def optimize(self, option: OptimizeOption = OptimizeOption()) -> None:
"""Optimize the collection (e.g., merge segments, rebuild index).
Args:
option (Optional[OptimizeOption], optional): Optimization options.
Defaults to ``OptimizeOption()``.
"""
self._obj.Optimize(option)
# ========== COLUMN DDL Methods ==========
def add_column(
self,
field_schema: FieldSchema,
expression: str = "",
option: AddColumnOption = AddColumnOption(),
) -> None:
"""Add a new column to the collection.
The column is populated using the provided expression (e.g., SQL-like formula).
Args:
field_schema (FieldSchema): Schema definition for the new column.
expression (str): Expression to compute values for existing documents.
option (Optional[AddColumnOption], optional): Options for the operation.
Defaults to ``AddColumnOption()``.
"""
self._obj.AddColumn(
field_schema.name, field_schema._get_object(), expression, option
)
self._schema = CollectionSchema._from_core(self._obj.Schema())
def drop_column(self, field_name: str) -> None:
"""Remove a column from the collection.
Args:
field_name (str): Name of the column to drop.
"""
self._obj.DropColumn(field_name)
self._schema = CollectionSchema._from_core(self._obj.Schema())
def alter_column(
self,
old_name: str,
new_name: Optional[str] = None,
field_schema: Optional[FieldSchema] = None,
option: AlterColumnOption = AlterColumnOption(),
) -> None:
"""Rename a column, update its schema.
This method supports three atomic operations:
1. Rename only (when `field_schema` is None).
2. Modify schema only (when `new_name` is None or empty string).
Args:
old_name (str): The current name of the column to be altered.
new_name (Optional[str]): The new name for the column.
- If provided and non-empty, the column will be renamed.
- If `None` or empty string, no rename occurs.
field_schema (Optional[FieldSchema]): The new schema definition.
- If provided, the column's type, dimension, or other properties will be updated.
- If `None`, only renaming (if requested) is performed.
option (AlterColumnOption, optional): Options controlling the alteration behavior.
Defaults to ``AlterColumnOption()``.
**Limitation**: This operation **only supports scalar numeric columns**. such as:
- `DOUBLE`, `FLOAT`,
- `INT32`, `INT64`, `UINT32`, `UINT64`
Note:
- Schema modification may trigger data migration or index rebuild.
Examples:
>>> # Rename column only
>>> results = collection.alter_column(old_name="id", new_name="doc_id")
>>> # Modify schema only
>>> new_schema = FieldSchema(name="doc_id", dtype=DataType.INT64)
>>> collection.alter_column("id", field_schema=new_schema)
"""
self._obj.AlterColumn(
old_name,
new_name or "",
field_schema._get_object() if field_schema else None,
option,
)
self._schema = CollectionSchema._from_core(self._obj.Schema())
# ========== Collection DDL Methods ==========
def insert(self, docs: Union[Doc, list[Doc]]) -> Union[Status, list[Status]]:
"""Insert new documents into the collection.
Documents must have unique IDs and conform to the schema.
Args:
docs (Union[Doc, list[Doc]]): One or more documents to insert.
Returns:
Union[Status, list[Status]]: If a single Doc was given, returns its Status;
if a list was given, returns a list of Status objects.
"""
is_single = isinstance(docs, Doc)
doc_list = [docs] if is_single else docs
results = self._obj.Insert(
[convert_to_cpp_doc(doc, self.schema) for doc in doc_list]
)
return results[0] if is_single else results
def upsert(self, docs: Union[Doc, list[Doc]]) -> Union[Status, list[Status]]:
"""Insert new documents or update existing ones by ID.
Args:
docs (Union[Doc, list[Doc]]): Documents to upsert.
Returns:
Union[Status, list[Status]]: If a single Doc was given, returns its Status;
if a list was given, returns a list of Status objects.
"""
is_single = isinstance(docs, Doc)
doc_list = [docs] if is_single else docs
results = self._obj.Upsert(
[convert_to_cpp_doc(doc, self.schema) for doc in doc_list]
)
return results[0] if is_single else results
def update(self, docs: Union[Doc, list[Doc]]) -> Union[Status, list[Status]]:
"""Update existing documents by ID.
Only specified fields are updated; others remain unchanged.
Args:
docs (Union[Doc, list[Doc]]): Documents containing updated fields.
Returns:
Union[Status, list[Status]]: If a single Doc was given, returns its Status;
if a list was given, returns a list of Status objects.
"""
is_single = isinstance(docs, Doc)
doc_list = [docs] if is_single else docs
results = self._obj.Update(
[convert_to_cpp_doc(doc, self.schema) for doc in doc_list]
)
return results[0] if is_single else results
def delete(self, ids: Union[str, list[str]]) -> Union[Status, list[Status]]:
"""Delete documents by ID.
Args:
ids (Union[str, list[str]]): One or more document IDs to delete.
Returns:
Union[Status, list[Status]]: If a single id was given, returns its Status;
if a list was given, returns a list of Status objects.
"""
is_single = isinstance(ids, str)
id_list = [ids] if isinstance(ids, str) else ids
results = self._obj.Delete(id_list)
return results[0] if is_single else results
def delete_by_filter(self, filter: str) -> None:
"""Delete documents matching a filter expression.
Args:
filter (str): Boolean expression (e.g., ``"age > 30"``).
"""
self._obj.DeleteByFilter(filter)
# ========== Collection DQL-fetch Methods ==========
def fetch(self, ids: Union[str, list[str]]) -> dict[str, Doc]:
"""Retrieve documents by ID.
Args:
ids (Union[str, list[str]]): Document IDs to fetch.
Returns:
dict[str, Doc]: Mapping from ID to document. Missing IDs are omitted.
"""
ids = [ids] if isinstance(ids, str) else ids
docs = self._obj.Fetch(ids)
return {
doc_id: py_doc
for doc_id, core_doc in docs.items()
if (py_doc := convert_to_py_doc(core_doc, self.schema)) is not None
}
# ========== Collection DQL-Query Methods ==========
def query(
self,
vectors: Optional[Union[VectorQuery, list[VectorQuery]]] = None,
*,
topk: int = 10,
filter: Optional[str] = None,
include_vector: bool = False,
output_fields: Optional[list[str]] = None,
reranker: Optional[ReRanker] = None,
) -> list[Doc]:
"""Perform vector similarity search with optional filtering and re-ranking.
At least one `VectorQuery` must be provided.
Args:
vectors (Optional[Union[VectorQuery, list[VectorQuery]]], optional):
One or more vector queries. Defaults to None.
topk (int, optional): Number of nearest neighbors to return.
Defaults to 10.
filter (Optional[str], optional): Boolean expression to pre-filter candidates.
Defaults to None.
include_vector (bool, optional): Whether to include vector data in results.
Defaults to False.
output_fields (Optional[list[str]], optional): Scalar fields to include.
If None, all fields are returned. Defaults to None.
reranker (Optional[ReRanker], optional): Re-ranker to refine results.
Defaults to None.
Returns:
list[Doc]: Top-k matching documents, sorted by relevance score.
Examples:
>>> from zvec import VectorQuery
>>> results = collection.query(
... vectors=VectorQuery("embedding", vector=[0.1, 0.2]),
... topk=5,
... filter="category == 'tech'",
... output_fields=["title", "url"]
... )
"""
ctx = QueryContext(
topk=topk,
filter=filter,
queries=[vectors] if isinstance(vectors, VectorQuery) else vectors,
include_vector=include_vector,
output_fields=output_fields,
reranker=reranker,
)
return self._querier.execute(ctx, self._obj)
+54
View File
@@ -0,0 +1,54 @@
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
from __future__ import annotations
from _zvec import _Doc
from .doc import Doc
from .schema import CollectionSchema
def convert_to_cpp_doc(doc: Doc, collection_schema: CollectionSchema) -> _Doc:
if not doc or not collection_schema:
return None
_doc = _Doc()
# set pk
_doc.set_pk(doc.id)
# set scalar fields
for k, v in doc.fields.items():
field_schema = collection_schema.field(k)
if not field_schema:
raise ValueError(
f"schema validate failed: {k} not found in collection schema"
)
_doc.set_any(k, field_schema._get_object(), v)
# set vector fields
for k, v in doc.vectors.items():
vector_schema = collection_schema.vector(k)
if not vector_schema:
raise ValueError(
f"schema validate failed: {k} not found in collection schema"
)
_doc.set_any(k, vector_schema._get_object(), v)
return _doc
def convert_to_py_doc(doc: _Doc, collection_schema: CollectionSchema) -> Doc:
if not doc or not collection_schema:
return None
data_tuple = doc.get_all(collection_schema._get_object())
return Doc._from_tuple(data_tuple)
+173
View File
@@ -0,0 +1,173 @@
# Copyright 2025-present the zvec project
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
from __future__ import annotations
import json
from typing import Any, Optional
from ..common import VectorType
__all__ = [
"Doc",
]
class Doc:
"""Represents a retrieved document with optional metadata, fields, and vectors.
This immutable data class encapsulates the result of a search or retrieval
operation. It includes the document ID, relevance score (if applicable),
scalar fields, and vector embeddings.
During initialization, any `numpy.ndarray` in `vectors` is automatically
converted to a plain Python list for JSON serialization and immutability.
Attributes:
id (str): Unique identifier of the document.
score (Optional[float], optional): Relevance score from search.
Defaults to None.
vectors (Optional[dict[str, VectorType]], optional): Named vector
embeddings associated with the document. Values are converted to
lists if originally `np.ndarray`. Defaults to None.
fields (Optional[dict[str, Any]], optional): Scalar metadata fields
(e.g., title, timestamp). Defaults to None.
Examples:
>>> import numpy as np
>>> import zvec
>>> doc = zvec.Doc(
... id="doc1",
... score=0.95,
... vectors={"emb": np.array([0.1, 0.2, 0.3])},
... fields={"title": "Hello World"}
... )
>>> print(doc.vector("emb"))
[0.1, 0.2, 0.3]
>>> print(doc.has_field("title"))
True
"""
__slots__ = ("id", "score", "vectors", "fields")
def __init__(
self,
id: str,
score: Optional[float] = None,
vectors: Optional[dict[str, VectorType]] = None,
fields: Optional[dict[str, Any]] = None,
):
self.id = id
self.score = score
self.vectors = vectors or {}
self.fields = fields or {}
def has_field(self, name: str) -> bool:
"""Check if the document contains a scalar field with the given name.
Args:
name (str): Name of the field to check.
Returns:
bool: True if the field exists, False otherwise.
"""
return name in self.fields
def has_vector(self, name: str) -> bool:
"""Check if the document contains a vector with the given name.
Args:
name (str): Name of the vector to check.
Returns:
bool: True if the vector exists, False otherwise.
"""
return name in self.vectors
def vector(self, name: str):
"""Get a vector by name.
Args:
name (str): Name of the vector.
Returns:
Any: The vector (as a list) if it exists, otherwise None.
"""
return self.vectors and self.vectors.get(name)
def field(self, name: str):
"""Get a scalar field by name.
Args:
name (str): Name of the field.
Returns:
Any: The field value if it exists, otherwise None.
"""
return self.fields and self.fields.get(name)
def vector_names(self) -> list[str]:
"""Get the list of all vector names in this document.
Returns:
list[str]: A list of vector field names. Empty if no vectors.
"""
return [] if not self.vectors else list(self.vectors.keys())
def field_names(self) -> list[str]:
"""Get the list of all scalar field names in this document.
Returns:
list[str]: A list of field names. Empty if no fields.
"""
return [] if not self.fields else list(self.fields.keys())
def __repr__(self) -> str:
try:
schema = {
"id": self.id,
"score": self.score,
"fields": self.fields,
"vectors": self.vectors,
}
return json.dumps(schema, indent=2, ensure_ascii=False)
except Exception as e:
return f"<Doc error during repr: {e}>"
def _replace(self, **changes):
new_tuple = (
changes.get("id", self.id),
changes.get("score", self.score),
changes.get("fields", self.fields.copy() if self.fields else None),
changes.get("vectors", self.vectors.copy() if self.vectors else None),
)
return type(self)._from_tuple(new_tuple)
@classmethod
def _from_tuple(
cls, data_tuple: tuple[str, float, dict[str, Any], dict[str, VectorType]]
):
obj = object.__new__(cls)
obj.id = data_tuple[0]
obj.score = data_tuple[1]
obj.fields = data_tuple[2] or {}
vectors = data_tuple[3]
if vectors is not None:
obj.vectors = {
name: (vec.tolist() if hasattr(vec, "tolist") else vec)
for name, vec in vectors.items()
}
else:
obj.vectors = {}
return obj
+42
View File
@@ -0,0 +1,42 @@
# Copyright 2025-present the zvec project
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
from __future__ import annotations
from _zvec.param import (
AddColumnOption,
AlterColumnOption,
CollectionOption,
FlatIndexParam,
HnswIndexParam,
HnswQueryParam,
IndexOption,
InvertIndexParam,
IVFIndexParam,
IVFQueryParam,
OptimizeOption,
)
__all__ = [
"AddColumnOption",
"AlterColumnOption",
"CollectionOption",
"FlatIndexParam",
"HnswIndexParam",
"HnswQueryParam",
"IVFIndexParam",
"IVFQueryParam",
"IndexOption",
"InvertIndexParam",
"OptimizeOption",
]
+692
View File
@@ -0,0 +1,692 @@
"""
This module contains the params of Zvec
"""
from __future__ import annotations
import collections
import typing
import _zvec.typing
__all__: list[str] = [
"AddColumnOption",
"AlterColumnOption",
"CollectionOption",
"FlatIndexParam",
"HnswIndexParam",
"HnswQueryParam",
"IVFIndexParam",
"IVFQueryParam",
"IndexOption",
"IndexParam",
"InvertIndexParam",
"OptimizeOption",
"QueryParam",
"SegmentOption",
"VectorIndexParam",
]
class AddColumnOption:
"""
Options for adding a new column to a collection.
Attributes:
concurrency (int): Number of threads to use when backfilling data
for the new column. If 0, auto-detect is used. Default is 0.
Examples:
>>> opt = AddColumnOption(concurrency=1)
>>> print(opt.concurrency)
1
"""
def __getstate__(self) -> tuple: ...
def __init__(self, concurrency: typing.SupportsInt = 0) -> None:
"""
Constructs an AddColumnOption instance.
Args:
concurrency (int, optional): Number of threads for data backfill.
0 means auto-detect. Defaults to 0.
"""
def __setstate__(self, arg0: tuple) -> None: ...
@property
def concurrency(self) -> int:
"""
int: Number of threads used when adding a column (0 = auto).
"""
class AlterColumnOption:
"""
Options for altering an existing column (e.g., changing index settings).
Attributes:
concurrency (int): Number of threads to use during the alteration process.
If 0, the system will choose an optimal value automatically.
Default is 0.
Examples:
>>> opt = AlterColumnOption(concurrency=1)
>>> print(opt.concurrency)
1
"""
def __getstate__(self) -> tuple: ...
def __init__(self, concurrency: typing.SupportsInt = 0) -> None:
"""
Constructs an AlterColumnOption instance.
Args:
concurrency (int, optional): Number of threads for column alteration.
0 means auto-detect. Defaults to 0.
"""
def __setstate__(self, arg0: tuple) -> None: ...
@property
def concurrency(self) -> int:
"""
int: Number of threads used when altering a column (0 = auto).
"""
class CollectionOption:
"""
Options for opening or creating a collection.
Attributes:
read_only (bool): Whether the collection is opened in read-only mode.
Default is False.
enable_mmap (bool): Whether to use memory-mapped I/O for data files.
Default is True.
Examples:
>>> opt = CollectionOption(read_only=True, enable_mmap=False)
>>> print(opt.read_only)
True
"""
def __getstate__(self) -> tuple: ...
def __init__(self, read_only: bool = False, enable_mmap: bool = True) -> None:
"""
Constructs a CollectionOption instance.
Args:
read_only (bool, optional): Open collection in read-only mode.
Defaults to False.
enable_mmap (bool, optional): Enable memory-mapped I/O.
Defaults to True.
"""
def __repr__(self) -> str: ...
def __setstate__(self, arg0: tuple) -> None: ...
@property
def enable_mmap(self) -> bool: ...
@property
def read_only(self) -> bool: ...
class FlatIndexParam(VectorIndexParam):
"""
Parameters for configuring a flat (brute-force) index.
A flat index performs exact nearest neighbor search by comparing the query
vector against all vectors in the collection. It is simple, accurate, and
suitable for small to medium datasets or as a baseline.
Attributes:
metric_type (MetricType): Distance metric used for similarity computation.
Default is ``MetricType.IP`` (inner product).
quantize_type (QuantizeType): Optional quantization type for vector
compression (e.g., FP16, INT8). Use ``QuantizeType.UNDEFINED`` to
disable quantization. Default is ``QuantizeType.UNDEFINED``.
Examples:
>>> from zvec.typing import MetricType, QuantizeType
>>> params = FlatIndexParam(
... metric_type=MetricType.L2,
... quantize_type=QuantizeType.FP16
... )
>>> print(params)
{'metric_type': 'L2', 'quantize_type': 'FP16'}
"""
def __getstate__(self) -> tuple: ...
def __init__(
self,
metric_type: _zvec.typing.MetricType = ...,
quantize_type: _zvec.typing.QuantizeType = ...,
) -> None:
"""
Constructs a FlatIndexParam instance.
Args:
metric_type (MetricType, optional): Distance metric. Defaults to MetricType.IP.
quantize_type (QuantizeType, optional): Vector quantization type.
Defaults to QuantizeType.UNDEFINED (no quantization).
"""
def __repr__(self) -> str: ...
def __setstate__(self, arg0: tuple) -> None: ...
def to_dict(self) -> dict:
"""
Convert to dictionary with all fields
"""
class HnswIndexParam(VectorIndexParam):
"""
Parameters for configuring an HNSW (Hierarchical Navigable Small World) index.
HNSW is a graph-based approximate nearest neighbor search index. This class
encapsulates its construction hyperparameters.
Attributes:
metric_type (MetricType): Distance metric used for similarity computation.
Default is ``MetricType.IP`` (inner product).
m (int): Number of bi-directional links created for every new element
during construction. Higher values improve accuracy but increase
memory usage and construction time. Default is 100.
ef_construction (int): Size of the dynamic candidate list for nearest
neighbors during index construction. Larger values yield better
graph quality at the cost of slower build time. Default is 500.
quantize_type (QuantizeType): Optional quantization type for vector
compression (e.g., FP16, INT8). Default is `QuantizeType.UNDEFINED` to
disable quantization.
Examples:
>>> from zvec.typing import MetricType, QuantizeType
>>> params = HnswIndexParam(
... metric_type=MetricType.COSINE,
... m=16,
... ef_construction=200,
... quantize_type=QuantizeType.INT8
... )
>>> print(params)
{'metric_type': 'IP', 'm': 16, 'ef_construction': 200, 'quantize_type': 'INT8'}
"""
def __getstate__(self) -> tuple: ...
def __init__(
self,
metric_type: _zvec.typing.MetricType = ...,
m: typing.SupportsInt = 100,
ef_construction: typing.SupportsInt = 500,
quantize_type: _zvec.typing.QuantizeType = ...,
) -> None: ...
def __repr__(self) -> str: ...
def __setstate__(self, arg0: tuple) -> None: ...
def to_dict(self) -> dict:
"""
Convert to dictionary with all fields
"""
@property
def ef_construction(self) -> int:
"""
int: Candidate list size during index construction.
"""
@property
def m(self) -> int:
"""
int: Maximum number of neighbors per node in upper layers.
"""
class HnswQueryParam(QueryParam):
"""
Query parameters for HNSW (Hierarchical Navigable Small World) index.
Controls the trade-off between search speed and accuracy via the `ef` parameter.
Attributes:
type (IndexType): Always ``IndexType.HNSW``.
ef (int): Size of the dynamic candidate list during search.
Larger values improve recall but slow down search.
Default is 300.
radius (float): Search radius for range queries. Default is 0.0.
is_linear (bool): Force linear search. Default is False.
is_using_refiner (bool, optional): Whether to use refiner for the query. Default is False.
Examples:
>>> params = HnswQueryParam(ef=300)
>>> print(params.ef)
300
>>> print(params.to_dict() if hasattr(params, 'to_dict') else params)
{"type":"HNSW", "ef":300}
"""
def __getstate__(self) -> tuple: ...
def __init__(
self,
ef: typing.SupportsInt = 300,
radius: typing.SupportsFloat = 0.0,
is_linear: bool = False,
is_using_refiner: bool = False,
) -> None:
"""
Constructs an HnswQueryParam instance.
Args:
ef (int, optional): Search-time candidate list size.
Higher values improve accuracy. Defaults to 100.
radius (float, optional): Search radius for range queries. Default is 0.0.
is_linear (bool, optional): Force linear search. Default is False.
is_using_refiner (bool, optional): Whether to use refiner for the query. Default is False.
"""
def __repr__(self) -> str: ...
def __setstate__(self, arg0: tuple) -> None: ...
@property
def ef(self) -> int:
"""
int: Size of the dynamic candidate list during HNSW search.
"""
class IVFIndexParam(VectorIndexParam):
"""
Parameters for configuring an IVF (Inverted File Index) index.
IVF partitions the vector space into clusters (inverted lists). At query time,
only a subset of clusters is searched, providing a trade-off between speed
and accuracy.
Attributes:
metric_type (MetricType): Distance metric used for similarity computation.
Default is ``MetricType.IP`` (inner product).
n_list (int): Number of clusters (inverted lists) to partition the dataset into.
If set to 0, the system will auto-select a reasonable value based on data size.
Default is 0 (auto).
n_iters (int): Number of iterations for k-means clustering during index training.
Higher values yield more stable centroids. Default is 10.
use_soar (bool): Whether to enable SOAR (Scalable Optimized Adaptive Routing)
for improved IVF search performance. Default is False.
quantize_type (QuantizeType): Optional quantization type for vector
compression (e.g., FP16, INT8). Default is ``QuantizeType.UNDEFINED``.
Examples:
>>> from zvec.typing import MetricType, QuantizeType
>>> params = IVFIndexParam(
... metric_type=MetricType.COSINE,
... n_list=100,
... n_iters=15,
... use_soar=True,
... quantize_type=QuantizeType.INT8
... )
>>> print(params.n_list)
100
"""
def __getstate__(self) -> tuple: ...
def __init__(
self,
metric_type: _zvec.typing.MetricType = ...,
n_list: typing.SupportsInt = 0,
n_iters: typing.SupportsInt = 10,
use_soar: bool = False,
quantize_type: _zvec.typing.QuantizeType = ...,
) -> None:
"""
Constructs an IVFIndexParam instance.
Args:
metric_type (MetricType, optional): Distance metric. Defaults to MetricType.IP.
n_list (int, optional): Number of inverted lists (clusters). Set to 0 for auto.
Defaults to 0.
n_iters (int, optional): Number of k-means iterations during training.
Defaults to 10.
use_soar (bool, optional): Enable SOAR optimization. Defaults to False.
quantize_type (QuantizeType, optional): Vector quantization type.
Defaults to QuantizeType.UNDEFINED.
"""
def __repr__(self) -> str: ...
def __setstate__(self, arg0: tuple) -> None: ...
def to_dict(self) -> dict:
"""
Convert to dictionary with all fields
"""
@property
def n_iters(self) -> int:
"""
int: Number of k-means iterations during training.
"""
@property
def n_list(self) -> int:
"""
int: Number of inverted lists (0 = auto).
"""
@property
def use_soar(self) -> bool:
"""
bool: Whether SOAR optimization is enabled.
"""
class IVFQueryParam(QueryParam):
"""
Query parameters for IVF (Inverted File Index) index.
Controls how many inverted lists (`nprobe`) to visit during search.
Attributes:
type (IndexType): Always ``IndexType.IVF``.
nprobe (int): Number of closest clusters (inverted lists) to search.
Higher values improve recall but increase latency.
Default is 10.
radius (float): Search radius for range queries. Default is 0.0.
is_linear (bool): Force linear search. Default is False.
Examples:
>>> params = IVFQueryParam(nprobe=20)
>>> print(params.nprobe)
20
"""
def __getstate__(self) -> tuple: ...
def __init__(self, nprobe: typing.SupportsInt = 10) -> None:
"""
Constructs an IVFQueryParam instance.
Args:
nprobe (int, optional): Number of inverted lists to probe during search.
Higher values improve accuracy. Defaults to 10.
"""
def __repr__(self) -> str: ...
def __setstate__(self, arg0: tuple) -> None: ...
@property
def nprobe(self) -> int:
"""
int: Number of inverted lists to search during IVF query.
"""
class IndexOption:
"""
Options for creating an index.
Attributes:
concurrency (int): Number of threads to use during index creation.
If 0, the system will choose an optimal value automatically.
Default is 0.
Examples:
>>> opt = IndexOption(concurrency=4)
>>> print(opt.concurrency)
4
"""
def __getstate__(self) -> tuple: ...
def __init__(self, concurrency: typing.SupportsInt = 0) -> None:
"""
Constructs an IndexOption instance.
Args:
concurrency (int, optional): Number of concurrent threads.
0 means auto-detect. Defaults to 0.
"""
def __setstate__(self, arg0: tuple) -> None: ...
@property
def concurrency(self) -> int:
"""
int: Number of threads used for index creation (0 = auto).
"""
class IndexParam:
"""
Base class for all index parameter configurations.
This abstract base class defines the common interface for index types.
It should not be instantiated directly; use derived classes instead.
Attributes:
type (IndexType): The type of the index (e.g., HNSW, FLAT, INVERT).
"""
__hash__: typing.ClassVar[None] = None
def __eq__(self, arg0: typing.Any) -> bool: ...
def __getstate__(self) -> tuple: ...
def __setstate__(self, arg0: tuple) -> None: ...
def clone(self) -> IndexParam: ...
def to_dict(self) -> dict:
"""
Convert to dictionary with all fields
"""
@property
def type(self) -> _zvec.typing.IndexType:
"""
IndexType: The type of the index.
"""
class InvertIndexParam(IndexParam):
"""
Parameters for configuring an invert index.
This class controls whether range query
optimization is enabled for invert index structures.
Attributes:
type (IndexType): Always `IndexType.INVERTED`.
enable_range_optimization (bool): Whether range optimization is enabled.
enable_extended_wildcard (bool): Whether extended wildcard (suffix and infix) search is enabled.
Examples:
>>> params = InvertIndexParam(enable_range_optimization=True, enable_extended_wildcard=False)
>>> print(params.enable_range_optimization)
True
>>> print(params.enable_extended_wildcard)
False
>>> config = params.to_dict()
>>> print(config)
{'enable_range_optimization': True, 'enable_extended_wildcard': False}
"""
def __getstate__(self) -> tuple: ...
def __init__(
self,
enable_range_optimization: bool = False,
enable_extended_wildcard: bool = False,
) -> None:
"""
Constructs an InvertIndexParam instance.
Args:
enable_range_optimization (bool, optional): If True, enables range query
optimization for the invert index. Defaults to False.
enable_extended_wildcard (bool, optional): If True, enables extended wildcard
search including suffix and infix patterns. Defaults to False.
"""
def __repr__(self) -> str: ...
def __setstate__(self, arg0: tuple) -> None: ...
def to_dict(self) -> dict:
"""
Convert to dictionary with all fields
"""
@property
def enable_extended_wildcard(self) -> bool:
"""
bool: Whether extended wildcard (suffix and infix) search is enabled.
Note: Prefix search is always enabled regardless of this setting.
"""
@property
def enable_range_optimization(self) -> bool:
"""
bool: Whether range optimization is enabled for this inverted index.
"""
class OptimizeOption:
"""
Options for optimizing a collection (e.g., merging segments).
Attributes:
concurrency (int): Number of threads to use during optimization.
If 0, the system will choose an optimal value automatically.
Default is 0.
Examples:
>>> opt = OptimizeOption(concurrency=2)
>>> print(opt.concurrency)
2
"""
def __getstate__(self) -> tuple: ...
def __init__(self, concurrency: typing.SupportsInt = 0) -> None:
"""
Constructs an OptimizeOption instance.
Args:
concurrency (int, optional): Number of concurrent threads.
0 means auto-detect. Defaults to 0.
"""
def __setstate__(self, arg0: tuple) -> None: ...
@property
def concurrency(self) -> int:
"""
int: Number of threads used for optimization (0 = auto).
"""
class QueryParam:
"""
Base class for all query parameter configurations.
This abstract base class defines common query settings such as search radius
and whether to force linear (brute-force) search. It should not be instantiated
directly; use derived classes like `HnswQueryParam` or `IVFQueryParam`.
Attributes:
type (IndexType): The index type this query is configured for.
radius (float): Search radius for range queries. Used in combination with
top-k to filter results. Default is 0.0 (disabled).
is_linear (bool): If True, forces brute-force linear search instead of
using the index. Useful for debugging or small datasets. Default is False.
is_using_refiner (bool, optional): Whether to use refiner for the query. Default is False.
"""
def __getstate__(self) -> tuple: ...
def __setstate__(self, arg0: tuple) -> None: ...
@property
def is_linear(self) -> bool:
"""
bool: Whether to bypass the index and use brute-force linear search.
"""
@property
def is_using_refiner(self) -> bool:
"""
bool: Whether to use refiner for the query.
"""
@property
def radius(self) -> float:
"""
IndexType: The type of index this query targets.
"""
@property
def type(self) -> _zvec.typing.IndexType:
"""
IndexType: The type of index this query targets.
"""
class SegmentOption:
"""
Options for segment-level operations.
Currently, this class mirrors CollectionOption and is used internally.
It supports read-only mode, memory mapping, and buffer configuration.
Note:
This class is primarily for internal use. Most users should use
CollectionOption instead.
Examples:
>>> opt = SegmentOption()
>>> print(opt.enable_mmap)
True
"""
def __getstate__(self) -> tuple: ...
def __init__(self) -> None:
"""
Constructs a SegmentOption with default settings.
"""
def __repr__(self) -> str: ...
def __setstate__(self, arg0: tuple) -> None: ...
@property
def enable_mmap(self) -> bool:
"""
bool: Whether memory-mapped I/O is enabled.
"""
@property
def max_buffer_size(self) -> int:
"""
int: Maximum buffer size in bytes (internal use).
"""
@property
def read_only(self) -> bool:
"""
bool: Whether the segment is read-only.
"""
class VectorIndexParam(IndexParam):
"""
Base class for vector index parameter configurations.
Encapsulates common settings for all vector index types.
Attributes:
type (IndexType): The specific vector index type (e.g., HNSW, FLAT).
metric_type (MetricType): Distance metric used for similarity search.
quantize_type (QuantizeType): Optional vector quantization type.
"""
def __getstate__(self) -> tuple: ...
def __setstate__(self, arg0: tuple) -> None: ...
def to_dict(self) -> dict:
"""
Convert to dictionary with all fields
"""
@property
def metric_type(self) -> _zvec.typing.MetricType:
"""
MetricType: Distance metric (e.g., IP, COSINE, L2).
"""
@property
def quantize_type(self) -> _zvec.typing.QuantizeType:
"""
QuantizeType: Vector quantization type (e.g., FP16, INT8).
"""
class _VectorQuery:
field_name: str
filter: str
include_vector: bool
query_params: QueryParam
def __getstate__(self) -> tuple: ...
def __init__(self) -> None: ...
def __setstate__(self, arg0: tuple) -> None: ...
def set_vector(self, arg0: ..., arg1: typing.Any) -> None: ...
@property
def output_fields(self) -> list[str] | None: ...
@output_fields.setter
def output_fields(self, arg0: collections.abc.Sequence[str] | None) -> None: ...
@property
def topk(self) -> int: ...
@topk.setter
def topk(self, arg0: typing.SupportsInt) -> None: ...
+80
View File
@@ -0,0 +1,80 @@
# Copyright 2025-present the zvec project
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
from __future__ import annotations
from dataclasses import dataclass
from typing import Optional, Union
from ...common import VectorType
from . import HnswQueryParam, IVFQueryParam
__all__ = ["VectorQuery"]
@dataclass(frozen=True)
class VectorQuery:
"""Represents a vector search query for a specific field in a collection.
A `VectorQuery` can be constructed using either a document ID (to look up
its vector) or an explicit vector. It may optionally include index-specific
query parameters to control search behavior (e.g., `ef` for HNSW, `nprobe` for IVF).
Exactly one of `id` or `vector` should be provided. If both are given,
behavior is implementation-defined (typically `id` takes precedence).
Attributes:
field_name (str): Name of the vector field to query.
id (Optional[str], optional): Document ID to fetch vector from. Default is None.
vector (VectorType, optional): Explicit query vector. Default is None.
param (Optional[Union[HnswQueryParam, IVFQueryParam]], optional):
Index-specific query parameters. Default is None.
Examples:
>>> import zvec
>>> # Query by ID
>>> q1 = zvec.VectorQuery(field_name="embedding", id="doc123")
>>> # Query by vector
>>> q2 = zvec.VectorQuery(
... field_name="embedding",
... vector=[0.1, 0.2, 0.3],
... param=HnswQueryParam(ef=300)
... )
"""
field_name: str
id: Optional[str] = None
vector: VectorType = None
param: Optional[Union[HnswQueryParam, IVFQueryParam]] = None
def has_id(self) -> bool:
"""Check if the query is based on a document ID.
Returns:
bool: True if `id` is set, False otherwise.
"""
return self.id is not None
def has_vector(self) -> bool:
"""Check if the query contains an explicit vector.
Returns:
bool: True if `vector` is non-empty, False otherwise.
"""
return self.vector is not None and len(self.vector) > 0
def _validate(self) -> None:
if self.field_name is None:
raise ValueError("Field name cannot be empty")
if self.id and self.vector:
raise ValueError("Cannot provide both id and vector")
+21
View File
@@ -0,0 +1,21 @@
# Copyright 2025-present the zvec project
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
from __future__ import annotations
from _zvec.schema import CollectionStats
from .collection_schema import CollectionSchema
from .field_schema import FieldSchema, VectorSchema
__all__ = ["CollectionSchema", "CollectionStats", "FieldSchema", "VectorSchema"]
+109
View File
@@ -0,0 +1,109 @@
"""
This module contains the schema of Zvec
"""
from __future__ import annotations
import collections.abc
import typing
import _zvec.param
import _zvec.typing
from .collection_schema import CollectionSchema
from .field_schema import FieldSchema, VectorSchema
__all__: list[str] = [
"CollectionSchema",
"CollectionStats",
"FieldSchema",
"VectorSchema",
]
class CollectionStats:
def __init__(self) -> None: ...
def __repr__(self) -> str: ...
@property
def doc_count(self) -> int: ...
@property
def index_completeness(self) -> dict[str, float]: ...
class _CollectionSchema:
__hash__: typing.ClassVar[None] = None
def __eq__(self, arg0: _CollectionSchema) -> bool: ...
def __init__(
self, name: str, fields: collections.abc.Sequence[_FieldSchema]
) -> None:
"""
Construct with name and list of fields
"""
def __ne__(self, arg0: _CollectionSchema) -> bool: ...
def fields(self) -> list[_FieldSchema]:
"""
Return list of all field schemas.
"""
def forward_fields(self) -> list[_FieldSchema]:
"""
Return list of forward-indexed fields.
"""
def get_field(self, field_name: str) -> _FieldSchema:
"""
Get field by name (const pointer), returns None if not found.
"""
def get_forward_field(self, field_name: str) -> _FieldSchema:
"""
Get forward field (used for filtering).
"""
def get_vector_field(self, field_name: str) -> _FieldSchema:
"""
Get vector field by name.
"""
def has_field(self, field_name: str) -> bool:
"""
Check if a field exists.
"""
def vector_fields(self) -> list[_FieldSchema]:
"""
Return list of vector fields.
"""
@property
def name(self) -> str: ...
class _FieldSchema:
__hash__: typing.ClassVar[None] = None
def __eq__(self, arg0: _FieldSchema) -> bool: ...
def __init__(
self,
name: str,
data_type: _zvec.typing.DataType,
nullable: bool = False,
dimension: typing.SupportsInt = 0,
index_param: _zvec.param.IndexParam = None,
) -> None: ...
def __ne__(self, arg0: _FieldSchema) -> bool: ...
@property
def data_type(self) -> _zvec.typing.DataType: ...
@property
def dimension(self) -> int: ...
@property
def index_param(self) -> typing.Any: ...
@property
def index_type(self) -> _zvec.typing.IndexType: ...
@property
def is_dense_vector(self) -> bool: ...
@property
def is_sparse_vector(self) -> bool: ...
@property
def name(self) -> str: ...
@property
def nullable(self) -> bool: ...
@@ -0,0 +1,215 @@
# Copyright 2025-present the zvec project
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
from __future__ import annotations
import json
from typing import Optional, Union
from _zvec.schema import _CollectionSchema, _FieldSchema
from .field_schema import FieldSchema, VectorSchema
__all__ = [
"CollectionSchema",
]
class CollectionSchema:
"""Defines the structure of a collection in Zvec.
A collection schema specifies the name of the collection and its fields,
including both scalar fields (e.g., int, string) and vector fields.
Field names must be unique across both scalar and vector fields.
Args:
name (str): Name of the collection.
fields (Optional[Union[FieldSchema, list[FieldSchema]]], optional):
One or more scalar field definitions. Defaults to None.
vectors (Optional[Union[VectorSchema, list[VectorSchema]]], optional):
One or more vector field definitions. Defaults to None.
Raises:
TypeError: If `fields` or `vectors` are of unsupported types.
ValueError: If any field or vector name is duplicated.
Examples:
>>> from zvec import FieldSchema, VectorSchema, DataType, IndexType
>>> id_field = FieldSchema("id", DataType.INT64, is_primary=True)
>>> emb_field = VectorSchema("embedding", dim=128, data_type=DataType.VECTOR_FP32)
>>> schema = CollectionSchema(
... name="my_collection",
... fields=id_field,
... vectors=emb_field
... )
>>> print(schema.name)
my_collection
"""
def __init__(
self,
name: str,
fields: Optional[Union[FieldSchema, list[FieldSchema]]] = None,
vectors: Optional[Union[VectorSchema, list[VectorSchema]]] = None,
):
if name is None or not isinstance(name, str):
raise ValueError(
f"schema validate failed: collection name must be str, got {type(name).__name__}"
)
# handle fields
_fields_name: list[str] = []
_fields_list: list[_FieldSchema] = []
self._check_fields(fields, _fields_name, _fields_list)
self._check_vectors(vectors, _fields_name, _fields_list)
# init
self._cpp_obj = _CollectionSchema(
name=name,
fields=_fields_list,
)
def _check_fields(
self,
fields: Optional[Union[FieldSchema, list[FieldSchema]]],
_fields_name: list[str],
_fields_list: list[_FieldSchema],
) -> None:
field_items = []
if isinstance(fields, FieldSchema):
field_items = [fields]
elif isinstance(fields, list):
field_items = fields
elif fields is None:
field_items = []
else:
raise TypeError(
f"schema validate failed: invalid 'fields' type, expected FieldSchema or list[FieldSchema], "
f"got {type(fields).__name__}"
)
for idx, field in enumerate(field_items):
if not isinstance(field, FieldSchema):
raise TypeError(
f"schema validate failed: invalid field type in 'fields' list, expected FieldSchema, "
f"got {type(field).__name__} at index {idx}"
)
if field.name in _fields_name:
raise ValueError(
f"schema validate failed: duplicate field name '{field.name}': field names must be unique"
)
_fields_name.append(field.name)
_fields_list.append(field._get_object())
def _check_vectors(
self,
vectors: Optional[Union[VectorSchema, list[VectorSchema]]],
_fields_name: list[str],
_fields_list: list[_FieldSchema],
) -> None:
# handle vector
if isinstance(vectors, VectorSchema):
vectors_items = [vectors]
elif isinstance(vectors, list):
vectors_items = vectors
elif vectors is None:
vectors_items = []
else:
raise TypeError(
f"schema validate failed: invalid 'vectors' type, expected VectorSchema or list[VectorSchema], "
f"got {type(vectors).__name__}"
)
for idx, vector in enumerate(vectors_items):
if not isinstance(vector, VectorSchema):
raise TypeError(
f"schema validate failed: invalid vector type in 'vectors' list, expected VectorSchema, "
f"got {type(vector).__name__} at index {idx}"
)
if vector.name in _fields_name:
raise ValueError(
f"schema validate failed: duplicate vector name '{vector.name}', vector names must be unique "
f"(conflicts with existing field or vector)"
)
_fields_name.append(vector.name)
_fields_list.append(vector._get_object())
@classmethod
def _from_core(cls, core_collection_schema: _CollectionSchema):
inst = cls.__new__(cls)
if not core_collection_schema:
raise ValueError("schema validate failed: schema is null")
inst._cpp_obj = core_collection_schema
return inst
@property
def name(self) -> str:
"""str: The name of the collection."""
return self._cpp_obj.name
def field(self, name: str) -> Optional[FieldSchema]:
"""Retrieve a scalar field by name.
Args:
name (str): Name of the field.
Returns:
Optional[FieldSchema]: The field if found, otherwise None.
"""
_field = self._cpp_obj.get_forward_field(name)
return FieldSchema._from_core(_field) if _field else None
def vector(self, name: str) -> Optional[VectorSchema]:
"""Retrieve a vector field by name.
Args:
name (str): Name of the vector field.
Returns:
Optional[VectorSchema]: The vector field if found, otherwise None.
"""
_field = self._cpp_obj.get_vector_field(name)
return VectorSchema._from_core(_field) if _field else None
@property
def fields(self) -> list[FieldSchema]:
"""list[FieldSchema]: All scalar (non-vector) fields in the schema."""
_fields = self._cpp_obj.forward_fields()
return [FieldSchema._from_core(_field) for _field in _fields]
@property
def vectors(self) -> list[VectorSchema]:
"""list[VectorSchema]: All vector fields in the schema."""
_vectors = self._cpp_obj.vector_fields()
return [VectorSchema._from_core(_vector) for _vector in _vectors]
def _get_object(self) -> _CollectionSchema:
return self._cpp_obj
def __repr__(self) -> str:
try:
schema = {
"name": self.name,
"fields": {field.name: field.__dict__() for field in self.fields},
"vectors": {vector.name: vector.__dict__() for vector in self.vectors},
}
return json.dumps(schema, indent=2, ensure_ascii=False)
except Exception as e:
return f"<CollectionSchema error during repr: {e}>"
def __str__(self) -> str:
return self.__repr__()
+300
View File
@@ -0,0 +1,300 @@
# Copyright 2025-present the zvec project
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
from __future__ import annotations
import json
from typing import Any, Optional, Union
from _zvec.schema import _FieldSchema
from zvec.model.param import (
FlatIndexParam,
HnswIndexParam,
InvertIndexParam,
IVFIndexParam,
)
from zvec.typing import DataType
__all__ = [
"FieldSchema",
"VectorSchema",
]
SUPPORT_VECTOR_DATA_TYPE = [
DataType.VECTOR_FP16,
DataType.VECTOR_FP32,
DataType.VECTOR_FP64,
DataType.VECTOR_INT8,
DataType.SPARSE_VECTOR_FP16,
DataType.SPARSE_VECTOR_FP32,
]
SUPPORT_SCALAR_DATA_TYPE = [
DataType.INT32,
DataType.INT64,
DataType.UINT32,
DataType.UINT64,
DataType.FLOAT,
DataType.DOUBLE,
DataType.STRING,
DataType.BOOL,
DataType.ARRAY_INT32,
DataType.ARRAY_INT64,
DataType.ARRAY_UINT32,
DataType.ARRAY_UINT64,
DataType.ARRAY_FLOAT,
DataType.ARRAY_DOUBLE,
DataType.ARRAY_STRING,
DataType.ARRAY_BOOL,
]
class FieldSchema:
"""Represents a scalar (non-vector) field in a collection schema.
A `FieldSchema` defines the name, data type, nullability, and optional
inverted index configuration for a regular field (e.g., ID, timestamp, category).
Args:
name (str): Name of the field. Must be unique within the collection.
data_type (DataType): Data type of the field (e.g., INT64, STRING).
nullable (bool, optional): Whether the field can contain null values.
Defaults to False.
index_param (Optional[InvertIndexParam], optional): Inverted index
parameters for this field. Only applicable to fields that support
indexing (e.g., scalar fields used in filtering). Defaults to None.
Examples:
>>> from zvec.typing import DataType
>>> from zvec.model.param import InvertIndexParam
>>> id_field = FieldSchema(
... name="id",
... data_type=DataType.INT64,
... nullable=False,
... index_param=InvertIndexParam(enable_range_optimization=True)
... )
"""
def __init__(
self,
name: str,
data_type: DataType,
nullable: bool = False,
index_param: Optional[InvertIndexParam] = None,
):
if name is None or not isinstance(name, str):
raise ValueError(
f"schema validate failed: field name must be str, got {type(name).__name__}"
)
if data_type not in SUPPORT_SCALAR_DATA_TYPE:
raise ValueError(
f"schema validate failed: scalar_field's data_type must be one of "
f"{', '.join(str(dt) for dt in SUPPORT_SCALAR_DATA_TYPE)}, "
f"but field[{name}]'s data_type is {data_type}"
)
self._cpp_obj = _FieldSchema(
name=name,
data_type=data_type,
dimension=0,
nullable=nullable,
index_param=index_param,
)
@classmethod
def _from_core(cls, core_field_schema: _FieldSchema):
if core_field_schema is None:
raise ValueError("schema validate failed: field schema is None")
inst = cls.__new__(cls)
inst._cpp_obj = core_field_schema
return inst
def _get_object(self) -> _FieldSchema:
return self._cpp_obj
@property
def name(self) -> str:
"""str: The name of the field."""
return self._cpp_obj.name
@property
def data_type(self) -> DataType:
"""DataType: The data type of the field (e.g., INT64, STRING)."""
return self._cpp_obj.data_type
@property
def nullable(self) -> bool:
"""bool: Whether the field allows null values."""
return self._cpp_obj.nullable
@property
def index_param(self) -> Optional[InvertIndexParam]:
"""Optional[InvertIndexParam]: Inverted index configuration, if any."""
return self._cpp_obj.index_param
def __dict__(self) -> dict[str, Any]:
return {
"name": self.name,
"data_type": (
self.data_type.name
if hasattr(self.data_type, "name")
else str(self.data_type)
),
"nullable": self.nullable,
"index_param": (
self.index_param.to_dict() if self.index_param is not None else None
),
}
def __repr__(self) -> str:
try:
schema = self.__dict__()
return json.dumps(schema, indent=2, ensure_ascii=False)
except Exception as e:
return f"<FieldSchema error during repr: {e}>"
def __str__(self) -> str:
return self.__repr__()
def __eq__(self, other: object) -> bool:
if not isinstance(other, FieldSchema):
return False
return self._cpp_obj == other._cpp_obj
def __hash__(self) -> int:
return hash((self.name, self.data_type, self.nullable))
class VectorSchema:
"""Represents a vector field in a collection schema.
A `VectorSchema` defines the name, data type, dimensionality, and index
configuration for a vector field used in similarity search.
Args:
name (str): Name of the vector field. Must be unique within the collection.
data_type (DataType): Vector data type (e.g., VECTOR_FP32, VECTOR_INT8).
dimension (int, optional): Dimensionality of the vector. Must be > 0 for dense vectors;
may be `None` for sparse vectors.
index_param (Union[HnswIndexParam, IVFIndexParam, FlatIndexParam], optional):
Index configuration for this vector field. Defaults to
``HnswIndexParam()``.
Examples:
>>> from zvec.typing import DataType
>>> from zvec.model.param import HnswIndexParam
>>> emb_field = VectorSchema(
... name="embedding",
... data_type=DataType.VECTOR_FP32,
... dimension=128,
... index_param=HnswIndexParam(ef_construction=200, m=16)
... )
"""
def __init__(
self,
name: str,
data_type: DataType,
dimension: Optional[int] = 0,
index_param: Optional[
Union[HnswIndexParam, FlatIndexParam, IVFIndexParam]
] = None,
):
if name is None or not isinstance(name, str):
raise ValueError(
f"schema validate failed: field name must be str, got {type(name).__name__}"
)
if not isinstance(dimension, int) or dimension < 0:
raise ValueError("schema validate failed: vector's dimension must be >= 0")
if data_type not in SUPPORT_VECTOR_DATA_TYPE:
raise ValueError(
f"schema validate failed: vector's data_type must be one of "
f"{', '.join(str(dt) for dt in SUPPORT_VECTOR_DATA_TYPE)}, "
f"but field[{name}]'s data_type is {data_type}"
)
if index_param is None:
index_param = FlatIndexParam()
self._cpp_obj = _FieldSchema(
name=name,
data_type=data_type,
dimension=dimension,
nullable=False,
index_param=index_param,
)
@classmethod
def _from_core(cls, core_field_schema: _FieldSchema):
inst = cls.__new__(cls)
inst._cpp_obj = core_field_schema
return inst
def _get_object(self) -> _FieldSchema:
return self._cpp_obj
@property
def name(self) -> str:
"""str: The name of the vector field."""
return self._cpp_obj.name
@property
def data_type(self) -> DataType:
"""DataType: The vector data type (e.g., VECTOR_FP32)."""
return self._cpp_obj.data_type
@property
def dimension(self) -> int:
"""int: The dimensionality of the vector."""
return self._cpp_obj.dimension
@property
def index_param(self) -> Union[HnswIndexParam, IVFIndexParam, FlatIndexParam]:
"""Union[HnswIndexParam, IVFIndexParam, FlatIndexParam]: Index configuration for the vector."""
return self._cpp_obj.index_param
def __dict__(self) -> dict[str, Any]:
return {
"name": self.name,
"data_type": (
self.data_type.name
if hasattr(self.data_type, "name")
else str(self.data_type)
),
"dimension": self.dimension,
"index_param": (
self.index_param.to_dict() if self.index_param is not None else None
),
}
def __repr__(self) -> str:
try:
schema = self.__dict__()
return json.dumps(schema, indent=2, ensure_ascii=False)
except Exception as e:
return f"<FieldSchema error during repr: {e}>"
def __str__(self) -> str:
return self.__repr__()
def __eq__(self, other: object) -> bool:
if not isinstance(other, VectorSchema):
return False
return self._cpp_obj == other._cpp_obj
def __hash__(self) -> int:
return hash((self.name, self.data_type, self.dimension))
View File
+18
View File
@@ -0,0 +1,18 @@
# Copyright 2025-present the zvec project
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
from __future__ import annotations
from .util import require_module
__all__ = ["require_module"]
+63
View File
@@ -0,0 +1,63 @@
# Copyright 2025-present the zvec project
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
from __future__ import annotations
import importlib
from typing import Any, Optional
def require_module(module: str, mitigation: Optional[str] = None) -> Any:
"""Import a Python module and raise a user-friendly error if it is not available.
This utility helps provide actionable error messages when optional dependencies
are missing. It attempts to import the given module and, on failure, suggests
a `pip install` command based on either the module name or an optional
mitigation package name.
Args:
module (str): The full module name to import (e.g., ``"numpy"``, ``"pandas.io.parquet"``).
mitigation (Optional[str], optional): The package name to suggest for installation
if the import fails. If not provided, the top-level package of `module`
will be used (e.g., ``"pandas"`` for ``"pandas.io.parquet"``).
Returns:
Any: The imported module object.
Raises:
ImportError: If the module cannot be imported, with a clear installation hint.
Examples:
>>> import zvec
>>> np = zvec.require_module("numpy")
>>> pq = zvec.require_module("pyarrow.parquet", mitigation="pyarrow")
Note:
This function is intended for lazy-loading optional dependencies
with helpful error messages, not for core dependencies.
"""
try:
return importlib.import_module(module)
except ImportError as e:
package = mitigation or module
msg = f"Required package '{package}' is not installed. "
if "." in module:
top_level = module.split(".")[0]
msg += f"Module '{module}' is part of '{top_level}', "
if mitigation:
msg += f"please pip install '{mitigation}'."
else:
msg += f"please pip install '{top_level}'."
else:
msg += f"Please pip install '{package}."
raise ImportError(msg) from e
+32
View File
@@ -0,0 +1,32 @@
# Copyright 2025-present the zvec project
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
from __future__ import annotations
from _zvec.typing import (
DataType,
IndexType,
MetricType,
QuantizeType,
Status,
StatusCode,
)
__all__ = [
"DataType",
"IndexType",
"MetricType",
"QuantizeType",
"Status",
"StatusCode",
]
+404
View File
@@ -0,0 +1,404 @@
"""
This module contains the basic data types of Zvec
"""
from __future__ import annotations
import typing
__all__: list[str] = [
"DataType",
"IndexType",
"MetricType",
"QuantizeType",
"Status",
"StatusCode",
]
class DataType:
"""
Enumeration of supported data types in Zvec.
Includes scalar types, dense/sparse vector types, and array types.
Examples:
>>> import zvec
>>> print(zvec.DataType.FLOAT)
DataType.FLOAT
>>> print(zvec.DataType.VECTOR_FP32)
DataType.VECTOR_FP32
Members:
STRING
BOOL
INT32
INT64
FLOAT
DOUBLE
UINT32
UINT64
VECTOR_FP16
VECTOR_FP32
VECTOR_FP64
VECTOR_INT8
SPARSE_VECTOR_FP32
SPARSE_VECTOR_FP16
ARRAY_STRING
ARRAY_INT32
ARRAY_INT64
ARRAY_FLOAT
ARRAY_DOUBLE
ARRAY_BOOL
ARRAY_UINT32
ARRAY_UINT64
"""
ARRAY_BOOL: typing.ClassVar[DataType] # value = <DataType.ARRAY_BOOL: 42>
ARRAY_DOUBLE: typing.ClassVar[DataType] # value = <DataType.ARRAY_DOUBLE: 48>
ARRAY_FLOAT: typing.ClassVar[DataType] # value = <DataType.ARRAY_FLOAT: 47>
ARRAY_INT32: typing.ClassVar[DataType] # value = <DataType.ARRAY_INT32: 43>
ARRAY_INT64: typing.ClassVar[DataType] # value = <DataType.ARRAY_INT64: 44>
ARRAY_STRING: typing.ClassVar[DataType] # value = <DataType.ARRAY_STRING: 41>
ARRAY_UINT32: typing.ClassVar[DataType] # value = <DataType.ARRAY_UINT32: 45>
ARRAY_UINT64: typing.ClassVar[DataType] # value = <DataType.ARRAY_UINT64: 46>
BOOL: typing.ClassVar[DataType] # value = <DataType.BOOL: 3>
DOUBLE: typing.ClassVar[DataType] # value = <DataType.DOUBLE: 9>
FLOAT: typing.ClassVar[DataType] # value = <DataType.FLOAT: 8>
INT32: typing.ClassVar[DataType] # value = <DataType.INT32: 4>
INT64: typing.ClassVar[DataType] # value = <DataType.INT64: 5>
SPARSE_VECTOR_FP16: typing.ClassVar[
DataType
] # value = <DataType.SPARSE_VECTOR_FP16: 30>
SPARSE_VECTOR_FP32: typing.ClassVar[
DataType
] # value = <DataType.SPARSE_VECTOR_FP32: 31>
STRING: typing.ClassVar[DataType] # value = <DataType.STRING: 2>
UINT32: typing.ClassVar[DataType] # value = <DataType.UINT32: 6>
UINT64: typing.ClassVar[DataType] # value = <DataType.UINT64: 7>
VECTOR_FP16: typing.ClassVar[DataType] # value = <DataType.VECTOR_FP16: 22>
VECTOR_FP32: typing.ClassVar[DataType] # value = <DataType.VECTOR_FP32: 23>
VECTOR_FP64: typing.ClassVar[DataType] # value = <DataType.VECTOR_FP64: 24>
VECTOR_INT8: typing.ClassVar[DataType] # value = <DataType.VECTOR_INT8: 26>
__members__: typing.ClassVar[
dict[str, DataType]
] # value = {'STRING': <DataType.STRING: 2>, 'BOOL': <DataType.BOOL: 3>, 'INT32': <DataType.INT32: 4>, 'INT64': <DataType.INT64: 5>, 'FLOAT': <DataType.FLOAT: 8>, 'DOUBLE': <DataType.DOUBLE: 9>, 'UINT32': <DataType.UINT32: 6>, 'UINT64': <DataType.UINT64: 7>, 'VECTOR_FP16': <DataType.VECTOR_FP16: 22>, 'VECTOR_FP32': <DataType.VECTOR_FP32: 23>, 'VECTOR_FP64': <DataType.VECTOR_FP64: 24>, 'VECTOR_INT8': <DataType.VECTOR_INT8: 26>, 'SPARSE_VECTOR_FP32': <DataType.SPARSE_VECTOR_FP32: 31>, 'SPARSE_VECTOR_FP16': <DataType.SPARSE_VECTOR_FP16: 30>, 'ARRAY_STRING': <DataType.ARRAY_STRING: 41>, 'ARRAY_INT32': <DataType.ARRAY_INT32: 43>, 'ARRAY_INT64': <DataType.ARRAY_INT64: 44>, 'ARRAY_FLOAT': <DataType.ARRAY_FLOAT: 47>, 'ARRAY_DOUBLE': <DataType.ARRAY_DOUBLE: 48>, 'ARRAY_BOOL': <DataType.ARRAY_BOOL: 42>, 'ARRAY_UINT32': <DataType.ARRAY_UINT32: 45>, 'ARRAY_UINT64': <DataType.ARRAY_UINT64: 46>}
def __eq__(self, other: typing.Any) -> bool: ...
def __getstate__(self) -> int: ...
def __hash__(self) -> int: ...
def __index__(self) -> int: ...
def __init__(self, value: typing.SupportsInt) -> None: ...
def __int__(self) -> int: ...
def __ne__(self, other: typing.Any) -> bool: ...
def __repr__(self) -> str: ...
def __setstate__(self, state: typing.SupportsInt) -> None: ...
def __str__(self) -> str: ...
@property
def name(self) -> str: ...
@property
def value(self) -> int: ...
class IndexType:
"""
Enumeration of supported index types in Zvec.
Examples:
>>> import zvec
>>> print(zvec.IndexType.HNSW)
IndexType.HNSW
Members:
UNDEFINED
HNSW
IVF
FLAT
INVERT
"""
FLAT: typing.ClassVar[IndexType] # value = <IndexType.FLAT: 4>
HNSW: typing.ClassVar[IndexType] # value = <IndexType.HNSW: 1>
INVERT: typing.ClassVar[IndexType] # value = <IndexType.INVERT: 10>
IVF: typing.ClassVar[IndexType] # value = <IndexType.IVF: 3>
UNDEFINED: typing.ClassVar[IndexType] # value = <IndexType.UNDEFINED: 0>
__members__: typing.ClassVar[
dict[str, IndexType]
] # value = {'UNDEFINED': <IndexType.UNDEFINED: 0>, 'HNSW': <IndexType.HNSW: 1>, 'IVF': <IndexType.IVF: 3>, 'FLAT': <IndexType.FLAT: 4>, 'INVERT': <IndexType.INVERT: 10>}
def __eq__(self, other: typing.Any) -> bool: ...
def __getstate__(self) -> int: ...
def __hash__(self) -> int: ...
def __index__(self) -> int: ...
def __init__(self, value: typing.SupportsInt) -> None: ...
def __int__(self) -> int: ...
def __ne__(self, other: typing.Any) -> bool: ...
def __repr__(self) -> str: ...
def __setstate__(self, state: typing.SupportsInt) -> None: ...
def __str__(self) -> str: ...
@property
def name(self) -> str: ...
@property
def value(self) -> int: ...
class MetricType:
"""
Enumeration of supported distance/similarity metrics.
- COSINE: Cosine similarity.
- IP: Inner product (dot product).
- L2: Euclidean distance (L2 norm).
Examples:
>>> import zvec
>>> print(zvec.MetricType.COSINE)
MetricType.COSINE
Members:
COSINE
IP
L2
"""
COSINE: typing.ClassVar[MetricType] # value = <MetricType.COSINE: 3>
IP: typing.ClassVar[MetricType] # value = <MetricType.IP: 2>
L2: typing.ClassVar[MetricType] # value = <MetricType.L2: 1>
__members__: typing.ClassVar[
dict[str, MetricType]
] # value = {'COSINE': <MetricType.COSINE: 3>, 'IP': <MetricType.IP: 2>, 'L2': <MetricType.L2: 1>}
def __eq__(self, other: typing.Any) -> bool: ...
def __getstate__(self) -> int: ...
def __hash__(self) -> int: ...
def __index__(self) -> int: ...
def __init__(self, value: typing.SupportsInt) -> None: ...
def __int__(self) -> int: ...
def __ne__(self, other: typing.Any) -> bool: ...
def __repr__(self) -> str: ...
def __setstate__(self, state: typing.SupportsInt) -> None: ...
def __str__(self) -> str: ...
@property
def name(self) -> str: ...
@property
def value(self) -> int: ...
class QuantizeType:
"""
Enumeration of supported quantization types for vector compression.
Examples:
>>> import zvec
>>> print(zvec.QuantizeType.INT8)
QuantizeType.INT8
Members:
UNDEFINED
FP16
INT8
INT4
"""
FP16: typing.ClassVar[QuantizeType] # value = <QuantizeType.FP16: 1>
INT4: typing.ClassVar[QuantizeType] # value = <QuantizeType.INT4: 3>
INT8: typing.ClassVar[QuantizeType] # value = <QuantizeType.INT8: 2>
UNDEFINED: typing.ClassVar[QuantizeType] # value = <QuantizeType.UNDEFINED: 0>
__members__: typing.ClassVar[
dict[str, QuantizeType]
] # value = {'UNDEFINED': <QuantizeType.UNDEFINED: 0>, 'FP16': <QuantizeType.FP16: 1>, 'INT8': <QuantizeType.INT8: 2>, 'INT4': <QuantizeType.INT4: 3>}
def __eq__(self, other: typing.Any) -> bool: ...
def __getstate__(self) -> int: ...
def __hash__(self) -> int: ...
def __index__(self) -> int: ...
def __init__(self, value: typing.SupportsInt) -> None: ...
def __int__(self) -> int: ...
def __ne__(self, other: typing.Any) -> bool: ...
def __repr__(self) -> str: ...
def __setstate__(self, state: typing.SupportsInt) -> None: ...
def __str__(self) -> str: ...
@property
def name(self) -> str: ...
@property
def value(self) -> int: ...
class Status:
"""
Represents the outcome of a Zvec operation.
A `Status` object is either OK (success) or carries an error code and message.
Examples:
>>> from zvec.typing import Status, StatusCode
>>> s = Status()
>>> print(s.ok())
True
>>> s = Status(StatusCode.INVALID_ARGUMENT, "Field not found")
>>> print(s.code() == StatusCode.INVALID_ARGUMENT)
True
>>> print(s.message())
Field not found
"""
__hash__: typing.ClassVar[None] = None
@staticmethod
def AlreadyExists(message: str) -> Status: ...
@staticmethod
def InternalError(message: str) -> Status: ...
@staticmethod
def InvalidArgument(message: str) -> Status: ...
@staticmethod
def NotFound(message: str) -> Status: ...
@staticmethod
def OK() -> Status:
"""
Create an OK status.
"""
@staticmethod
def PermissionDenied(message: str) -> Status: ...
def __eq__(self, arg0: Status) -> bool: ...
@typing.overload
def __init__(self) -> None: ...
@typing.overload
def __init__(self, code: StatusCode, message: str = "") -> None:
"""
Construct a status with the given code and optional message.
Args:
code (StatusCode): The status code.
message (str, optional): Error message. Defaults to empty string.
"""
def __ne__(self, arg0: Status) -> bool: ...
def __repr__(self) -> str: ...
def code(self) -> StatusCode:
"""
StatusCode: Returns the status code.
"""
def message(self) -> str:
"""
str: Returns the error message (may be empty).
"""
def ok(self) -> bool:
"""
bool: Returns True if the status is OK.
"""
class StatusCode:
"""
Enumeration of possible status codes for Zvec operations.
Used by the `Status` class to indicate success or failure reason.
Members:
OK
NOT_FOUND
ALREADY_EXISTS
INVALID_ARGUMENT
PERMISSION_DENIED
FAILED_PRECONDITION
RESOURCE_EXHAUSTED
UNAVAILABLE
INTERNAL_ERROR
NOT_SUPPORTED
UNKNOWN
"""
ALREADY_EXISTS: typing.ClassVar[
StatusCode
] # value = <StatusCode.ALREADY_EXISTS: 2>
FAILED_PRECONDITION: typing.ClassVar[
StatusCode
] # value = <StatusCode.FAILED_PRECONDITION: 5>
INTERNAL_ERROR: typing.ClassVar[
StatusCode
] # value = <StatusCode.INTERNAL_ERROR: 8>
INVALID_ARGUMENT: typing.ClassVar[
StatusCode
] # value = <StatusCode.INVALID_ARGUMENT: 3>
NOT_FOUND: typing.ClassVar[StatusCode] # value = <StatusCode.NOT_FOUND: 1>
NOT_SUPPORTED: typing.ClassVar[StatusCode] # value = <StatusCode.NOT_SUPPORTED: 9>
OK: typing.ClassVar[StatusCode] # value = <StatusCode.OK: 0>
PERMISSION_DENIED: typing.ClassVar[
StatusCode
] # value = <StatusCode.PERMISSION_DENIED: 4>
RESOURCE_EXHAUSTED: typing.ClassVar[
StatusCode
] # value = <StatusCode.RESOURCE_EXHAUSTED: 6>
UNAVAILABLE: typing.ClassVar[StatusCode] # value = <StatusCode.UNAVAILABLE: 7>
UNKNOWN: typing.ClassVar[StatusCode] # value = <StatusCode.UNKNOWN: 10>
__members__: typing.ClassVar[
dict[str, StatusCode]
] # value = {'OK': <StatusCode.OK: 0>, 'NOT_FOUND': <StatusCode.NOT_FOUND: 1>, 'ALREADY_EXISTS': <StatusCode.ALREADY_EXISTS: 2>, 'INVALID_ARGUMENT': <StatusCode.INVALID_ARGUMENT: 3>, 'PERMISSION_DENIED': <StatusCode.PERMISSION_DENIED: 4>, 'FAILED_PRECONDITION': <StatusCode.FAILED_PRECONDITION: 5>, 'RESOURCE_EXHAUSTED': <StatusCode.RESOURCE_EXHAUSTED: 6>, 'UNAVAILABLE': <StatusCode.UNAVAILABLE: 7>, 'INTERNAL_ERROR': <StatusCode.INTERNAL_ERROR: 8>, 'NOT_SUPPORTED': <StatusCode.NOT_SUPPORTED: 9>, 'UNKNOWN': <StatusCode.UNKNOWN: 10>}
def __eq__(self, other: typing.Any) -> bool: ...
def __getstate__(self) -> int: ...
def __hash__(self) -> int: ...
def __index__(self) -> int: ...
def __init__(self, value: typing.SupportsInt) -> None: ...
def __int__(self) -> int: ...
def __ne__(self, other: typing.Any) -> bool: ...
def __repr__(self) -> str: ...
def __setstate__(self, state: typing.SupportsInt) -> None: ...
def __str__(self) -> str: ...
@property
def name(self) -> str: ...
@property
def value(self) -> int: ...
+62
View File
@@ -0,0 +1,62 @@
# Copyright 2025-present the zvec project
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
from __future__ import annotations
from enum import IntEnum
__all__ = ["LogLevel", "LogType"]
class LogLevel(IntEnum):
"""Enumeration of logging severity levels, ordered from lowest to highest priority.
Used to control verbosity and filtering of log messages. Higher numeric values
indicate more severe conditions.
Note:
``WARNING`` is an alias for ``WARN`` to match Python's built-in :mod:`logging`
module convention.
Attributes:
DEBUG (int): Detailed information, typically of interest only when diagnosing problems.
INFO (int): Confirmation that things are working as expected.
WARN (int): An indication that something unexpected happened, or indicative of
potential future problems. (Alias: ``WARNING``)
WARNING (int): Same as ``WARN``.
ERROR (int): Due to a more serious problem, the software has not been able
to perform some function.
FATAL (int): A serious error, indicating that the program itself may be unable
to continue running.
"""
DEBUG = 0
INFO = 1
WARN = 2
WARNING = 2
ERROR = 3
FATAL = 4
class LogType(IntEnum):
"""Enumeration of supported log output destinations.
Specifies where log messages should be written.
Attributes:
CONSOLE (int): Output logs to standard output/error (e.g., terminal or IDE console).
FILE (int): Write logs to a persistent file on disk.
"""
CONSOLE = 0
FILE = 1
+226
View File
@@ -0,0 +1,226 @@
# Copyright 2025-present the zvec project
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
from __future__ import annotations
from typing import Optional
from _zvec import Initialize, _Collection
from .model import Collection
from .model.param import CollectionOption
from .model.schema import CollectionSchema
__all__ = ["create_and_open", "init", "open"]
from .typing.enum import LogLevel, LogType
def init(
*,
log_type: Optional[LogType] = LogType.CONSOLE,
log_level: Optional[LogLevel] = LogLevel.WARN,
log_dir: Optional[str] = "./logs",
log_basename: Optional[str] = "zvec.log",
log_file_size: Optional[int] = 2048,
log_overdue_days: Optional[int] = 7,
query_threads: Optional[int] = None,
optimize_threads: Optional[int] = None,
invert_to_forward_scan_ratio: Optional[float] = None,
brute_force_by_keys_ratio: Optional[float] = None,
memory_limit_mb: Optional[int] = None,
) -> None:
"""Initialize Zvec with configuration options.
This function must be called before any other operation.
It can only be called once — subsequent calls raise a ``RuntimeError``.
Parameters set to ``None`` are **omitted** from the configuration and
fall back to Zvec's internal defaults, which may be derived from the runtime
environment (e.g., cgroup CPU/memory limits). Explicitly provided values
always override defaults.
Args:
log_type (Optional[LogType], optional): Logger destination.
- ``LogType.CONSOLE`` (default if omitted or set to this)
- ``LogType.FILE``
- If ``None``, uses internal default (currently ``CONSOLE``).
log_level (Optional[LogLevel], optional): Minimum log severity.
Default: ``LogLevel.WARN``.
Accepted values: ``DEBUG``, ``INFO``, ``WARN``, ``ERROR``, ``FATAL``.
If ``None``, uses internal default (``WARN``).
log_dir (Optional[str], optional):
Directory for log files (only used when ``log_type=FILE``).
Parent directories are **not** created automatically.
Default: ``"./logs"``.
If ``None``, internal default is used.
log_basename (Optional[str], optional):
Base name for rotated log files (e.g., ``zvec.log.1``, ``zvec.log.2``).
Default: ``"zvec.log"``.
log_file_size (Optional[int], optional):
Max size per log file in **MB** before rotation.
Default: ``2048`` MB (2 GB).
log_overdue_days (Optional[int], optional):
Days to retain rotated log files before deletion.
Default: ``7`` days.
query_threads (Optional[int], optional):
Number of threads for query execution.
If ``None`` (default), inferred from available CPU cores (via cgroup).
Must be ≥ 1 if provided.
optimize_threads (Optional[int], optional):
Threads for background tasks (e.g., compaction, indexing).
If ``None``, defaults to same as ``query_threads`` or CPU count.
invert_to_forward_scan_ratio (Optional[float], optional):
Threshold to switch from inverted index to full forward scan.
Range: [0.0, 1.0]. Higher → more aggressive index skipping.
Default: ``0.9`` (if omitted).
brute_force_by_keys_ratio (Optional[float], optional):
Threshold to use brute-force key lookup over index.
Lower → prefer index; higher → prefer brute-force.
Range: [0.0, 1.0]. Default: ``0.1``.
memory_limit_mb (Optional[int], optional):
Soft memory cap in MB. Zvec may throttle or fail operations
approaching this limit.
If ``None``, inferred from cgroup memory limit * 0.8 (e.g., in Docker).
Must be > 0 if provided.
Raises:
RuntimeError: If Zvec is already initialized.
ValueError: On invalid values (e.g., negative thread count, log level out of range).
TypeError: If a value has incorrect type (e.g., string for ``query_threads``).
Note:
- All ``None`` arguments are **excluded** from the configuration payload,
allowing the core library to apply environment-aware defaults.
- This design ensures container-friendliness: in Kubernetes/Docker,
omitting ``memory_limit_mb`` and thread counts lets Zvec auto-adapt.
Examples:
Initialize with defaults (log to console, auto-detect resources):
>>> import zvec
>>> zvec.init()
Customize logging to file with rotation:
>>> zvec.init(
... log_type=LogType.FILE,
... log_dir="/var/log/zvec",
... log_file_size=1024,
... log_overdue_days=30
... )
Limit resources explicitly:
>>> zvec.init(
... memory_limit_mb=2048,
... query_threads=4,
... optimize_threads=2
... )
Fine-tune query heuristics:
>>> zvec.init(
... invert_to_forward_scan_ratio=0.95,
... brute_force_by_keys_ratio=0.05
... )
"""
# Build config dict, skipping None values
config_dict = {}
if log_type is not None:
if not isinstance(log_type, LogType):
raise TypeError("log_type must be LogType")
config_dict["log_type"] = log_type.name
if log_level is not None:
if not isinstance(log_level, LogLevel):
raise TypeError("log_level must be LogLevel")
config_dict["log_level"] = log_level.name
if log_dir is not None:
config_dict["log_dir"] = log_dir
if log_basename is not None:
config_dict["log_basename"] = log_basename
if log_file_size is not None:
config_dict["log_file_size"] = log_file_size
if log_overdue_days is not None:
config_dict["log_overdue_days"] = log_overdue_days
if query_threads is not None:
config_dict["query_threads"] = query_threads
if optimize_threads is not None:
config_dict["optimize_threads"] = optimize_threads
if invert_to_forward_scan_ratio is not None:
config_dict["invert_to_forward_scan_ratio"] = invert_to_forward_scan_ratio
if brute_force_by_keys_ratio is not None:
config_dict["brute_force_by_keys_ratio"] = brute_force_by_keys_ratio
if memory_limit_mb is not None:
config_dict["memory_limit_mb"] = memory_limit_mb
Initialize(config_dict)
def create_and_open(
path: str,
schema: CollectionSchema,
option: Optional[CollectionOption] = None,
) -> Collection:
"""Create a new collection and open it for use.
If a collection already exists at the given path, it may raise an error
depending on the underlying implementation.
Args:
path (str): Path or name of the collection to create.
schema (CollectionSchema): Schema defining the structure of the collection.
option (CollectionOption): Configuration options
for opening the collection. Defaults to a default-constructed
``CollectionOption()`` if not provided.
Returns:
Collection: An opened collection instance ready for operations.
Examples:
>>> import zvec
>>> schema = zvec.CollectionSchema(
... name="my_collection",
... fields=[zvec.FieldSchema("id", zvec.DataType.INT64, nullable=True)]
... )
>>> coll = create_and_open("./my_collection", schema)
"""
if not isinstance(path, str):
raise TypeError("path must be a string")
if not isinstance(schema, CollectionSchema):
raise TypeError("schema must be a CollectionSchema")
option = option or CollectionOption()
if not isinstance(option, CollectionOption):
raise TypeError("option must be a CollectionOption")
_collection = _Collection.CreateAndOpen(path, schema._get_object(), option)
return Collection._from_core(_collection)
def open(path: str, option: CollectionOption = CollectionOption()) -> Collection:
"""Open an existing collection from disk.
The collection must have been previously created with ``create_and_open``.
Args:
path (str): Path or name of the existing collection.
option (CollectionOption): Configuration options
for opening the collection. Defaults to a default-constructed
``CollectionOption()`` if not provided.
Returns:
Collection: An opened collection instance.
Examples:
>>> import zvec
>>> coll = zvec.open("./my_collection")
"""
_collection = _Collection.Open(path, option)
return Collection._from_core(_collection)