ann_router.backends package

Submodules

Module contents

Backend adapters, one module per ANN engine.

Each module here wraps a single vector-search engine behind the shared ann_router.base.ANNIndex contract. Only ann_router.backends.exact has no optional dependency; every other module defers its heavy import so that import ann_router (and importing this package) never fails because an engine is not installed. Use ann_router.registry to look adapters up by name.

Author: Warith Harchaoui <warith.harchaoui@deraison.ai>

class ann_router.backends.AnnoyIndex(dim, metric='cosine', **kwargs)[source]

Bases: ANNIndex

Annoy random-projection-forest index: build once, then read-only.

Because Annoy has no external-id concept, this adapter keeps its own position -> id table so search returns the caller’s ids. External ids must therefore be supplied at build time and are fixed thereafter.

Parameters:
  • dim (int) – Embedding dimensionality.

  • metric ({"cosine", "l2", "ip"}, optional) – Distance metric. Defaults to "cosine" (Annoy “angular”).

  • n_trees (int, optional) – Number of projection trees (more == better recall, larger index). Defaults to 50.

  • search_k (int, optional) – Nodes inspected at query time (-1 == Annoy’s n_trees * k default).

  • kwargs (object)

Examples

>>> AnnoyIndex.capabilities().supports_remove
False
add(vectors)[source]

Not supported — Annoy is frozen after build().

Parameters:

vectors (numpy.ndarray) – Shape (m, dim). Unused — always raises.

Raises:

NotSupported – Always; the forest is frozen after build().

Return type:

None

add_with_ids(vectors, ids)[source]

Not supported — Annoy is frozen after build().

Parameters:
  • vectors (numpy.ndarray) – Shape (m, dim). Unused — always raises.

  • ids (numpy.ndarray) – Shape (m,). Unused — always raises.

Raises:

NotSupported – Always; the forest is frozen after build().

Return type:

None

build(vectors, ids=None)[source]

Build and freeze the projection forest.

Parameters:
  • vectors (numpy.ndarray) – Shape (n, dim).

  • ids (numpy.ndarray, optional) – Shape (n,); defaults to range(n).

Returns:

self.

Return type:

AnnoyIndex

classmethod capabilities()[source]

Return the Annoy capability descriptor (frozen: no add/remove).

Return type:

Capabilities

classmethod is_available()[source]

Return True if annoy is importable.

Examples

>>> isinstance(AnnoyIndex.is_available(), bool)
True
Return type:

bool

load(path)[source]

Memory-map a forest written by save().

Parameters:

path (str) – Source path produced by save().

Returns:

self, populated from disk.

Return type:

AnnoyIndex

remove(ids)[source]

Not supported — Annoy cannot delete; rebuild without the ids.

Parameters:

ids (numpy.ndarray) – Shape (m,). Unused — always raises.

Raises:

NotSupported – Always; Annoy has no delete operation.

Return type:

None

save(path)[source]

Persist the forest and the id table.

The Annoy file itself has no room for external ids, so the id table is written alongside as <path>.ids.npy.

Parameters:

path (str) – Destination file path.

Return type:

None

search(queries, k)[source]

Return approximate top-k neighbours per query.

Parameters:
  • queries (numpy.ndarray) – Shape (q, dim).

  • k (int) – Neighbours per query.

Returns:

  • ids (numpy.ndarray) – Shape (q, k) neighbour ids.

  • distances (numpy.ndarray) – Shape (q, k) distances under the index metric.

Return type:

tuple[ndarray, ndarray]

class ann_router.backends.ExactIndex(dim, metric='cosine', **kwargs)[source]

Bases: ANNIndex

Exact k-NN by full matrix multiplication over the stored corpus.

The corpus is kept as a contiguous float32 matrix; for cosine/inner-product metrics search is a single queries @ corpus.T followed by a top-k partition, and for L2 it is the standard ||q||^2 - 2 q·x + ||x||^2 expansion. All operations are exact, so this class defines “truth” for the recall benchmarks.

