refactor(search): structural per-instance query side-channel; doc search billing gap

Round-7 claude-review (no blockers):

- 🟡 query_token_count/query_embedding were class-level defaults on
  SearchAlgorithm, relying on each subclass's __init__ to shadow them. Added
  SearchAlgorithm.__init__ that sets both as instance attributes and had
  BM25HybridSearchAlgorithm + SemanticSearchAlgorithm call super().__init__(),
  so per-request concurrency isolation is structural, not by convention.
- 🟡 Documented the v1 search-path billing gap: record_search_usage fires only
  on a fully successful search, so if the query embed succeeded (provider billed
  + Prometheus recorded) but a later step (Qdrant/verify) raised, no
  tokens_embedded billing row is written. Added a NOTE at the call site.

Left as-is (reasons in PR reply): deployment sequencing (CP METRIC_EVENT_NAMES
already renamed; pipeline inert); Ollama _detect_dimension double dimension-set
(idempotent, same value); SonarQube issues — 1 is the deliberate TODO(#282)
(INFO), 4 are S7503 false positives on async test stubs that must be awaitable
(gate green).

Deck #284.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Chris Coutinho
2026-06-08 13:52:51 +02:00
co-authored by Claude Opus 4.8
parent 973f80e7b9
commit 9369832977
4 changed files with 25 additions and 7 deletions
+6 -7
View File
@@ -56,16 +56,15 @@ class BM25HybridSearchAlgorithm(SearchAlgorithm):
f"Invalid fusion algorithm '{fusion}'. Must be 'rrf' or 'dbsf'"
)
# super() sets the per-instance query_embedding / query_token_count
# side-channel; this adds the cache key for it.
super().__init__()
self.score_threshold = score_threshold
self.fusion = models.Fusion.RRF if fusion == "rrf" else models.Fusion.DBSF
self.fusion_name = fusion
# Per-request query-embedding cache. ``_embedded_query`` is the query
# string whose dense embedding is held in ``query_embedding`` — repeated
# search() calls on this instance (the doc_types loop) reuse it. These
# shadow the class-level defaults on SearchAlgorithm; set here so all
# three cache fields are instance attributes from construction.
self.query_embedding: list[float] | None = None
self.query_token_count: int | None = None
# ``_embedded_query`` is the query string whose dense embedding is held
# in ``query_embedding`` — repeated search() calls on this per-request
# instance (the doc_types loop) reuse it instead of re-embedding.
self._embedded_query: str | None = None
@property