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; add and remove keep the index up to date. If another process adds documents, reload() picks them up.
  • search(query, k) embeds the question and returns the best k hits (id, text, metadata, score); where keeps only documents whose metadata matches.
  • as_tool(name, description) gives an agent a read-only search tool: the model sends a query (and optionally k, 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 with retriever_tool.

Which index#

Flat (default)HNSW (index="hnsw")
Resultsexactapproximate
Query at 100 000 documents17 ms0.2 ms
Recall (share of the exact top 10)1.00.88 to 1.0 on clustered data
Opening the collectionunder a secondabout 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.