VectorStore
Store dense, sparse, and hybrid vectors for retrieval and semantic lookup.
VectorStore is Crux's similarity-search interface.
Use it when a feature needs dense, sparse, or hybrid vector search. Keep JSON records in RecordStore, then store searchable vectors in VectorStore using the same keys.
import {
inMemoryRecordStore,
inMemoryVectorStore,
storage,
} from "@use-crux/core/storage";
const records = inMemoryRecordStore();
const vectors = inMemoryVectorStore();
const appStorage = storage({ records, vectors });search() returns keys and scores. Crux hydrates full records through RecordStore when it needs document content or metadata.
Dense, Sparse, Hybrid
Dense search uses one dense vector:
await vectors.search({
mode: "dense",
dense: [0.12, 0.4, 0.9],
limit: 10,
});Sparse search uses token indices and weights:
await vectors.search({
mode: "sparse",
sparse: {
indices: [12, 98, 322],
values: [0.8, 0.3, 0.6],
},
limit: 10,
});Hybrid search sends both:
await vectors.search({
mode: "hybrid",
dense: [0.12, 0.4, 0.9],
sparse: {
indices: [12, 98, 322],
values: [0.8, 0.3, 0.6],
},
fusion: "dbsf",
limit: 10,
});Unsupported modes should throw StorageError code unsupported_capability. Never silently degrade hybrid to dense-only or sparse-only.
Filtering
Vector filters use the same exact top-level scalar equality as records:
await vectors.search({
mode: "dense",
dense,
limit: 10,
filter: { namespace: "product-docs", active: true },
});Capability truth matters:
capabilities().filter | Meaning |
|---|---|
'pre' | The backend applies filters before top-k scoring. This is required for production filtered retrieval. |
'post' | The adapter can filter returned hits after top-k. Useful for local/dev tooling, but approximate. |
false | Filtered vector searches throw. |
Crux retrieval and indexed-knowledge paths that rely on exact filtered top-k results reject post and false vector stores.
With Retriever
Most users do not call VectorStore directly. They pass records and vectors to retriever():
const docs = retriever({
id: "docs",
namespace: "product-docs",
records,
vectors,
dense,
sparse,
search: { mode: "hybrid", fusion: "dbsf", limit: 8 },
});The retriever handles embedding the query, searching vectors, hydrating records, and returning hits.
Bundled Vector Stores
Use inMemoryVectorStore() for tests and examples:
import { inMemoryVectorStore } from "@use-crux/core/storage";
const vectors = inMemoryVectorStore();Use Upstash Vector for managed retrieval:
import { upstashVectorStore } from "@use-crux/upstash";
import { Index } from "@upstash/vector";
const vectors = upstashVectorStore({
index: new Index({
url: process.env.UPSTASH_VECTOR_REST_URL!,
token: process.env.UPSTASH_VECTOR_REST_TOKEN!,
}),
namespace: "product-docs",
capabilities: {
dense: true,
sparse: true,
hybrid: true,
fusion: ["rrf", "dbsf"],
},
});Upstash index mode is deployment-owned, so the adapter defaults conservatively and only claims sparse, hybrid, or fusion support when you configure those capabilities.
See Retrieval for the user-facing retrieval APIs, Upstash storage for the adapter reference, and the Postgres storage cookbook for a dense pgvector example.
Custom VectorStore
Implement VectorStore when your vector database can upsert records by key and search them by dense, sparse, or hybrid query. This is the full contract:
import type { VectorStore } from "@use-crux/core/storage";
const vectors: VectorStore = {
async upsert(records) {
// records: { key, dense?, sparse?, metadata? }[]
},
async delete(keys) {
// delete vector records by key
},
async search(query) {
// query.mode is 'dense', 'sparse', or 'hybrid'
return [];
},
capabilities() {
return {
dense: true,
sparse: false,
hybrid: false,
fusion: [],
filter: "pre",
consistency: "strong",
};
},
};A vector-database-backed implementation:
import type { VectorStore } from "@use-crux/core/storage";
export function myVectorStore(): VectorStore {
return {
async upsert(records) {
await vectorDb.upsert(
records.map((record) => ({
id: record.key,
vector: record.dense,
sparseVector: record.sparse,
metadata: record.metadata,
})),
);
},
async delete(keys) {
await vectorDb.delete(keys);
},
async search(query) {
const results = await vectorDb.query({
mode: query.mode,
vector: query.mode !== "sparse" ? query.dense : undefined,
sparseVector: query.mode !== "dense" ? query.sparse : undefined,
topK: query.limit ?? 10,
fusion: query.mode === "hybrid" ? query.fusion : undefined,
filter: query.filter,
});
return results.map((result) => ({
key: result.id,
score: result.score,
metadata: result.metadata,
}));
},
capabilities() {
return {
dense: true,
sparse: true,
hybrid: true,
fusion: ["rrf", "dbsf"],
filter: "pre",
consistency: "eventual",
};
},
};
}If your vector database also stores full JSON documents, expose that separately as a RecordStore. The public Crux contract stays clear: records hydrate data; vectors find similar records.