Parameters:
  • dim (int) – Embedding dimensionality.

  • metric ({"cosine", "l2", "ip"}, optional) – Distance metric. Defaults to "cosine".

  • kwargs (object)

Examples

>>> rng = np.random.default_rng(0)
>>> vecs = rng.standard_normal((100, 16)).astype(np.float32)
>>> idx = ExactIndex(dim=16).build(vecs)
>>> ids, dists = idx.search(vecs[:1], k=1)
>>> int(ids[0, 0])              # nearest neighbour of a point is itself
0
add(vectors)[source]

Append vectors with the next contiguous ids.

Parameters:

vectors (numpy.ndarray) – Shape (m, dim).

Return type:

None

Examples

>>> idx = ExactIndex(dim=4).build(np.ones((2, 4), dtype=np.float32))
>>> idx.add(np.ones((3, 4), dtype=np.float32))
>>> idx.size
5
add_with_ids(vectors, ids)[source]

Append vectors with explicit ids.

Parameters:
  • vectors (numpy.ndarray) – Shape (m, dim).

  • ids (numpy.ndarray) – Shape (m,).

Return type:

None

Examples

>>> idx = ExactIndex(dim=4).build(np.ones((2, 4), dtype=np.float32))
>>> idx.add_with_ids(np.zeros((1, 4), dtype=np.float32), np.array([99]))
>>> idx.size
3
build(vectors, ids=None)[source]

Store the initial corpus.

Parameters:
  • vectors (numpy.ndarray) – Shape (n, dim).

  • ids (numpy.ndarray, optional) – Shape (n,); defaults to range(n).

Returns:

self.

Return type:

ExactIndex

Examples

>>> idx = ExactIndex(dim=4).build(np.ones((5, 4), dtype=np.float32))
>>> idx.search(np.ones((1, 4), dtype=np.float32), k=3)[0].shape
(1, 3)
classmethod capabilities()[source]

Return the exact backend’s capability descriptor.

Returns:

Exact, mutable, in-memory, no GPU, no metadata filter.

Return type:

Capabilities

Examples

>>> ExactIndex.capabilities().approximate
False
classmethod is_available()[source]

Return True — numpy is a core dependency, so exact always works.

Returns:

Always True.

Return type:

bool

Examples

>>> ExactIndex.is_available()
True
load(path)[source]

Load a corpus previously written by save().

Parameters:

path (str) – Source .npz path.

Returns:

self, populated from disk.

Return type:

ExactIndex

remove(ids)[source]

Delete vectors by id (true deletion — the rows are dropped).

Parameters:

ids (numpy.ndarray) – Ids to remove.

Return type:

None

Examples

>>> idx = ExactIndex(dim=4).build(np.ones((3, 4), dtype=np.float32))
>>> idx.remove(np.array([1]))
>>> idx.size
2
save(path)[source]

Persist the corpus and ids to a .npz file.

Parameters:

path (str) – Destination path (.npz appended by numpy if absent).

Return type:

None

Examples

>>> import tempfile, os
>>> idx = ExactIndex(dim=4).build(np.ones((3, 4), dtype=np.float32))
>>> p = os.path.join(tempfile.mkdtemp(), "exact.npz")
>>> idx.save(p); os.path.exists(p) or os.path.exists(p + ".npz")
True
search(queries, k)[source]

Return exact top-k neighbours per query.

Parameters:
  • queries (numpy.ndarray) – Shape (q, dim).

  • k (int) – Neighbours per query.

Returns:

  • ids (numpy.ndarray) – Shape (q, k) neighbour ids (-1 where fewer than k exist).

  • distances (numpy.ndarray) – Shape (q, k) distances (similarity for cosine/ip, euclidean^2 for l2).

Return type:

tuple[ndarray, ndarray]

Examples

