Skip to content

Commit a97abcc

Browse files
phernandezclaude
andauthored
perf(core): rank search by id and fetch note text for returned rows only (#1755)
Signed-off-by: phernandez <paul@basicmachines.co> Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
1 parent 9f26e5e commit a97abcc

19 files changed

Lines changed: 667 additions & 105 deletions

‎src/basic_memory/repository/postgres_search_query.py‎

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@
3131
SearchIndexKey,
3232
SearchIndexRow,
3333
capped_snippet_columns,
34+
snippet_length_columns,
3435
)
3536
from basic_memory.repository.search_query import (
3637
PreparedSearchQuery,
@@ -48,8 +49,10 @@
4849
# tsquery special characters that must not reach the parser as text.
4950
_TSQUERY_SPECIAL_CHARS = ("&", "|", "!", "(", ")", ":")
5051

51-
# Every FTS statement returns these columns plus a score.
52-
_RESULT_COLUMNS = f"""
52+
53+
def _result_columns(text_columns: str) -> str:
54+
"""The columns every FTS statement returns, plus a score."""
55+
return f"""
5356
search_index.project_id,
5457
search_index.id,
5558
search_index.title,
@@ -61,7 +64,7 @@
6164
search_index.to_id,
6265
search_index.relation_type,
6366
search_index.entity_id,
64-
{capped_snippet_columns("search_index")},
67+
{text_columns},
6568
search_index.category,
6669
search_index.created_at,
6770
search_index.updated_at"""
@@ -712,6 +715,7 @@ async def search(
712715
session: AsyncSession | None = None,
713716
candidate_keys: Sequence[SearchIndexKey] | None = None,
714717
trace: SearchTraceCollector | None = None,
718+
with_text: bool = True,
715719
) -> list[SearchIndexRow]:
716720
"""Run one tsquery page, retrying a malformed or empty strict query relaxed."""
717721
search_text = query.search_text
@@ -721,9 +725,14 @@ async def search(
721725
params = compiled.params
722726
params["limit"] = limit
723727
params["offset"] = offset
728+
text_columns = (
729+
capped_snippet_columns("search_index")
730+
if with_text
731+
else snippet_length_columns("search_index")
732+
)
724733

725734
sql = f"""
726-
SELECT{_RESULT_COLUMNS},
735+
SELECT{_result_columns(text_columns)},
727736
{compiled.score_expression} as score
728737
FROM {compiled.from_clause}
729738
WHERE {compiled.where_clause}

‎src/basic_memory/repository/search_filters.py‎

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -90,7 +90,15 @@ async def search(
9090
session: AsyncSession | None = None,
9191
candidate_keys: Sequence[SearchIndexKey] | None = None,
9292
trace: SearchTraceCollector | None = None,
93-
) -> list[SearchIndexRow]: ...
93+
with_text: bool = True,
94+
) -> list[SearchIndexRow]:
95+
"""Run one full-text page.
96+
97+
``with_text=False`` returns rows without note text (``content_snippet`` is None,
98+
``content_snippet_length`` set), for callers that rank first and fetch the text
99+
of the rows they keep.
100+
"""
101+
...
94102

95103
async def count(
96104
self,

‎src/basic_memory/repository/search_index_row.py‎

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,16 @@
2020
SNIPPET_READ_LIMIT = 4000
2121

2222

23+
def snippet_length_columns(table: str) -> str:
24+
"""SELECT columns carrying no note text, only the stored content_snippet's length.
25+
26+
Ranking reads ids and scores; the text of the rows a search returns is fetched once
27+
they are known (#1746). The length still answers content_length and the small-note
28+
rule before that text arrives.
29+
"""
30+
return f"NULL AS content_snippet, length({table}.content_snippet) AS content_snippet_length"
31+
32+
2333
def capped_snippet_columns(table: str) -> str:
2434
"""SELECT columns carrying a capped content_snippet and the stored text's length."""
2535
return (

0 commit comments

Comments
 (0)