EmbedAll previously ran the full text-staleness CTE on every page, paying
five item_people LATERAL joins per eligible row just to detect whether an
item's canonical text had drifted - even when the real work was embedding
brand-new (missing) items during active backfill.
Restructure EmbedAll into two passes:
- Pass 1 (cheap): drain missing/model-stale items via the repurposed
ItemsNeedingEmbedding query (single LEFT JOIN, no LATERAL), paged by a
content_id cursor so a failed/skipped item is retried next run instead of
stalling the page.
- Pass 2 (expensive): only once Pass 1 fully drains, run one bounded
ListEmbeddingTextCandidates scan (LIMIT embeddingTextStaleQuotaPerRun=200)
to re-embed text-drifted items. Re-embedding refreshes canonical_text, so
handled rows drop out next run - no Pass 2 cursor needed.
Coverage-first tradeoff (documented in EmbedAll): under steady state Pass 1
drains every run so text-stale items stay fresh; only under pathological
continuous heavy ingest does Pass 2 get skipped, deliberately prioritizing
covering new items over re-embedding changed ones.
Supporting changes:
- ItemsNeedingEmbedding gains an afterID cursor + ORDER BY; SQL extracted to
buildItemsNeedingEmbeddingSQL for a cheap-shape unit test (no item_people /
LATERAL).
- Add an `embedder` interface seam (Engine.embClient) so EmbedAll is testable
with a fake; *embeddings.Client still satisfies it.
- Extract embedBatch to DRY both passes, preserving quota/billing early-return,
single-item fallback, ensureEmbeddingLock-before-upsert, and skip-on-store-error.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Add recommendations.Engine.ActiveEmbeddingModel, which reads the
embedding lock from server_settings and returns the locked model (or ""
when no lock is established). Define the catalog
CatalogSemanticModelProvider interface so a later task can scope vector
coverage checks to the active model; the recommendations Engine already
satisfies it. No wiring yet (Task 4 injects recEngine as this provider).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>