From 4db56be3c3fc15307a70cd7cb445b5683796a5f6 Mon Sep 17 00:00:00 2001 From: Amir Mohammadi Date: Sat, 29 Nov 2025 12:18:43 +0330 Subject: [PATCH] Add list shard keys API --- proto/collections.proto | 15 ++- proto/collections_service.proto | 4 + proto/points.proto | 2 +- proto/qdrant_common.proto | 142 ++++++++++++++++++++ src/grpc_conversions/primitives.rs | 18 ++- src/qdrant.rs | 101 ++++++++++++++ src/qdrant_client/sharding_keys.rs | 26 ++++ tests/snippet_tests/mod.rs | 1 + tests/snippet_tests/test_list_shard_keys.rs | 15 +++ tests/snippets/list_shard_keys.rs | 5 + 10 files changed, 322 insertions(+), 7 deletions(-) create mode 100644 proto/qdrant_common.proto create mode 100644 tests/snippet_tests/test_list_shard_keys.rs create mode 100644 tests/snippets/list_shard_keys.rs diff --git a/proto/collections.proto b/proto/collections.proto index df9c27bd..f325b0f0 100644 --- a/proto/collections.proto +++ b/proto/collections.proto @@ -4,7 +4,7 @@ package qdrant; option csharp_namespace = "Qdrant.Client.Grpc"; import "json_with_int.proto"; -import "common.proto"; +import "qdrant_common.proto"; enum Datatype { Default = 0; @@ -790,6 +790,10 @@ message DeleteShardKeyRequest { optional uint64 timeout = 3; // Wait timeout for operation commit in seconds, if not specified - default value will be supplied } +message ListShardKeysRequest { + string collection_name = 1; // Name of the collection +} + message CreateShardKeyResponse { bool result = 1; } @@ -797,3 +801,12 @@ message CreateShardKeyResponse { message DeleteShardKeyResponse { bool result = 1; } + +message ShardKeyDescription { + ShardKey key = 1; +} + +message ListShardKeysResponse { + repeated ShardKeyDescription shard_keys = 1; + double time = 2; // Time spent to process +} diff --git a/proto/collections_service.proto b/proto/collections_service.proto index 52caa8d2..354b8788 100644 --- a/proto/collections_service.proto +++ b/proto/collections_service.proto @@ -58,4 +58,8 @@ service Collections { Delete shard key */ rpc DeleteShardKey (DeleteShardKeyRequest) returns (DeleteShardKeyResponse) {} + /* + List shard keys + */ + rpc ListShardKeys (ListShardKeysRequest) returns (ListShardKeysResponse) {} } diff --git a/proto/points.proto b/proto/points.proto index fd50fa93..57ed3e86 100644 --- a/proto/points.proto +++ b/proto/points.proto @@ -4,7 +4,7 @@ package qdrant; option csharp_namespace = "Qdrant.Client.Grpc"; import "collections.proto"; -import "common.proto"; +import "qdrant_common.proto"; import "google/protobuf/timestamp.proto"; import "json_with_int.proto"; diff --git a/proto/qdrant_common.proto b/proto/qdrant_common.proto new file mode 100644 index 00000000..60370ae9 --- /dev/null +++ b/proto/qdrant_common.proto @@ -0,0 +1,142 @@ +syntax = "proto3"; +package qdrant; + +option csharp_namespace = "Qdrant.Client.Grpc"; +option java_outer_classname = "Common"; + +import "google/protobuf/timestamp.proto"; + +message PointId { + oneof point_id_options { + uint64 num = 1; // Numerical ID of the point + string uuid = 2; // UUID + } +} + +message GeoPoint { + double lon = 1; + double lat = 2; +} + +message Filter { + repeated Condition should = 1; // At least one of those conditions should match + repeated Condition must = 2; // All conditions must match + repeated Condition must_not = 3; // All conditions must NOT match + optional MinShould min_should = 4; // At least minimum amount of given conditions should match +} + +message MinShould { + repeated Condition conditions = 1; + uint64 min_count = 2; +} + +message Condition { + oneof condition_one_of { + FieldCondition field = 1; + IsEmptyCondition is_empty = 2; + HasIdCondition has_id = 3; + Filter filter = 4; + IsNullCondition is_null = 5; + NestedCondition nested = 6; + HasVectorCondition has_vector = 7; + } +} + +message IsEmptyCondition { + string key = 1; +} + +message IsNullCondition { + string key = 1; +} + +message HasIdCondition { + repeated PointId has_id = 1; +} + +message HasVectorCondition { + string has_vector = 1; +} + +message NestedCondition { + string key = 1; // Path to nested object + Filter filter = 2; // Filter condition +} + +message FieldCondition { + string key = 1; + Match match = 2; // Check if point has field with a given value + Range range = 3; // Check if points value lies in a given range + GeoBoundingBox geo_bounding_box = 4; // Check if points geolocation lies in a given area + GeoRadius geo_radius = 5; // Check if geo point is within a given radius + ValuesCount values_count = 6; // Check number of values for a specific field + GeoPolygon geo_polygon = 7; // Check if geo point is within a given polygon + DatetimeRange datetime_range = 8; // Check if datetime is within a given range + optional bool is_empty = 9; // Check if field is empty + optional bool is_null = 10; // Check if field is null +} + +message Match { + oneof match_value { + string keyword = 1; // Match string keyword + int64 integer = 2; // Match integer + bool boolean = 3; // Match boolean + string text = 4; // Match text + RepeatedStrings keywords = 5; // Match multiple keywords + RepeatedIntegers integers = 6; // Match multiple integers + RepeatedIntegers except_integers = 7; // Match any other value except those integers + RepeatedStrings except_keywords = 8; // Match any other value except those keywords + string phrase = 9; // Match phrase text + string text_any = 10; // Match any word in the text + } +} + +message RepeatedStrings { + repeated string strings = 1; +} + +message RepeatedIntegers { + repeated int64 integers = 1; +} + +message Range { + optional double lt = 1; + optional double gt = 2; + optional double gte = 3; + optional double lte = 4; +} + +message DatetimeRange { + optional google.protobuf.Timestamp lt = 1; + optional google.protobuf.Timestamp gt = 2; + optional google.protobuf.Timestamp gte = 3; + optional google.protobuf.Timestamp lte = 4; +} + +message GeoBoundingBox { + GeoPoint top_left = 1; // north-west corner + GeoPoint bottom_right = 2; // south-east corner +} + +message GeoRadius { + GeoPoint center = 1; // Center of the circle + float radius = 2; // In meters +} + +message GeoLineString { + repeated GeoPoint points = 1; // Ordered sequence of GeoPoints representing the line +} + +// For a valid GeoPolygon, both the exterior and interior GeoLineStrings must consist of a minimum of 4 points. +// Additionally, the first and last points of each GeoLineString must be the same. +message GeoPolygon { + GeoLineString exterior = 1; // The exterior line bounds the surface + repeated GeoLineString interiors = 2; // Interior lines (if present) bound holes within the surface +} + +message ValuesCount { + optional uint64 lt = 1; + optional uint64 gt = 2; + optional uint64 gte = 3; + optional uint64 lte = 4; +} \ No newline at end of file diff --git a/src/grpc_conversions/primitives.rs b/src/grpc_conversions/primitives.rs index a4cdd20d..7a451834 100644 --- a/src/grpc_conversions/primitives.rs +++ b/src/grpc_conversions/primitives.rs @@ -7,11 +7,11 @@ use crate::qdrant::{ shard_key, with_payload_selector, with_vectors_selector, CollectionClusterInfoRequest, CollectionExistsRequest, CreateSnapshotRequest, DeleteAlias, DeleteCollectionBuilder, DeleteFullSnapshotRequest, GetCollectionInfoRequest, IsEmptyCondition, IsNullCondition, - ListCollectionAliasesRequest, ListSnapshotsRequest, PayloadExcludeSelector, - PayloadIncludeSelector, PointId, RepeatedIntegers, RepeatedStrings, ShardKey, ShardKeySelector, - SparseIndices, SparseVectorConfig, SparseVectorParams, Struct, VectorParams, VectorParamsDiff, - VectorParamsDiffMap, VectorParamsMap, VectorsSelector, WithPayloadSelector, - WithVectorsSelector, + ListCollectionAliasesRequest, ListShardKeysRequest, ListSnapshotsRequest, + PayloadExcludeSelector, PayloadIncludeSelector, PointId, RepeatedIntegers, RepeatedStrings, + ShardKey, ShardKeySelector, SparseIndices, SparseVectorConfig, SparseVectorParams, Struct, + VectorParams, VectorParamsDiff, VectorParamsDiffMap, VectorParamsMap, VectorsSelector, + WithPayloadSelector, WithVectorsSelector, }; impl From for WithPayloadSelector { @@ -365,3 +365,11 @@ impl> From for DeleteFullSnapshotRequest { } } } + +impl> From for ListShardKeysRequest { + fn from(value: S) -> Self { + Self { + collection_name: value.into(), + } + } +} diff --git a/src/qdrant.rs b/src/qdrant.rs index 99c8f3ff..5dc9d72b 100644 --- a/src/qdrant.rs +++ b/src/qdrant.rs @@ -1613,6 +1613,12 @@ pub struct DeleteShardKeyRequest { #[prost(uint64, optional, tag = "3")] pub timeout: ::core::option::Option, } +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct ListShardKeysRequest { + /// Name of the collection + #[prost(string, tag = "1")] + pub collection_name: ::prost::alloc::string::String, +} #[derive(Clone, Copy, PartialEq, ::prost::Message)] pub struct CreateShardKeyResponse { #[prost(bool, tag = "1")] @@ -1623,6 +1629,19 @@ pub struct DeleteShardKeyResponse { #[prost(bool, tag = "1")] pub result: bool, } +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct ShardKeyDescription { + #[prost(message, optional, tag = "1")] + pub key: ::core::option::Option, +} +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct ListShardKeysResponse { + #[prost(message, repeated, tag = "1")] + pub shard_keys: ::prost::alloc::vec::Vec, + /// Time spent to process + #[prost(double, tag = "2")] + pub time: f64, +} #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord, ::prost::Enumeration)] #[repr(i32)] pub enum Datatype { @@ -2527,6 +2546,32 @@ pub mod collections_client { .insert(GrpcMethod::new("qdrant.Collections", "DeleteShardKey")); self.inner.unary(req, path, codec).await } + /// + /// List shard keys + pub async fn list_shard_keys( + &mut self, + request: impl tonic::IntoRequest, + ) -> std::result::Result< + tonic::Response, + tonic::Status, + > { + self.inner + .ready() + .await + .map_err(|e| { + tonic::Status::unknown( + format!("Service was not ready: {}", e.into()), + ) + })?; + let codec = tonic::codec::ProstCodec::default(); + let path = http::uri::PathAndQuery::from_static( + "/qdrant.Collections/ListShardKeys", + ); + let mut req = request.into_request(); + req.extensions_mut() + .insert(GrpcMethod::new("qdrant.Collections", "ListShardKeys")); + self.inner.unary(req, path, codec).await + } } } /// Generated server implementations. @@ -2659,6 +2704,15 @@ pub mod collections_server { tonic::Response, tonic::Status, >; + /// + /// List shard keys + async fn list_shard_keys( + &self, + request: tonic::Request, + ) -> std::result::Result< + tonic::Response, + tonic::Status, + >; } #[derive(Debug)] pub struct CollectionsServer { @@ -3330,6 +3384,51 @@ pub mod collections_server { }; Box::pin(fut) } + "/qdrant.Collections/ListShardKeys" => { + #[allow(non_camel_case_types)] + struct ListShardKeysSvc(pub Arc); + impl< + T: Collections, + > tonic::server::UnaryService + for ListShardKeysSvc { + type Response = super::ListShardKeysResponse; + type Future = BoxFuture< + tonic::Response, + tonic::Status, + >; + fn call( + &mut self, + request: tonic::Request, + ) -> Self::Future { + let inner = Arc::clone(&self.0); + let fut = async move { + ::list_shard_keys(&inner, request).await + }; + Box::pin(fut) + } + } + let accept_compression_encodings = self.accept_compression_encodings; + let send_compression_encodings = self.send_compression_encodings; + let max_decoding_message_size = self.max_decoding_message_size; + let max_encoding_message_size = self.max_encoding_message_size; + let inner = self.inner.clone(); + let fut = async move { + let method = ListShardKeysSvc(inner); + let codec = tonic::codec::ProstCodec::default(); + let mut grpc = tonic::server::Grpc::new(codec) + .apply_compression_config( + accept_compression_encodings, + send_compression_encodings, + ) + .apply_max_message_size_config( + max_decoding_message_size, + max_encoding_message_size, + ); + let res = grpc.unary(method, req).await; + Ok(res) + }; + Box::pin(fut) + } _ => { Box::pin(async move { let mut response = http::Response::new(empty_body()); @@ -3437,7 +3536,9 @@ pub struct Vector { /// Vector data (flatten for multi vectors), deprecated #[deprecated] #[prost(float, repeated, packed = "false", tag = "1")] + /** +Deprecated since 1.16.0, use [`vector`](crate::qdrant::Vector::vector) field instead.*/ pub data: ::prost::alloc::vec::Vec, /// Sparse indices for sparse vectors, deprecated #[deprecated] diff --git a/src/qdrant_client/sharding_keys.rs b/src/qdrant_client/sharding_keys.rs index 259ecc41..b46cf688 100644 --- a/src/qdrant_client/sharding_keys.rs +++ b/src/qdrant_client/sharding_keys.rs @@ -1,5 +1,6 @@ use crate::qdrant::{ CreateShardKeyRequest, CreateShardKeyResponse, DeleteShardKeyRequest, DeleteShardKeyResponse, + ListShardKeysRequest, ListShardKeysResponse, }; use crate::qdrant_client::{Qdrant, QdrantResult}; @@ -78,4 +79,29 @@ impl Qdrant { }) .await } + + /// List shard keys for a specific collection. + /// + /// ```no_run + ///# use qdrant_client::{Qdrant, QdrantError}; + ///# async fn list_shard_keys(client: &Qdrant) + ///# -> Result<(), QdrantError> { + /// client.list_shard_keys("my_collection").await?; + ///# Ok(()) + ///# } + /// ``` + /// + /// Documentation: + pub async fn list_shard_keys( + &self, + request: impl Into, + ) -> QdrantResult { + let request = &request.into(); + + self.with_collections_client(|mut collection_api| async move { + let result = collection_api.list_shard_keys(request.clone()).await?; + Ok(result.into_inner()) + }) + .await + } } diff --git a/tests/snippet_tests/mod.rs b/tests/snippet_tests/mod.rs index 453928c4..5d6f6770 100644 --- a/tests/snippet_tests/mod.rs +++ b/tests/snippet_tests/mod.rs @@ -27,6 +27,7 @@ mod test_get_collections; mod test_get_collections_aliases; mod test_get_points; mod test_list_full_snapshots; +mod test_list_shard_keys; mod test_list_snapshots; mod test_overwrite_payload; mod test_query_document; diff --git a/tests/snippet_tests/test_list_shard_keys.rs b/tests/snippet_tests/test_list_shard_keys.rs new file mode 100644 index 00000000..c76ef21e --- /dev/null +++ b/tests/snippet_tests/test_list_shard_keys.rs @@ -0,0 +1,15 @@ + +#[tokio::test] +async fn test_list_shard_keys() { + async fn list_shard_keys() -> Result<(), Box> { + // WARNING: This is a generated test snippet. + // Please, modify the snippet in the `../snippets/list_shard_keys.rs` file + use qdrant_client::Qdrant; + + let client = Qdrant::from_url("http://localhost:6334").build()?; + + client.list_shard_keys("{collection_name}").await?; + Ok(()) + } + let _ = list_shard_keys().await; +} diff --git a/tests/snippets/list_shard_keys.rs b/tests/snippets/list_shard_keys.rs new file mode 100644 index 00000000..16936c23 --- /dev/null +++ b/tests/snippets/list_shard_keys.rs @@ -0,0 +1,5 @@ +use qdrant_client::Qdrant; + +let client = Qdrant::from_url("http://localhost:6334").build()?; + +client.list_shard_keys("{collection_name}").await?;