Skip to content

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

  1. Embeddings use OpenAI text-embedding-3-small (1536 dimensions) through a small dedicated adapter called directly with the existing OPENAI_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.
  2. The embedded text is the eight company_profile_signals fields 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.
  3. The text recipe, model, and dimensions together form the embedding_version stored beside each vector. Vectors from different versions are never compared; a change is a re-backfill, not an in-place rewrite.
  4. 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.
  5. 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.
  6. 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, 22023 caller 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.