Skip to content

Vector Stores

A vector store holds documents alongside their embeddings and returns the ones most similar to a query. In Rig they are the storage layer behind RAG: embed your data once, insert it into a store, and query the store at prompt time.

Every integration implements the same two traits, so the code that inserts and queries documents is nearly identical whether you use the built-in in-memory store or a database such as Qdrant, MongoDB or LanceDB. This page covers those shared pieces; the per-store pages cover setup.

The in-memory store ships in rig, so it needs no feature and no running database:

use rig::prelude::*;
use serde::{Deserialize, Serialize};
use rig::providers::openai::{self, OpenAI};
#[derive(Embed, Clone, Serialize, Deserialize, Debug)]
struct WordDefinition {
id: String,
#[embed]
definition: String,
}
#[tokio::main]
async fn main() -> Result<(), anyhow::Error> {
let model = OpenAI::from_env()?.embedding(openai::TEXT_EMBEDDING_3_SMALL, None);
let documents = vec![
WordDefinition { id: "doc0".into(), definition: "A flurbo is a green alien.".into() },
WordDefinition { id: "doc1".into(), definition: "A glarb-glarb is an ancient farming tool.".into() },
];
// Embed each document's `#[embed]` field.
let embeddings = EmbeddingsBuilder::new(model.clone())
.documents(documents)?
.build()
.await?;
// Store the documents and build an index that embeds queries with the same model.
let index = InMemoryVectorStore::from_documents(embeddings).index(model);
let req = VectorSearchRequest::builder()
.query("What is a flurbo?")
.samples(1)
.build();
for result in index.top_n::<WordDefinition>(req).await? {
println!("{:.3} {}: {}", result.score, result.id, result.document.definition);
}
Ok(())
}

Switching to a database only changes how you build the store; the EmbeddingsBuilder and VectorSearchRequest steps stay the same.

Derive Embed on your type and mark the field (or fields) to embed with #[embed]. EmbeddingsBuilder batches the embedding calls:

use rig::prelude::*;
use rig::providers::openai::{self, OpenAI};
#[derive(Embed, Clone, Serialize, Deserialize)]
struct Document {
id: String,
#[embed]
content: String,
}
let model = OpenAI::from_env()?.embedding(openai::TEXT_EMBEDDING_3_SMALL, None);
let docs = vec![Document { id: "doc0".into(), content: "Rig is a Rust library.".into() }];
let embeddings: Vec<(Document, Vec<rig::embeddings::Embedding>)> =
EmbeddingsBuilder::new(model)
.documents(docs)?
.build()
.await?;

