Skip to content

Pipeline lifecycle

Every product row carries a pipeline_status that tracks where it is in the enrich → index → search pipeline. Search only returns rows in ready status (when the collection has an enrich pipeline). The status is explicit, durable, and queryable — not inferred from timestamps alone.

StatusMeaningSearchable?
pendingIngested but not yet enriched/indexed, or awaiting first successful indexNo
readyPassed the indexing gate and indexed successfullyYes
quarantinedEnriched but rejected by the indexing gate (low confidence, empty surface, etc.)No
failedA pipeline stage threw; row has last_error, attempt_count, and next_attempt_atNo
deadExceeded max retry attempts (attempt_count >= maxAttempts)No
pending ──enrich+gate──► ready (surfaces + fts written; vectors when an embedder is configured)
│ │
│ └── gate rejects ──► quarantined (vectors nulled)
└── stage throws ──► failed ──retryFailed──► ready | failed | dead
└── attempt_count >= max ──► dead

During app.enrich.enrich(...), each row runs the enrich stages, then indexing.surfaces builders persist doc, rerank_doc, and fts_src. The required indexing.gate decides whether the row is indexable:

  • Gate returns { index: true }pipeline_status = 'ready' (vectors are written when an embedder is configured).
  • Gate returns { index: false, reason }pipeline_status = 'quarantined' with gate_reason. Previously indexed vectors are nulled so stale hits cannot leak into search.
  • An empty surface text → quarantined with reason empty:<surface-key>.

The fashion template’s gate (fashion.indexing().gate) quarantines non-apparel rows, low-confidence enrichments, uncertain load-bearing fields, and cross-signal disagreements. See Tuning search relevance for the data-quality loop that feeds the gate.

Enrich writes surfaces and optional vectors

Section titled “Enrich writes surfaces and optional vectors”

In the old API, matcher.index(...) was a distinct step: it embedded persisted doc / image bytes, wrote the vector column, set indexed_at, and set pipeline_status = 'ready'. In the new package graph, Enricher.enrich() (@samesake/enrich’s createEnricher) derives the same surfaces (doc, rerank_doc, fts_src) and status/gate outcome. When createEnricher receives embed, it batch-embeds every ready dense surface, L2-normalizes the vectors, and passes them to EnrichStore.writeEnriched in the same call — no separate index verb. Without embed, the factory remains surfaces-only.

When enrich throws, recordFailure sets pipeline_status = 'failed', increments attempt_count, stores last_error, and schedules next_attempt_at with exponential backoff (capped at one hour).

app.enrich.retryFailed({ limit }) drains retryable rows (failed, next_attempt_at <= now(), attempt_count < maxAttempts):

  • Rows that never enriched → re-run enrich.
  • Rows at or above maxAttempts (default 5) → promoted to dead.

Enrich runs also abort when the per-run failure rate exceeds a threshold (default 50% after 10 samples) to avoid hammering a broken upstream.

Surfaces and gate live on the collection config — there is no separate compose step and no string template on the embeddings block:

import { collection, gates } from "@samesake/core";
export const products = collection("products", {
fields: { /* ... */ },
enrich: { stages: [] },
indexing: {
surfaces: {
embed_doc: {
kind: "dense",
embedding: "doc",
build: ({ data, enriched }) => `${data.title} ${enriched.color_text}`.trim(),
},
rerank_doc: {
kind: "rerank",
build: ({ data, enriched }) => `${data.title}. Colors: ${enriched.colors?.join(", ")}`,
},
fts_doc: {
kind: "fts",
build: ({ data }) => `${data.title} ${data.brand}`,
},
},
gate: gates.always, // or a custom gate like fashion.indexing().gate
},
embeddings: { doc: { model: "your-model", dim: 1536 } },
search: { /* channels, rankingPolicy, rerank */ },
});

Use fashion.indexing() from @samesake/presets for the fashion vertical — it wires embed, rerank, FTS surfaces and the confidence/cross-signal gate. The rerank_doc surface feeds second-stage reranking — see Reranking. The same LLM judge powers eval and optional llmRerank — see Relevance judge.

await app.enrich.enrich();
await app.enrich.retryFailed({ limit: 500 });

Quarantined rows remain visible through the existing review endpoint (max_confidence filter) so operators can inspect low-quality enrichments before fixing data or gate thresholds.