Skip to main content

qdrant_client/serverless/
client.rs

1//! Client for Qdrant Serverless.
2//!
3//! **In development — do not use yet.** This API is experimental and unstable;
4//! it may change without notice and is not ready for production or general use.
5//!
6//! Serverless exposes the same point-level API as a regular Qdrant cluster (minus
7//! read consistency, shard selection, write ordering and filtered updates), but a
8//! much simpler, tenant-facing collection management API. Point operations are
9//! delegated to the regular gRPC client; collection operations talk to the
10//! serverless CollectionsService.
11
12use std::future::Future;
13
14use tonic::codegen::InterceptedService;
15use tonic::transport::{Channel, Uri};
16use tonic::Status;
17
18use crate::auth::MetadataInterceptor;
19use crate::config::{AsOptionApiKey, AsTimeout, CompressionEncoding, QdrantConfig};
20use crate::qdrant::{
21    ClearPayloadPoints, CountPoints, CountResponse, DeletePayloadPoints, DeletePointVectors,
22    DeletePoints, GetPoints, GetResponse, PointsOperationResponse, QueryBatchPoints,
23    QueryBatchResponse, QueryGroupsResponse, QueryPointGroups, QueryPoints, QueryResponse,
24    ScrollPoints, ScrollResponse, SetPayloadPoints, UpdateBatchPoints, UpdateBatchResponse,
25    UpdatePointVectors, UpsertPoints,
26};
27use crate::qdrant_client::QdrantResult;
28use crate::serverless::conversions::{collection_config_from_grpc, collection_config_to_grpc};
29use crate::serverless::grpc::collections_service_client::CollectionsServiceClient;
30use crate::serverless::grpc::{
31    CreateCollectionRequest, DeleteCollectionRequest, GetCollectionRequest, ListCollectionsRequest,
32};
33use crate::serverless::models::{
34    CollectionConfig, CollectionInfo, CollectionSummary, CollectionsList, CreateCollectionResult,
35    DeleteCollectionResult, ListCollections,
36};
37use crate::Qdrant;
38
39/// Default gRPC port for serverless when the URL omits an explicit port.
40///
41/// Serverless is exposed on the standard TLS port, not on Qdrant's 6334.
42pub const DEFAULT_SERVERLESS_GRPC_PORT: u16 = 443;
43
44/// Entry point to a Qdrant Serverless space.
45///
46/// **In development — do not use yet.** This client is experimental and unstable;
47/// the API may change without notice and is not ready for production or general use.
48///
49/// Point operations behave like in the regular [`Qdrant`] client, except that
50/// parameters serverless does not support (read consistency, shard selection,
51/// write ordering, filtered updates, cross-collection lookups) are cleared
52/// before the request is sent. Collection management uses the simplified
53/// serverless API: only the tenant-facing configuration is exposed; storage
54/// internals (quantization, WAL, segments, ...) are decided by the serverless
55/// manager.
56///
57/// # Example
58///
59/// ```no_run
60/// use qdrant_client::serverless::{
61///     CollectionConfig, DenseVectorConfig, Distance, KeywordIndex, QdrantServerless,
62/// };
63/// use qdrant_client::qdrant::{PointStruct, QueryPointsBuilder, UpsertPointsBuilder};
64/// use qdrant_client::Payload;
65///
66/// # async fn run() -> Result<(), qdrant_client::QdrantError> {
67/// let client = QdrantServerless::from_url("https://serverless.example.cloud.qdrant.io")
68///     .api_key("<your api key>")
69///     .build()?;
70///
71/// client
72///     .create_collection(
73///         "my-collection",
74///         CollectionConfig::new()
75///             .dense_vector(DenseVectorConfig::new(4, Distance::Cosine))
76///             .payload_index("color", KeywordIndex::new()),
77///     )
78///     .await?;
79///
80/// client
81///     .upsert_points(UpsertPointsBuilder::new(
82///         "my-collection",
83///         vec![PointStruct::new(1, vec![0.1, 0.2, 0.3, 0.4], Payload::default())],
84///     ))
85///     .await?;
86///
87/// client
88///     .query(QueryPointsBuilder::new("my-collection").query(vec![0.1, 0.2, 0.3, 0.4]))
89///     .await?;
90/// # Ok(())
91/// # }
92/// ```
93#[derive(Clone)]
94pub struct QdrantServerless {
95    points: Qdrant,
96}
97
98/// Builder for [`QdrantServerless`].
99///
100/// Defaults differ from the regular [`Qdrant`] client:
101/// - compatibility checks are skipped (serverless has no matching server version)
102/// - URLs without an explicit port default to port [`DEFAULT_SERVERLESS_GRPC_PORT`] (443)
103#[derive(Clone)]
104pub struct QdrantServerlessBuilder {
105    config: QdrantConfig,
106}
107
108impl QdrantServerless {
109    /// Start configuring a serverless client from a base URL.
110    ///
111    /// ```no_run
112    /// use qdrant_client::serverless::QdrantServerless;
113    ///
114    /// # fn main() -> Result<(), qdrant_client::QdrantError> {
115    /// let client = QdrantServerless::from_url("https://serverless.example.cloud.qdrant.io")
116    ///     .api_key(std::env::var("QDRANT_API_KEY"))
117    ///     .build()?;
118    /// # Ok(())
119    /// # }
120    /// ```
121    pub fn from_url(url: &str) -> QdrantServerlessBuilder {
122        QdrantServerlessBuilder {
123            config: QdrantConfig {
124                uri: normalize_serverless_url(url),
125                check_compatibility: false,
126                ..QdrantConfig::default()
127            },
128        }
129    }
130
131    /// Create a serverless client from an existing config.
132    ///
133    /// Compatibility checks are forced off. Prefer [`Self::from_url`].
134    pub fn new(mut config: QdrantConfig) -> QdrantResult<Self> {
135        config.check_compatibility = false;
136        config.uri = normalize_serverless_url(&config.uri);
137        Ok(Self {
138            points: Qdrant::new(config)?,
139        })
140    }
141
142    async fn with_collections_client<T, O: Future<Output = Result<T, Status>>>(
143        &self,
144        f: impl Fn(CollectionsServiceClient<InterceptedService<Channel, MetadataInterceptor>>) -> O,
145    ) -> QdrantResult<T> {
146        let result = self
147            .points
148            .channel()
149            .with_channel(
150                |channel| {
151                    let service = self.points.with_api_key(channel);
152                    let mut client = CollectionsServiceClient::new(service)
153                        .max_decoding_message_size(usize::MAX);
154                    if let Some(compression) = self.points.config.compression {
155                        client = client
156                            .send_compressed(compression.into())
157                            .accept_compressed(compression.into());
158                    }
159                    f(client)
160                },
161                true,
162            )
163            .await?;
164        Ok(result)
165    }
166}
167
168/// # Construct and connect
169impl QdrantServerlessBuilder {
170    /// Set an optional API key (sent as `api-key` metadata).
171    pub fn api_key(mut self, api_key: impl AsOptionApiKey) -> Self {
172        self.config.api_key = api_key.api_key();
173        self
174    }
175
176    /// Set the timeout for API requests.
177    pub fn timeout(mut self, timeout: impl AsTimeout) -> Self {
178        self.config.timeout = timeout.timeout();
179        self
180    }
181
182    /// Set the connect timeout.
183    pub fn connect_timeout(mut self, timeout: impl AsTimeout) -> Self {
184        self.config.connect_timeout = timeout.timeout();
185        self
186    }
187
188    /// Set optional request compression.
189    pub fn compression(mut self, compression: Option<CompressionEncoding>) -> Self {
190        self.config.compression = compression;
191        self
192    }
193
194    /// Add a custom header to send with every request.
195    pub fn header(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
196        self.config.custom_headers.push((key.into(), value.into()));
197        self
198    }
199
200    /// Keep idle connections alive.
201    pub fn keep_alive_while_idle(mut self) -> Self {
202        self.config.keep_alive_while_idle = true;
203        self
204    }
205
206    /// Build the configured [`QdrantServerless`] client.
207    pub fn build(self) -> QdrantResult<QdrantServerless> {
208        QdrantServerless::new(self.config)
209    }
210}
211
212/// # Collection operations
213///
214/// Simplified, tenant-facing collection management. Storage internals are not exposed.
215impl QdrantServerless {
216    /// Creates a collection with the given tenant-facing configuration.
217    ///
218    /// At least one dense or sparse vector is required. Unlike the regular client,
219    /// no storage internals (quantization, WAL, segment number, ...) can be
220    /// configured: the serverless manager decides those.
221    pub async fn create_collection(
222        &self,
223        collection_name: impl Into<String>,
224        config: CollectionConfig,
225    ) -> QdrantResult<CreateCollectionResult> {
226        let request = CreateCollectionRequest {
227            collection_name: collection_name.into(),
228            config: Some(collection_config_to_grpc(&config)),
229        };
230        let request = &request;
231        self.with_collections_client(|mut api| async move {
232            let response = api.create_collection(request.clone()).await?.into_inner();
233            Ok(CreateCollectionResult {
234                collection_name: response.collection_name,
235                result: response.result,
236                time: response.time,
237            })
238        })
239        .await
240    }
241
242    /// Deletes a collection and all of its data.
243    pub async fn delete_collection(
244        &self,
245        collection_name: impl Into<String>,
246    ) -> QdrantResult<DeleteCollectionResult> {
247        let request = DeleteCollectionRequest {
248            collection_name: collection_name.into(),
249        };
250        let request = &request;
251        self.with_collections_client(|mut api| async move {
252            let response = api.delete_collection(request.clone()).await?.into_inner();
253            Ok(DeleteCollectionResult {
254                deleted: response.deleted,
255                time: response.time,
256            })
257        })
258        .await
259    }
260
261    /// Returns a collection's configuration and stats.
262    ///
263    /// Unlike the regular client, does not error if the collection is missing:
264    /// check the [`CollectionInfo::exists`] field of the result.
265    pub async fn get_collection(
266        &self,
267        collection_name: impl Into<String>,
268    ) -> QdrantResult<CollectionInfo> {
269        let request = GetCollectionRequest {
270            collection_name: collection_name.into(),
271        };
272        let request = &request;
273        let response = self
274            .with_collections_client(|mut api| async move {
275                Ok(api.get_collection(request.clone()).await?.into_inner())
276            })
277            .await?;
278        Ok(CollectionInfo {
279            exists: response.exists,
280            config: response
281                .config
282                .as_ref()
283                .map(collection_config_from_grpc)
284                .transpose()?,
285            point_count: response.point_count,
286            time: response.time,
287        })
288    }
289
290    /// Checks whether a collection exists.
291    pub async fn collection_exists(
292        &self,
293        collection_name: impl Into<String>,
294    ) -> QdrantResult<bool> {
295        Ok(self.get_collection(collection_name).await?.exists)
296    }
297
298    /// Lists a page of collections in the space.
299    ///
300    /// Defaults to the server page size (20, max 100). Pass `offset_token` from a
301    /// previous response's `next_offset_token` to fetch the next page.
302    ///
303    /// ```no_run
304    /// # use qdrant_client::serverless::{ListCollectionsBuilder, QdrantServerless};
305    /// # async fn run(client: QdrantServerless) -> Result<(), qdrant_client::QdrantError> {
306    /// let page = client
307    ///     .list_collections(ListCollectionsBuilder::new().limit(50))
308    ///     .await?;
309    /// if let Some(token) = page.next_offset_token {
310    ///     let next = client
311    ///         .list_collections(ListCollectionsBuilder::new().offset_token(token))
312    ///         .await?;
313    /// }
314    /// # Ok(())
315    /// # }
316    /// ```
317    pub async fn list_collections(
318        &self,
319        request: impl Into<ListCollections>,
320    ) -> QdrantResult<CollectionsList> {
321        let ListCollections {
322            limit,
323            offset_token,
324        } = request.into();
325        let request = ListCollectionsRequest {
326            limit,
327            offset_token,
328        };
329        let request = &request;
330        self.with_collections_client(|mut api| async move {
331            let response = api.list_collections(request.clone()).await?.into_inner();
332            Ok(CollectionsList {
333                collections: response
334                    .collections
335                    .into_iter()
336                    .map(|c| CollectionSummary {
337                        collection_name: c.collection_name,
338                        point_count: c.point_count,
339                    })
340                    .collect(),
341                next_offset_token: response.next_offset_token,
342                time: response.time,
343            })
344        })
345        .await
346    }
347}
348
349/// # Point operations
350///
351/// Same semantics as the regular client, minus parameters serverless does not
352/// support. Unsupported fields on the request (`read_consistency`,
353/// `shard_key_selector`, `ordering`, `lookup_from`, `with_lookup`,
354/// `update_filter`, `update_mode`) are cleared before the RPC.
355impl QdrantServerless {
356    /// Query points in a collection.
357    pub async fn query(&self, request: impl Into<QueryPoints>) -> QdrantResult<QueryResponse> {
358        let mut request = request.into();
359        sanitize_query_points(&mut request);
360        self.points.query(request).await
361    }
362
363    /// Batch multiple point queries in a collection.
364    pub async fn query_batch(
365        &self,
366        request: impl Into<QueryBatchPoints>,
367    ) -> QdrantResult<QueryBatchResponse> {
368        let mut request = request.into();
369        request.read_consistency = None;
370        for query in &mut request.query_points {
371            sanitize_query_points(query);
372        }
373        self.points.query_batch(request).await
374    }
375
376    /// Query points and group results by a payload field.
377    pub async fn query_groups(
378        &self,
379        request: impl Into<QueryPointGroups>,
380    ) -> QdrantResult<QueryGroupsResponse> {
381        let mut request = request.into();
382        request.read_consistency = None;
383        request.shard_key_selector = None;
384        request.lookup_from = None;
385        request.with_lookup = None;
386        self.points.query_groups(request).await
387    }
388
389    /// Retrieve points by IDs.
390    pub async fn get_points(&self, request: impl Into<GetPoints>) -> QdrantResult<GetResponse> {
391        let mut request = request.into();
392        request.read_consistency = None;
393        request.shard_key_selector = None;
394        self.points.get_points(request).await
395    }
396
397    /// Scroll over points, optionally filtered.
398    pub async fn scroll(&self, request: impl Into<ScrollPoints>) -> QdrantResult<ScrollResponse> {
399        let mut request = request.into();
400        request.read_consistency = None;
401        request.shard_key_selector = None;
402        self.points.scroll(request).await
403    }
404
405    /// Count points in a collection.
406    pub async fn count(&self, request: impl Into<CountPoints>) -> QdrantResult<CountResponse> {
407        let mut request = request.into();
408        request.read_consistency = None;
409        request.shard_key_selector = None;
410        self.points.count(request).await
411    }
412
413    /// Insert or update points.
414    ///
415    /// Clears `ordering`, `shard_key_selector`, `update_filter`, and `update_mode`.
416    pub async fn upsert_points(
417        &self,
418        request: impl Into<UpsertPoints>,
419    ) -> QdrantResult<PointsOperationResponse> {
420        let mut request = request.into();
421        request.ordering = None;
422        request.shard_key_selector = None;
423        request.update_filter = None;
424        request.update_mode = None;
425        self.points.upsert_points(request).await
426    }
427
428    /// Update vectors of existing points.
429    pub async fn update_vectors(
430        &self,
431        request: impl Into<UpdatePointVectors>,
432    ) -> QdrantResult<PointsOperationResponse> {
433        let mut request = request.into();
434        request.ordering = None;
435        request.shard_key_selector = None;
436        request.update_filter = None;
437        self.points.update_vectors(request).await
438    }
439
440    /// Delete named vectors from points.
441    pub async fn delete_vectors(
442        &self,
443        request: impl Into<DeletePointVectors>,
444    ) -> QdrantResult<PointsOperationResponse> {
445        let mut request = request.into();
446        request.ordering = None;
447        request.shard_key_selector = None;
448        self.points.delete_vectors(request).await
449    }
450
451    /// Delete points by selector.
452    ///
453    /// Prefer selecting by explicit IDs: serverless rejects filtered updates.
454    pub async fn delete_points(
455        &self,
456        request: impl Into<DeletePoints>,
457    ) -> QdrantResult<PointsOperationResponse> {
458        let mut request = request.into();
459        request.ordering = None;
460        request.shard_key_selector = None;
461        self.points.delete_points(request).await
462    }
463
464    /// Set (merge) payload on points.
465    pub async fn set_payload(
466        &self,
467        request: impl Into<SetPayloadPoints>,
468    ) -> QdrantResult<PointsOperationResponse> {
469        let mut request = request.into();
470        request.ordering = None;
471        request.shard_key_selector = None;
472        self.points.set_payload(request).await
473    }
474
475    /// Overwrite the entire payload of points.
476    pub async fn overwrite_payload(
477        &self,
478        request: impl Into<SetPayloadPoints>,
479    ) -> QdrantResult<PointsOperationResponse> {
480        let mut request = request.into();
481        request.ordering = None;
482        request.shard_key_selector = None;
483        self.points.overwrite_payload(request).await
484    }
485
486    /// Delete payload keys from points.
487    pub async fn delete_payload(
488        &self,
489        request: impl Into<DeletePayloadPoints>,
490    ) -> QdrantResult<PointsOperationResponse> {
491        let mut request = request.into();
492        request.ordering = None;
493        request.shard_key_selector = None;
494        self.points.delete_payload(request).await
495    }
496
497    /// Clear the entire payload of points.
498    pub async fn clear_payload(
499        &self,
500        request: impl Into<ClearPayloadPoints>,
501    ) -> QdrantResult<PointsOperationResponse> {
502        let mut request = request.into();
503        request.ordering = None;
504        request.shard_key_selector = None;
505        self.points.clear_payload(request).await
506    }
507
508    /// Batch point update operations.
509    ///
510    /// Operations with filter-based selectors are rejected by the serverless service.
511    pub async fn batch_update_points(
512        &self,
513        request: impl Into<UpdateBatchPoints>,
514    ) -> QdrantResult<UpdateBatchResponse> {
515        let mut request = request.into();
516        request.ordering = None;
517        self.points.update_points_batch(request).await
518    }
519}
520
521fn sanitize_query_points(request: &mut QueryPoints) {
522    request.read_consistency = None;
523    request.shard_key_selector = None;
524    request.lookup_from = None;
525}
526
527/// Ensure a serverless URL uses port 443 when none is specified.
528fn normalize_serverless_url(url: &str) -> String {
529    match url.parse::<Uri>() {
530        Ok(uri) => {
531            if uri.port().is_some() {
532                return url.to_string();
533            }
534            let scheme = uri.scheme_str().unwrap_or("https");
535            let Some(authority) = uri.authority() else {
536                return url.to_string();
537            };
538            let host = authority.host();
539            let path_and_query = uri.path_and_query().map(|pq| pq.as_str()).unwrap_or("");
540            // Avoid duplicating "/" path when the URI has only the default path.
541            let path = if path_and_query == "/" {
542                ""
543            } else {
544                path_and_query
545            };
546            format!("{scheme}://{host}:{DEFAULT_SERVERLESS_GRPC_PORT}{path}")
547        }
548        Err(_) => url.to_string(),
549    }
550}
551
552#[cfg(test)]
553mod tests {
554    use super::*;
555
556    #[test]
557    fn normalize_adds_default_port() {
558        assert_eq!(
559            normalize_serverless_url("https://serverless.example.qdrant.io"),
560            "https://serverless.example.qdrant.io:443"
561        );
562        assert_eq!(
563            normalize_serverless_url("https://serverless.example.qdrant.io:8443"),
564            "https://serverless.example.qdrant.io:8443"
565        );
566    }
567
568    #[test]
569    fn client_construction_is_offline() {
570        let client = QdrantServerless::from_url("https://serverless.example.qdrant.io")
571            .api_key("secret")
572            .build()
573            .unwrap();
574        assert_eq!(
575            client.points.config.uri,
576            "https://serverless.example.qdrant.io:443"
577        );
578        assert_eq!(client.points.config.api_key.as_deref(), Some("secret"));
579        assert!(!client.points.config.check_compatibility);
580    }
581}