Initial commit
This commit is contained in:
@@ -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"
|
||||
@@ -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: ...
|
||||
@@ -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"]
|
||||
@@ -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]]
|
||||
@@ -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",
|
||||
]
|
||||
@@ -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)
|
||||
@@ -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",
|
||||
]
|
||||
@@ -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"))
|
||||
@@ -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
|
||||
@@ -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"]
|
||||
@@ -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)
|
||||
@@ -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)
|
||||
@@ -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
|
||||
@@ -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",
|
||||
]
|
||||
@@ -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: ...
|
||||
@@ -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")
|
||||
@@ -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"]
|
||||
@@ -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__()
|
||||
@@ -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))
|
||||
@@ -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"]
|
||||
@@ -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
|
||||
@@ -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",
|
||||
]
|
||||
@@ -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: ...
|
||||
@@ -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
|
||||
@@ -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)
|
||||
Reference in New Issue
Block a user