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 DataStore, then store searchable vectors in VectorStore using the same keys.

import { inMemoryDataStore, inMemoryVectorStore, storage } from '@crux/core/storage'

const data = inMemoryDataStore()
const vectors = inMemoryVectorStore()

const appStorage = storage({ data, vectors })

Interface

import type { VectorStore } from '@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: { dense?, sparse?, fusion?, limit?, threshold?, filter? }
    return []
  },

  capabilities() {
    return { dense: true, sparse: true, hybrid: true }
  },
}

search() returns keys and scores. Crux hydrates full records through DataStore when it needs document content or metadata.

Dense, Sparse, Hybrid

Dense search uses one dense vector:

await vectors.search({
  dense: [0.12, 0.4, 0.9],
  limit: 10,
})

Sparse search uses token indices and weights:

await vectors.search({
  sparse: {
    indices: [12, 98, 322],
    values: [0.8, 0.3, 0.6],
  },
  limit: 10,
})

Hybrid search sends both:

await vectors.search({
  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 clearly. Never silently degrade hybrid to dense-only or sparse-only.

With Retriever

Most users do not call VectorStore directly. They pass data and vectors to retriever():

const docs = retriever({
  id: 'docs',
  namespace: 'product-docs',
  data,
  vectors,
  dense,
  sparse,
  search: { mode: 'hybrid', fusion: 'dbsf', limit: 8 },
})

The retriever handles embedding the query, searching vectors, hydrating records from data, and returning hits.

Bundled Vector Stores

Use inMemoryVectorStore() for tests and examples:

import { inMemoryVectorStore } from '@crux/core/storage'

const vectors = inMemoryVectorStore()

Use Upstash Vector for production dense, sparse, or hybrid retrieval:

import { upstashVectorStore } from '@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',
})

See Retrieval for the user-facing retrieval APIs, Upstash storage for the bundled hybrid-capable adapter, 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.

import type { VectorStore } from '@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({
        vector: query.dense,
        sparseVector: query.sparse,
        topK: query.limit ?? 10,
        fusion: query.fusion,
      })

      return results.map((result) => ({
        key: result.id,
        score: result.score,
        metadata: result.metadata,
      }))
    },
  }
}

If your vector database also stores full JSON documents, you can expose that separately as a DataStore. The public Crux contract stays clear: data hydrates records; vectors find similar records.

On this page