Crux
GuidesStorage

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().filterMeaning
'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.
falseFiltered 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.

On this page