TuringDB has a built-in vector index that lets you run k-nearest-neighbor searches over embedding vectors. You bring your own embeddings, from any model or provider, and TuringDB handles the indexing, storage, and fast retrieval.
Each vector is stored with a numerical ID. When that ID is the native node ID of the node the vector describes (id(n)), search results are the nodes themselves, so you can read their properties and expand from them with no extra lookup.
Vector indexes live at the TuringDB root level (<turing-dir>/vector/), independent of graphs and versioning. A single vector index can serve searches across multiple graphs and commits, and it persists across restarts.
Embedding literals use parentheses: (1.2, 2.0, 0.0). Square brackets [1.2, 2.0, 0.0] make a list, which is a different type. An embedding literal needs at least 2 numbers.
Create a vector index
A vector index is defined by a name, a dimension, a distance metric and, optionally, an index type.
Syntax:
CREATE VECTOR INDEX <name> WITH DIMENSION <dim> METRIC <metric> [TYPE <type>]
| Parameter | Description |
|---|---|
<name> | Identifier for the vector index |
<dim> | Dimension of the embedding vectors (positive integer, at most 8192) |
<metric> | COSINE or EUCLID (uppercase) |
<type> | Optional. FLAT (exact, the default) or HNSW (approximate, for large collections) |
- Cypher
- Python SDK
CREATE VECTOR INDEX doc_embeddings WITH DIMENSION 768 METRIC COSINE
CREATE VECTOR INDEX doc_hnsw WITH DIMENSION 768 METRIC EUCLID TYPE HNSWclient.query("CREATE VECTOR INDEX doc_embeddings WITH DIMENSION 768 METRIC COSINE")
client.query("CREATE VECTOR INDEX doc_hnsw WITH DIMENSION 768 METRIC EUCLID TYPE HNSW")The metrics return raw scores. COSINE scores are the inner product of the stored and query vectors, so L2-normalise both to get true cosine similarity. EUCLID scores are the squared L2 distance.
Load embeddings
Once the index exists, load your pre-computed embeddings from a header-less CSV file. Each line is id,v1,v2,...,vd, where id is normally the native TuringDB node ID (id(n)).
The file path is relative to your TuringDB data directory (~/.turing/data by default).
The TuringDB data directory defaults to ~/.turing/data. You can change it at startup with the -turing-dir flag.
Syntax:
LOAD VECTOR FROM "<filepath>" IN <index_name>
| Parameter | Description |
|---|---|
<filepath> | Path to the embeddings file, relative to the TuringDB data directory |
<index_name> | Name of the target vector index |
- Cypher
- Python SDK
LOAD VECTOR FROM "document_vectors.csv" IN doc_embeddingsclient.query('LOAD VECTOR FROM "document_vectors.csv" IN doc_embeddings')To build that file from your graph, export id(n) next to the text you want to embed, compute the embeddings in Python, and write id,v1,...,vd lines:
docs = client.query("MATCH (d:Document) RETURN id(d) AS id, d.text AS text")
vectors = model.encode(docs["text"].tolist()) # any embedding model
with open("/home/me/.turing/data/document_vectors.csv", "w") as f:
for node_id, vec in zip(docs["id"], vectors):
f.write(f"{node_id}," + ",".join(f"{x:.6f}" for x in vec) + "\n")Search
VECTOR SEARCH finds the k nearest neighbors of a query vector and yields their IDs and scores. It is a read statement, so you can chain it with MATCH, WHERE and RETURN to pull back the actual graph data.
Syntax:
VECTOR SEARCH IN <index_name> FOR <k> (<vector>) YIELD ids [AS <node>] [, score [AS <s>]]
| Parameter | Description |
|---|---|
<index_name> | Name of the vector index to search |
<k> | Number of nearest neighbors to return (positive integer) |
<vector> | Query vector as an embedding literal, in parentheses. Its dimension must match the index |
ids | The matched IDs, best first. Bound as nodes when the index is keyed by native node IDs |
score | The score of each match |
Standalone search
- Cypher
- Python SDK
VECTOR SEARCH IN doc_embeddings FOR 5 (0.12, 0.45, 0.78, 0.33) YIELD ids, score
RETURN ids, scoredf = client.query("""
VECTOR SEARCH IN doc_embeddings FOR 5 (0.12, 0.45, 0.78, 0.33) YIELD ids, score
RETURN ids, score
""")
print(df)Combining with MATCH
This is where it gets interesting. When the vector file is keyed by native node IDs, ids is the node. Read its properties directly, filter on score, or expand from it with a pattern:
- Cypher
- Python SDK
VECTOR SEARCH IN doc_embeddings FOR 10 (0.12, 0.45, 0.78, 0.33) YIELD ids AS doc, score
WHERE score > 0.8
MATCH (doc)-[:ABOUT]->(t:Topic)
RETURN doc.title, t.name, scoredf = client.query("""
VECTOR SEARCH IN doc_embeddings FOR 10 (0.12, 0.45, 0.78, 0.33) YIELD ids AS doc, score
WHERE score > 0.8
MATCH (doc)-[:ABOUT]->(t:Topic)
RETURN doc.title, t.name, score
""")
print(df)Use ids directly. ids is already the node, so read properties from it (ids.title) or expand from it (MATCH (ids)-[...]->(...)). Re-matching it with MATCH (d:Document) WHERE d.id = ids scans the whole label and cross-joins it with the results, so key the vector file by native node IDs, as above. If you key the file by your own IDs, joining them back needs a label scan.
Node IDs are renumbered by MERGE_DATAPARTS. After compacting a graph, rebuild any vector index keyed by node IDs.
Manage indexes
List all vector indexes
SHOW VECTOR INDEXESThis returns the name and dimension of each index.
Delete a vector index
DELETE VECTOR INDEX doc_embeddingsThis removes the index and frees the associated resources.
Complete workflow
- Cypher
- Python SDK
// 1. Create a vector index for product embeddings
CREATE VECTOR INDEX product_vectors WITH DIMENSION 384 METRIC COSINE
// 2. Load embeddings generated by your model (keyed by native node ID)
LOAD VECTOR FROM "product_embeddings.csv" IN product_vectors
// 3. Find the 10 most similar products to a query embedding
VECTOR SEARCH IN product_vectors FOR 10 (0.15, 0.82, 0.44, 0.91) YIELD ids AS p, score
RETURN p.name, p.price, p.category, score
// 4. Inspect existing indexes
SHOW VECTOR INDEXES
// 5. Clean up
DELETE VECTOR INDEX product_vectorsfrom turingdb import TuringDB
client = TuringDB(host="http://localhost:6666")
# 1. Create a vector index for product embeddings
client.query("CREATE VECTOR INDEX product_vectors WITH DIMENSION 384 METRIC COSINE")
# 2. Load embeddings generated by your model (keyed by native node ID)
client.query('LOAD VECTOR FROM "product_embeddings.csv" IN product_vectors')
# 3. Find the 10 most similar products to a query embedding
df = client.query("""
VECTOR SEARCH IN product_vectors FOR 10 (0.15, 0.82, 0.44, 0.91) YIELD ids AS p, score
RETURN p.name, p.price, p.category, score
""")
print(df)
# 4. Inspect existing indexes
client.query("SHOW VECTOR INDEXES")
# 5. Clean up
client.query("DELETE VECTOR INDEX product_vectors")Embeddings as node properties
The vector index above is a standalone, root-level structure keyed by numeric IDs. Separately, you can store an embedding directly as a node or edge property (type Embedding) and compare embeddings inline with the cosine_similarity and euclidean_distance functions, with no index required.
// Set an embedding property (inside a change)
MATCH (n:Document {name: 'intro'}) SET n.emb = (0.12, 0.45, 0.78, 0.33)
// Compare embeddings inline
MATCH (n:Document) RETURN n.name, cosine_similarity(n.emb, (0.12, 0.45, 0.78, 0.33))
MATCH (n:Document) RETURN n.name, euclidean_distance(n.emb, (0.12, 0.45, 0.78, 0.33))cosine_similarity is true cosine similarity and euclidean_distance is true (not squared) L2 distance. Both accept Embeddings only, not lists, and work in RETURN, WHERE and ORDER BY. This is a brute-force scan, so for large collections use a vector index.
Bulk-loading embeddings from Parquet — LOAD EMBEDDING FROM
To attach embeddings to existing nodes in bulk, load them from a Parquet file. This is a write, so run it inside a change. LOAD EMBEDDING is the fastest way to write many embeddings: about 0.1 s for 20,000 embeddings of 384 dimensions, against about 2.5 ms per node for a loop of SET n.emb = (...).
Syntax:
LOAD EMBEDDING FROM "<filepath>" AS <property_name>
The Parquet file (relative to the TuringDB data directory) must have exactly two columns:
| Column | Type | Description |
|---|---|---|
node_id | INT64 | Native TuringDB node ID (id(n)) to attach the embedding to |
embedding | FIXED_LEN_BYTE_ARRAY (dim × 4 bytes) | Little-endian float32 vector; dimension inferred from width |
import numpy as np, pyarrow as pa, pyarrow.parquet as pq
from pathlib import Path
docs = client.query("MATCH (d:Document) RETURN id(d) AS node_id, d.text AS text")
vecs = model.encode(docs["text"].tolist()).astype("<f4") # shape (n, dim), float32
dim = vecs.shape[1]
pq.write_table(
pa.table({
"node_id": pa.array(docs["node_id"].astype("int64")),
"embedding": pa.array([v.tobytes() for v in vecs], pa.binary(dim * 4)),
}),
Path.home() / ".turing" / "data" / "doc_embeddings.parquet",
)
client.new_change()
client.query('LOAD EMBEDDING FROM "doc_embeddings.parquet" AS emb')
client.query("CHANGE SUBMIT")
client.checkout()- The embedding column is a fixed-size binary column (
pa.binary(dim * 4)above), rather than a list or fixed-size list. - The file can cover any subset of nodes. Only those nodes get the property.
- Running it again with the same property name overwrites the stored vectors, which is how you refresh embeddings after a model change.
- Every
node_idmust exist in the graph, and the property name must be new or already an embedding property.
When importing a graph from JSONL, you can mark embedding properties at load time instead. See LOAD JSONL ... WITH EMBEDDINGS.

