from __future__ import annotations
from collections.abc import Mapping
from typing import Any, cast
import numpy as np
from numpy.typing import NDArray
from pyvicinity._core import DistanceMetric, HNSWIndex, IVFPQIndex
Float32Array = NDArray[np.float32]
Int64Array = NDArray[np.int64]
class VicinityHNSW:
def __init__(self, metric: str, method_param: Mapping[str, Any]) -> None:
self._metric_name = metric
self._metric = _parse_metric(metric)
self._m = int(cast(int, method_param.get("M", 16)))
self._ef_construction = int(cast(int, method_param.get("efConstruction", 200)))
self._ef_search = 50
self._index: HNSWIndex | None = None
self._batch_results: Int64Array | None = None
def fit(self, X: Float32Array) -> None:
if X.ndim != 2:
msg = f"fit() expects a 2-D array, got shape {X.shape}"
raise ValueError(msg)
_n, dim = X.shape
X = np.ascontiguousarray(X, dtype=np.float32)
auto_norm = self._metric_name.lower() in ("angular", "cosine")
self._index = HNSWIndex(
dim=dim,
m=self._m,
ef_construction=self._ef_construction,
ef_search=self._ef_search,
metric=self._metric,
auto_normalize=auto_norm,
)
self._index.add_items(X)
self._index.build()
def set_query_arguments(self, ef_search: int) -> None:
self._ef_search = int(ef_search)
if self._index is not None:
self._index.set_ef_search(self._ef_search)
def query(self, q: Float32Array, n: int) -> Int64Array:
index = self._require_index()
q = np.ascontiguousarray(q, dtype=np.float32)
ids, _dists = index.search(q, k=n, ef=self._ef_search)
return ids
def batch_query(self, X: Float32Array, n: int) -> None:
index = self._require_index()
X = np.ascontiguousarray(X, dtype=np.float32)
ids, _dists = index.batch_search(X, k=n, ef=self._ef_search)
self._batch_results = ids
def get_batch_results(self) -> Int64Array:
if self._batch_results is None:
msg = "no batch results: call batch_query(...) first"
raise RuntimeError(msg)
return self._batch_results
def get_additional(self) -> dict[str, Any]:
return {}
def get_memory_usage(self) -> float | None:
try:
import resource
except ImportError:
return None
return resource.getrusage(resource.RUSAGE_SELF).ru_maxrss / 1024
def done(self) -> None:
self._index = None
self._batch_results = None
def _require_index(self) -> HNSWIndex:
if self._index is None:
msg = "index not built: call fit(X) first"
raise RuntimeError(msg)
return self._index
def __str__(self) -> str:
return f"vicinity-hnsw(M={self._m},ef={self._ef_search})"
class VicinityIVFPQ:
def __init__(self, metric: str, method_param: Mapping[str, Any]) -> None:
_require_cosine_metric(metric)
self._num_clusters = int(cast(int, method_param.get("num_clusters", 256)))
self._num_codebooks = _optional_int(method_param.get("num_codebooks"))
self._codebook_size = int(cast(int, method_param.get("codebook_size", 256)))
self._training_sample_size = _optional_int(
method_param.get("training_sample_size")
)
self._kmeans_max_iter = int(cast(int, method_param.get("kmeans_max_iter", 100)))
self._use_opq = bool(method_param.get("use_opq", False))
self._seed = int(cast(int, method_param.get("seed", 42)))
self._nprobe = int(cast(int, method_param.get("nprobe", 1)))
self._rerank_pool = _normalize_rerank_pool(method_param.get("rerank_pool"))
self._index: IVFPQIndex | None = None
self._batch_results: Int64Array | None = None
def fit(self, X: Float32Array) -> None:
if X.ndim != 2:
msg = f"fit() expects a 2-D array, got shape {X.shape}"
raise ValueError(msg)
_n, dim = X.shape
X = np.ascontiguousarray(X, dtype=np.float32)
self._index = IVFPQIndex(
dim=dim,
num_clusters=self._num_clusters,
num_codebooks=self._num_codebooks,
codebook_size=self._codebook_size,
nprobe=self._nprobe,
use_opq=self._use_opq,
seed=self._seed,
)
self._index.add_items(X)
self._index.build(
training_sample_size=self._training_sample_size,
kmeans_max_iter=self._kmeans_max_iter,
)
def set_query_arguments(
self,
nprobe: int,
rerank_pool: int | None = None,
) -> None:
self._nprobe = int(nprobe)
if rerank_pool is not None:
self._rerank_pool = _normalize_rerank_pool(rerank_pool)
if self._index is not None:
self._index.set_nprobe(self._nprobe)
def query(self, q: Float32Array, n: int) -> Int64Array:
index = self._require_index()
q = np.ascontiguousarray(q, dtype=np.float32)
ids, _dists = index.search(
q,
k=n,
nprobe=self._nprobe,
rerank_pool=self._rerank_pool,
)
return ids
def batch_query(self, X: Float32Array, n: int) -> None:
index = self._require_index()
X = np.ascontiguousarray(X, dtype=np.float32)
ids, _dists = index.batch_search(
X,
k=n,
nprobe=self._nprobe,
rerank_pool=self._rerank_pool,
)
self._batch_results = ids
def get_batch_results(self) -> Int64Array:
if self._batch_results is None:
msg = "no batch results: call batch_query(...) first"
raise RuntimeError(msg)
return self._batch_results
def get_additional(self) -> dict[str, Any]:
return {}
def get_memory_usage(self) -> float | None:
try:
import resource
except ImportError:
return None
return resource.getrusage(resource.RUSAGE_SELF).ru_maxrss / 1024
def done(self) -> None:
self._index = None
self._batch_results = None
def _require_index(self) -> IVFPQIndex:
if self._index is None:
msg = "index not built: call fit(X) first"
raise RuntimeError(msg)
return self._index
def __str__(self) -> str:
return (
"vicinity-ivfpq("
f"clusters={self._num_clusters},"
f"codebooks={self._num_codebooks},"
f"codebook_size={self._codebook_size},"
f"nprobe={self._nprobe},"
f"rerank_pool={self._rerank_pool})"
)
def _parse_metric(metric: str) -> DistanceMetric:
m = metric.lower()
if m in ("angular", "cosine"):
return DistanceMetric.Cosine
if m in ("euclidean", "l2"):
return DistanceMetric.L2
if m in ("ip", "inner", "inner_product", "dot"):
return DistanceMetric.InnerProduct
msg = f"unknown metric: {metric!r}"
raise ValueError(msg)
def _require_cosine_metric(metric: str) -> None:
if metric.lower() not in ("angular", "cosine"):
msg = "IVF-PQ wrapper only supports angular/cosine metrics"
raise ValueError(msg)
def _optional_int(value: object) -> int | None:
if value is None:
return None
return int(cast(int, value))
def _normalize_rerank_pool(value: object) -> int | None:
pool = _optional_int(value)
if pool is None or pool <= 0:
return None
return pool