Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,10 @@ uuid = ["dep:uuid"]
name = "query"
required-features = ["serde"]

[[example]]
name = "serverless"
required-features = ["serde"]

[package.metadata.docs.rs]
features = ["download_snapshots", "serde"]
no-default-features = true
76 changes: 76 additions & 0 deletions examples/serverless.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
//! Example of using the Qdrant Serverless client.
//!
//! **In development — do not use yet.** This client is experimental and unstable;
//! it may change without notice and is not ready for production or general use.
//!
//! Collection management uses the simplified serverless API; point operations
//! (query, upsert, ...) work like in the regular client (unsupported cluster
//! parameters are stripped before each RPC).
//!
//! ```bash
//! cargo run --example serverless --features serde
//! ```

use qdrant_client::qdrant::{PointStruct, QueryPointsBuilder, UpsertPointsBuilder};
use qdrant_client::serverless::{
CollectionConfig, DenseVectorConfig, Distance, KeywordIndex, QdrantServerless,
};
use qdrant_client::{Payload, QdrantError};

#[tokio::main]
async fn main() -> Result<(), QdrantError> {
let client =
QdrantServerless::from_url("https://serverless.plush-volt.aws.development-cloud.qdrant.io")
.api_key("<your api key>")
.build()?;

let collection_name = "my-collection";

// make the example rerunnable: creating an existing collection returns ALREADY_EXISTS
if client.collection_exists(collection_name).await? {
client.delete_collection(collection_name).await?;
}

// serverless-specific collection management: no quantization, wal,
// segment number etc. - the serverless manager decides those
let result = client
.create_collection(
collection_name,
CollectionConfig::new()
.dense_vector(DenseVectorConfig::new(4, Distance::Cosine))
.payload_index("color", KeywordIndex),
)
.await?;
println!("create_collection: {result}");

println!("collections: {:?}", client.list_collections().await?);
println!("info: {:?}", client.get_collection(collection_name).await?);

let points = vec![
PointStruct::new(
1,
vec![0.1, 0.2, 0.3, 0.4],
Payload::try_from(serde_json::json!({"color": "red"})).unwrap(),
),
PointStruct::new(
2,
vec![0.4, 0.3, 0.2, 0.1],
Payload::try_from(serde_json::json!({"color": "blue"})).unwrap(),
),
];
client
.upsert_points(UpsertPointsBuilder::new(collection_name, points))
.await?;

let hits = client
.query(
QueryPointsBuilder::new(collection_name)
.query(vec![0.1, 0.2, 0.3, 0.4])
.limit(10),
)
.await?;
println!("query: {hits:?}");

client.delete_collection(collection_name).await?;
Ok(())
}
232 changes: 232 additions & 0 deletions proto/serverless_collections.proto
Original file line number Diff line number Diff line change
@@ -0,0 +1,232 @@
// Source: https://github.com/qdrant/qdrant-cloud-public-api/blob/main/proto/qdrant/serverless/collections.proto
// Renamed to serverless_collections.proto to sit alongside the regular collections.proto.
// Regenerate with: cargo test --test serverless_protos -- --ignored
syntax = "proto3";

package qdrant.serverless;

// CollectionsService manages the collections of a qdrant serverless space.
// Unlike the qdrant server API, it exposes only a simplified configuration:
// the manager turns it into concrete qdrant settings, which are never
// exposed to the tenant.
service CollectionsService {
// Creates a collection with the given tenant-facing configuration.
rpc CreateCollection(CreateCollectionRequest) returns (CreateCollectionResponse);
// Deletes a collection and all of its data.
rpc DeleteCollection(DeleteCollectionRequest) returns (DeleteCollectionResponse);
// Returns a single collection's configuration and stats.
rpc GetCollection(GetCollectionRequest) returns (GetCollectionResponse);
// Lists the caller's collections.
rpc ListCollections(ListCollectionsRequest) returns (ListCollectionsResponse);
}

