4f7f915348
Fuse a vector-similarity branch into #529's lexical RRF. Query embed via a global TEI sidecar (or per-workspace provider), degrading transparently to the byte-identical Phase-A lexical path on any failure; a kill-switch and env knobs gate it. - ai.service: resolveEmbeddingProvider (workspace→global TEI fallback) with a deterministic config fingerprint; embedQuery (query prefix + short 800ms timeout); extract embedWithModel core shared with embedTexts. - page-embedding.repo: vectorCandidateArm fragment (page-level NN, dim + active-fingerprint filtered) + fingerprint on insertChunks. - search.service: 3-branch RRF over lexical ∪ vector candidates; try/catch wraps only the embed (permission filter stays outside, fail-closed); semantic degrade emits search.semantic.degraded; response gains semantic{state,available,reason}. - indexer: resolve provider, prepend doc prefix, stamp fingerprint per row. - migration 20260712T120000: add nullable page_embeddings.fingerprint + composite index. - infra: TEI embeddings sidecar in docker-compose + EMBEDDING_*/SEARCH_* in .env.example. - tests: 7 semantic int cases (vector-only hit, sidecar-down, no-provider, permission-over-union fail-closed, hung sidecar, lexical∪vector de-dup, fingerprint isolation) + fingerprint/prefix/timeout unit tests. total now = permission-filtered size of (lexical ∪ vector top-N) — a documented change from Phase A's exact lexical count (falls back to it on degrade). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>