>>> rng = np.random.default_rng(1)
>>> vecs = rng.standard_normal((50, 8)).astype(np.float32)
>>> ids, _ = ExactIndex(dim=8).build(vecs).search(vecs[:3], k=5)
>>> ids.shape
(3, 5)
property size: int

Number of vectors currently stored.

Examples

>>> ExactIndex(dim=4).build(np.ones((7, 4), dtype=np.float32)).size
7
class ann_router.backends.FaissIndex(dim, metric='cosine', **kwargs)[source]

Bases: ANNIndex

FAISS IVF(-PQ) index with id mapping and auto-sized coarse quantiser.

Parameters:
  • dim (int) – Embedding dimensionality.

  • metric ({"cosine", "l2", "ip"}, optional) – Distance metric. Defaults to "cosine".

  • nlist (int, optional) – Number of IVF cells. Defaults to auto (~sqrt(n)*4, clamped).

  • nprobe (int, optional) – Cells probed at query time (recall/latency trade). Defaults to 16.

  • use_pq (bool or "auto", optional) – Enable Product Quantisation. "auto" (default) turns it on above pq_threshold vectors.

  • m (int, optional) – PQ sub-quantiser count (must divide dim). Defaults to a divisor near dim/2.

  • kwargs (object)

Examples

>>> FaissIndex.capabilities().name
'faiss'
add(vectors)[source]

Append vectors with the next contiguous ids.

Parameters:

vectors (numpy.ndarray) – Shape (m, dim).

Return type:

None

add_with_ids(vectors, ids)[source]

Append vectors with explicit ids (index must already be trained).

Parameters:
  • vectors (numpy.ndarray) – Shape (m, dim).

  • ids (numpy.ndarray) – Shape (m,) integer ids.

Raises:

NotSupported – If called before build() (IVF needs training first).

Return type:

None

build(vectors, ids=None)[source]

Train and populate the IVF(-PQ) index.

Parameters:
  • vectors (numpy.ndarray) – Shape (n, dim).

  • ids (numpy.ndarray, optional) – Shape (n,); defaults to range(n).

Returns:

self.

Return type:

FaissIndex

classmethod capabilities()[source]

Return the FAISS capability descriptor (GPU-capable, add/remove ok).

Return type:

Capabilities

classmethod is_available()[source]

Return True if faiss is importable.

Examples

>>> isinstance(FaissIndex.is_available(), bool)
True
Return type:

bool

load(path)[source]

Load an index written by save().

Parameters:

path (str) – Source path produced by save().

Returns:

self, populated from disk.

Return type:

FaissIndex

remove(ids)[source]

Remove vectors by id via the id map.

Parameters:

ids (numpy.ndarray) – Shape (m,) integer ids to drop.

Return type:

None

save(path)[source]

Persist via faiss.write_index.

Parameters:

path (str) – Destination file path.

Return type:

None

search(queries, k)[source]

Return approximate top-k neighbours per query.

Parameters:
  • queries (numpy.ndarray) – Shape (q, dim).

  • k (int) – Neighbours per query.

Returns:

  • ids (numpy.ndarray) – Shape (q, k) neighbour ids.

  • distances (numpy.ndarray) – Shape (q, k) distances under the index metric.

Return type:

tuple[ndarray, ndarray]

class ann_router.backends.HNSWIndex(dim, metric='cosine', **kwargs)[source]

Bases: ANNIndex

hnswlib-backed graph index tuned for high recall on a fixed corpus.

Build knobs (M, ef_construction) and the query knob (ef) are passed through and default to values that hit ~0.95+ recall on typical 768-d embeddings. The index is grown to max_elements lazily and doubled on overflow so streaming add still works within the “stable corpus” caveat.

Parameters:
  • dim (int) – Embedding dimensionality.

  • metric ({"cosine", "l2", "ip"}, optional) – Distance metric. Defaults to "cosine".

  • M (int, optional) – Graph out-degree. Defaults to 16.

  • ef_construction (int, optional) – Build-time search width. Defaults to 200.

  • ef (int, optional) – Query-time search width (recall/latency trade). Defaults to 64.

  • kwargs (object)

