Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Vector Search

patinaDB supports vector / embedding search with Neo4j-compatible syntax, backed by an IVF-Flat (inverted-file, k-means clustering) approximate nearest-neighbour index. You store embeddings as list-of-float properties, create a vector index over them, then query it through the vector procedures, getting back ranked nodes and normalized similarity scores.

Storing vectors

A vector is an ordinary list-of-numbers property. Set it with plain Cypher:

CREATE (n:Product {id: 1, embedding: [0.12, -0.03, 0.88, 0.41]})
MATCH (n:Product {id: 1}) SET n.embedding = [0.10, -0.01, 0.90, 0.40]

When a vector index covers that (label, property), the index is maintained synchronously on every write — inserts, updates, and deletes are reflected immediately (no eventual consistency), on every replica.

Creating an index

CREATE VECTOR INDEX product_embeddings
FOR (n:Product) ON (n.embedding)
OPTIONS { indexConfig: {
  `vector.dimensions`: 4,
  `vector.similarity_function`: 'cosine'
} }
  • `vector.dimensions` (required) — the fixed vector length. Vectors of a different length, or non-numeric lists, are simply not indexed.
  • `vector.similarity_function`'cosine' (default) or 'euclidean'.
  • IF NOT EXISTS is supported.
  • Unknown option keys (e.g. Neo4j’s HNSW-specific params) are accepted and ignored, so existing Neo4j DDL runs unchanged.

IVF tuning (patinaDB extensions)

The index partitions vectors into nlist clusters (via k-means); a query scans the nprobe clusters nearest the query vector. Defaults: nlist = clamp(round(sqrt(count)), 1, 4096), nprobe = clamp(nlist / 16, 1, 64). Override them:

OPTIONS { indexConfig: {
  `vector.dimensions`: 384,
  `vector.similarity_function`: 'cosine',
  `patinadb.ivf.lists`: 256,
  `patinadb.ivf.nprobe`: 16
} }

Higher nprobe → higher recall, more work per query. nprobe = nlist scans every cluster (exact search).

Build the index after loading data. The k-means centroids are trained from the vectors present at CREATE VECTOR INDEX time. Creating an index over an empty label produces an index with no centroids, and vectors added later are then not indexed until you DROP and re-CREATE it. This is inherent to IVF (there is nothing to cluster over an empty set).

Managing indexes

SHOW VECTOR INDEXES
DROP VECTOR INDEX product_embeddings
DROP VECTOR INDEX product_embeddings IF EXISTS

SHOW VECTOR INDEXES yields name, label, property, dimensions, similarityFunction, lists, and nprobe.

Querying

CALL db.index.vector.queryNodes('product_embeddings', 5, [0.1, -0.02, 0.9, 0.4])
YIELD node, score
RETURN node, score ORDER BY score DESC

Arguments: the index name, the number of nearest neighbours k, and the query vector. Results are real graph nodes plus a score, ordered by descending similarity. The alias patinadb.vector.queryNodes is equivalent. A query vector whose length differs from the index’s dimensions is an error.

Similarity scalar functions

Two namespaced functions score a pair of vectors directly, using the same normalized formulas as the index score:

RETURN vector.similarity.cosine([1.0, 0.0, 0.0], [1.0, 0.0, 0.0])      -- 1.0
RETURN vector.similarity.euclidean([0.0, 0.0], [3.0, 4.0])            -- 1/26
  • vector.similarity.cosine(a, b) → Neo4j-normalized cosine (1 + rawCosine) / 2 ∈ [0, 1].
  • vector.similarity.euclidean(a, b)1 / (1 + euclideanDistanceSquared) ∈ (0, 1].

Both raise an error on a dimension mismatch or a non-numeric element, and propagate null when either argument is null.

In the server (High Availability)

CREATE / DROP VECTOR INDEX are replicated as Raft control commands. This is where the IVF choice matters:

patinaDB replicates a deterministic apply loop — every node applies the same operations and must converge to a byte-identical index, or the same query would return different results on different replicas. HNSW is randomized and insertion-order-dependent, so it would diverge. IVF-Flat’s k-means centroids are trained once on the leader and replicated as part of the index definition. Every node then assigns each vector to the nearest centroid by a pure, deterministic function → identical posting lists.

Index definitions (including their centroids) are carried in Raft snapshots, so a node that joins or restarts rebuilds identical posting lists from its restored graph. Ongoing writes are indexed synchronously on every node as they apply.

Works end-to-end over Bolt (including the Neo4j Browser): create an index and run db.index.vector.queryNodes, and matching nodes come back as graph nodes with scores.

How it works / limitations

  • IVF-Flat, not HNSW. Vectors are partitioned into nlist k-means clusters; a query scans only the nprobe clusters nearest the query, then computes the exact similarity to each candidate and keeps a bounded top-k. This is approximate: recall is tunable via nprobe (higher = better recall, more work; nprobe = nlist is exact).
  • Heavy one-time build. Assigning every existing vector to a centroid is O(N · nlist · dim) over the whole label — inherent to any ANN build. Incremental maintenance per write is cheap (one nearest-centroid assignment + a few KV ops).
  • Brute-force candidate scan. Within the probed clusters the candidate scan is a linear posting-list scan. It is deliberately isolated behind one internal function (VectorIndex::candidates), so a graph-based backend (e.g. HNSW per cluster) could replace it later without changing the query surface — but that backend would need to preserve cross-replica determinism.
  • Nodes only (v1). Vector indexes cover node properties; relationship vector indexes are not yet supported.
  • Build after loading (see the note under Creating an index).