Collections and retrieval
A collection holds documents and finds the ones closest in meaning to a question: "How many vacation days?" finds "Annual leave is 25 days" without sharing a word. Give it to an agent as a tool and the agent can look things up before it answers.
from criogenai import Agent, Collection, Embedder
handbook = await Collection.open("handbook", embedder=Embedder.openai(), storage=storage)
await handbook.add(
[
{"id": "leave", "text": "Annual leave is 25 days a year.", "metadata": {"section": "hr"}},
{"id": "expenses", "text": "Expenses are paid within 30 days.", "metadata": {"section": "finance"}},
]
)
hits = await handbook.search("How many vacation days?", k=3)
hr_only = await handbook.search("deadlines", k=3, where={"section": "hr"})
agent = Agent(
"openai:gpt-5-mini",
instructions="Answer from the handbook; say which section you used.",
tools=[handbook.as_tool("search_handbook", "Search the staff handbook.")],
)
print((await agent.run("How much leave do I get?")).text)const handbook = await Collection.open("handbook", { embedder: Embedder.openai(), storage });
await handbook.add([
{ id: "leave", text: "Annual leave is 25 days a year.", metadata: { section: "hr" } },
{ id: "expenses", text: "Expenses are paid within 30 days.", metadata: { section: "finance" } },
]);
const hits = await handbook.search("How many vacation days?", { k: 3 });
const hrOnly = await handbook.search("deadlines", { k: 3, where: { section: "hr" } });
const agent = new Agent({
model: openai("gpt-5-mini"),
instructions: "Answer from the handbook; say which section you used.",
tools: [handbook.asTool("search_handbook", "Search the staff handbook.")],
});
console.log((await agent.run("How much leave do I get?")).text);use std::sync::Arc;
use criogenai_agent::Agent;
use criogenai_agent::retrieval::{Collection, Document};
use criogenai_providers::{Endpoint, ProviderEmbedder, ProviderModel};
use criogenai_storage::Storage;
let key = std::env::var("OPENAI_API_KEY")?;
let storage = Storage::connect("sqlite://handbook.db").await?;
let embedder = Arc::new(ProviderEmbedder::new(
Endpoint::openai(key.clone()),
"text-embedding-3-small",
)?);
let handbook = Collection::open(storage.memory().clone(), "handbook", embedder).await?;
handbook
.add(vec![
Document::new("leave", "Annual leave is 25 days a year.").meta("section", "hr"),
Document::new("expenses", "Expenses are paid within 30 days.")
.meta("section", "finance"),
])
.await?;
let hits = handbook.search("How many vacation days?", 3).await?;
println!("{}", hits[0].text);
let model = ProviderModel::new(Endpoint::openai(key), "gpt-5-mini")?;
let agent = Agent::builder(model)
.tool(handbook.as_tool("search_handbook", "Search the staff handbook."))
.build()?;
agent.run("How much leave do I get?").await?;How it works#
- Each document is embedded (turned into a vector by your embedding model) and stored in your storage with its text and metadata: SQLite, Postgres or memory, encrypted when the storage is, kept and deleted like any memory.
- When the collection opens, its vectors are read once and indexed in your
process's memory;
addandremovekeep the index up to date. If another process adds documents,reload()picks them up. search(query, k)embeds the question and returns the bestkhits (id,text,metadata,score);wherekeeps only documents whose metadata matches.as_tool(name, description)gives an agent a read-only search tool: the model sends aquery(and optionallyk, up to 20) and gets the hits, each text cut to 2 000 characters.- In Rust, anything can be a
Retriever(a web search, your database) and become a tool withretriever_tool.
Which index#
| Flat (default) | HNSW (index="hnsw") | |
|---|---|---|
| Results | exact | approximate |
| Query at 100 000 documents | 17 ms | 0.2 ms |
| Recall (share of the exact top 10) | 1.0 | 0.88 to 1.0 on clustered data |
| Opening the collection | under a second | about 54 s at 100 000 documents |
Numbers from one machine, 768-dimension vectors (docs/benchmarks.md).
Keep the default unless a large collection is queried very often; raise
ef_search if HNSW misses results.
Limits#
Up to 100 000 characters of text per document (split longer ones), 8 KB of metadata, ids up to 256 bytes, and 2 000 000 vectors per index. Documents are embedded 64 at a time. Vectors from another embedding model are ignored, so changing models means re-adding the documents.
Why not pgvector or sqlite-vec? Both keep vectors unencrypted in the
database, and an embedding can be turned back into text. CriogenAI keeps them
encrypted and searches in memory instead.