Skip to content
Open
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 19 additions & 2 deletions graphiti_core/driver/falkordb_driver.py
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@
from graphiti_core.driver.operations.next_episode_edge_ops import NextEpisodeEdgeOperations
from graphiti_core.driver.operations.saga_node_ops import SagaNodeOperations
from graphiti_core.driver.operations.search_ops import SearchOperations
from graphiti_core.graph_queries import get_fulltext_indices, get_range_indices
from graphiti_core.graph_queries import get_fulltext_indices, get_range_indices, get_vector_indices
from graphiti_core.utils.datetime_utils import convert_datetimes_to_strings

logger = logging.getLogger(__name__)
Expand Down Expand Up @@ -292,14 +292,31 @@ async def delete_all_indexes(self) -> None:
f'DROP FULLTEXT INDEX FOR ()-[e:{label}]-() ON (e.{field_name})'
)
)
elif 'VECTOR' in index_type:
if entity_type == 'NODE':
drop_tasks.append(
self.execute_query(
f'DROP VECTOR INDEX FOR (n:{label}) ON (n.{field_name})'
)
)
elif entity_type == 'RELATIONSHIP':
drop_tasks.append(
self.execute_query(
f'DROP VECTOR INDEX FOR ()-[e:{label}]-() ON (e.{field_name})'
)
)
Comment on lines +295 to +307
Copy link

Copilot AI Mar 1, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

delete_all_indexes() now drops VECTOR indexes here, but the FalkorDB GraphMaintenanceOperations implementation (graphiti_core/driver/falkordb/operations/graph_ops.py) still only drops RANGE/FULLTEXT. This duplication can lead to different behavior depending on which API a caller uses; consider updating the operations implementation too or consolidating index management in one place.

Suggested change
elif 'VECTOR' in index_type:
if entity_type == 'NODE':
drop_tasks.append(
self.execute_query(
f'DROP VECTOR INDEX FOR (n:{label}) ON (n.{field_name})'
)
)
elif entity_type == 'RELATIONSHIP':
drop_tasks.append(
self.execute_query(
f'DROP VECTOR INDEX FOR ()-[e:{label}]-() ON (e.{field_name})'
)
)

Copilot uses AI. Check for mistakes.
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

delete_all_indexes duplication — Added elif 'VECTOR' in index_type: branch to FalkorGraphMaintenanceOperations.delete_all_indexes() in graph_ops.py, matching the pattern already in FalkorDriver.delete_all_indexes().


if drop_tasks:
await asyncio.gather(*drop_tasks)

async def build_indices_and_constraints(self, delete_existing=False):
if delete_existing:
await self.delete_all_indexes()
index_queries = get_range_indices(self.provider) + get_fulltext_indices(self.provider)
index_queries = (
get_range_indices(self.provider)
+ get_fulltext_indices(self.provider)
+ get_vector_indices(self.provider)
)
Comment on lines +315 to +319
Copy link

Copilot AI Mar 1, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

build_indices_and_constraints() now includes vector index creation, but the FalkorDB GraphMaintenanceOperations implementation builds only range/fulltext indices. To avoid inconsistent index state depending on entrypoint, update the operations implementation to include get_vector_indices() (or remove the duplication).

Copilot uses AI. Check for mistakes.
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

build_indices_and_constraints duplication — Added get_vector_indices() to FalkorGraphMaintenanceOperations.build_indices_and_constraints(), keeping it in sync with FalkorDriver.build_indices_and_constraints().

for query in index_queries:
await self.execute_query(query)

