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({
mode: "hybrid",
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,
vectors,
dense,
sparse,
search: { mode: "hybrid", limit: 8 },
});Store-backed retrievers read indexed chunk and parent records written by indexer() or knowledgeBase().index().
Search Modes
Dense is the natural-language default:
await docs.retrieve({ query: "refund policy", mode: "dense", limit: 5 });Sparse is useful for exact terms, codes, identifiers, and product names:
await docs.retrieve({ query: "ERR_BILLING_CREDIT_409", mode: "sparse" });Hybrid combines dense and sparse signals when the vector store supports it:
await docs.retrieve({
query: "enterprise SSO setup",
mode: "hybrid",
fusion: { strategy: "rrf", k: 60 },
});Unsupported combinations fail clearly. Crux does not silently downgrade sparse or hybrid 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 hybrid retriever runs dense-only for media because sparse embeddings are text-only. 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 vectors, 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,
vectors,
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 promptDocs = docs.retriever({
limit: 6,
});
const answerDocs = prompt({
id: "answer-docs",
use: [
promptDocs.asContext({
query: ({ question }) => question,
}),
],
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,
vectors,
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 VectorStore 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.