Qdrant connector
Write points to Qdrant collections with single or named dense vectors, sparse vectors, multi-vector support, and schemaless JSON payloads — configured via CollectionSchema, QdrantVectorDef and QdrantSparseVectorDef.
The qdrant connector provides utilities for writing points to Qdrant vector databases, with support for single vectors, named dense vectors, sparse vectors, and multi-vector configurations.
use cocoindex::connectors::qdrant::{
self, CollectionSchema, Distance, NamedPointVector, QdrantConnection,
};The module is compiled with the qdrant Cargo feature of the cocoindex crate. The qdrant-client crate compiles protobufs, so a protoc binary must be on PATH (or named by PROTOC) to build.
Connection setup
QdrantConnection holds a shared qdrant_client::Qdrant client, which speaks gRPC. It is cheap to clone.
impl QdrantConnection {
pub async fn connect(url: &str) -> Result<Self>
pub fn from_client(state_id: impl Into<String>, client: Qdrant) -> Self
pub fn client(&self) -> &Qdrant
}QdrantConnection::connect(url) takes the gRPC URL (for example http://localhost:6334) and uses the client’s defaults. For an API key, TLS or timeouts, build a qdrant_client::Qdrant yourself and wrap it with from_client; state_id should name the server, not hold a secret.
Example:
let conn = QdrantConnection::connect(&url).await?;
// With an API key
let url = "https://example.cloud.qdrant.io:6334";
let client = qdrant_client::Qdrant::from_url(url)
.api_key(std::env::var("QDRANT_API_KEY").ok())
.build()?;
let conn = QdrantConnection::from_client(url, client);As target
The qdrant connector provides target state APIs for writing points to collections. CocoIndex tracks what points should exist and automatically handles upserts and deletions.
Declaring target states
Setting up a connection
Create a ContextKey<QdrantConnection> to identify your Qdrant connection, then provide it in your lifespan:
The key name is load-bearing across runs — it’s the stable identity CocoIndex uses to track managed collections. See ContextKey as stable identity before renaming.
use cocoindex::connectors::qdrant::QdrantConnection;
cocoindex::context_key!(static DB: QdrantConnection);
let conn = QdrantConnection::connect(&url).await?;
let app = Environment::builder()
.db_path(tempdir.path().join(".cocoindex_db"))
.provide_key(&DB, conn)
.build()
.await?
.app("QdrantTargetTest")
.await?;context_key!(static DB: QdrantConnection) declares the key; its name is the stable identity. provide_key(&DB, conn) provides the connection, in a #[cocoindex::lifespan] function or on Environment::builder() as here.
Collections (parent state)
Declares a collection as a target state. Returns a CollectionTarget for declaring points.
pub fn declare_collection_target(
ctx: &Ctx,
conn: &ContextKey<QdrantConnection>,
collection_name: impl Into<String>,
schema: CollectionSchema,
) -> Result<CollectionTarget>
pub async fn mount_collection_target(
ctx: &Ctx,
conn: &ContextKey<QdrantConnection>,
collection_name: impl Into<String>,
schema: CollectionSchema,
) -> Result<CollectionTarget>
pub async fn mount_collection_target_with_options(
ctx: &Ctx,
conn: &ContextKey<QdrantConnection>,
collection_name: impl Into<String>,
schema: CollectionSchema,
options: ManagedTargetOptions,
) -> Result<CollectionTarget>managed_by is set with ManagedTargetOptions::user_managed() on the _with_options variants. declare_collection_target returns a handle whose points resolve when the current component commits; mount_collection_target(...).await? returns a handle that accepts points at once.
Parameters:
ctx— The current component’sCtx.conn— AContextKey<QdrantConnection>identifying the Qdrant connection to use.collection_name— Name of the collection.schema— Schema definition specifying vector configurations (see Collection Schema).options—ManagedTargetOptions;ManagedTargetOptions::user_managed()assumes the collection exists instead of creating and dropping it.
Points (child states)
Once a CollectionTarget is resolved, declare points to be upserted with one of its declare_*point methods:
impl CollectionTarget {
/// A single-vector point.
pub fn declare_point(
&self,
ctx: &Ctx,
id: impl Into<QdrantPointId>,
vector: Vec<f32>,
payload: Map<String, JsonValue>,
) -> Result<()>
/// A point for a named-vector collection: each value is a single vector,
/// a multivector or a sparse vector.
pub fn declare_named_vectors_point<I, K>(
&self,
ctx: &Ctx,
id: impl Into<QdrantPointId>,
vectors: I,
payload: Map<String, JsonValue>,
) -> Result<()>
where
I: IntoIterator<Item = (K, NamedPointVector)>,
K: Into<String>
/// A multi-vector point for a collection created with
/// `CollectionSchema::multivector`.
pub fn declare_multivector_point(
&self,
ctx: &Ctx,
id: impl Into<QdrantPointId>,
vectors: Vec<Vec<f32>>,
payload: Map<String, JsonValue>,
) -> Result<()>
}The id, the vector and the payload are separate arguments, and the shape of the vector picks the method. The payload is a serde_json::Map<String, Value>. Each method checks the point against the collection schema and the id rules (see Point IDs) when it is called, and returns an error for a mismatch: a vector of the wrong length, an empty multivector, a NaN or infinite element, or a payload integer beyond the i64 range Qdrant stores. Writes wait until Qdrant has applied them.
Collection schema
Define vector configurations for a collection using CollectionSchema. Unlike row-oriented databases, Qdrant uses a point-oriented model where each point has schemaless payload and one or more vectors with predefined dimensions.
impl CollectionSchema {
// One unnamed dense vector per point
pub fn new(vector_size: u64, distance: Distance) -> Self
pub fn from_vector_schema(schema: VectorSchema, distance: Distance) -> Result<Self>
pub async fn from_vector_provider(
provider: &(impl VectorSchemaProvider + ?Sized),
distance: Distance,
) -> Result<Self>
// One unnamed multivector per point
pub fn multivector(vector_size: u64, distance: Distance) -> Self
pub fn from_multivector_schema(schema: MultiVectorSchema, distance: Distance) -> Result<Self>
pub async fn from_multivector_provider(
provider: &(impl MultiVectorSchemaProvider + ?Sized),
distance: Distance,
) -> Result<Self>
// Named vectors, dense and sparse
pub fn named<I, K, V>(vectors: I) -> Result<Self>
where
I: IntoIterator<Item = (K, V)>,
K: Into<String>,
V: Into<QdrantNamedVectorDef>
// Named dense vectors from a `#[derive(SchemaFields)]` row
pub fn from_row<T: SchemaFields>(distance: Distance) -> Result<Self>
pub fn with_vector_dim(self, field_name: &str, dim: usize) -> Result<Self>
}from_row turns each #[coco(vector)] field of a row struct into a named dense vector; the other fields stay payload. with_vector_dim sets the dimension of one such field, for a field whose size the struct does not fix.
The Rust schema has one constructor per shape instead of one create with a union argument.
Dense and sparse vectors share one namespace in Qdrant (the server rejects duplicate names across the two kinds), so CollectionSchema::named takes both in one list of (name, QdrantNamedVectorDef) entries and returns an error for a name that appears twice. Sparse vectors are always named.
QdrantVectorDef
Specifies vector configuration including dimension, distance metric, and multi-vector settings:
pub struct QdrantVectorDef {
pub distance: Distance,
pub multivector_comparator: MultivectorComparator,
// the vector schema, set by the constructors
}
impl QdrantVectorDef {
pub fn new(schema: VectorSchema, distance: Distance) -> Result<Self>
pub fn f32(size: u64, distance: Distance) -> Result<Self>
pub async fn from_vector_provider(
provider: &(impl VectorSchemaProvider + ?Sized),
distance: Distance,
) -> Result<Self>
pub fn multivector(schema: MultiVectorSchema, distance: Distance) -> Result<Self>
pub async fn from_multivector_provider(
provider: &(impl MultiVectorSchemaProvider + ?Sized),
distance: Distance,
) -> Result<Self>
}
pub enum Distance { Cosine, Dot, Euclid }
pub enum MultivectorComparator { MaxSim }The constructor picks the shape: new and f32 for a dense vector, multivector for a multi-vector field. from_vector_provider and from_multivector_provider take the dimension from a provider, typically the embedder. multivector_comparator is MaxSim, the only comparator.
QdrantSparseVectorDef
Specifies a named sparse vector configuration:
pub struct QdrantSparseVectorDef {
pub modifier: Option<SparseModifier>,
}
pub enum SparseModifier { Idf }Sparse vectors use Qdrant’s native sparse-vector storage and dot-product scoring. The optional modifier: Some(SparseModifier::Idf) enables Qdrant’s IDF modifier for sparse vectors.
Single (unnamed) vector
For collections with a single unnamed vector:
let target = qdrant::mount_collection_target(
&ctx,
&DB,
&collection,
CollectionSchema::new(size, Distance::Cosine),
)
.await?;
for (id, v, filename, text) in &points {
target.declare_point(&ctx, *id, v.clone(), payload(filename, text))?;
}CollectionSchema::new(size, distance) takes the dimension as a number; CollectionSchema::from_vector_provider(&embedder, distance).await? takes it from an embedder.
Named vectors
For collections with multiple named vectors:
use cocoindex::connectors::qdrant::{
CollectionSchema, Distance, QdrantNamedVectorDef, QdrantSparseVectorDef,
QdrantVectorDef,
};
let schema = CollectionSchema::named([
(
"dense",
QdrantNamedVectorDef::from(QdrantVectorDef::f32(384, Distance::Cosine)?),
),
("sparse", QdrantSparseVectorDef::default().into()),
])?;Or derive the named vectors from a row struct, as the text_embedding_qdrant example does:
/// A computed point: id + vector + payload fields.
#[derive(Clone, Serialize, Deserialize, SchemaFields)]
struct PointData {
id: u64,
#[coco(vector)]
vector: Vec<f32>,
filename: String,
chunk_start: i64,
chunk_end: i64,
text: String,
}
let vector_dim = ctx.get_key(&EMBEDDER)?.dimension();
let target = qdrant::mount_collection_target(
&ctx,
&DB,
COLLECTION,
CollectionSchema::from_row::<PointData>(Distance::Cosine)?
.with_vector_dim("vector", vector_dim)?,
)
.await?;
let payload = json!({
"filename": p.filename,
"chunk_start": p.chunk_start,
"chunk_end": p.chunk_end,
"text": p.text,
})
.as_object()
.unwrap()
.clone();
target.declare_named_vectors_point(
&ctx,
p.id,
[("vector", NamedPointVector::Single(p.vector.clone()))],
payload,
)?;Dense + sparse vectors
For hybrid retrieval, declare a named dense vector and a named sparse vector in the same collection:
Points put both vectors in the named-vector list. The sparse vector is a native Qdrant sparse vector, not payload:
let schema = CollectionSchema::named([
(
"dense",
QdrantNamedVectorDef::from(QdrantVectorDef::f32(3, Distance::Cosine)?),
),
("sparse", QdrantSparseVectorDef { modifier }.into()),
])?;
let target =
qdrant::mount_collection_target(&ctx, &DB, &collection, schema).await?;
for (id, dense, indices, values, text) in points {
target.declare_named_vectors_point(
&ctx,
id,
[
("dense", NamedPointVector::Single(dense.to_vec())),
("sparse", NamedPointVector::Sparse { indices, values }),
],
payload("f.md", text),
)?;
}modifier is Some(SparseModifier::Idf) or None. NamedPointVector::Sparse { indices, values } takes Vec<u32> indices and Vec<f32> values; the indices must be unique.
VectorSchemaProvider
QdrantVectorDef::new(VectorSchema::f32(384), distance) takes an explicit schema and QdrantVectorDef::from_vector_provider(&embedder, distance).await? takes a VectorSchemaProvider. An f16 schema is stored as Qdrant’s Float16 datatype. See Vector Schema for details.
Multi-vector support
For multi-vector configurations (multiple vectors per point stored together):
let target = qdrant::mount_collection_target(
&ctx,
&DB,
COLLECTION,
CollectionSchema::multivector(EMBED_DIM, Distance::Cosine),
)
.await?;
target.declare_multivector_point(&ctx, p.id, p.vectors.clone(), payload)?;CollectionSchema::multivector(size, distance) scores with MAX_SIM, the only comparator. See examples/rust/image_search_colpali for the full program.
Distance metrics
The Distance argument specifies the similarity metric:
Distance::Cosine— Cosine similarity (normalized dot product)Distance::Dot— Dot product similarityDistance::Euclid— Euclidean distance (L2)
Every Rust constructor takes the distance explicitly; there is no default.
Example: single vector
The single-vector shape is CollectionSchema::new(size, Distance::Cosine) with declare_point, shown under Single (unnamed) vector. The end-to-end example, examples/rust/text_embedding_qdrant, uses a named vector derived from a row struct; see Example: named vectors.
Example: named vectors
use std::path::PathBuf;
use cocoindex::connectors::qdrant::{
self, CollectionSchema, Distance, NamedPointVector, QdrantConnection,
};
use cocoindex::ops::sentence_transformers::SentenceTransformerEmbedder;
use cocoindex::prelude::*;
use serde_json::json;
const COLLECTION: &str = "TextEmbedding";
cocoindex::context_key!(static DB: QdrantConnection);
cocoindex::context_key!(
static EMBEDDER: SentenceTransformerEmbedder,
detect_change
);
/// A computed point: id + vector + payload fields.
#[derive(Clone, Serialize, Deserialize, SchemaFields)]
struct PointData {
id: u64,
#[coco(vector)]
vector: Vec<f32>,
filename: String,
chunk_start: i64,
chunk_end: i64,
text: String,
}
#[cocoindex::function]
async fn process_file(ctx: &Ctx, file: FileEntry) -> Result<Vec<PointData>> {
// chunk and embed the file ...
}
async fn app_main(ctx: Ctx, sourcedir: PathBuf) -> Result<()> {
let vector_dim = ctx.get_key(&EMBEDDER)?.dimension();
let target = qdrant::mount_collection_target(
&ctx,
&DB,
COLLECTION,
CollectionSchema::from_row::<PointData>(Distance::Cosine)?
.with_vector_dim("vector", vector_dim)?,
)
.await?;
let files = walk_items(&sourcedir, &["**/*.md"])?;
let points_by_file = use_mount_each!(files, |file| process_file(ctx, file)).await?;
for points in &points_by_file {
for p in points {
let payload = json!({
"filename": p.filename,
"chunk_start": p.chunk_start,
"chunk_end": p.chunk_end,
"text": p.text,
})
.as_object()
.unwrap()
.clone();
target.declare_named_vectors_point(
&ctx,
p.id,
[("vector", NamedPointVector::Single(p.vector.clone()))],
payload,
)?;
}
}
Ok(())
}See examples/rust/text_embedding_qdrant for the full program.
Example: dense + sparse hybrid vectors
See the snippet under Dense + sparse vectors: CollectionSchema::named with a QdrantSparseVectorDef, and declare_named_vectors_point with NamedPointVector::Sparse.
Point IDs
Qdrant only accepts two point ID types — this is enforced by the Qdrant server itself (see Qdrant’s Point IDs documentation):
- unsigned 64-bit integers — a
u64id becomesQdrantPointId::Num - UUIDs — a
uuid::Uuid,Stringor&strid becomesQdrantPointId::Uuid; a string may be in any textual form Qdrant accepts (hyphenated, upper case, 32-character hex, URN or braced)
The id argument of each declare_*point method is impl Into<QdrantPointId>. The method validates the ID when it is called and returns an error for a string that is not a UUID, rather than letting the write fail later at the Qdrant server.
Qdrant stores every textual form of one UUID as the same point. CocoIndex therefore tracks a UUID point ID in its canonical form (hyphenated, lower case). If you declare two forms of one UUID in the same run, the second declaration fails as a duplicate.
To derive a stable point ID from an arbitrary string key (e.g. a document path or chunk key), use a deterministic UUID:
let point_id = uuid::Uuid::new_v5(&uuid::Uuid::NAMESPACE_URL, format!("doc/{chunk_key}").as_bytes());The uuid crate needs its v5 feature for Uuid::new_v5.
Payloads
Point payloads are schemaless JSON objects: a serde_json::Map<String, Value>. The json! macro builds one:
let payload = json!({
"filename": p.filename,
"chunk_start": p.chunk_start,
"chunk_end": p.chunk_end,
"text": p.text,
})
.as_object()
.unwrap()
.clone();Vector search
The connector focuses on writing points to Qdrant. For vector search, use the Qdrant client directly or the connector’s search helpers:
// Single unnamed vector
let hits = qdrant::vector_search(&conn, &collection, vec![0.0, 0.9, 0.1], 1).await?;
// Named vector
let hits = qdrant::named_vector_search(conn, COLLECTION, "vector", query_vec, TOP_K).await?;
for hit in hits {
let filename = hit
.payload
.get("filename")
.and_then(|v| v.as_str())
.unwrap_or("");
println!("[{:.3}] {filename}", hit.score);
}The connector has search helpers: vector_search, named_vector_search, multivector_search and named_multivector_search, each returning Vec<QdrantHit> with a score and a payload. For any other query, such as a hybrid query with prefetch and fusion, use conn.client(), the underlying qdrant_client::Qdrant.