You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Neo4j vector similarity search is O(N) brute-force; HNSW support was added in #859 then removed in #894
Summary
On the Neo4j backend, every vector similarity search computes vector.similarity.cosine() against every matching node/edge in the graph. There is no vector index, and build_indices_and_constraints() never creates one.
On a modest graph (48,647 Entity nodes / 76,264 RELATES_TO edges, 2560-dim embeddings) this makes a single edge_similarity_search take ~41 seconds. Recreating the same query against a manually-created Neo4j HNSW index returns the identical top-20 result set in ~82 ms — a ~500x difference.
Neo4j has supported native vector indexes and db.index.vector.queryRelationships / queryNodes since 5.18. Graphiti currently does not use them on the Neo4j path.
Current behavior
graphiti_core/graph_queries.py (line numbers from 993e081, current main):
A repo-wide search confirms the gap on current main:
db.index.vector → 0 hits
CREATE VECTOR INDEX → 1 hit, in examples/ecommerce/runner.ipynb only
This was implemented once and then removed
This is not a feature that was never attempted:
use hnsw indexes #859 "use hnsw indexes" (merged 2025-08-25) introduced a USE_HNSW env flag, a CREATE VECTOR INDEX ... OPTIONS { indexConfig: { \vector.dimensions`: 1024, `vector.similarity_function`: 'cosine' }}statement, and aGraphProvider.NEO4J and USE_HNSWbranch innode_similarity_searchusingCALL db.index.vector.queryNodes(...)`.
cleanup #894 "cleanup" (1f5a1b8, 2025-09-05) removed USE_HNSW along with the associated branches.
USE_HNSW has 0 occurrences on current main.
I could not find a recorded rationale for the removal, so this issue is written as a question as much as a report: was the HNSW path dropped intentionally (e.g. hardcoded 1024 dimensions, per-group index naming, recall concerns), or did it fall out as collateral during cleanup? The answer determines whether a re-introduction is welcome and what shape it should take.
Reproduction / measurements
Environment:
graphiti-core 0.29.2 (behavior verified verbatim against 993e081 on main)
No other workload on the database (the Graphiti MCP server was stopped for the benchmark; with it running, concurrent brute-force scans saturated ~29 CPU-equivalents and made all timings unusable)
All queries issued over the HTTP transaction endpoint. Baseline RETURN 1 measured at 149–162 ms; subtract that from the numbers below.
A — current behavior (brute-force):
WITH [xINrange(1,2560) | rand()] ASvMATCH (n:Entity)-[e:RELATES_TO]->(m:Entity)
WHEREe.group_idIN ['<group>']
WITHDISTINCTe, n, m, vector.similarity.cosine(e.fact_embedding, v) ASscoreWHEREscore>0.0RETURNe.uuidASuuid, n.uuidASsrc, m.uuidASdst, scoreORDER BYscoreDESCLIMIT20
→ 45208 ms / 42723 ms / 41014 ms
B — same result via HNSW index:
CREATEVECTORINDEXedge_fact_embedding_hnswIFNOTEXISTSFOR ()-[e:RELATES_TO]-() ON (e.fact_embedding)
OPTIONS{indexConfig: {`vector.dimensions`:2560,`vector.similarity_function`:'cosine'}}
Index build took ~5 minutes for 76,264 edges and reached state=ONLINE, populationPercent=100.0.
Recall check. Using a real edge's fact_embedding as the query vector, top-20 from A and top-20 from B were compared as sets:
brute-force returned: 20
HNSW returned: 20
intersection: 20
only in brute-force: (none)
only in HNSW: (none)
This is a single seed, so it is evidence rather than proof — HNSW is approximate by construction and a broader recall sweep would be needed to characterize it properly. Over-fetching (limit * 3) before post-filtering is what makes the group filter safe here, matching the approach in #1335.
Why this matters beyond raw latency
The cost is not paid once per user query. While the Graphiti MCP server was running normally, SHOW TRANSACTIONS consistently showed 5+ concurrent brute-force scans, each already 2+ minutes elapsed, with Neo4j pinned at multiple thousand percent CPU. Stopping the MCP server dropped Neo4j to 0.85% CPU. On the Neo4j path the cost scales with graph size × concurrency, so it degrades exactly as a memory graph becomes worth having.
Both target FalkorDB. Neo4j — the default backend in the README quickstart — has no equivalent open work that I could find, despite having had a partial implementation in-tree a year ago.
Proposed direction
Assuming the removal in #894 was not a deliberate rejection, a re-introduction would need to address the reasons the original was fragile:
Create HNSW vector indexes in Neo4jDriver.build_indices_and_constraints() — on Entity.name_embedding, RELATES_TO.fact_embedding, and Community.name_embedding. Dimension must be derived from the configured embedder rather than hardcoded (use hnsw indexes #859 hardcoded 1024; this deployment uses 2560).
Route Neo4j similarity searches through db.index.vector.queryNodes / queryRelationships with over-fetch + post-filter, so group_ids and SearchFilters semantics are preserved.
Keep the brute-force path available for exact-recall requirements and for graphs whose index has not been built, rather than reintroducing a bare env flag as the only control.
I have the benchmark harness and a fork set up, and I am happy to open a PR for the Neo4j path. Before writing it I would rather have maintainer input on two points, since they determine the shape of the change:
Is a Neo4j HNSW path wanted at all, given cleanup #894 removed the previous one?
Should index creation be automatic in build_indices_and_constraints(), or opt-in? Automatic creation on an existing large graph triggers a background population that is expensive once; opt-in avoids surprising existing deployments but leaves the default slow.
Happy to provide the full benchmark scripts, or to run a wider recall sweep across many seeds if that would help the decision.
Neo4j vector similarity search is O(N) brute-force; HNSW support was added in #859 then removed in #894
Summary
On the Neo4j backend, every vector similarity search computes
vector.similarity.cosine()against every matching node/edge in the graph. There is no vector index, andbuild_indices_and_constraints()never creates one.On a modest graph (48,647
Entitynodes / 76,264RELATES_TOedges, 2560-dim embeddings) this makes a singleedge_similarity_searchtake ~41 seconds. Recreating the same query against a manually-created Neo4j HNSW index returns the identical top-20 result set in ~82 ms — a ~500x difference.Neo4j has supported native vector indexes and
db.index.vector.queryRelationships/queryNodessince 5.18. Graphiti currently does not use them on the Neo4j path.Current behavior
graphiti_core/graph_queries.py(line numbers from993e081, currentmain):This helper is called from 11 sites in
graphiti_core/search/search_utils.py, including the three primary search entry points:edge_similarity_searchnode_similarity_searchcommunity_similarity_searchNeo4jDriver.build_indices_and_constraints()creates range and fulltext indexes only:A repo-wide search confirms the gap on current
main:db.index.vector→ 0 hitsCREATE VECTOR INDEX→ 1 hit, inexamples/ecommerce/runner.ipynbonlyThis was implemented once and then removed
This is not a feature that was never attempted:
USE_HNSWenv flag, aCREATE VECTOR INDEX ... OPTIONS { indexConfig: { \vector.dimensions`: 1024, `vector.similarity_function`: 'cosine' }}statement, and aGraphProvider.NEO4J and USE_HNSWbranch innode_similarity_searchusingCALL db.index.vector.queryNodes(...)`.1f5a1b8, 2025-09-05) removedUSE_HNSWalong with the associated branches.USE_HNSWhas 0 occurrences on currentmain.I could not find a recorded rationale for the removal, so this issue is written as a question as much as a report: was the HNSW path dropped intentionally (e.g. hardcoded 1024 dimensions, per-group index naming, recall concerns), or did it fall out as collateral during cleanup? The answer determines whether a re-introduction is welcome and what shape it should take.
Reproduction / measurements
Environment:
993e081onmain)Entitynodes, 76,264RELATES_TOedges,fact_embeddingdimension 2560All queries issued over the HTTP transaction endpoint. Baseline
RETURN 1measured at 149–162 ms; subtract that from the numbers below.A — current behavior (brute-force):
→ 45208 ms / 42723 ms / 41014 ms
B — same result via HNSW index:
→ 1456 ms (cold) / 390 ms / 232 ms
Index build took ~5 minutes for 76,264 edges and reached
state=ONLINE, populationPercent=100.0.Recall check. Using a real edge's
fact_embeddingas the query vector, top-20 from A and top-20 from B were compared as sets:This is a single seed, so it is evidence rather than proof — HNSW is approximate by construction and a broader recall sweep would be needed to characterize it properly. Over-fetching (
limit * 3) before post-filtering is what makes the group filter safe here, matching the approach in #1335.Why this matters beyond raw latency
The cost is not paid once per user query. While the Graphiti MCP server was running normally,
SHOW TRANSACTIONSconsistently showed 5+ concurrent brute-force scans, each already 2+ minutes elapsed, with Neo4j pinned at multiple thousand percent CPU. Stopping the MCP server dropped Neo4j to 0.85% CPU. On the Neo4j path the cost scales with graph size × concurrency, so it degrades exactly as a memory graph becomes worth having.Related work
feat(falkordb): Use HNSW vector index procedures for similarity search. Same root cause, FalkorDB driver. Reports 6-8 s per query on 3072-dim embeddings.perf: add FalkorDB HNSW vector indices and fix O(n) fulltext re-match. Reports 12-65 s per search.Both target FalkorDB. Neo4j — the default backend in the README quickstart — has no equivalent open work that I could find, despite having had a partial implementation in-tree a year ago.
Proposed direction
Assuming the removal in #894 was not a deliberate rejection, a re-introduction would need to address the reasons the original was fragile:
Neo4jDriver.build_indices_and_constraints()— onEntity.name_embedding,RELATES_TO.fact_embedding, andCommunity.name_embedding. Dimension must be derived from the configured embedder rather than hardcoded (use hnsw indexes #859 hardcoded 1024; this deployment uses 2560).db.index.vector.queryNodes/queryRelationshipswith over-fetch + post-filter, sogroup_idsandSearchFilterssemantics are preserved.I have the benchmark harness and a fork set up, and I am happy to open a PR for the Neo4j path. Before writing it I would rather have maintainer input on two points, since they determine the shape of the change:
build_indices_and_constraints(), or opt-in? Automatic creation on an existing large graph triggers a background population that is expensive once; opt-in avoids surprising existing deployments but leaves the default slow.Happy to provide the full benchmark scripts, or to run a wider recall sweep across many seeds if that would help the decision.