Expand Down
25 changes: 25 additions & 0 deletions graphiti_core/graph_queries.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@
supporting index creation, fulltext search, and bulk operations.
"""

import os

from typing_extensions import LiteralString

from graphiti_core.driver.driver import GraphProvider
Expand Down Expand Up @@ -140,6 +142,29 @@ def get_fulltext_indices(provider: GraphProvider) -> list[LiteralString]:
]


def get_vector_indices(provider: GraphProvider) -> list[LiteralString]:
"""Return HNSW vector index creation queries for the given provider."""
if provider == GraphProvider.FALKORDB:
dim = int(os.getenv('EMBEDDING_DIM', 1024))
# FalkorDB requires dimension at index creation time.
# cast() keeps pyright happy with LiteralString expectations.
from typing import cast

return cast(
list[LiteralString],
[
f'CREATE VECTOR INDEX FOR (n:Entity) ON (n.name_embedding)'
f" OPTIONS {{dimension: {dim}, similarityFunction: 'cosine'}}",
f'CREATE VECTOR INDEX FOR (n:Community) ON (n.name_embedding)'
f" OPTIONS {{dimension: {dim}, similarityFunction: 'cosine'}}",
f'CREATE VECTOR INDEX FOR ()-[e:RELATES_TO]-() ON (e.fact_embedding)'
f" OPTIONS {{dimension: {dim}, similarityFunction: 'cosine'}}",
],
)

return []


def get_nodes_query(name: str, query: str, limit: int, provider: GraphProvider) -> str:
if provider == GraphProvider.FALKORDB:
label = NEO4J_TO_FALKORDB_MAPPING[name]
Expand Down
141 changes: 139 additions & 2 deletions graphiti_core/search/search_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -231,8 +231,8 @@ async def edge_fulltext_search(
"""
UNWIND $ids as id
MATCH (n:Entity)-[e:RELATES_TO]->(m:Entity)
WHERE e.group_id IN $group_ids
AND id(e)=id
WHERE e.group_id IN $group_ids
AND id(e)=id
"""
+ filter_query
+ """
Expand Down Expand Up @@ -265,6 +265,34 @@ async def edge_fulltext_search(
)
else:
return []
elif driver.provider == GraphProvider.FALKORDB:
# FalkorDB's queryRelationships returns the actual relationship object,
# so use startNode/endNode directly instead of re-matching by uuid (which
# causes an O(n) scan of all RELATES_TO edges).
query = (
get_relationships_query('edge_name_and_fact', limit=limit, provider=driver.provider)
+ """
YIELD relationship AS e, score
WITH e, score, startNode(e) AS n, endNode(e) AS m
"""
+ filter_query
+ """
RETURN
"""
+ get_entity_edge_return_query(driver.provider)
+ """
ORDER BY score DESC
LIMIT $limit
"""
)

