|
29 | 29 | from basic_memory.repository.sqlite_search_repository import SQLiteSearchRepository |
30 | 30 | from basic_memory.schemas.search import SearchItemType, SearchRetrievalMode |
31 | 31 | 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 | +) |
92 | 40 |
|
93 | 41 |
|
94 | 42 | class _BadReranker: |
@@ -156,24 +104,6 @@ def test_validate_rerank_scores_rejects_non_numeric_value(): |
156 | 104 | validate_rerank_scores(["not-a-score"], expected_count=1) |
157 | 105 |
|
158 | 106 |
|
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 | | - |
177 | 107 | def _row(**overrides) -> SearchIndexRow: |
178 | 108 | now = datetime.now(timezone.utc) |
179 | 109 | base: dict[str, Any] = dict( |
@@ -508,41 +438,6 @@ async def test_rerank_paginate_surfaces_permanent_faults(exc): |
508 | 438 | # --- End-to-end through both repository backends --- |
509 | 439 |
|
510 | 440 |
|
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 | | - |
546 | 441 | async def _index_two_auth_notes(repo: BackendSearchRepository) -> None: |
547 | 442 | await repo.init_search_index() |
548 | 443 | await repo.bulk_index_items( |
|
0 commit comments