build() returns each document paired with a Vec<Embedding>. A document can carry several embeddings (one per #[embed] field entry, or one per chunk), and stores rank it by its best match. Use .document(doc)? to add one document at a time.

Use the same embedding model to fill a store and to query it. The store’s vector width (when it has one) must match the model’s: model.capabilities().ndims.

See Embeddings for the Embed derive and embedding models.

Both traits live in rig::vector_store.

The read side: query a store by similarity.

pub trait VectorStoreIndex: Send + Sync {
/// The backend's filter type.
type Filter: SearchFilter + Send + Sync;
/// The top `samples` documents, most similar first, deserialized as `T`.
async fn top_n<T: DeserializeOwned + Send>(
&self,
req: VectorSearchRequest<Self::Filter>,
) -> Result<Vec<VectorSearchResult<T>>, VectorStoreError>;
/// The same ranking, ids and scores only.
async fn top_n_ids(
&self,
req: VectorSearchRequest<Self::Filter>,
) -> Result<Vec<VectorSearchIdResult>, VectorStoreError>;
}
pub struct VectorSearchResult<T> {
pub score: f64, // scale and direction follow the backend's metric
pub id: String,
pub document: T,
}

The write side: add documents with precomputed embeddings. Pass it the output of EmbeddingsBuilder::build():

pub trait InsertDocuments: Send + Sync {
async fn insert_documents<Doc: Serialize + Embed + Send>(
&self,
documents: Vec<(Doc, Vec<Embedding>)>,
) -> Result<(), VectorStoreError>;
}

Most database stores implement both traits. The in-memory store is filled through its constructors and add_documents, and the LanceDB index reads a table you populate with LanceDB’s own API.

Every query is a VectorSearchRequest: the query text, the number of results (samples), an optional threshold that drops weak matches, and an optional metadata filter. Searching an index directly shows a full query and its results.

How the threshold compares depends on the backend’s metric; the store pages note where it differs. The default filter type, rig::vector_store::request::Filter, is backend-neutral and is what the in-memory store uses. Database stores have their own filter types with the same SearchFilter methods (eq, gt, lt, and, or) plus backend-specific ones, for example QdrantFilter, LanceDBFilter (SQL predicates) and MongoDbSearchFilter. Name the type on the request, as in VectorSearchRequest::<QdrantFilter>::builder(); a neutral Filter converts to a backend filter with filter.interpret().

Every VectorStoreIndex whose filter can be deserialized from JSON is also a tool, so an agent can search it when it decides to:

use rig::prelude::*;
use rig::providers::openai::{self, OpenAI};
let client = OpenAI::from_env()?;
let index = InMemoryVectorStore::<String>::default()
.index(client.embedding(openai::TEXT_EMBEDDING_3_SMALL, None));
let agent = AgentBuilder::new(client.completion(openai::GPT_5_5))
.preamble("Search the knowledge base before answering.")
.tool(index)
.build();

To retrieve context automatically on every prompt instead, use .dynamic_context(samples, index). See RAG.

Store operations return VectorStoreError: EmbeddingError (the embedding call failed), JsonError (a document didn’t serialize or deserialize), DatastoreError (the backend failed), FilterError (a filter couldn’t be built or translated), MissingIdError, SamplesOutOfRange, and Http / ExternalAPIError for stores reached over HTTP.

The in-memory store is built into rig. Every other store is a companion crate, enabled with a feature on rig and reached through a module of the same name:

[dependencies]
rig = { version = "0.44.0", features = ["qdrant"] }
StoreFeatureMain typesNotes
In-memorynoneInMemoryVectorStore, InMemoryVectorIndexRAM only; brute force or LSH. For development, tests and small datasets.
LanceDBlancedbrig::lancedb::LanceDbVectorIndexEmbedded columnar store on local disk or S3/GCS/Azure; exact or IVF-PQ search.
MongoDBmongodbrig::mongodb::MongoDbVectorIndexAtlas Vector Search.
Neo4jneo4jrig::neo4j::Neo4jClient, Neo4jVectorIndexVector index next to your graph data.
Qdrantqdrantrig::qdrant::QdrantVectorStoreDedicated vector database.
SurrealDBsurrealdbrig::surrealdb::SurrealVectorStoreIn-memory or remote SurrealDB.
SQLitesqliterig::sqlite::SqliteVectorStore, SqliteVectorIndexSingle file, via the sqlite-vec extension; you describe the table with SqliteVectorStoreTable.
PostgreSQLpostgresrig::postgres::PostgresVectorStorepgvector column with a choice of distance function.
Milvusmilvusrig::milvus::MilvusVectorStoreMilvus v2 HTTP API.
ScyllaDBscylladbrig::scylladb::ScyllaDbVectorStoreStores vectors in ScyllaDB and scores them in your process.
HelixDBhelixdbrig::helixdb::HelixDBVectorStoreThrough HelixDB’s HTTP client.
AWS S3 Vectorss3vectorsrig::s3vectors::S3VectorsVectorStoreTakes your AWS SDK client.
Cloudflare Vectorizevectorizerig::vectorize::VectorizeVectorStoreOver Cloudflare’s HTTP API.

For embeddings that never leave your machine, the fastembed feature adds rig::fastembed, local embedding models that work with every store above. See Local Models.