Problem
Search results truncate content_snippet to 250 chars via SearchIndexRow.content property (CONTENT_DISPLAY_LIMIT = 250 in src/basic_memory/repository/search_index_row.py:45). This means search always returns the beginning of the note, not the relevant section that matched the query.
Evidence (Benchmark)
Ran 38-query retrieval benchmark against plugin MCP server:
- Recall@5: 94.6% — we find the right notes
- Content Hit Rate: 21.4% — but the returned snippet rarely contains the specific fact
Example: query "What is the team plan pricing?" finds the correct note (2026-02-12.md) but returns:
# 2026-02-12 — Wednesday\n\n## Observations\n- [date] 2026-02-12\n- [type] daily-note\n\n## Standup\n- Maya integrating Linear cycle time API...
The pricing info ($9/mo per seat) is deeper in the note, past the 250-char cutoff.
Root Cause Chain
SearchIndexRow.content_snippet stores full content (good)
SearchIndexRow.content property truncates to first 250 chars (bad for relevance)
SearchResult.content gets the truncated version (src/basic_memory/api/v2/utils.py:183: content=r.content)
- MCP search tool returns truncated content to agents
- Agents/benchmarks only see note header, not the relevant section
Proposed Fix
Add matched_chunk field to SearchResult
The vector search pipeline already matches against specific chunks (chunk_key = "{type}:{id}:{chunk_index}"). The matching chunk text contains the relevant section but is discarded before results are returned.
Changes needed:
1. src/basic_memory/schemas/search.py — Add field to SearchResult
class SearchResult(BaseModel):
# ... existing fields ...
content: Optional[str] = None
matched_chunk: Optional[str] = None # NEW: the chunk text that scored highest in vector search
2. src/basic_memory/repository/search_index_row.py — Add chunk text field
@dataclass
class SearchIndexRow:
# ... existing fields ...
matched_chunk_text: Optional[str] = None # Populated by vector search
3. src/basic_memory/repository/sqlite_search_repository.py — Thread chunk text through vector results
In _vector_search(), the query already JOINs search_vector_chunks. The chunk_text column is available but not selected. Add it to the result:
-- Current (approx line 807):
SELECT c.entity_id, c.chunk_key, vector_matches.distance AS best_distance
FROM ... JOIN search_vector_chunks c ON c.id = vector_matches.rowid
-- Proposed:
SELECT c.entity_id, c.chunk_key, c.chunk_text, vector_matches.distance AS best_distance
FROM ... JOIN search_vector_chunks c ON c.id = vector_matches.rowid
Then populate matched_chunk_text on the SearchIndexRow when building results.
4. src/basic_memory/repository/postgres_search_repository.py — Same change for Postgres
5. src/basic_memory/api/v2/utils.py — Pass through to SearchResult
SearchResult(
# ... existing fields ...
content=r.content,
matched_chunk=r.matched_chunk_text, # NEW
)
6. Fallback for FTS-only search
When vector search is not used (text-only mode), matched_chunk will be None. Consider a lightweight fallback: find the paragraph in content_snippet that has the best keyword overlap with the query and return that as the snippet.
Impact
- Benchmark content hit rate should jump from 21.4% to 70%+ with chunk-level snippets
- Agent quality improves — agents get the relevant section, not the note header
- No breaking changes —
matched_chunk is a new optional field, content behavior unchanged
- Token efficiency — agents get a focused snippet instead of needing to
read_note for every search result
Files to Modify
src/basic_memory/schemas/search.py (SearchResult model)
src/basic_memory/repository/search_index_row.py (SearchIndexRow dataclass)
src/basic_memory/repository/sqlite_search_repository.py (vector search query + result mapping)
src/basic_memory/repository/postgres_search_repository.py (same for postgres)
src/basic_memory/repository/search_repository_base.py (hybrid merge logic)
src/basic_memory/api/v2/utils.py (SearchResult construction)
- Tests in
tests/repository/ and tests/api/
Benchmark Reference
Related
Problem
Search results truncate
content_snippetto 250 chars viaSearchIndexRow.contentproperty (CONTENT_DISPLAY_LIMIT = 250insrc/basic_memory/repository/search_index_row.py:45). This means search always returns the beginning of the note, not the relevant section that matched the query.Evidence (Benchmark)
Ran 38-query retrieval benchmark against plugin MCP server:
Example: query "What is the team plan pricing?" finds the correct note (2026-02-12.md) but returns:
The pricing info (
$9/mo per seat) is deeper in the note, past the 250-char cutoff.Root Cause Chain
SearchIndexRow.content_snippetstores full content (good)SearchIndexRow.contentproperty truncates to first 250 chars (bad for relevance)SearchResult.contentgets the truncated version (src/basic_memory/api/v2/utils.py:183:content=r.content)Proposed Fix
Add
matched_chunkfield to SearchResultThe vector search pipeline already matches against specific chunks (
chunk_key = "{type}:{id}:{chunk_index}"). The matching chunk text contains the relevant section but is discarded before results are returned.Changes needed:
1.
src/basic_memory/schemas/search.py— Add field to SearchResult2.
src/basic_memory/repository/search_index_row.py— Add chunk text field3.
src/basic_memory/repository/sqlite_search_repository.py— Thread chunk text through vector resultsIn
_vector_search(), the query already JOINssearch_vector_chunks. Thechunk_textcolumn is available but not selected. Add it to the result:Then populate
matched_chunk_texton theSearchIndexRowwhen building results.4.
src/basic_memory/repository/postgres_search_repository.py— Same change for Postgres5.
src/basic_memory/api/v2/utils.py— Pass through to SearchResult6. Fallback for FTS-only search
When vector search is not used (text-only mode),
matched_chunkwill be None. Consider a lightweight fallback: find the paragraph incontent_snippetthat has the best keyword overlap with the query and return that as the snippet.Impact
matched_chunkis a new optional field,contentbehavior unchangedread_notefor every search resultFiles to Modify
src/basic_memory/schemas/search.py(SearchResult model)src/basic_memory/repository/search_index_row.py(SearchIndexRow dataclass)src/basic_memory/repository/sqlite_search_repository.py(vector search query + result mapping)src/basic_memory/repository/postgres_search_repository.py(same for postgres)src/basic_memory/repository/search_repository_base.py(hybrid merge logic)src/basic_memory/api/v2/utils.py(SearchResult construction)tests/repository/andtests/api/Benchmark Reference
benchmark/run.tsandbenchmark/results/bm-mcp-small-results.jsonRelated