Retrieval
@betterdb/retrieval is a developer-facing retrieval SDK over Valkey Search (FT.*): a typed index schema, idempotent index lifecycle, upsert/delete, and vector + filtered + hybrid query. It is built on @betterdb/valkey-search-kit.
Use it when you want vector search with structured metadata filtering and a clean, typed API, without hand-writing FT.CREATE / FT.SEARCH strings.
Prerequisites
- Valkey 8.0+ with the
valkey-searchmodule loaded - Or Amazon ElastiCache for Valkey (8.0+)
- Or Google Cloud Memorystore for Valkey
- Node.js >= 20
Installation
npm install @betterdb/retrieval iovalkey
iovalkey is a peer dependency - install it alongside the package. You also provide an embedding function.
Quick start
import Valkey from 'iovalkey';
import { Retriever } from '@betterdb/retrieval';
const client = new Valkey('redis://localhost:6379');
const retriever = new Retriever({
client,
name: 'docs',
schema: {
fields: {
category: { type: 'tag' },
year: { type: 'numeric', sortable: true },
},
vector: { algorithm: 'hnsw', metric: 'cosine' },
},
embedFn: async (text) => embed(text), // returns number[]
});
// Create the index if it doesn't exist (idempotent; dims resolved from embedFn).
await retriever.createIndex();
await retriever.upsert([
{
id: 'doc1',
text: 'Valkey is a high-performance key-value store',
fields: { category: 'db', year: 2024 },
},
]);
const hits = await retriever.query({
text: 'fast in-memory database',
k: 5,
filter: { category: 'db' },
});
Retriever API
| Method | Description |
|---|---|
createIndex() | Create the index if absent (idempotent). Vector dimension is taken from schema.vector.dims or resolved by probing embedFn. |
upsert(entries) | Embed each entry’s text and write it as a hash with its fields. |
delete(ids) | Delete documents by id. |
query(options) | KNN search. Provide text (embedded for you) or a precomputed vector, a positive k, an optional filter (tag/numeric fields), and hybrid: 'rerank' to post-process hits through a rerankFn. Returns QueryHit[]. |
describeIndex() / health() | Index stats: doc count, indexing state, dimension, percent indexed, and an optional estimated recall. |
dropIndex() | Drop the index (no-op if it doesn’t exist). |
register() / unregister() | Publish/remove a discovery marker in the shared __betterdb:caches registry, ownership-checked so it never clobbers a foreign cache type. |
QueryHit.scoreis the raw KNN vector distance (lower is closer), not a similarity. Rank ascending.
Schema
The schema declares the metadata fields you filter on plus the vector configuration:
schema: {
fields: {
category: { type: 'tag' },
year: { type: 'numeric', sortable: true },
},
vector: {
algorithm: 'hnsw', // or 'flat'
metric: 'cosine', // 'cosine' | 'l2' | 'ip'
// dims: 1536, // optional - otherwise resolved by probing embedFn
},
}
tag fields support exact-match filtering; numeric fields support range filters and (when sortable) ordering.
Filtered and hybrid query
// Filter on indexed fields:
await retriever.query({ text: 'database', k: 5, filter: { category: 'db' } });
// Bring your own precomputed vector:
await retriever.query({ vector: myEmbedding, k: 10 });
// Hybrid rerank - retrieve k candidates, reorder with your own function:
await retriever.query({
text: 'database',
k: 10,
hybrid: 'rerank',
rerankFn: async (query, hits) => hits, // return reordered hits
});
Observability
Pass a metrics (RetrievalMetrics) and/or tracer (RetrievalTracer) to instrument every operation. createPrometheusMetrics() provides a ready-made Prometheus implementation.
See also
- Retrieval (Python) - the Python port with the same surface and on-disk format.
- Valkey Search Kit - the low-level helpers this package is built on.
- Agent Memory - a semantic memory tier that layers recency and importance scoring on top of vector search.