Examples

>>> HNSWIndex.capabilities().name
'hnsw'
add(vectors)[source]

Append vectors with the next contiguous ids.

Parameters:

vectors (numpy.ndarray) – Shape (m, dim).

Return type:

None

add_with_ids(vectors, ids)[source]

Append vectors with explicit ids, growing capacity if needed.

Parameters:
  • vectors (numpy.ndarray) – Shape (m, dim).

  • ids (numpy.ndarray) – Shape (m,) integer ids.

Return type:

None

build(vectors, ids=None)[source]

Build the graph from an initial corpus.

Parameters:
  • vectors (numpy.ndarray) – Shape (n, dim).

  • ids (numpy.ndarray, optional) – Shape (n,); defaults to range(n).

Returns:

self.

Return type:

HNSWIndex

classmethod capabilities()[source]

Return the HNSW capability descriptor (remove is tombstone-only).

Return type:

Capabilities

classmethod is_available()[source]

Return True if hnswlib is importable.

Examples

>>> isinstance(HNSWIndex.is_available(), bool)
True
Return type:

bool

load(path)[source]

Load a graph written by save().

Parameters:

path (str) – Source path produced by save().

Returns:

self, populated from disk.

Return type:

HNSWIndex

remove(ids)[source]

Tombstone the given ids (graph is not reclaimed — rebuild for that).

Parameters:

ids (numpy.ndarray) – Shape (m,) integer ids to tombstone.

Return type:

None

save(path)[source]

Persist the graph via hnswlib’s native serialiser.

Parameters:

path (str) – Destination file path.

Return type:

None

search(queries, k)[source]

Return approximate top-k neighbours per query.

Parameters:
  • queries (numpy.ndarray) – Shape (q, dim).

  • k (int) – Neighbours per query.

Returns:

  • ids (numpy.ndarray) – Shape (q, k) neighbour ids.

  • distances (numpy.ndarray) – Shape (q, k) distances under the index metric.

Return type:

tuple[ndarray, ndarray]

class ann_router.backends.PgVectorIndex(dim, metric='cosine', **kwargs)[source]

Bases: ANNIndex

A single vectors table in PostgreSQL, indexed with pgvector HNSW.

Requires a DSN (dsn= kwarg or the ANN_ROUTER_PG_DSN env var). Points live in a (id bigint, embedding vector(dim), payload jsonb) table so the SQL WHERE path can filter on payload.

Parameters:
  • dim (int) – Embedding dimensionality.

  • metric ({"cosine", "l2", "ip"}, optional) – Distance metric. Defaults to "cosine".

  • dsn (str, optional) – PostgreSQL connection string. Falls back to ANN_ROUTER_PG_DSN.

  • table (str, optional) – Table name. Defaults to "ann_router".

  • kwargs (object)

Examples

>>> PgVectorIndex.capabilities().persistent
True
add(vectors)[source]

Append vectors with ids continuing past the current max.

Parameters:

vectors (numpy.ndarray) – Shape (m, dim).

Return type:

None

add_with_ids(vectors, ids)[source]

Append vectors with explicit ids.

Parameters:
  • vectors (numpy.ndarray) – Shape (m, dim).

  • ids (numpy.ndarray) – Shape (m,) integer ids.

Return type:

None

build(vectors, ids=None, payloads=None)[source]

(Re)create the table, insert the corpus, and build the HNSW index.

Parameters:
  • vectors (numpy.ndarray) – Shape (n, dim).

  • ids (numpy.ndarray, optional) – Shape (n,); defaults to range(n).

  • payloads (list of dict, optional) – One JSON-serialisable payload per row, aligned with vectors.

Returns:

self.

Return type:

PgVectorIndex

classmethod capabilities()[source]

Return the pgvector capability descriptor (persistent + filterable).

Return type:

Capabilities

classmethod is_available()[source]

Return True if psycopg + pgvector import (a live DSN is still needed).

