Neo4j Vector Index Skill
Create and manage Neo4j vector indexes, run vector similarity search (ANN/kNN),
- Skill ID
- neo4j-contrib/neo4j-skills/neo4j-vector-index-skill
- Publisher
- neo4j-contrib
- Repository
- neo4j-skills
- Installs
- 0
- Files
- 3
- Requires
- Neo4j >= 2025.01; SEARCH clause requires 2026.01+
- Synced
- Sep 23, 2026
Open any RiverX project, open the Skills panel in the chat, and search for this identifier. The files are fetched from the source repository at install time.
neo4j-contrib/neo4j-skills/neo4j-vector-index-skillInstalls these files- README.md
- SKILL.md
- references/hybrid-search.md
What this skill tells the agent
When to Use
- Creating a vector index (
CREATE VECTOR INDEX) on nodes or relationships - Running vector similarity / nearest-neighbor search
- Storing embeddings on graph nodes during ingestion
- Indexing/querying embeddings already written by GDS algorithms
- Choosing similarity function, dimensions, HNSW params, or quantization
- Using
SEARCHclause (2026.01+) ordb.index.vector.queryNodes()(2025.x) - Batch-updating embeddings after model change
- Combining vector results with immediate graph neighborhood (full retrieval_query pipelines →
neo4j-graphrag-skill) - Hybrid search that combines vector results with fulltext or other ranked sources
When NOT to Use
- GraphRAG pipelines (VectorCypherRetriever, HybridCypherRetriever, retrieval_query) →
neo4j-graphrag-skill - Fulltext-only / keyword-only search (FULLTEXT INDEX,
db.index.fulltext.queryNodes) →neo4j-cypher-skill - Computing GDS graph embeddings (FastRP, Node2Vec, GraphSAGE) →
neo4j-gds-skill - Index admin (list all indexes, drop range/text/lookup indexes) →
neo4j-cypher-skill
Pre-flight — Determine Version
Drives syntax choice:
CALL dbms.components() YIELD versions RETURN versions[0] AS neo4j_version| Version | Use |
|---|---|
2026.01 or higher | SEARCH clause (in-index filtering, preferred) |
2025.x | db.index.vector.queryNodes() procedure (deprecated 2026.04 — use SEARCH when on 2026.x) |
Step 1 — Create Vector Index
Node index (single label):
CYPHER 25
CREATE VECTOR INDEX chunk_embedding IF NOT EXISTS
FOR (c:Chunk) ON (c.embedding)
OPTIONS {
indexConfig: {
`vector.dimensions`: 1536,
`vector.similarity_function`: 'cosine',
`vector.quantization.type`: 'SCALAR',
`vector.hnsw.m`: 16,
`vector.hnsw.ef_construction`: 100
}
}Node index with filterable properties [2026.01+] — WITH declares which properties can be used in SEARCH ... WHERE:
CYPHER 25
CREATE VECTOR INDEX chunk_embedding IF NOT EXISTS
FOR (c:Chunk) ON (c.embedding)
WITH [c.source, c.lang, c.published_year] // stored as metadata; filterable in SEARCH WHERE
OPTIONS { indexConfig: { `vector.dimensions`: 1536, `vector.similarity_function`: 'cosine' } }Multi-label index with filterable properties [2026.01+]:
CYPHER 25
CREATE VECTOR INDEX doc_embedding IF NOT EXISTS
FOR (n:Document|Article) ON n.embedding
WITH [n.author, n.published_year, n.lang]
OPTIONS { indexConfig: { `vector.dimensions`: 1536, `vector.similarity_function`: 'cosine' } }Relationship index:
CYPHER 25
CREATE VECTOR INDEX rel_embedding IF NOT EXISTS
FOR ()-[r:HAS_CHUNK]-() ON (r.embedding)
OPTIONS { indexConfig: { `vector.dimensions`: 768, `vector.similarity_function`: 'cosine' } }`WITH` property types — only scalar types allowed: INTEGER, FLOAT, STRING, BOOLEAN, DATE, ZONED DATETIME, LOCAL DATETIME, ZONED TIME, LOCAL TIME, DURATION. Not allowed: LIST, POINT, or the vector property itself.
Index config reference:
| Parameter | Type | Default | Notes |
|---|---|---|---|
vector.dimensions | INTEGER 1–4096 | none | Required; must match embedding model exactly |
vector.similarity_function | STRING | 'cosine' | 'cosine' or 'euclidean' |
vector.quantization.type | STRING | 'scalar' | 'none', 'scalar', 'binary' [2026.06+, GA 2026.07]; reduces storage; binary smallest (1 bit per dimension), most aggressive; needs vector-2.0+ (5.18+) |
vector.quantization.enabled | BOOLEAN | true | Deprecated 2026.06 — use vector.quantization.type; false without vector.quantization.type: 'none' fails index creation before 2026.07 |
vector.default_search_expansion_factor | FLOAT 1.0–10000.0 | 1.0 none / 1.5 scalar / 3.0 binary (was 2.0 before 2026.07) | [2026.06+, GA 2026.07]; value >1.0 on quantized vectors enables automatic rescoring with full-precision vectors (High-Fidelity Quantized search, HFQ); not settable at query time; existing indexes keep their stored value until rebuilt |
vector.hnsw.m | INTEGER 1–512 | 16 | HNSW graph connections; higher = better recall, more memory |
vector.hnsw.ef_construction | INTEGER 1–3200 | 100 | Build-time candidates; higher = better recall, slower build |
Provider is not settable in Cypher 25 — Neo4j picks the most feature-rich one (vector-2026.06 on 2026.06+, required for binary quantization + rescoring). Check with SHOW VECTOR INDEXES YIELD name, indexProvider. Changing quantization requires dropping and recreating the index.
Unquantized vectors: set vector.quantization.type: 'none' alone — on 2026.06, vector.quantization.enabled: false without it errors (fixed 2026.07).
Similarity function choice:
| Use case | Function |
|---|---|
| Normalized embeddings (OpenAI, Cohere, Voyage, Google) | 'cosine' |
| Unnormalized / raw distance matters | 'euclidean' |
Index providers — latest selected automatically; not specifiable in Cypher 25. Check with SHOW VECTOR INDEXES YIELD name, indexProvider:
| Provider | Quantization support |
|---|---|
vector-2026.07 | High-Fidelity Quantized search for scalar and binary |
vector-2026.06 | scalar and binary |
vector-2.0 (5.18+) | scalar |
Changing quantization type or expansion factor requires index re-create + re-population.
Step 2 — Wait for Index ONLINE
Index builds asynchronously — do NOT query until ONLINE:
SHOW VECTOR INDEXES YIELD name, state, populationPercent
WHERE name = 'chunk_embedding'
RETURN name, state, populationPercentPoll every 5s until state = 'ONLINE' and populationPercent = 100.0. If state = 'FAILED' → stop, check logs.
Shell poll (cypher-shell):
until cypher-shell -u neo4j -p "$NEO4J_PASSWORD" \
"SHOW VECTOR INDEXES YIELD name, state WHERE name='chunk_embedding' RETURN state" \
| grep -q ONLINE; do
sleep 5
doneStep 3 — Ingest Embeddings
Batch UNWIND pattern (use for > 100 nodes — never one-node-per-transaction):
from neo4j import GraphDatabase
driver = GraphDatabase.driver(uri, auth=(user, password))
def embed_batch(texts: list[str]) -> list[list[float]]:
response = openai_client.embeddings.create(
model="text-embedding-3-small", input=texts
)
return [r.embedding for r in response.data]
def store_embeddings(records: list[dict], batch_size: int = 500):
expected_dim = 1536 # must match vector.dimensions
texts = [r["text"] for r in records]
embeddings = embed_batch(texts)
for emb in embeddings:
assert len(emb) == expected_dim, f"Dim mismatch: {len(emb)} != {expected_dim}"
rows = [{"id": r["id"], "embedding": emb}
for r, emb in zip(records, embeddings)]
for i in range(0, len(rows), batch_size):
driver.execute_query(
"UNWIND $rows AS row MATCH (c:Chunk {id: row.id}) SET c.embedding = row.embedding",
rows=rows[i:i+batch_size]
)❌ Never create index after embeddings are already stored — always create index first. ✅ Create index → poll ONLINE → ingest embeddings.
Step 4 — Run Vector Search
SEARCH clause (2026.01+, preferred)
CYPHER 25
MATCH (c:Chunk)
SEARCH c IN (
VECTOR INDEX chunk_embedding
FOR $queryEmbedding
LIMIT 10
) SCORE AS score
RETURN c.text, score
ORDER BY score DESCWith in-index filter [2026.01+] — properties must be declared in WITH at index creation:
// Index must have been created with: WITH [c.source, c.lang, c.published_year]
CYPHER 25
MATCH (c:Chunk)
SEARCH c IN (
VECTOR INDEX chunk_embedding
FOR $queryEmbedding
WHERE c.source = $source AND c.lang = 'en' AND c.published_year >= 2024
LIMIT 10
) SCORE AS score
RETURN c.text, c.source, score
ORDER BY score DESCFiltering strategy — choose one:
| Strategy | When to use | Tradeoff |
|---|
More from neo4j-contrib
Use this skill in a project
Open any RiverX project, search the Skills panel for neo4j-contrib/neo4j-skills/neo4j-vector-index-skill, and install it in one click.
