# Qdrant connector

> **CocoIndex v1, Rust SDK (experimental).** This page documents the CocoIndex **v1** Rust SDK (the `cocoindex` crate). The Rust SDK is experimental: any release may change or remove its API, and it is not yet published on crates.io. The Python SDK pages are at https://cocoindex.io/docs/.
>
> Source: https://cocoindex.io/docs/rust/connectors/qdrant/ · Rust docs index: https://cocoindex.io/docs/rust/llms.txt · Rust SDK tour: https://github.com/cocoindex-io/cocoindex/blob/main/rust/sdk/SHOWCASE.md · Rust examples: https://github.com/cocoindex-io/cocoindex/tree/main/examples/rust

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.

```rust
use cocoindex::connectors::qdrant::{
    self, CollectionSchema, Distance, NamedPointVector, QdrantConnection,
};
```

**Note — Dependencies**
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.

```rust
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:**

```rust
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:

**Note**
The key name is load-bearing across runs — it's the stable identity CocoIndex uses to track managed collections. See [ContextKey as stable identity](/docs/rust/programming_guide/context/#contextkey-as-stable-identity) before renaming.

```rust
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.

```rust
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's `Ctx`.
- `conn` — A `ContextKey<QdrantConnection>` identifying the Qdrant connection to use.
- `collection_name` — Name of the collection.
- `schema` — Schema definition specifying vector configurations (see [Collection Schema](#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:

```rust
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](#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.

```rust
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:

```rust
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:

```rust
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:

```rust
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:

```rust
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:

```rust
/// 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:

```rust
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`](/docs/rust/common_resources/vector_schema/#vectorschemaprovider). An `f16` schema is stored as Qdrant's `Float16` datatype. See [Vector Schema](/docs/rust/common_resources/vector_schema/#vectorschemaprovider) for details.

#### Multi-vector support

For multi-vector configurations (multiple vectors per point stored together):

```rust
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 similarity
- `Distance::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](#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).

### Example: named vectors

```rust
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](#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](https://qdrant.tech/documentation/manage-data/points/#point-ids)):

- unsigned 64-bit integers — a `u64` id becomes `QdrantPointId::Num`
- UUIDs — a `uuid::Uuid`, `String` or `&str` id becomes `QdrantPointId::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:

```rust
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:

```rust
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:

```rust
// 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`.
