You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
This repository was archived by the owner on Oct 14, 2026. It is now read-only.
Repository navigation
Commit 2d4ef8f
Browse filesBrowse the repository at this point in the historyBrowse files
Tanase Butcaru
committed
feat(data-access): semantic type registries, type-list lookup and shared embedding config (LLMO-7445)
Copy file name to clipboardExpand all lines: docs/adr/0002-embedding-client-and-semantic-index-utils.md
+8-2Lines changed: 8 additions & 2 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -40,23 +40,29 @@ Following ADR-0001's test (*true for every consumer → shared; specific to one
40
40
41
41
**Consumers own:**
42
42
- Caching. The durable query-embedding cache (`semantic_query_embedding`) and its key policy (`hash(normalizedText)+model+dims`) live in the data-access helpers/consumer, not the client — staleness/keying is a consumer concern.
43
-
- Model/dimension policy at the call site (the cache keyand the stored `vector(N)`column pin the model+dims contract).
43
+
- Model/dimension values come from `SEMANTIC_MATCHING_CONFIG` (`embeddingModel`, `embeddingDims`) in `data-access`, which owns the stored index; consumers import it and pass it as `model` / `dims`. It is a code constant, not env config: the label is persisted on every row and in the cache key, so writer and reader must never disagree, and changing it is a re-embed. `embeddingDims` must match the `vector(N)`columns. The helpers still accept any `model` / `dims`, so a same-dims model change can run the online re-embed below.
44
44
- Translating a client failure into an HTTP response (api-service returns 503; the audit-worker write path degrades best-effort).
45
45
46
46
### 4. Semantic-index helpers live in `data-access`, mirroring the URL index
47
47
48
48
`semantic-index.utils.js` mirrors `url-index.utils.js`: `syncOpportunitySemantic` (full-replace per `source_type`), `lookupOpportunitiesByVectors` (ANN read RPC wrapper, many query vectors per call), the batched `semantic_query_embedding` cache helpers (`getQueryEmbeddings` / `upsertQueryEmbeddings` / `touchQueryEmbeddings`), and `copyEntityVectors` (a thin wrapper over the `wrpc_copy_opportunity_semantic_vectors`**server-side** write RPC — the copy runs as `INSERT … SELECT`, no rows materialized in the caller). Text normalization + hashing live in this layer so a value written always matches a later read/re-sync. **The shared layer stores and reads pre-embedded vectors; it never embeds** — embedding is the writer's (audit-worker's) job via the client above.
49
49
50
+
**Type registries.**`OPPORTUNITY_SEMANTIC_SOURCE_TYPES` (kinds of embedded text: `topic`, later `claim`) and `OPPORTUNITY_SEMANTIC_ENTITY_TYPES` (opportunity types, from `OPPORTUNITY_TYPES`) are exported from this layer and are the only values the writer stores and the reader accepts; anything else is a `ValidationError`. The reader takes `sourceTypes` (required, non-empty) and optional `entityTypes` (omitted or empty = all), deduped, so a list can never exceed its registry. The DB does not duplicate the registries: the RPC only rejects an empty source-type list and NULL elements. A team that wants its opportunities searchable adds its source kind and/or opportunity type to the registries, indexes with them, and callers opt in by passing them.
51
+
50
52
## Consequences
51
53
52
54
-`gpt-client` gains an embeddings surface and its own `AZURE_EMBEDDING_*` config (documented in its README); the intended cost, not a drawback.
53
55
- The shared embedding model is a cross-repo contract: both the write and read paths must point at the **same deployment**. ANN search is **scoped to one generation** — `lookupOpportunitiesByVectors` (and the `rpc_opportunity_semantic_search` RPC) filter on `model`+`dims`, and the query cache is keyed on `model+dims`. This makes a **same-dims model change an online, forward-only re-embed** (chosen over an atomic swap — see Alternatives): the read path queries the new generation immediately while the write path back-populates it opportunity-by-opportunity on the normal refresh cadence. **Accepted cost:** during the migration window, opportunities not yet re-embedded don't match new-generation queries (reduced recall), self-healing as the backfill completes. A **dimension change** is out of this online path — it needs a new `vector(N)` column and an atomic swap, not a forward-only re-embed.
54
-
-**Deploy ordering:**`lookupOpportunitiesByVectors` calls `rpc_opportunity_semantic_search` with `p_query_embeddings` (a list of vectors) plus `p_model`/`p_dims`, so the data-service release carrying that signature must be deployed **before** any consumer upgrades to this library version — otherwise PostgREST can't resolve the function (`PGRST202`).
56
+
-**Deploy ordering:**`lookupOpportunitiesByVectors` calls `rpc_opportunity_semantic_search` with `p_source_types`/`p_entity_types` (lists) and `p_query_embeddings` (a list of vectors) plus `p_model`/`p_dims`, so the data-service release carrying that signature must be deployed **before** any consumer upgrades to this library version — otherwise PostgREST can't resolve the function (`PGRST202`).
55
57
- Consumers stay thin: they inject the `EmbeddingProvider`, own their cache/status mapping, and call the `data-access` helpers — no matching logic is reimplemented per consumer.
56
58
57
59
## Alternatives considered
58
60
59
61
-**A new `spacecat-shared-embedding-client` package.** Rejected for now: same vendor/auth as `gpt-client`; deferred behind the `EmbeddingProvider` seam so a later extraction is non-breaking.
60
62
-**Overloading `AzureOpenAIClient`.** Rejected: embeddings are a different API surface; a distinct class keeps each client cohesive.
61
63
-**Embedding inside the shared data-access layer.** Rejected: transport/auth belongs in a client (ADR-0001 boundary), and the write side (audit-worker) owns when/what to embed.
64
+
-**Producer-namespaced source types (e.g. `offsite_topic`).** Rejected: `entity_type` already identifies the producer, so namespacing `source_type` would duplicate it. Generic kinds plus an optional `entityTypes` filter keep the two axes separate. Accepted cost: a new producer that indexes `topic` shows up in by-topics results by default unless callers pass `entityTypes`.
65
+
-**A `*` (all types) wildcard.** Rejected: results would change silently whenever a new producer starts indexing, and an exact scan's cost grows with every type included. Callers that want every kind pass `Object.values(OPPORTUNITY_SEMANTIC_SOURCE_TYPES)` explicitly.
66
+
-**Registries enforced in the DB (CHECK/enum).** Rejected: a migration for every new type; the registries live next to the helpers that enforce them.
67
+
-**Embedding model label from env (e.g. derived from `AZURE_EMBEDDING_DEPLOYMENT`).** Rejected: writer and reader have separate env configs, so a mismatch would silently return no matches; a deployment name is an alias, not a model id; and changing the label is a re-embed, which should be a reviewed release.
62
68
-**Atomic-swap migration (full index rebuild + coordinated cutover) instead of the online, generation-scoped re-embed.** Rejected: it would require rebuilding the whole opportunity index and flipping the read path over in lockstep across four repos — high operational risk and coordination cost for a rare event. The chosen online path (ANN search filtered by `model`+`dims`) lets the read path move to the new generation immediately while the write path back-populates on its normal cadence; the accepted cost is reduced recall for not-yet-re-embedded opportunities during the migration window (see Consequences). Atomic swap remains the required approach for a **dimension** change, which the online path deliberately does not cover.
`OPPORTUNITY_SEMANTIC_SOURCE_TYPES` (kinds of embedded text) and `OPPORTUNITY_SEMANTIC_ENTITY_TYPES` (opportunity types) are the registries for `source_type` and `entity_type`: the writer and reader reject any value outside them. To index a new kind or opportunity type, add it to the registry first.
182
+
183
+
`SEMANTIC_MATCHING_CONFIG` (`embeddingModel`, `embeddingDims`) is the embedding generation the writer and reader both use; pass it as `model` / `dims` rather than hardcoding the values. It is a code constant, not env config, so the two sides can never disagree.
184
+
175
185
The batched helpers group their calls internally (search in groups of up to 20 vectors, cache reads/touches in groups of 50 hashes), so callers pass whole lists rather than looping per item.
0 commit comments