Skip to content
Open
Show file tree
Hide file tree
Changes from all 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
8 changes: 5 additions & 3 deletions graphiti_core/driver/falkordb/operations/search_ops.py
Original file line number Diff line number Diff line change
Expand Up @@ -314,8 +314,8 @@ async def edge_fulltext_search(
'edge_name_and_fact', limit=limit, provider=GraphProvider.FALKORDB
)
+ """
YIELD relationship AS rel, score
MATCH (n:Entity)-[e:RELATES_TO {uuid: rel.uuid}]->(m:Entity)
YIELD relationship AS e, score
WITH e, score, startNode(e) AS n, endNode(e) AS m
"""
+ filter_query
+ """
Expand Down Expand Up @@ -427,7 +427,9 @@ async def edge_bfs_search(
UNWIND $bfs_origin_node_uuids AS origin_uuid
MATCH path = (origin {{uuid: origin_uuid}})-[:RELATES_TO|MENTIONS*1..{max_depth}]->(:Entity)
UNWIND relationships(path) AS rel
MATCH (n:Entity)-[e:RELATES_TO {{uuid: rel.uuid}}]-(m:Entity)
WITH rel AS e, startNode(rel) AS n, endNode(rel) AS m
WHERE type(e) = 'RELATES_TO'
WITH e, n, m
"""
+ filter_query
+ """
Expand Down
4 changes: 2 additions & 2 deletions graphiti_core/search/search_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -201,8 +201,8 @@ async def edge_fulltext_search(
return []

match_query = """
YIELD relationship AS rel, score
MATCH (n:Entity)-[e:RELATES_TO {uuid: rel.uuid}]->(m:Entity)
YIELD relationship AS e, score
WITH e, score, startNode(e) AS n, endNode(e) AS m
"""
if driver.provider == GraphProvider.KUZU:
match_query = """
Expand Down
68 changes: 68 additions & 0 deletions tests/test_falkordb_search_ops.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
"""Query-shape regression tests for FalkorDB edge search (no live database).

Guards the fix for the O(matches×graph) re-MATCH in edge search: the edge
fulltext and BFS queries must derive endpoints from the relationship returned by
the index (``startNode``/``endNode``) instead of re-matching every yielded edge
by uuid against the whole graph (``MATCH (n)-[e {uuid: rel.uuid}]->(m)``), which
caused multi-minute query times on large graphs.

Harvested and adapted from #1494's driver test; runs as a unit test (a recording
executor captures the emitted Cypher, so no FalkorDB connection is required).
"""

from typing import Any

import pytest

from graphiti_core.driver.falkordb.operations.search_ops import FalkorSearchOperations
from graphiti_core.search.search_filters import SearchFilters


class RecordingExecutor:
"""Captures the Cypher and params a search method emits, returning no rows."""

def __init__(self):
self.cypher_query = ''
self.params: dict[str, Any] = {}

async def execute_query(self, cypher_query_: str, **kwargs: Any):
self.cypher_query = cypher_query_
self.params = kwargs
return [], None, None


@pytest.mark.asyncio
async def test_edge_fulltext_search_uses_returned_relationship_endpoints():
executor = RecordingExecutor()
operations = FalkorSearchOperations()

await operations.edge_fulltext_search(
executor,
'api test system',
SearchFilters(),
group_ids=['group-a'],
)

assert 'YIELD relationship AS e, score' in executor.cypher_query
assert 'WITH e, score, startNode(e) AS n, endNode(e) AS m' in executor.cypher_query
# The expensive per-row re-MATCH must be gone.
assert 'uuid: rel.uuid' not in executor.cypher_query


@pytest.mark.asyncio
async def test_edge_bfs_search_uses_path_relationship_endpoints():
executor = RecordingExecutor()
operations = FalkorSearchOperations()

await operations.edge_bfs_search(
executor,
['origin-uuid'],
2,
SearchFilters(),
group_ids=['group-a'],
)

assert 'WITH rel AS e, startNode(rel) AS n, endNode(rel) AS m' in executor.cypher_query
# rel is rebound to e before the type guard, so the filter is on type(e).
assert "WHERE type(e) = 'RELATES_TO'" in executor.cypher_query
assert 'uuid: rel.uuid' not in executor.cypher_query
Loading