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 EXISTSis 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 INDEXtime. Creating an index over an empty label produces an index with no centroids, and vectors added later are then not indexed until youDROPand re-CREATEit. 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
nlistk-means clusters; a query scans only thenprobeclusters nearest the query, then computes the exact similarity to each candidate and keeps a bounded top-k. This is approximate: recall is tunable vianprobe(higher = better recall, more work;nprobe = nlistis 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).