Examples

>>> isinstance(PgVectorIndex.is_available(), bool)
True
Return type:

bool

load(path)[source]

Reconnect to the existing table (path may override the DSN).

Parameters:

path (str) – A DSN to reconnect with, or falsy to reuse the constructor’s DSN.

Returns:

self, reconnected.

Return type:

PgVectorIndex

remove(ids)[source]

Delete rows by id.

Parameters:

ids (numpy.ndarray) – Shape (m,) integer ids to drop.

Return type:

None

save(path)[source]

No-op — the table lives in PostgreSQL and is already durable.

Parameters:

path (str) – Unused — accepted only to satisfy the shared interface.

Return type:

None

Notes

Persistence is the database’s job; there is nothing to serialise. Use the same DSN + table to load() the index in another process.

search(queries, k)[source]

Return approximate top-k neighbours per query (no filter).

Parameters:
  • queries (numpy.ndarray) – Shape (q, dim).

  • k (int) – Neighbours per query.

Returns:

  • ids (numpy.ndarray) – Shape (q, k) neighbour ids.

  • distances (numpy.ndarray) – Shape (q, k) distances under the metric operator.

Return type:

tuple[ndarray, ndarray]

search_filter(queries, k, where=None)[source]

Return top-k neighbours, optionally filtered by a payload clause.

Parameters:
  • queries (numpy.ndarray) – Shape (q, dim).

  • k (int) – Neighbours per query.

  • where (dict, optional) – {field: value} equality constraints matched against the JSONB payload column. None searches the whole table.

Returns:

  • ids (numpy.ndarray) – Shape (q, k) (-1 pads short rows).

  • distances (numpy.ndarray) – Shape (q, k) distances under the metric operator.

Return type:

tuple[ndarray, ndarray]

class ann_router.backends.QdrantIndex(dim, metric='cosine', **kwargs)[source]

Bases: ANNIndex

Qdrant collection wrapper (embedded by default) with payload filtering.

Parameters:
  • dim (int) – Embedding dimensionality.

  • metric ({"cosine", "l2", "ip"}, optional) – Distance metric. Defaults to "cosine".

  • location (str, optional) – ":memory:" (default, embedded), a directory path (embedded on-disk), or a URL for a remote server.

  • collection (str, optional) – Collection name. Defaults to "ann_router".

  • kwargs (object)

Examples

>>> QdrantIndex.capabilities().supports_filter
True
add(vectors)[source]

Append vectors with the next contiguous ids.

Parameters:

vectors (numpy.ndarray) – Shape (m, dim).

Return type:

None

add_with_ids(vectors, ids)[source]

Append vectors with explicit ids.

Parameters:
  • vectors (numpy.ndarray) – Shape (m, dim).

  • ids (numpy.ndarray) – Shape (m,) integer ids.

Return type:

None

build(vectors, ids=None, payloads=None)[source]

Create the collection and upsert the initial corpus.

Parameters:
  • vectors (numpy.ndarray) – Shape (n, dim).

  • ids (numpy.ndarray, optional) – Point ids; defaults to range(n).

  • payloads (list of dict, optional) – Per-point metadata for the filtering path.

Return type:

QdrantIndex

classmethod capabilities()[source]

Return the Qdrant capability descriptor (persistent + filterable).

Return type:

Capabilities

classmethod is_available()[source]

Return True if qdrant-client is importable.

Examples

>>> isinstance(QdrantIndex.is_available(), bool)
True
Return type:

bool

load(path)[source]

Reconnect to an on-disk collection at path.

Parameters:

path (str) – The on-disk location to reconnect to.

Returns:

self, reconnected.

Return type:

QdrantIndex

remove(ids)[source]

Delete points by id.

Parameters:

ids (numpy.ndarray) – Shape (m,) integer ids to drop.

Return type:

None

save(path)[source]

No-op for embedded on-disk / remote collections (already persistent).

Parameters:

path (str) – Unused — accepted only to satisfy the shared interface.

