Skip to main content

qdrant_edge/shard/operations/
mod.rs

1pub mod optimization;
2pub mod payload_ops;
3pub mod point_ops;
4#[cfg(feature = "staging")]
5pub mod staging;
6pub mod vector_name_ops;
7pub mod vector_ops;
8
9use crate::segment::json_path::JsonPath;
10use crate::segment::types::{PayloadFieldSchema, PointIdType};
11use serde::{Deserialize, Serialize};
12use strum::{EnumDiscriminants, EnumIter};
13
14pub use self::vector_name_ops::{
15    CreateVectorName, DeleteVectorName, VectorNameConfig, VectorNameOperations,
16};
17use crate::shard::PeerId;
18use crate::shard::operations::point_ops::PointOperations;
19
20#[derive(Clone, Debug, PartialEq, Deserialize, Serialize, EnumDiscriminants, Hash)]
21#[strum_discriminants(derive(EnumIter))]
22#[serde(untagged, rename_all = "snake_case")]
23pub enum CollectionUpdateOperations {
24    PointOperation(point_ops::PointOperations),
25    VectorOperation(vector_ops::VectorOperations),
26    PayloadOperation(payload_ops::PayloadOps),
27    FieldIndexOperation(FieldIndexOperations),
28    VectorNameOperation(VectorNameOperations),
29    /// Staging-only operations for testing and debugging purposes
30    #[cfg(feature = "staging")]
31    StagingOperation(staging::StagingOperations),
32}
33
34impl CollectionUpdateOperations {
35    pub fn is_upsert_points(&self) -> bool {
36        matches!(
37            self,
38            Self::PointOperation(point_ops::PointOperations::UpsertPoints(_))
39        )
40    }
41
42    pub fn is_delete_points(&self) -> bool {
43        matches!(
44            self,
45            Self::PointOperation(point_ops::PointOperations::DeletePoints { .. })
46        )
47    }
48
49    pub fn point_ids(&self) -> Option<Vec<PointIdType>> {
50        match self {
51            Self::PointOperation(op) => op.point_ids(),
52            Self::VectorOperation(op) => op.point_ids(),
53            Self::PayloadOperation(op) => op.point_ids(),
54            Self::FieldIndexOperation(_) => None,
55            Self::VectorNameOperation(_) => None,
56            #[cfg(feature = "staging")]
57            Self::StagingOperation(_) => None,
58        }
59    }
60
61    /// List point IDs that can be created during the operation.
62    /// Do not list IDs that are deleted or modified.
63    pub fn upsert_point_ids(&self) -> Option<Vec<PointIdType>> {
64        match self {
65            Self::PointOperation(op) => match op {
66                PointOperations::UpsertPoints(op) => Some(op.point_ids()),
67                PointOperations::UpsertPointsConditional(op) => Some(op.points_op.point_ids()),
68                PointOperations::DeletePoints { .. } => None,
69                PointOperations::DeletePointsByFilter(_) => None,
70                PointOperations::SyncPoints(op) => {
71                    Some(op.points.iter().map(|point| point.id).collect())
72                }
73            },
74            Self::VectorOperation(_) => None,
75            Self::PayloadOperation(_) => None,
76            Self::FieldIndexOperation(_) => None,
77            Self::VectorNameOperation(_) => None,
78            #[cfg(feature = "staging")]
79            Self::StagingOperation(_) => None,
80        }
81    }
82
83    pub fn retain_point_ids<F>(&mut self, filter: F)
84    where
85        F: Fn(&PointIdType) -> bool,
86    {
87        match self {
88            Self::PointOperation(op) => op.retain_point_ids(filter),
89            Self::VectorOperation(op) => op.retain_point_ids(filter),
90            Self::PayloadOperation(op) => op.retain_point_ids(filter),
91            Self::FieldIndexOperation(_) => (),
92            Self::VectorNameOperation(_) => (),
93            #[cfg(feature = "staging")]
94            Self::StagingOperation(_) => (),
95        }
96    }
97}
98
99#[derive(Clone, Debug, PartialEq, Deserialize, Serialize, EnumDiscriminants, Hash)]
100#[strum_discriminants(derive(EnumIter))]
101#[serde(rename_all = "snake_case")]
102pub enum FieldIndexOperations {
103    /// Create index for payload field
104    CreateIndex(CreateIndex),
105    /// Delete index for the field
106    DeleteIndex(JsonPath),
107}
108
109#[derive(Clone, Debug, PartialEq, Deserialize, Serialize, Hash)]
110#[serde(rename_all = "snake_case")]
111pub struct CreateIndex {
112    pub field_name: JsonPath,
113    pub field_schema: Option<PayloadFieldSchema>,
114}
115
116#[derive(Clone, Debug, PartialEq, Deserialize, Serialize)]
117pub struct OperationWithClockTag {
118    #[serde(flatten)]
119    pub operation: CollectionUpdateOperations,
120
121    #[serde(default, skip_serializing_if = "Option::is_none")]
122    pub clock_tag: Option<ClockTag>,
123}
124
125impl OperationWithClockTag {
126    pub fn new(
127        operation: impl Into<CollectionUpdateOperations>,
128        clock_tag: Option<ClockTag>,
129    ) -> Self {
130        Self {
131            operation: operation.into(),
132            clock_tag,
133        }
134    }
135}
136
137impl From<CollectionUpdateOperations> for OperationWithClockTag {
138    fn from(operation: CollectionUpdateOperations) -> Self {
139        Self::new(operation, None)
140    }
141}
142
143#[derive(Copy, Clone, Debug, Eq, PartialEq, Deserialize, Serialize)]
144pub struct ClockTag {
145    pub peer_id: PeerId,
146    pub clock_id: u32,
147    pub clock_tick: u64,
148    /// A unique token for each clock tag.
149    pub token: ClockToken,
150    pub force: bool,
151}
152
153pub type ClockToken = u64;
154
155impl ClockTag {
156    pub fn new(peer_id: PeerId, clock_id: u32, clock_tick: u64) -> Self {
157        let random_token = rand::random();
158        Self::new_with_token(peer_id, clock_id, clock_tick, random_token)
159    }
160
161    pub fn new_with_token(
162        peer_id: PeerId,
163        clock_id: u32,
164        clock_tick: u64,
165        token: ClockToken,
166    ) -> Self {
167        Self {
168            peer_id,
169            clock_id,
170            clock_tick,
171            token,
172            force: false,
173        }
174    }
175
176    pub fn force(mut self, force: bool) -> Self {
177        self.force = force;
178        self
179    }
180}
181
182#[cfg(feature = "api")]
183impl From<api::grpc::qdrant::ClockTag> for ClockTag {
184    fn from(tag: api::grpc::qdrant::ClockTag) -> Self {
185        let api::grpc::qdrant::ClockTag {
186            peer_id,
187            clock_id,
188            clock_tick,
189            token,
190            force,
191        } = tag;
192        Self {
193            peer_id,
194            clock_id,
195            clock_tick,
196            token,
197            force,
198        }
199    }
200}
201
202#[cfg(feature = "api")]
203impl From<ClockTag> for api::grpc::qdrant::ClockTag {
204    fn from(tag: ClockTag) -> Self {
205        let ClockTag {
206            peer_id,
207            clock_id,
208            clock_tick,
209            token,
210            force,
211        } = tag;
212        Self {
213            peer_id,
214            clock_id,
215            clock_tick,
216            token,
217            force,
218        }
219    }
220}
221
222#[cfg(test)]
223mod tests {
224    #![expect(clippy::wildcard_enum_match_arm, reason = "test code")]
225
226    use proptest::prelude::*;
227    use crate::segment::types::*;
228
229    use super::payload_ops::*;
230    use super::point_ops::*;
231    use super::vector_ops::*;
232    use super::*;
233
234    proptest::proptest! {
235        #[test]
236        fn operation_with_clock_tag_json(operation in any::<OperationWithClockTag>()) {
237            // Assert that `OperationWithClockTag` can be serialized
238            let input = serde_json::to_string(&operation).unwrap();
239            let output: OperationWithClockTag = serde_json::from_str(&input).unwrap();
240            assert_eq!(operation, output);
241
242            // Assert that `OperationWithClockTag` can be deserialized from `CollectionUpdateOperation`
243            let input = serde_json::to_string(&operation.operation).unwrap();
244            let output: OperationWithClockTag = serde_json::from_str(&input).unwrap();
245            assert_eq!(operation.operation, output.operation);
246
247            // Assert that `CollectionUpdateOperation` serializes into JSON object with a single key
248            // (e.g., `{ "upsert_points": <upsert points object> }`)
249            match serde_json::to_value(&operation.operation).unwrap() {
250                serde_json::Value::Object(map) if map.len() == 1 => (),
251                _ => panic!("TODO"),
252            };
253        }
254
255        #[test]
256        fn operation_with_clock_tag_cbor(operation in any::<OperationWithClockTag>()) {
257            // Assert that `OperationWithClockTag` can be serialized
258            let input = serde_cbor::to_vec(&operation).unwrap();
259            let output: OperationWithClockTag = serde_cbor::from_slice(&input).unwrap();
260            assert_eq!(operation, output);
261
262            // Assert that `OperationWithClockTag` can be deserialized from `CollectionUpdateOperation`
263            let input = serde_cbor::to_vec(&operation.operation).unwrap();
264            let output: OperationWithClockTag = serde_cbor::from_slice(&input).unwrap();
265            assert_eq!(operation.operation, output.operation);
266        }
267    }
268
269    impl Arbitrary for OperationWithClockTag {
270        type Parameters = ();
271        type Strategy = BoxedStrategy<Self>;
272
273        fn arbitrary_with(_: Self::Parameters) -> Self::Strategy {
274            any::<(CollectionUpdateOperations, Option<ClockTag>)>()
275                .prop_map(|(operation, clock_tag)| Self::new(operation, clock_tag))
276                .boxed()
277        }
278    }
279
280    impl Arbitrary for ClockTag {
281        type Parameters = ();
282        type Strategy = BoxedStrategy<Self>;
283
284        fn arbitrary_with(_: Self::Parameters) -> Self::Strategy {
285            any::<(PeerId, u32, u64)>()
286                .prop_map(|(peer_id, clock_id, clock_tick)| {
287                    Self::new(peer_id, clock_id, clock_tick)
288                })
289                .boxed()
290        }
291    }
292
293    impl Arbitrary for CollectionUpdateOperations {
294        type Parameters = ();
295        type Strategy = BoxedStrategy<Self>;
296
297        fn arbitrary_with(_: Self::Parameters) -> Self::Strategy {
298            prop_oneof![
299                any::<point_ops::PointOperations>().prop_map(Self::PointOperation),
300                any::<vector_ops::VectorOperations>().prop_map(Self::VectorOperation),
301                any::<payload_ops::PayloadOps>().prop_map(Self::PayloadOperation),
302                any::<FieldIndexOperations>().prop_map(Self::FieldIndexOperation),
303                any::<VectorNameOperations>().prop_map(Self::VectorNameOperation),
304            ]
305            .boxed()
306        }
307    }
308
309    impl Arbitrary for point_ops::PointOperations {
310        type Parameters = ();
311        type Strategy = BoxedStrategy<Self>;
312
313        fn arbitrary_with(_: Self::Parameters) -> Self::Strategy {
314            let upsert = Self::UpsertPoints(PointInsertOperationsInternal::PointsList(Vec::new()));
315            let delete = Self::DeletePoints { ids: Vec::new() };
316
317            let delete_by_filter = Self::DeletePointsByFilter(Filter {
318                should: None,
319                min_should: None,
320                must: None,
321                must_not: None,
322            });
323
324            let sync = Self::SyncPoints(PointSyncOperation {
325                from_id: None,
326                to_id: None,
327                points: Vec::new(),
328            });
329
330            prop_oneof![
331                Just(upsert),
332                Just(delete),
333                Just(delete_by_filter),
334                Just(sync),
335            ]
336            .boxed()
337        }
338    }
339
340    impl Arbitrary for vector_ops::VectorOperations {
341        type Parameters = ();
342        type Strategy = BoxedStrategy<Self>;
343
344        fn arbitrary_with(_: Self::Parameters) -> Self::Strategy {
345            let update = Self::UpdateVectors(UpdateVectorsOp {
346                points: Vec::new(),
347                update_filter: None,
348            });
349
350            let delete = Self::DeleteVectors(
351                PointIdsList {
352                    points: Vec::new(),
353                    #[cfg(feature = "api")]
354                    shard_key: None,
355                },
356                Vec::new(),
357            );
358
359            let delete_by_filter = Self::DeleteVectorsByFilter(
360                Filter {
361                    should: None,
362                    min_should: None,
363                    must: None,
364                    must_not: None,
365                },
366                Vec::new(),
367            );
368
369            prop_oneof![Just(update), Just(delete), Just(delete_by_filter),].boxed()
370        }
371    }
372
373    impl Arbitrary for payload_ops::PayloadOps {
374        type Parameters = ();
375        type Strategy = BoxedStrategy<Self>;
376
377        fn arbitrary_with(_: Self::Parameters) -> Self::Strategy {
378            let set = Self::SetPayload(SetPayloadOp {
379                payload: Payload(Default::default()),
380                points: None,
381                filter: None,
382                key: None,
383            });
384
385            let overwrite = Self::OverwritePayload(SetPayloadOp {
386                payload: Payload(Default::default()),
387                points: None,
388                filter: None,
389                key: None,
390            });
391
392            let delete = Self::DeletePayload(DeletePayloadOp {
393                keys: Vec::new(),
394                points: None,
395                filter: None,
396            });
397
398            let clear = Self::ClearPayload { points: Vec::new() };
399
400            let clear_by_filter = Self::ClearPayloadByFilter(Filter {
401                should: None,
402                min_should: None,
403                must: None,
404                must_not: None,
405            });
406
407            prop_oneof![
408                Just(set),
409                Just(overwrite),
410                Just(delete),
411                Just(clear),
412                Just(clear_by_filter),
413            ]
414            .boxed()
415        }
416    }
417
418    impl Arbitrary for FieldIndexOperations {
419        type Parameters = ();
420        type Strategy = BoxedStrategy<Self>;
421
422        fn arbitrary_with(_: Self::Parameters) -> Self::Strategy {
423            let create = Self::CreateIndex(CreateIndex {
424                field_name: "field_name".parse().unwrap(),
425                field_schema: None,
426            });
427
428            let delete = Self::DeleteIndex("field_name".parse().unwrap());
429
430            prop_oneof![Just(create), Just(delete),].boxed()
431        }
432    }
433
434    impl Arbitrary for VectorNameOperations {
435        type Parameters = ();
436        type Strategy = BoxedStrategy<Self>;
437
438        fn arbitrary_with(_: Self::Parameters) -> Self::Strategy {
439            use crate::shard::operations::vector_name_ops::{
440                self as vnops, DenseVectorConfig, SparseVectorConfig,
441            };
442
443            let create_dense = Self::CreateVectorName(CreateVectorName {
444                vector_name: "test_vector".into(),
445                config: vnops::VectorNameConfig::dense(DenseVectorConfig {
446                    size: 4,
447                    distance: Distance::Cosine,
448                    multivector_config: None,
449                    datatype: None,
450                }),
451            });
452
453            let create_sparse = Self::CreateVectorName(CreateVectorName {
454                vector_name: "sparse_test".into(),
455                config: vnops::VectorNameConfig::sparse(SparseVectorConfig {
456                    modifier: None,
457                    datatype: None,
458                }),
459            });
460
461            let delete = Self::DeleteVectorName(DeleteVectorName {
462                vector_name: "test_vector".into(),
463            });
464
465            prop_oneof![Just(create_dense), Just(create_sparse), Just(delete),].boxed()
466        }
467    }
468
469    #[test]
470    fn test_delete_by_filter_with_has_id_uuids_cbor_roundtrip() {
471        let uuids: Vec<PointIdType> = vec![ExtendedPointId::Uuid(
472            uuid::Uuid::parse_str("6ba7b810-9dad-11d1-80b4-00c04fd430c8").unwrap(),
473        )];
474
475        let filter = Filter {
476            should: None,
477            min_should: None,
478            must: None,
479            must_not: Some(vec![Condition::HasId(HasIdCondition::from(
480                uuids.into_iter().collect::<ahash::AHashSet<_>>(),
481            ))]),
482        };
483
484        let operation = CollectionUpdateOperations::PointOperation(
485            PointOperations::DeletePointsByFilter(filter),
486        );
487
488        let cbor_bytes = serde_cbor::to_vec(&operation).unwrap();
489        let deserialized: CollectionUpdateOperations = serde_cbor::from_slice(&cbor_bytes).unwrap();
490
491        assert_eq!(operation, deserialized);
492    }
493
494    #[test]
495    fn test_wal_roundtrip_delete_by_filter_with_has_id_uuids() {
496        use crate::shard::wal::WalRawRecord;
497
498        let uuids: Vec<PointIdType> = vec![ExtendedPointId::Uuid(
499            uuid::Uuid::parse_str("6ba7b810-9dad-11d1-80b4-00c04fd430c8").unwrap(),
500        )];
501
502        let filter = Filter {
503            should: None,
504            min_should: None,
505            must: None,
506            must_not: Some(vec![Condition::HasId(HasIdCondition::from(
507                uuids.into_iter().collect::<ahash::AHashSet<_>>(),
508            ))]),
509        };
510
511        let operation = CollectionUpdateOperations::PointOperation(
512            PointOperations::DeletePointsByFilter(filter),
513        );
514
515        let raw = WalRawRecord::new(&operation).unwrap();
516        let deserialized: CollectionUpdateOperations = raw.deserialize().unwrap();
517
518        assert_eq!(operation, deserialized);
519    }
520}