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 #[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 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 CreateIndex(CreateIndex),
105 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 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 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 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 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 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 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}