Return type:

None

Notes

Qdrant persists itself when location is a directory or a server URL; the :memory: client is ephemeral by design. Point save at a directory location instead of calling this for durability.

search(queries, k)[source]

Return approximate top-k neighbours per query (no filter).

Parameters:
  • queries (numpy.ndarray) – Shape (q, dim).

  • k (int) – Neighbours per query.

Returns:

  • ids (numpy.ndarray) – Shape (q, k) neighbour ids.

  • distances (numpy.ndarray) – Shape (q, k) scores.

Return type:

tuple[ndarray, ndarray]

search_filter(queries, k, where=None)[source]

Return top-k neighbours, optionally restricted by a payload filter.

Parameters:
  • queries (numpy.ndarray) – Shape (q, dim).

  • k (int) – Neighbours per query.

  • where (dict, optional) – {field: value} equality constraints ANDed together. None (default) searches the whole collection.

Returns:

  • ids (numpy.ndarray) – Shape (q, k) (-1 pads short rows when a filter is strict).

  • distances (numpy.ndarray) – Shape (q, k) scores.

Return type:

tuple[ndarray, ndarray]

class ann_router.backends.TurboVecIndex(dim, metric='cosine', **kwargs)[source]

Bases: ANNIndex

turbovec IdMapIndex: mutable, quantised, id-native.

turbovec is id-native (add_with_ids / remove(id)) and returns (distances, ids) from search — this adapter flips that to the package’s (ids, distances) order. Vectors are normalised for cosine so the quantiser’s inner product matches cosine similarity.

Parameters:
  • dim (int) – Embedding dimensionality.

  • metric ({"cosine", "l2", "ip"}, optional) – Distance metric. Defaults to "cosine". turbovec is inner-product / cosine oriented; L2 is approximated on normalised vectors.

  • bit_width (int, optional) – TurboQuant bit width (2 or 4). Defaults to 4 — the recall/size sweet spot measured in the roitelet study.

  • kwargs (object)

Examples

>>> TurboVecIndex.capabilities().supports_remove
True
add(vectors)[source]

Append vectors with the next contiguous ids.

Parameters:

vectors (numpy.ndarray) – Shape (m, dim).

Return type:

None

add_with_ids(vectors, ids)[source]

Append vectors with explicit ids (O(1), no rebuild).

Parameters:
  • vectors (numpy.ndarray) – Shape (m, dim).

  • ids (numpy.ndarray) – Shape (m,) integer ids.

Return type:

None

build(vectors, ids=None)[source]

Create the index and insert the initial corpus.

Parameters:
  • vectors (numpy.ndarray) – Shape (n, dim).

  • ids (numpy.ndarray, optional) – Shape (n,); defaults to range(n).

Returns:

self.

Return type:

TurboVecIndex

classmethod capabilities()[source]

Return the turbovec capability descriptor (fully mutable).

Return type:

Capabilities

classmethod is_available()[source]

Return True if turbovec is importable.

Examples

>>> isinstance(TurboVecIndex.is_available(), bool)
True
Return type:

bool

load(path)[source]

Load an index written by save().

Parameters:

path (str) – Source path produced by save().

Returns:

self, populated from disk.

Return type:

TurboVecIndex

remove(ids)[source]

Delete vectors by id — O(1) each, no structural degradation.

Parameters:

ids (numpy.ndarray) – Shape (m,) integer ids to drop.

Return type:

None

save(path)[source]

Persist via turbovec’s native write.

Parameters:

path (str) – Destination file path.

Return type:

None

search(queries, k)[source]

Return approximate top-k neighbours per query.

Parameters:
  • queries (numpy.ndarray) – Shape (q, dim).

  • k (int) – Neighbours per query.

Returns:

  • ids (numpy.ndarray) – Shape (q, k) neighbour ids.

  • distances (numpy.ndarray) – Shape (q, k) distances under the index metric.

Return type:

tuple[ndarray, ndarray]