// Distance metric used to compare dense vectors.
enum Distance {
// Unset; a concrete metric is required.
DISTANCE_UNSPECIFIED = 0;
// Cosine similarity.
COSINE = 1;
// Euclidean (L2) distance.
EUCLID = 2;
// Dot product.
DOT = 3;
// Manhattan (L1) distance.
MANHATTAN = 4;
}

// How much of the original vector precision may be traded for cost. The manager
// turns this into a concrete quantization / datatype choice; the tenant never
// picks scalar/product/binary quantization or its parameters directly.
enum PrecisionTier {
// Unset: HIGH.
PRECISION_TIER_UNSPECIFIED = 0;
// Aggressive compression, lowest cost, approximate results.
LOW = 1;
// Moderate compression with a small accuracy trade-off.
MEDIUM = 2;
// No lossy compression: exact stored vectors.
HIGH = 3;
}

// Configuration of a single dense (embedding) vector.
message DenseVectorConfig {
// Dimensionality of the embedding, e.g. 512 (CLIP), 1536, 3072.
uint64 size = 1;
// Distance metric used to compare vectors.
Distance distance = 2;
// Store several sub-vectors per point and compare with max-sim, for
// late-interaction models (ColBERT, ColPali, ...).
bool multivector = 3;
// Precision/cost trade-off for this vector. Unset: HIGH.
optional PrecisionTier precision_tier = 4;
}

// Configuration of a single sparse vector.
message SparseVectorConfig {
// Apply the IDF modifier at query time. Enable for BM25-style models that
// expect inverse-document-frequency weighting.
bool use_idf = 1;
// Precision/cost trade-off for this vector. Unset: HIGH.
optional PrecisionTier precision_tier = 2;
}

// Full-text tokenizer, mirrors qdrant's `TokenizerType`.
enum Tokenizer {
// Unset: WHITESPACE.
TOKENIZER_UNSPECIFIED = 0;
// Index every prefix of each token.
PREFIX = 1;
// Split on whitespace.
WHITESPACE = 2;
// Split on word boundaries.
WORD = 3;
// Language-aware tokenization.
MULTILINGUAL = 4;
}

// Exact match on string values, e.g. `color: "red"`.
message KeywordIndex {}

// Exact match and/or range filters on integers, e.g. `age: 25`. Both are on
// by default; turning one off shrinks the index.
message IntegerIndex {
// Support exact-match filters.
optional bool lookup = 1;
// Support range filters.
optional bool range = 2;
}

// Range filters on floating point (and integer) numbers, e.g. `price: 99.5`.
message FloatIndex {}

// Exact match on UUID strings; like keyword but stored compactly.
message UuidIndex {}

// Range filters on RFC 3339 datetimes, e.g. `created_at: "2023-02-08T10:49:00Z"`.
message DatetimeIndex {}

// Full-text filtering on string values.
message TextIndex {
// Tokenizer to split text with. Unset: WHITESPACE.
optional Tokenizer tokenizer = 1;
// Lowercase text before indexing. Default true.
optional bool lowercase = 2;
// Support phrase queries; extra index structure. Default true.
optional bool phrase_matching = 3;
// Minimum token length to index.
optional uint64 min_token_len = 4;
// Maximum token length to index.
optional uint64 max_token_len = 5;
}

// Geo radius / bounding box / polygon filters on `{lon, lat}` values.
message GeoIndex {}

// Exact match on booleans, e.g. `is_active: true`.
message BoolIndex {}

// One payload index. Only the *kind* of filter the field supports is chosen
// here; storage placement of the index is the manager's decision.
message PayloadIndexConfig {
// The kind of filter the field supports.
oneof index {
// Exact match on strings.
KeywordIndex keyword = 1;
// Exact match and/or range filters on integers.
IntegerIndex integer = 2;
// Range filters on numbers.
FloatIndex float = 3;
// Exact match on UUIDs.
UuidIndex uuid = 4;
// Range filters on datetimes.
DatetimeIndex datetime = 5;
// Full-text filtering.
TextIndex text = 6;
// Geo filters.
GeoIndex geo = 7;
// Exact match on booleans.
BoolIndex bool = 8;
}
}

