Retrieval Strategies
Retrieve relevant document chunks using similarity search, MMR diversity, hybrid vector+keyword search, or multi-query expansion.
Overview
Retrievers take a query string, embed it, and search the vector store for relevant chunks. All retrievers implement the Retriever interface:
interface Retriever {
retrieve(query: string, options?: Partial<RetrievalConfig>): Promise<RetrievalResult[]>;
}Each RetrievalResult contains chunkId, documentId, the chunk content, a relevance score, source (the document path or URL) and metadata linking back to the source document (document metadata, documentId, offsets). Retrievers throw when the underlying search fails.
The examples below assume an embedding service and a store, e.g.:
import { InMemoryEmbeddingAdapter, OpenAIEmbeddingService } from '@cogitator-ai/memory';
const embeddings = new OpenAIEmbeddingService({ apiKey: process.env.OPENAI_API_KEY! });
const store = new InMemoryEmbeddingAdapter();Similarity Retriever
Pure cosine similarity search. Embeds the query and returns the closest vectors from the store. Simple and effective for most cases.
import { SimilarityRetriever } from '@cogitator-ai/rag';
const retriever = new SimilarityRetriever({
embeddingAdapter: store,
embeddingService: embeddings,
defaultTopK: 10,
defaultThreshold: 0.5,
});
const results = await retriever.retrieve('How do agents handle errors?');| Option | Type | Default | Description |
|---|---|---|---|
embeddingAdapter | EmbeddingAdapter | — | Vector store to search |
embeddingService | EmbeddingService | — | Embedding service for query vectors |
defaultTopK | number | 10 | Default number of results |
defaultThreshold | number | 0.0 | Minimum similarity score |
When to use: Default choice. Works well when your queries are specific and you don't need diversity in results.
MMR Retriever
Maximal Marginal Relevance balances relevance with diversity. It fetches 3x candidates, then iteratively selects results that are relevant to the query but dissimilar to already-selected results. This avoids returning near-duplicate chunks.
import { MMRRetriever } from '@cogitator-ai/rag';
const retriever = new MMRRetriever({
embeddingAdapter: store,
embeddingService: embeddings,
defaultLambda: 0.7,
defaultTopK: 10,
});
const results = await retriever.retrieve('What tools are available?', { mmrLambda: 0.5 });Each result's score is its MMR score; the original similarity is kept in metadata.originalScore. Pass mmrLambda per query to override defaultLambda.
Some vector stores (for example Qdrant) do not return stored vectors from a search; in that case the candidates are re-embedded with the embedding service so diversity can still be computed.
The lambda parameter controls the relevance/diversity tradeoff:
lambda = 1.0— pure relevance (same as similarity search)lambda = 0.5— balancedlambda = 0.0— maximum diversity
| Option | Type | Default | Description |
|---|---|---|---|
embeddingAdapter | EmbeddingAdapter | — | Vector store to search |
embeddingService | EmbeddingService | — | Embedding service for query vectors |
defaultLambda | number | 0.7 | Relevance vs diversity tradeoff (0.0 – 1.0) |
defaultTopK | number | 10 | Default number of results |
defaultThreshold | number | 0.0 | Minimum similarity score for candidates |
When to use: When top results tend to be repetitive (e.g., similar paragraphs from different sections). Great for building diverse context windows.
Hybrid Retriever
Combines vector similarity search with keyword search using Reciprocal Rank Fusion, via a HybridSearch instance from @cogitator-ai/memory.
import { HybridRetriever } from '@cogitator-ai/rag';
import { HybridSearch, PostgresAdapter, unwrap } from '@cogitator-ai/memory';
const db = new PostgresAdapter({
provider: 'postgres',
connectionString: process.env.DATABASE_URL!,
});
db.setVectorDimensions(embeddings.dimensions);
unwrap(await db.connect());
const hybridSearch = new HybridSearch({
embeddingAdapter: db,
embeddingService: embeddings,
keywordAdapter: db, // full-text search in Postgres; omit to use the in-process BM25 index
});
const retriever = new HybridRetriever({
hybridSearch,
defaultWeights: { bm25: 0.3, vector: 0.7 },
defaultTopK: 10,
});
const results = await retriever.retrieve('error handling in tool execution');When used inside a RAGPipeline (via withHybridSearch() or withRetriever()), every ingested chunk is also added to the HybridSearch in-process BM25 index (keyed by embedding id), and keyword-only hits are mapped back to their chunk id, document id and metadata. That index lives in memory and only holds chunks ingested in the current process — for a persistent store, pass a keywordAdapter (Postgres full-text search or InMemoryEmbeddingAdapter) so keyword search covers everything stored. Custom retrievers can receive the same notifications by implementing ChunkIndexer.indexChunk().
Each result includes vectorScore and keywordScore in its metadata, so you can inspect how each signal contributed.
| Option | Type | Default | Description |
|---|---|---|---|
hybridSearch | HybridSearch | — | HybridSearch from @cogitator-ai/memory |
defaultWeights | object | HybridSearch's ({ bm25: 0.4, vector: 0.6 }) | { bm25: number, vector: number } weights |
defaultTopK | number | 10 | Default number of results |
defaultThreshold | number | 0.0 | Minimum vector similarity for the vector half |
When to use: When queries contain specific terms (error codes, function names, product IDs) that benefit from exact keyword matching alongside semantic understanding.
MultiQuery Retriever
Expands a single query into multiple variants, retrieves results for each in parallel, and merges them by taking the highest score per chunk. You provide the query expansion function — typically an LLM call. The original query is always searched; generated variants are trimmed, de-duplicated (case-insensitive) and capped by defaultMaxQueries or the per-query multiQueryCount. If expansion throws, the original query is used alone; if every retrieval fails, the first error is thrown.
import { Cogitator } from '@cogitator-ai/core';
import { MultiQueryRetriever, SimilarityRetriever } from '@cogitator-ai/rag';
const cog = new Cogitator({
llm: { providers: { google: { apiKey: process.env.GOOGLE_API_KEY! } } },
});
const llm = cog.getLLMBackend('google/gemini-3.5-flash-lite');
const baseRetriever = new SimilarityRetriever({
embeddingAdapter: store,
embeddingService: embeddings,
});
const retriever = new MultiQueryRetriever({
baseRetriever,
defaultMaxQueries: 3,
expandQuery: async (query) => {
const response = await llm.chat({
model: 'gemini-3.5-flash-lite',
messages: [
{
role: 'user',
content:
`Generate 3 alternative search queries for: "${query}"\n` +
'Return one query per line, no numbering.',
},
],
});
return response.content.split('\n').filter(Boolean);
},
});
const results = await retriever.retrieve('How do I configure memory?');| Option | Type | Default | Description |
|---|---|---|---|
baseRetriever | Retriever | — | Underlying retriever for each query |
expandQuery | (query: string) => Promise<string[]> | — | Function that generates query variants |
defaultTopK | number | 10 | Results returned after merging |
defaultMaxQueries | number | all | Max generated variants used |
Per-query options are passed to the base retriever for every variant, so topK applies both per variant and to the merged list.
When to use: When user queries are ambiguous or under-specified. The LLM generates variations that cover different angles of the question.
createRetriever Factory
Create a retriever from a config object. Here topK, threshold, lambda, weights and maxQueries set the retriever's defaults:
import { createRetriever } from '@cogitator-ai/rag';
const retriever = createRetriever({
strategy: 'mmr',
embeddingAdapter: store,
embeddingService: embeddings,
lambda: 0.6,
topK: 15,
});Supported strategy configs:
createRetriever({
strategy: 'similarity',
embeddingAdapter: store,
embeddingService: embeddings,
});
createRetriever({
strategy: 'mmr',
embeddingAdapter: store,
embeddingService: embeddings,
lambda: 0.7,
});
createRetriever({
strategy: 'hybrid',
hybridSearch,
weights: { bm25: 0.3, vector: 0.7 },
});
createRetriever({
strategy: 'multi-query',
baseRetriever: similarityRetriever,
expandQuery: queryExpansionFn,
maxQueries: 3,
});Comparison
| Strategy | Diversity | Keyword Support | LLM Calls | Best For |
|---|---|---|---|---|
similarity | Low | No | No | Specific queries, default choice |
mmr | High | No | No | Diverse context windows |
hybrid | Medium | Yes | No | Queries with exact terms |
multi-query | High | Depends on base | Yes | Ambiguous or broad queries |
Per-Query Options
All retrievers accept per-query overrides:
const results = await retriever.retrieve('query', {
topK: 5,
threshold: 0.7,
});