Crux
GuidesRetrieval & RAG

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.

StepPhaseWhat it does
rewriteQuery()queries -> queriesRewrites the original question with a model-backed query.
fanout()queries -> queriesGenerates alternate queries and dedupes them.
retrieve()queries -> hitsRuns each planned query against the recipe source retriever or retrievers.
rerank()hits -> hitsReorders and optionally trims hits with a required, named Reranker engine.
expandParents()hits -> hitsReads parent records and attaches larger context while preserving child citations.
compressToBudget()hits -> hitsKeeps query-relevant verbatim excerpts inside a token budget.
retrievalStep()either phaseAdds 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.

On this page