Querying
Query knowledge bases, store-backed retrievers, and custom retrieval backends.
retriever() is the read side of a corpus. It turns a text or declared media query into scored hits and can also contribute prompt context or search tools.
Most apps create retrievers through knowledgeBase():
const docs = knowledgeBase({
id: "docs",
storage,
embeddings: dense,
sparseEmbeddings: sparse,
});
const hits = await docs
.retriever({
search: { dense: true, lexical: true },
limit: 8,
})
.retrieve("how do refunds work?");A store-backed retriever can also be created directly:
import { retriever } from "@use-crux/core/retrieval";
const docs = retriever({
id: "docs",
namespace: "product-docs",
records,
search,
dense,
sparse,
});Store-backed retrievers read indexed chunk and parent records written by indexer() or knowledgeBase().index().
Search Plans
Dense is the natural-language default:
await docs.retrieve({
query: "refund policy",
search: { dense: true },
limit: 5,
});Sparse is useful for exact terms, codes, identifiers, and product names:
await docs.retrieve({
query: "ERR_BILLING_CREDIT_409",
search: { sparse: true },
});Lexical uses normalized indexed content and a lexical-capable store:
await docs.retrieve({
query: "enterprise SSO setup",
search: { lexical: true },
});Fused retrieval combines multiple legs when the search store supports RRF:
await docs.retrieve({
query: "enterprise SSO setup",
search: {
dense: { candidates: 120 },
lexical: { candidates: 80 },
sparse: true,
fusion: { strategy: "rrf", k: 60 },
},
});Unsupported combinations fail clearly. Crux does not silently downgrade sparse, lexical, or fused retrieval to dense-only search.
Native media queries use the dense branch:
await docs.retrieve(dogPhoto);
await docs.retrieve({
input: { type: "image", source: dogPhoto },
limit: 5,
});A multi-leg retriever runs dense-only for media because sparse embeddings are text-only and lexical search uses indexed text. Custom retrievers and text-producing recipe steps reject media unless their own contract supports it. See Multimodal search for the complete path.
Before embedding a query or searching, store-backed retrievers compare
their dense space with the namespace record. EmbeddingSpaceMismatchError
means the namespace must be fully reindexed with the configured embedding, or
queried with the embedding that built it.
If the retriever id differs from the indexer() id that wrote the records, pass indexerId so hydration uses the correct read-model keys:
const docs = retriever({
id: "docs-search",
indexerId: "docs",
namespace: "product-docs",
records,
search,
dense,
});Typed Filters
When a knowledge base has a metadata schema, filter keys and literal values are inferred:
const docs = knowledgeBase({
id: "docs",
storage,
embeddings: dense,
metadataSchema: z.object({
section: z.enum(["guide", "reference", "api"]),
public: z.boolean(),
}),
});
const hits = await docs
.retriever({
filter: { section: "guide", public: true },
})
.retrieve("routing");Tool-exposed filters are opt-in:
const tools = docs.tools({
prefix: "docs",
include: ["search", "getSource"],
filters: ["section", "public"],
limit: { default: 5, max: 10 },
});Prompt Context Or Tools
Use context injection when the model should always receive retrieved evidence before generation:
const answerDocs = prompt({
id: "answer-docs",
use: [docs],
input: z.object({ question: z.string() }),
system: "Answer from the retrieved docs.",
});Use tools when the model should decide whether to search:
const searchableDocs = retriever({
id: "docs",
namespace: "product-docs",
records,
search,
dense,
inject: "tool",
tools: {
prefix: "docs",
include: ["search", "getSource"],
getSource: { visibility: "namespace" },
},
});
const assistant = prompt({
id: "assistant",
use: [searchableDocs],
system: "Use docsSearch before answering detailed product questions.",
});Manual tools are still available for integrations that do not run Crux prompt resolution:
const docsTools = docs.retriever().asTools({
prefix: "docs",
include: ["search", "getSource"],
});For citation validation, prefer grounding. It records both injected and tool-discovered hits in the grounding session.
Custom Retrieval
Use a custom retriever when your backend is not a Crux RecordStore plus SearchStore pair.
const docs = retriever({
id: "internal-search",
namespace: "product-docs",
async retrieve(query, options) {
const results = await internalSearch.search({
query,
limit: options.limit ?? 5,
filter: options.filter,
});
return results.map((result) => ({
namespace: "product-docs",
source: { id: result.documentId, url: result.url },
chunkId: result.sectionId,
content: result.text,
metadata: result.metadata,
score: result.score,
provenance: { rawScore: result.score },
}));
},
});Custom retrievers can be used directly, inside recipes, or as grounded evidence sources.