qdrant_client/serverless/
client.rs1use 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
39pub const DEFAULT_SERVERLESS_GRPC_PORT: u16 = 443;
43
44#[derive(Clone)]
94pub struct QdrantServerless {
95 points: Qdrant,
96}
97
98#[derive(Clone)]
104pub struct QdrantServerlessBuilder {
105 config: QdrantConfig,
106}
107
108impl QdrantServerless {
109 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 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
168impl QdrantServerlessBuilder {
170 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 pub fn timeout(mut self, timeout: impl AsTimeout) -> Self {
178 self.config.timeout = timeout.timeout();
179 self
180 }
181
182 pub fn connect_timeout(mut self, timeout: impl AsTimeout) -> Self {
184 self.config.connect_timeout = timeout.timeout();
185 self
186 }
187
188 pub fn compression(mut self, compression: Option<CompressionEncoding>) -> Self {
190 self.config.compression = compression;
191 self
192 }
193
194 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 pub fn keep_alive_while_idle(mut self) -> Self {
202 self.config.keep_alive_while_idle = true;
203 self
204 }
205
206 pub fn build(self) -> QdrantResult<QdrantServerless> {
208 QdrantServerless::new(self.config)
209 }
210}
211
212impl QdrantServerless {
216 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 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 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 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 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
349impl QdrantServerless {
356 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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
527fn 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 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}