// The tenant's collection config. Persisted verbatim. At least one dense or
// sparse vector is required.
message CollectionConfig {
// Keyed by vector name. The empty name "" is the unnamed default vector, as
// in qdrant; a collection either has the single unnamed vector or named ones.
map<string, DenseVectorConfig> dense_vectors = 1;
// Keyed by vector name.
map<string, SparseVectorConfig> sparse_vectors = 2;
// Keyed by payload field name (JSON path, e.g. `user_id` or `meta.tags`).
map<string, PayloadIndexConfig> payload_indexes = 3;
}

// Every request names the collection by its tenant-facing name only. The
// tenant (`x-account-id`, `x-space-id`) travels in gRPC metadata, injected by
// auth. The storage id is the manager's: minted on create, resolved
// internally on get/delete.
message CreateCollectionRequest {
// Tenant-facing name of the collection to create.
string collection_name = 1;
// The collection's configuration.
CollectionConfig config = 2;
}

// Result of a create.
message CreateCollectionResponse {
// Tenant-facing name of the collection.
string collection_name = 1;
// Outcome, e.g. "created", "already exists".
string result = 2;
}

// Names the collection to delete.
message DeleteCollectionRequest {
// Tenant-facing name of the collection to delete.
string collection_name = 1;
}

// Result of a delete.
message DeleteCollectionResponse {
// Whether the collection existed and was deleted.
bool deleted = 1;
// Number of storage objects removed.
uint32 objects_deleted = 2;
}

// Names the collection to fetch.
message GetCollectionRequest {
// Tenant-facing name of the collection.
string collection_name = 1;
}

// The collection's configuration and stats, if it exists.
message GetCollectionResponse {
// Whether the collection exists.
bool exists = 1;
// The configuration the collection was created with.
optional CollectionConfig config = 2;
// Available points as of the last applied write (eventually consistent);
// absent until the updater has written stats for the collection.
optional uint64 point_count = 3;
}

// Lists the caller's collections. The tenant travels in metadata, so there is
// nothing to name here.
message ListCollectionsRequest {}

// One collection in a listing.
message CollectionSummary {
// Tenant-facing name of the collection.
string collection_name = 1;
// Available points as of the last applied write (eventually consistent);
// absent until the updater has written stats for the collection.
optional uint64 point_count = 2;
}

// The caller's collections.
message ListCollectionsResponse {
// Ordered by name. A collection whose creation never published a manifest is
// not listed: it is not servable.
repeated CollectionSummary collections = 1;
}
2 changes: 2 additions & 0 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -146,6 +146,8 @@ mod payload;
mod qdrant_client;
#[cfg(feature = "serde")]
mod serde_impl;
/// Client for Qdrant Serverless (**in development — do not use yet**)
pub mod serverless;

#[cfg(feature = "serde")]
pub mod serde_deser;
Expand Down
10 changes: 9 additions & 1 deletion src/qdrant_client/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -190,8 +190,16 @@ impl Qdrant {
QdrantBuilder::from_url(url)
}

/// Shared connection pool (used by the serverless client for CollectionsService).
pub(crate) fn channel(&self) -> &Arc<ChannelPool> {
&self.channel
}

/// Wraps a channel with a metadata interceptor (api key + custom headers)
fn with_api_key(&self, channel: Channel) -> InterceptedService<Channel, MetadataInterceptor> {
pub(crate) fn with_api_key(
&self,
channel: Channel,
) -> InterceptedService<Channel, MetadataInterceptor> {
let interceptor = MetadataInterceptor::new(
self.config.api_key.clone(),
self.config.custom_headers.clone(),
Expand Down
Loading
Loading