Retrieval Recipes
Compose named, traceable retrieval work with fanout, federation, reranking, parent expansion, and compression.
Use retrievalRecipe() when one raw search pass is not enough. A recipe is named, inspectable, traceable, and can be used anywhere a retriever can be used.
import {
compressToBudget,
expandParents,
fanout,
retrievalRecipe,
retrieve,
rerank,
} from "@use-crux/core/retrieval";
const docsAnswer = retrievalRecipe({
id: "docs-answer",
retriever: docs.retriever(),
model,
steps: [
fanout({ maxQueries: 4 }),
retrieve({ limit: 30 }),
expandParents({ records, indexerId: "docs", maxParentChars: 4000 }),
rerank({ engine: judgeReranker({ name: "docs-judge", model }), topK: 8 }),
compressToBudget({ tokens: 3500 }),
],
});
const hits = await docsAnswer.retrieve("enterprise SSO rollout");Built-In Steps
Recipes move through two phases: planned queries, then hits. Query steps must come before the retrieve() step; hit steps run after retrieval.
| Step | Phase | What it does |
|---|---|---|
rewriteQuery() | queries -> queries | Rewrites the original question with a model-backed query. |
fanout() | queries -> queries | Generates alternate queries and dedupes them. |
retrieve() | queries -> hits | Runs each planned query against the recipe source retriever or retrievers. |
rerank() | hits -> hits | Reorders and optionally trims hits with a required, named Reranker engine. |
expandParents() | hits -> hits | Reads parent records and attaches larger context while preserving child citations. |
compressToBudget() | hits -> hits | Keeps query-relevant verbatim excerpts inside a token budget. |
retrievalStep() | either phase | Adds a custom typed step. |
Model-backed steps use a recipe-level model by default. A step can also provide its own model.
const recipe = retrievalRecipe({
id: "docs-answer",
retriever: docs.retriever(),
model: defaultRetrievalModel,
steps: [
fanout({ maxQueries: 4 }),
retrieve({ limit: 30 }),
rerank({
engine: judgeReranker({ name: "docs-judge", model: rankingModel }),
topK: 8,
}),
],
});If a model-backed step has no model in scope, recipe construction fails before the first query.
Reranking Engines
There is one recipe step: rerank(). It always takes an explicit, named engine; the engine's name is the rag.reranker:<name> identity the Project Index and runtime evidence join on, so there is no anonymous default path. You can build the engine two ways.
Use the shared judge implementation over a recipe/step model:
import { judgeReranker } from "@use-crux/core/retrieval";
const recipe = retrievalRecipe({
id: "docs-answer",
retriever: docs.retriever(),
model: retrievalModel,
steps: [
retrieve({ limit: 30 }),
rerank({
engine: judgeReranker({ name: "docs-judge", model: retrievalModel }),
topK: 8,
}),
],
});Use an adapter instance when you want that adapter to bind the reranker. Direct SDK adapters use Crux's judge implementation over their generation model:
import Anthropic from "@anthropic-ai/sdk";
import { createAnthropic } from "@use-crux/anthropic";
const anthropic = createAnthropic(
new Anthropic({ apiKey: process.env.ANTHROPIC_API_KEY }),
);
const recipe = retrievalRecipe({
id: "docs-answer",
retriever: docs.retriever(),
steps: [
retrieve({ limit: 30 }),
rerank({
engine: anthropic.reranker({
name: "anthropic-reranker",
model: "claude-haiku-4-5",
}),
topK: 8,
}),
],
});Use @use-crux/ai when the provider has a native AI SDK reranking model:
import { cohere } from "@ai-sdk/cohere";
import { reranker } from "@use-crux/ai";
const recipe = retrievalRecipe({
id: "docs-answer",
retriever: docs.retriever(),
steps: [
retrieve({ limit: 30 }),
rerank({
engine: reranker({
name: "cohere-reranker",
model: cohere.reranking("rerank-v3.5"),
}),
topK: 8,
}),
],
});The adapter surface is uniform: adapters expose the same verbs (generate, stream) and the same capability factories (embedding where available, retrievalModel, reranker). Native rerank endpoints are dedicated ranking calls. Judge-backed rerankers in Anthropic, OpenAI, and Google spend generation tokens because they ask the model to rank candidates.
Custom Steps
Use retrievalStep() for product-specific filtering, routing, enrichment, or validation.
const onlyPublic = retrievalStep({
id: "only-public",
kind: "filter",
phase: { in: "hits", out: "hits" },
run: ({ hits }) => ({
hits: hits.filter((hit) => hit.metadata.public === true),
}),
});
const publicDocs = retrievalRecipe({
id: "public-docs",
retriever: docs.retriever(),
steps: [retrieve({ limit: 20 }), onlyPublic],
});Custom steps participate in the same ordering checks, trace capture, and observability spans as built-ins.
Federation
The retrieve step accepts one or more sources. Use weighted sources when one corpus should dominate tie-breaks without excluding the others.
const supportAndDocs = retrievalRecipe({
id: "support-and-docs",
retriever: [
{ retriever: docs.retriever(), weight: 1 },
{ retriever: supportTickets, weight: 0.5 },
],
onSourceError: "skip-with-warning",
steps: [
retrieve({ limit: 25 }),
rerank({ engine: judgeReranker({ name: "docs-judge", model }), topK: 8 }),
],
});Federated retrieval runs sources concurrently, fuses duplicate hits by namespace/source.id/chunkId, and records per-source attribution in hit provenance and recipe traces.
Score And Provenance
hit.score always matches the current ranking currency:
- raw store score after
retrieve() - fused score after cross-query or cross-source fusion
- rerank score after
rerank()
Historical scores live in structured provenance:
const [hit] = await docsAnswer.retrieve("refund policy");
hit.score;
hit.provenance?.rawScore;
hit.provenance?.fusedScore;
hit.provenance?.rerankScore;
hit.provenance?.matchedQueries;
hit.provenance?.perSource;No recipe step writes internal _crux metadata keys.
Traces
Use retrieveWithTrace() when you need to debug retrieval behavior.
const { hits, trace } = await docsAnswer.retrieveWithTrace("pricing changes");
console.table(
trace.steps.map((step) => ({
step: step.stepId,
kind: step.kind,
status: step.status,
inputQueries: step.inputQueryCount,
outputHits: step.outputHitCount,
warnings: step.warnings.length,
})),
);If a step throws, the trace captures the failed step before the error is surfaced.
Prompt And Grounding Forms
Recipes expose the same useful forms as retrievers:
const recipeRetriever = docsAnswer.asRetriever();
const recipeTools = docsAnswer.asTools({
prefix: "docs",
include: ["search", "getSource"],
});
const groundedRecipe = docsAnswer.asGrounding({
query: ({ input }) => input.question as string,
citations: { required: true, quotes: "required" },
});Use asGrounding() or grounding({ retriever: recipe.asRetriever() }) when final answers must cite validated evidence.