Hybrid similarity embeds profile text through a dedicated OpenAI adapter¶
Status: accepted Date: 2026-08-31 Deciders: Chris (solo founder) Depends on: ADR-0014 and ADR-0017 Tracking: read-contract gaps #428 (Request 7)
Context¶
The sales repository's ADR-0005 requires a runtime Hybrid similarity contract:
Reciprocal Rank Fusion over a semantic ranking and a structured ranking, with
no structured-only fallback. The semantic ranking needs embeddings, and the
hub has none — no vector columns, no extension, no model call. The existing
LLM path runs chat completions through OpenRouter with a costed
llm_operations ledger; OpenRouter serves no embeddings endpoint.
The questions were which provider computes the vectors, what text the vectors represent, and when they are computed.
Decision¶
- Embeddings use OpenAI
text-embedding-3-small(1536 dimensions) through a small dedicated adapter called directly with the existingOPENAI_API_KEY. The adapter records one spend-ledger row per call, in the shape of the existing LLM ledger. OpenRouter stays the chat path; no compatibility layer is added. - The embedded text is the eight
company_profile_signalsfields from the current view, followed by a deterministic structured fact block: company name, primary industry code and text, employee bucket, revenue bucket, and observed top relationship types. The same recipe applies to every company, so thin-profile companies still embed anchored to their industry neighborhood. - The text recipe, model, and dimensions together form the
embedding_versionstored beside each vector. Vectors from different versions are never compared; a change is a re-backfill, not an in-place rewrite. - Embeddings are computed when the pipeline that produced the profile signals finishes, plus an idempotent backfill for already-collected profiles. The read contract never calls a model; it ranks over stored vectors only.
- The structured ranking uses three declared dimensions — industry match, size proximity, financial health — each banded from curated tables. Graph overlap is deliberately deferred to a later profile version, when the observed graph holds data to rank on.
- Hard filters (industry group in/ex, employee bucket range, revenue band
range, explicit CVR exclusions) apply before ranking. The contract surface
is a PostgREST function with the posture of
search_companies: security definer, keyset pagination, fixed page size,22023caller errors.
Consequences¶
The enrichment pipeline gains one new external dependency and its failure mode; a failed embedding stage leaves the company without a current vector and therefore outside the semantic ranking until the backfill heals it — which is the consumer's declared missing-profile state, not a silent degradation. Embedding spend is bounded by shortlist scope and recorded per call.