records, _, _ = await driver.execute_query(
query,
query=fuzzy_query,
limit=limit,
routing_='r',
**filter_params,
)
else:
query = (
get_relationships_query('edge_name_and_fact', limit=limit, provider=driver.provider)
Expand Down Expand Up @@ -410,6 +438,43 @@ async def edge_similarity_search(
)
else:
return []
elif driver.provider == GraphProvider.FALKORDB:
# Use HNSW vector index for O(log n) search instead of brute-force scan.
# Over-fetch to compensate for post-filtering on group_id, edge_uuids, etc.
over_fetch_limit = limit * 10

post_filter_parts = list(filter_queries)
post_filter_parts.append('score > $min_score')
post_filter = ' WHERE ' + ' AND '.join(post_filter_parts)

query = (
'CALL db.idx.vector.queryRelationships('
"'RELATES_TO', 'fact_embedding', $over_fetch_limit, vecf32($search_vector))"
"""
YIELD relationship AS e, score
MATCH (n:Entity)-[e]->(m:Entity)
WITH DISTINCT e, n, m, score
Copy link

Copilot AI Mar 1, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In the FalkorDB vector branch, you already have the relationship bound as e from db.idx.vector.queryRelationships. Re-matching with MATCH (n:Entity)-[e]->(m:Entity) is redundant and can be problematic (variable rebinding / extra work). Prefer WITH e, score, startNode(e) AS n, endNode(e) AS m (optionally assert labels) and drop the WITH DISTINCT if it’s no longer needed.

Suggested change
MATCH (n:Entity)-[e]->(m:Entity)
WITH DISTINCT e, n, m, score
WITH e, score, startNode(e) AS n, endNode(e) AS m

Copilot uses AI. Check for mistakes.
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Edge similarity search re-match — Replaced MATCH (n:Entity)-[e]->(m:Entity) / WITH DISTINCT e, n, m, score with WITH e, score, startNode(e) AS n, endNode(e) AS m, consistent with the fix already applied in edge_fulltext_search.

"""
+ post_filter
+ """
RETURN
"""
+ get_entity_edge_return_query(driver.provider)
+ """
ORDER BY score DESC
LIMIT $limit
"""
)

records, _, _ = await driver.execute_query(
query,
search_vector=search_vector,
over_fetch_limit=over_fetch_limit,
limit=limit,
min_score=min_score,
routing_='r',
**filter_params,
)
Comment on lines +441 to +476
Copy link

Copilot AI Mar 1, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Test coverage gap: the new FalkorDB HNSW branches (vector queries + post-filtering/min_score behavior) aren’t exercised by the existing search tests (they currently skip FalkorDB). Consider adding a FalkorDBLite-backed integration test or a unit test that asserts the generated Cypher + parameters for the FalkorDB provider.

Copilot uses AI. Check for mistakes.
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Test coverage — Added 11 unit tests for the FalkorDB HNSW branches in edge_similarity_search, node_similarity_search, and community_similarity_search. Tests verify correct HNSW index queries, startNode/endNode usage, over-fetch limits, group_id filtering, and min_score filtering.

else:
query = (
match_query
Expand Down Expand Up @@ -750,6 +815,41 @@ async def node_similarity_search(
)
else:
return []
elif driver.provider == GraphProvider.FALKORDB:
# Use HNSW vector index for O(log n) search instead of brute-force scan.
over_fetch_limit = limit * 10

post_filter_parts = list(filter_queries)
post_filter_parts.append('score > $min_score')
post_filter = ' WHERE ' + ' AND '.join(post_filter_parts)

query = (
'CALL db.idx.vector.queryNodes('
"'Entity', 'name_embedding', $over_fetch_limit, vecf32($search_vector))"
"""
YIELD node AS n, score
WITH n, score
"""
+ post_filter
+ """
RETURN
"""
+ get_entity_node_return_query(driver.provider)
+ """
ORDER BY score DESC
LIMIT $limit
"""
)

records, _, _ = await driver.execute_query(
query,
search_vector=search_vector,
over_fetch_limit=over_fetch_limit,
limit=limit,
min_score=min_score,
routing_='r',
**filter_params,
)
else:
query = (
"""
Expand Down Expand Up @@ -1134,6 +1234,43 @@ async def community_similarity_search(
)
else:
return []
elif driver.provider == GraphProvider.FALKORDB:
# Use HNSW vector index for O(log n) search instead of brute-force scan.
over_fetch_limit = limit * 10

post_filter_parts: list[str] = []
if group_ids is not None:
post_filter_parts.append('c.group_id IN $group_ids')
post_filter_parts.append('score > $min_score')
post_filter = ' WHERE ' + ' AND '.join(post_filter_parts)

query = (
'CALL db.idx.vector.queryNodes('
"'Community', 'name_embedding', $over_fetch_limit, vecf32($search_vector))"
"""
YIELD node AS c, score
WITH c, score
"""
+ post_filter
+ """
RETURN
"""
+ COMMUNITY_NODE_RETURN
+ """
ORDER BY score DESC
LIMIT $limit
"""
)

records, _, _ = await driver.execute_query(
query,
search_vector=search_vector,
over_fetch_limit=over_fetch_limit,
limit=limit,
min_score=min_score,
routing_='r',
**query_params,
)
else:
search_vector_var = '$search_vector'
if driver.provider == GraphProvider.KUZU:
Expand Down
Loading