Skip to content

Commit 2e1b9a0

Browse files
committed
test(core): share rerank fixtures across standalone suites
Signed-off-by: phernandez <paul@basicmachines.co>
1 parent 1815370 commit 2e1b9a0

5 files changed

Lines changed: 187 additions & 162 deletions

File tree

pyproject.toml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -192,6 +192,10 @@ style = "pep440"
192192
bump = true
193193
fallback-version = "0.0.0"
194194

195+
[tool.ty.environment]
196+
# Shared test helpers use the same import root as pytest's pythonpath above.
197+
extra-paths = ["tests"]
198+
195199
[tool.ty.rules]
196200
all = "error"
197201

test-int/test_stable_rerank_pagination.py

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -14,12 +14,10 @@
1414
from basic_memory.deps.services import get_search_service_v2_external
1515
from basic_memory.models import Project
1616
from basic_memory.services.search_service import SearchService
17-
from tests.repository.test_rerank_pipeline import (
17+
from semantic_search_helpers import (
1818
BackendSearchRepository,
1919
_FakeReranker,
2020
rerank_search_repository as rerank_search_repository,
21-
)
22-
from tests.repository.test_stable_rerank_pagination import (
2321
pagination_repository as pagination_repository,
2422
)
2523

tests/repository/test_rerank_pipeline.py

Lines changed: 8 additions & 113 deletions
Original file line numberDiff line numberDiff line change
@@ -29,66 +29,14 @@
2929
from basic_memory.repository.sqlite_search_repository import SQLiteSearchRepository
3030
from basic_memory.schemas.search import SearchItemType, SearchRetrievalMode
3131
from basic_memory.services.entity_service import EntityService
32-
33-
type BackendSearchRepository = SQLiteSearchRepository | PostgresSearchRepository
34-
35-
36-
class _StubEmbeddingProvider:
37-
"""Deterministic embeddings that give the two auth notes DIFFERENT similarity.
38-
39-
An "auth" doc containing "deep" is tilted slightly off the query axis, so vector
40-
retrieval ranks the plain-auth note strictly above it. That makes the pre-rerank
41-
baseline a real ordering (not a tie), so a rerank that promotes the lower note is
42-
a genuine "recover the below-cutoff doc" scenario (#950), not a coin flip.
43-
"""
44-
45-
model_name = "stub"
46-
dimensions = 4
47-
48-
async def embed_query(self, text: str) -> list[float]:
49-
return self._vectorize(text)
50-
51-
async def embed_documents(self, texts: list[str]) -> list[list[float]]:
52-
return [self._vectorize(t) for t in texts]
53-
54-
def runtime_log_attrs(self) -> dict[str, Any]:
55-
return {}
56-
57-
@staticmethod
58-
def _vectorize(text: str) -> list[float]:
59-
lowered = text.lower()
60-
if "auth" not in lowered:
61-
return [0.0, 0.0, 0.0, 1.0]
62-
# Unit vectors; cos with the query axis [1,0,0,0] is 1.0 vs 0.9.
63-
if "deep" in lowered:
64-
return [0.9, 0.4358898943540674, 0.0, 0.0]
65-
return [1.0, 0.0, 0.0, 0.0]
66-
67-
68-
class _FakeReranker:
69-
"""Scores a document by the marker substring it contains; records call count."""
70-
71-
model_name = "fake-reranker"
72-
73-
def __init__(self, score_by_marker: dict[str, float]):
74-
self.score_by_marker = score_by_marker
75-
self.calls = 0
76-
self.document_batches: list[list[str]] = []
77-
78-
async def rerank(self, query: str, documents: list[str]) -> list[float]:
79-
self.calls += 1
80-
self.document_batches.append(documents)
81-
scores = []
82-
for doc in documents:
83-
score = 0.0
84-
for marker, value in self.score_by_marker.items():
85-
if marker in doc:
86-
score = value
87-
scores.append(score)
88-
return scores
89-
90-
def runtime_log_attrs(self) -> dict[str, Any]:
91-
return {}
32+
from semantic_search_helpers import (
33+
BackendSearchRepository,
34+
_StubEmbeddingProvider,
35+
_FakeReranker,
36+
_entity_row,
37+
_semantic_search_repository,
38+
rerank_search_repository as rerank_search_repository,
39+
)
9240

9341

9442
class _BadReranker:
@@ -156,24 +104,6 @@ def test_validate_rerank_scores_rejects_non_numeric_value():
156104
validate_rerank_scores(["not-a-score"], expected_count=1)
157105

158106

159-
def _entity_row(*, project_id: int, row_id: int, title: str, permalink: str, content: str):
160-
now = datetime.now(timezone.utc)
161-
return SearchIndexRow(
162-
project_id=project_id,
163-
id=row_id,
164-
type=SearchItemType.ENTITY.value,
165-
title=title,
166-
permalink=permalink,
167-
file_path=f"{permalink}.md",
168-
metadata={"note_type": "spec"},
169-
entity_id=row_id,
170-
content_stems=content,
171-
content_snippet=content,
172-
created_at=now,
173-
updated_at=now,
174-
)
175-
176-
177107
def _row(**overrides) -> SearchIndexRow:
178108
now = datetime.now(timezone.utc)
179109
base: dict[str, Any] = dict(
@@ -508,41 +438,6 @@ async def test_rerank_paginate_surfaces_permanent_faults(exc):
508438
# --- End-to-end through both repository backends ---
509439

510440

511-
def _semantic_search_repository(
512-
session_maker: Any,
513-
project_id: int,
514-
app_config: BasicMemoryConfig,
515-
**config_updates: object,
516-
) -> BackendSearchRepository:
517-
config = app_config.model_copy(
518-
update={
519-
"semantic_search_enabled": True,
520-
"semantic_min_similarity": 0.0,
521-
**config_updates,
522-
}
523-
)
524-
repository_type = (
525-
PostgresSearchRepository
526-
if config.database_backend == DatabaseBackend.POSTGRES
527-
else SQLiteSearchRepository
528-
)
529-
return repository_type(
530-
session_maker,
531-
project_id=project_id,
532-
app_config=config,
533-
embedding_provider=_StubEmbeddingProvider(),
534-
)
535-
536-
537-
@pytest.fixture
538-
def rerank_search_repository(
539-
session_maker: Any,
540-
test_project: Any,
541-
app_config: BasicMemoryConfig,
542-
) -> BackendSearchRepository:
543-
return _semantic_search_repository(session_maker, test_project.id, app_config)
544-
545-
546441
async def _index_two_auth_notes(repo: BackendSearchRepository) -> None:
547442
await repo.init_search_index()
548443
await repo.bulk_index_items(

tests/repository/test_stable_rerank_pagination.py

Lines changed: 2 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -10,62 +10,18 @@
1010

1111
from basic_memory import db
1212
from basic_memory.config import DatabaseBackend
13-
from basic_memory.models import Entity
1413
from basic_memory.repository.search_index_row import SearchIndexRow
1514
from basic_memory.repository.search_trace import SearchTraceCollector
1615
from basic_memory.schemas.search import SearchRetrievalMode
17-
from tests.repository.test_rerank_pipeline import (
16+
from semantic_search_helpers import (
1817
BackendSearchRepository,
1918
_FakeReranker,
2019
_entity_row,
20+
pagination_repository as pagination_repository,
2121
rerank_search_repository as rerank_search_repository,
2222
)
2323

2424

25-
@pytest.fixture
26-
async def pagination_repository(
27-
rerank_search_repository: BackendSearchRepository,
28-
) -> BackendSearchRepository:
29-
repo = rerank_search_repository
30-
repo._semantic_vector_k = 8
31-
repo._reranker_candidates = 2
32-
repo._reranker_max_document_chars = 2000
33-
await repo.init_search_index()
34-
for index in range(32):
35-
# Distinct chunk passages straddle the fixed prefix. Some rows have only
36-
# lexical evidence, others only vector evidence, and one is filter-rejected.
37-
content = "auth session token " + ("deep " if index % 2 else "")
38-
if index < 2:
39-
content = "\n\n".join(f"{content}passage {n} " + "detail " * 160 for n in range(3))
40-
if index == 30:
41-
content = "oauth related concepts without lexical terms"
42-
row = _entity_row(
43-
project_id=repo.project_id,
44-
row_id=700 + index,
45-
title=f"Note {index:02d}",
46-
permalink=f"{'excluded' if index == 31 else 'notes'}/{index:02d}",
47-
content=content,
48-
)
49-
async with db.scoped_session(repo.session_maker) as session:
50-
session.add(
51-
Entity(
52-
id=row.id,
53-
project_id=repo.project_id,
54-
title=row.title,
55-
note_type="spec",
56-
content_type="text/markdown",
57-
permalink=row.permalink,
58-
file_path=row.file_path,
59-
entity_metadata={"status": "active"},
60-
)
61-
)
62-
await session.commit()
63-
await repo.index_item(row)
64-
if index != 29:
65-
await repo.sync_entity_vectors(row.id)
66-
return repo
67-
68-
6925
@pytest.mark.asyncio
7026
@pytest.mark.parametrize("mode", [SearchRetrievalMode.VECTOR, SearchRetrievalMode.HYBRID])
7127
@pytest.mark.parametrize("enabled", [False, True])

0 commit comments

Comments
 (0)