Skip to main content

qdrant_edge/edge/update_only/batch/
plan.rs

1//! Collapsing a batch of operations into one [`PointUpdates`] entry per point.
2
3use ahash::AHashMap;
4use crate::segment::common::operation_error::{OperationError, OperationResult};
5use crate::segment::data_types::named_vectors::NamedVectors;
6use crate::segment::types::{PointIdType, SeqNumberType};
7use crate::shard::operations::CollectionUpdateOperations;
8use crate::shard::operations::payload_ops::PayloadOps;
9use crate::shard::operations::point_ops::{
10    PointOperations, PointStructPersisted, PointStructRawPersisted,
11};
12use crate::shard::operations::vector_ops::{PointVectorsPersisted, VectorOperations};
13
14use super::mutation::{OperationVectors, PointMutation, PointUpdates};
15
16/// A batch of update operations, collapsed to one entry per touched point.
17pub struct UpdateBatchPlan {
18    /// Points in the order the batch first touched them, so the writer's
19    /// appends are deterministic for a given batch.
20    order: Vec<PointIdType>,
21    updates: AHashMap<PointIdType, PointUpdates>,
22}
23
24impl UpdateBatchPlan {
25    /// Fold `operations` — each paired with the operation number to record —
26    /// into one entry per point. Operations are expected in ascending
27    /// operation-number order; the fold is order-sensitive.
28    ///
29    /// Rejects everything outside the writer's contract: operations that
30    /// select points by filter, point sync, conditional upserts, and the
31    /// schema-level operations.
32    pub fn build(
33        operations: impl IntoIterator<Item = (SeqNumberType, CollectionUpdateOperations)>,
34    ) -> OperationResult<Self> {
35        let mut plan = Self {
36            order: Vec::new(),
37            updates: AHashMap::new(),
38        };
39
40        for (op_num, operation) in operations {
41            match operation {
42                CollectionUpdateOperations::PointOperation(operation) => {
43                    plan.push_point_operation(op_num, operation)?;
44                }
45                CollectionUpdateOperations::VectorOperation(operation) => {
46                    plan.push_vector_operation(op_num, operation)?;
47                }
48                CollectionUpdateOperations::PayloadOperation(operation) => {
49                    plan.push_payload_operation(op_num, operation)?;
50                }
51                CollectionUpdateOperations::FieldIndexOperation(_) => {
52                    return Err(unsupported("payload index operations"));
53                }
54                CollectionUpdateOperations::VectorNameOperation(_) => {
55                    return Err(unsupported("vector name operations"));
56                }
57                #[cfg(feature = "staging")]
58                CollectionUpdateOperations::StagingOperation(_) => {
59                    return Err(unsupported("staging operations"));
60                }
61            }
62        }
63
64        Ok(plan)
65    }
66
67    fn push(&mut self, id: PointIdType, version: SeqNumberType, mutation: PointMutation) {
68        match self.updates.entry(id) {
69            std::collections::hash_map::Entry::Occupied(mut entry) => {
70                entry.get_mut().push(version, mutation);
71            }
72            std::collections::hash_map::Entry::Vacant(entry) => {
73                entry.insert(PointUpdates::new(version, mutation));
74                self.order.push(id);
75            }
76        }
77    }
78
79    fn push_point_operation(
80        &mut self,
81        op_num: SeqNumberType,
82        operation: PointOperations,
83    ) -> OperationResult<()> {
84        match operation {
85            PointOperations::UpsertPoints(operation) => {
86                for point in operation.into_point_vec() {
87                    // Decode before destructuring: `get_vectors` reads the
88                    // still-owned `vector` field, and taking the payload by
89                    // value afterwards saves a clone of it.
90                    let vectors = OperationVectors::Decoded(point.get_vectors().into_owned());
91                    let PointStructPersisted {
92                        id,
93                        vector: _,
94                        payload,
95                    } = point;
96                    self.push(
97                        id,
98                        op_num,
99                        PointMutation::Replace {
100                            vectors,
101                            payload: payload.unwrap_or_default(),
102                        },
103                    );
104                }
105            }
106            PointOperations::UpsertPointsRaw(points) => {
107                for point in points {
108                    let PointStructRawPersisted {
109                        id,
110                        vectors,
111                        payload,
112                    } = point;
113                    self.push(
114                        id,
115                        op_num,
116                        PointMutation::Replace {
117                            vectors: OperationVectors::Raw(vectors),
118                            payload: payload.unwrap_or_default(),
119                        },
120                    );
121                }
122            }
123            PointOperations::DeletePoints { ids } => {
124                for id in ids {
125                    self.push(id, op_num, PointMutation::Delete);
126                }
127            }
128            PointOperations::UpsertPointsConditional(_) => {
129                return Err(unsupported("conditional upserts"));
130            }
131            PointOperations::DeletePointsByFilter(_) => {
132                return Err(unsupported("deleting points by filter"));
133            }
134            PointOperations::SyncPoints(_) | PointOperations::SyncPointsRaw(_) => {
135                return Err(unsupported("point sync"));
136            }
137        }
138        Ok(())
139    }
140
141    fn push_vector_operation(
142        &mut self,
143        op_num: SeqNumberType,
144        operation: VectorOperations,
145    ) -> OperationResult<()> {
146        match operation {
147            VectorOperations::UpdateVectors(operation) => {
148                if operation.update_filter.is_some() {
149                    return Err(unsupported("conditional vector updates"));
150                }
151                for point in operation.points {
152                    let PointVectorsPersisted { id, vector } = point;
153                    let vectors = NamedVectors::from(vector).into_owned();
154                    self.push(id, op_num, PointMutation::UpdateVectors(vectors));
155                }
156            }
157            VectorOperations::DeleteVectors(points, vector_names) => {
158                for id in points.points {
159                    self.push(
160                        id,
161                        op_num,
162                        PointMutation::DeleteVectors(vector_names.clone()),
163                    );
164                }
165            }
166            VectorOperations::DeleteVectorsByFilter(_, _) => {
167                return Err(unsupported("deleting vectors by filter"));
168            }
169        }
170        Ok(())
171    }
172
173    fn push_payload_operation(
174        &mut self,
175        op_num: SeqNumberType,
176        operation: PayloadOps,
177    ) -> OperationResult<()> {
178        match operation {
179            PayloadOps::SetPayload(operation) => {
180                let points = require_points(operation.points, operation.filter.is_some())?;
181                for id in points {
182                    self.push(
183                        id,
184                        op_num,
185                        PointMutation::SetPayload {
186                            payload: operation.payload.clone(),
187                            key: operation.key.clone(),
188                        },
189                    );
190                }
191            }
192            PayloadOps::OverwritePayload(operation) => {
193                let points = require_points(operation.points, operation.filter.is_some())?;
194                for id in points {
195                    self.push(
196                        id,
197                        op_num,
198                        PointMutation::OverwritePayload(operation.payload.clone()),
199                    );
200                }
201            }
202            PayloadOps::DeletePayload(operation) => {
203                let points = require_points(operation.points, operation.filter.is_some())?;
204                for id in points {
205                    self.push(
206                        id,
207                        op_num,
208                        PointMutation::DeletePayload(operation.keys.clone()),
209                    );
210                }
211            }
212            PayloadOps::ClearPayload { points } => {
213                for id in points {
214                    self.push(id, op_num, PointMutation::ClearPayload);
215                }
216            }
217            PayloadOps::ClearPayloadByFilter(_) => {
218                return Err(unsupported("clearing payload by filter"));
219            }
220        }
221        Ok(())
222    }
223
224    pub fn is_empty(&self) -> bool {
225        self.order.is_empty()
226    }
227
228    pub fn len(&self) -> usize {
229        self.order.len()
230    }
231
232    /// Every point the batch touches, in first-touched order.
233    pub fn point_ids(&self) -> impl Iterator<Item = PointIdType> + '_ {
234        self.order.iter().copied()
235    }
236
237    /// The points whose stored form has to be read before they can be
238    /// rewritten.
239    pub fn point_ids_needing_stored_point(&self) -> impl Iterator<Item = PointIdType> + '_ {
240        self.order
241            .iter()
242            .copied()
243            .filter(|id| self.updates[id].needs_stored_point())
244    }
245
246    /// Consume the plan, yielding one entry per point in first-touched order.
247    pub fn into_point_updates(mut self) -> impl Iterator<Item = (PointIdType, PointUpdates)> {
248        let order = std::mem::take(&mut self.order);
249        order.into_iter().filter_map(move |id| {
250            let updates = self.updates.remove(&id)?;
251            Some((id, updates))
252        })
253    }
254}
255
256/// Point-selecting operations must name their points: resolving a filter means
257/// querying payload indexes, which the writer never fetches.
258fn require_points(
259    points: Option<Vec<PointIdType>>,
260    has_filter: bool,
261) -> OperationResult<Vec<PointIdType>> {
262    match points {
263        Some(points) => Ok(points),
264        None if has_filter => Err(unsupported("selecting points by filter")),
265        None => Err(OperationError::validation_error(
266            "No points or filter specified",
267        )),
268    }
269}
270
271fn unsupported(what: &str) -> OperationError {
272    OperationError::validation_error(format!("The update-only writer does not support {what}"))
273}