mirror of
https://github.com/basicmachines-co/basic-memory
synced 2026-06-21 13:47:35 +00:00
8c05a9ec80
Signed-off-by: phernandez <paul@basicmachines.co>
168 lines
5.2 KiB
Python
168 lines
5.2 KiB
Python
"""Focused multi-term semantic retrieval integration tests.
|
|
|
|
These tests validate that multi-word and natural-language queries retrieve
|
|
relevant results over the semantic benchmark fixture corpus.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import pytest
|
|
|
|
from basic_memory.config import DatabaseBackend
|
|
from basic_memory.schemas.search import SearchItemType, SearchQuery, SearchRetrievalMode
|
|
|
|
from semantic.conftest import (
|
|
ALL_COMBOS,
|
|
SearchCombo,
|
|
create_search_service,
|
|
skip_if_needed,
|
|
_create_fastembed_provider,
|
|
)
|
|
from semantic.corpus import QueryCase, seed_benchmark_notes
|
|
from semantic.metrics import first_relevant_rank
|
|
|
|
|
|
FASTEMBED_COMBOS = [combo for combo in ALL_COMBOS if combo.provider_name == "fastembed"]
|
|
|
|
# High-signal natural-language multi-term queries mapped to benchmark topics.
|
|
NATURAL_LANGUAGE_MULTI_TERM_CASES = [
|
|
QueryCase(
|
|
text="How do access tokens and refresh tokens work for login sessions?",
|
|
expected_topic="auth",
|
|
),
|
|
QueryCase(
|
|
text="How do schema migrations and database indexes improve query performance?",
|
|
expected_topic="database",
|
|
),
|
|
QueryCase(
|
|
text="How does the filesystem watcher detect changes and trigger reindex?",
|
|
expected_topic="sync",
|
|
),
|
|
QueryCase(
|
|
text="How does the agent build memory context for retrieval across sessions?",
|
|
expected_topic="agent",
|
|
),
|
|
]
|
|
|
|
# Keep vector assertions on the most stable topic queries.
|
|
LEXICAL_MULTI_TERM_CASES = [
|
|
QueryCase(text="authentication session token oauth refresh login", expected_topic="auth"),
|
|
QueryCase(text="database migration schema sqlite postgres index", expected_topic="database"),
|
|
]
|
|
|
|
HYBRID_MIN_HITS_AT_5 = {
|
|
"sqlite-fastembed": 2,
|
|
"postgres-fastembed": 3,
|
|
}
|
|
|
|
VECTOR_MIN_HITS_AT_10 = {
|
|
"sqlite-fastembed": 1,
|
|
"postgres-fastembed": 2,
|
|
}
|
|
|
|
|
|
async def _create_service_for_combo(
|
|
combo: SearchCombo,
|
|
sqlite_engine_factory,
|
|
postgres_engine_factory,
|
|
tmp_path,
|
|
):
|
|
"""Create a configured SearchService for the selected backend/provider combo."""
|
|
if combo.backend == DatabaseBackend.SQLITE:
|
|
engine_factory_result = sqlite_engine_factory
|
|
else:
|
|
if postgres_engine_factory is None:
|
|
pytest.skip("Postgres engine not available")
|
|
engine_factory_result = postgres_engine_factory
|
|
|
|
provider = _create_fastembed_provider()
|
|
return await create_search_service(
|
|
engine_factory_result, combo, tmp_path, embedding_provider=provider
|
|
)
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
@pytest.mark.semantic
|
|
@pytest.mark.parametrize("combo", FASTEMBED_COMBOS, ids=[c.name for c in FASTEMBED_COMBOS])
|
|
async def test_multiterm_paraphrase_queries_rank_expected_topic_with_hybrid(
|
|
combo: SearchCombo,
|
|
sqlite_engine_factory,
|
|
postgres_engine_factory,
|
|
tmp_path,
|
|
):
|
|
"""Hybrid retrieval should rank relevant topic notes for natural-language multi-term queries."""
|
|
skip_if_needed(combo)
|
|
|
|
search_service = await _create_service_for_combo(
|
|
combo,
|
|
sqlite_engine_factory,
|
|
postgres_engine_factory,
|
|
tmp_path,
|
|
)
|
|
await seed_benchmark_notes(search_service, note_count=120)
|
|
|
|
hits_at_5 = 0
|
|
for case in NATURAL_LANGUAGE_MULTI_TERM_CASES:
|
|
results = await search_service.search(
|
|
SearchQuery(
|
|
text=case.text,
|
|
retrieval_mode=SearchRetrievalMode.HYBRID,
|
|
entity_types=[SearchItemType.ENTITY],
|
|
),
|
|
limit=10,
|
|
)
|
|
|
|
assert results, f"No results for query: {case.text}"
|
|
rank = first_relevant_rank(results, case.expected_topic, k=5)
|
|
if rank is not None:
|
|
hits_at_5 += 1
|
|
|
|
min_hits = HYBRID_MIN_HITS_AT_5[combo.name]
|
|
assert hits_at_5 >= min_hits, (
|
|
f"Hybrid multi-term relevance too low for {combo.name}: "
|
|
f"hits@5={hits_at_5}/{len(NATURAL_LANGUAGE_MULTI_TERM_CASES)} (min={min_hits})"
|
|
)
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
@pytest.mark.semantic
|
|
@pytest.mark.parametrize("combo", FASTEMBED_COMBOS, ids=[c.name for c in FASTEMBED_COMBOS])
|
|
async def test_multiterm_lexical_queries_return_relevant_topic_with_vector(
|
|
combo: SearchCombo,
|
|
sqlite_engine_factory,
|
|
postgres_engine_factory,
|
|
tmp_path,
|
|
):
|
|
"""Vector-only retrieval should return relevant notes for multi-term lexical queries."""
|
|
skip_if_needed(combo)
|
|
|
|
search_service = await _create_service_for_combo(
|
|
combo,
|
|
sqlite_engine_factory,
|
|
postgres_engine_factory,
|
|
tmp_path,
|
|
)
|
|
await seed_benchmark_notes(search_service, note_count=120)
|
|
|
|
hits_at_10 = 0
|
|
for case in LEXICAL_MULTI_TERM_CASES:
|
|
results = await search_service.search(
|
|
SearchQuery(
|
|
text=case.text,
|
|
retrieval_mode=SearchRetrievalMode.VECTOR,
|
|
entity_types=[SearchItemType.ENTITY],
|
|
),
|
|
limit=10,
|
|
)
|
|
|
|
assert results, f"No results for query: {case.text}"
|
|
rank = first_relevant_rank(results, case.expected_topic, k=10)
|
|
if rank is not None:
|
|
hits_at_10 += 1
|
|
|
|
min_hits = VECTOR_MIN_HITS_AT_10[combo.name]
|
|
assert hits_at_10 >= min_hits, (
|
|
f"Vector multi-term relevance too low for {combo.name}: "
|
|
f"hits@10={hits_at_10}/{len(LEXICAL_MULTI_TERM_CASES)} (min={min_hits})"
|
|
)
|