Skip to main content

qdrant_edge/edge/edge_shard/
update.rs

1use std::fmt;
2
3use crate::common::counter::hardware_counter::HardwareCounterCell;
4use crate::segment::common::operation_error::{OperationError, OperationResult};
5use crate::shard::operations::vector_name_ops::VectorNameConfig;
6use crate::shard::operations::{CollectionUpdateOperations, VectorNameOperations};
7use crate::shard::update::*;
8use crate::shard::wal::WalRawRecord;
9
10use crate::edge::EdgeShard;
11use crate::edge::config::vectors::{EdgeSparseVectorParams, EdgeVectorParams};
12
13impl EdgeShard {
14    pub fn update(&self, operation: CollectionUpdateOperations) -> OperationResult<()> {
15        // Reject a conflicting vector-name re-create before it reaches the WAL:
16        // the segment-level create is idempotent, so re-creating an existing
17        // name with different params silently no-ops storage. Failing loudly up
18        // front keeps the bad op out of the WAL and stops `config()` from
19        // drifting away from the stored vectors.
20        if let CollectionUpdateOperations::VectorNameOperation(
21            VectorNameOperations::CreateVectorName(create),
22        ) = &operation
23        {
24            self.check_vector_name_create_compatible(&create.vector_name, &create.config)?;
25        }
26
27        let record = WalRawRecord::new(&operation).map_err(service_error)?;
28
29        let mut wal = self.wal.lock();
30
31        let operation_id = wal.write(&record).map_err(service_error)?;
32        let hw_counter = HardwareCounterCell::disposable();
33        let _update_guard = self.segments.acquire_updates_lock();
34
35        let segments_guard = self.segments.read();
36
37        let result = match operation {
38            CollectionUpdateOperations::PointOperation(point_operation) => {
39                process_point_operation(&segments_guard, operation_id, point_operation, &hw_counter)
40            }
41            CollectionUpdateOperations::VectorOperation(vector_operation) => {
42                process_vector_operation(
43                    &segments_guard,
44                    operation_id,
45                    vector_operation,
46                    &hw_counter,
47                )
48            }
49            CollectionUpdateOperations::PayloadOperation(payload_operation) => {
50                process_payload_operation(
51                    &segments_guard,
52                    operation_id,
53                    payload_operation,
54                    &hw_counter,
55                )
56            }
57            CollectionUpdateOperations::FieldIndexOperation(index_operation) => {
58                process_field_index_operation(
59                    &segments_guard,
60                    operation_id,
61                    &index_operation,
62                    &hw_counter,
63                )
64            }
65            CollectionUpdateOperations::VectorNameOperation(ref vector_name_operation) => {
66                let result = process_vector_name_operation(
67                    &segments_guard,
68                    operation_id,
69                    vector_name_operation,
70                );
71                // Keep the edge shard config in sync with storage so queries can
72                // resolve the vector name — but only when storage actually
73                // changed. A re-create of an existing name is an idempotent
74                // no-op (0 segments changed); re-applying its params would
75                // desync `config()` from what is stored.
76                if let Ok(changed) = result {
77                    self.apply_vector_name_to_config(vector_name_operation, changed)?;
78                }
79                result
80            }
81            #[cfg(feature = "staging")]
82            CollectionUpdateOperations::StagingOperation(staging_operation) => {
83                crate::shard::update::process_staging_operation(
84                    &segments_guard,
85                    operation_id,
86                    staging_operation,
87                )
88            }
89        };
90
91        result.map(|_| ())
92    }
93
94    /// Rejects a re-create whose params conflict with the vector already
95    /// advertised under the same name. Called before the op reaches the WAL.
96    fn check_vector_name_create_compatible(
97        &self,
98        name: &str,
99        config: &VectorNameConfig,
100    ) -> OperationResult<()> {
101        // Compare only the identity fields a `CreateVectorName` op actually
102        // defines. Storage/tuning fields (`on_disk`, `quantization_config`,
103        // `hnsw_config`, `full_scan_threshold`) are populated from the initial
104        // config, the optimizer, or `set_*_config` — e.g. a config-defined
105        // vector is stored with `on_disk: Some(false)` while the op leaves it
106        // `None` — so full-struct equality would flag an otherwise-identical
107        // re-create as a false conflict.
108        let conflict = match requested_vector_params(config) {
109            RequestedVectorParams::Dense(params) => self
110                .config
111                .read()
112                .vectors
113                .get(name)
114                .is_some_and(|existing| !dense_identity_matches(existing, &params)),
115            RequestedVectorParams::Sparse(params) => self
116                .config
117                .read()
118                .sparse_vectors
119                .get(name)
120                .is_some_and(|existing| !sparse_identity_matches(existing, &params)),
121        };
122        if conflict {
123            return Err(OperationError::validation_error(format!(
124                "vector '{name}' already exists with different parameters; delete \
125                 it before recreating with a different configuration"
126            )));
127        }
128        Ok(())
129    }
130
131    /// Update the edge shard config to reflect a vector name create/delete operation.
132    fn apply_vector_name_to_config(
133        &self,
134        operation: &VectorNameOperations,
135        changed: usize,
136    ) -> OperationResult<()> {
137        match operation {
138            VectorNameOperations::CreateVectorName(create) => {
139                // Only record params when storage actually created the vector.
140                // A no-op re-create (`changed == 0`) must not overwrite the
141                // config; `update` has already rejected any conflicting
142                // re-create, so a no-op here is a benign duplicate whose params
143                // already match what is stored.
144                if changed == 0 {
145                    return Ok(());
146                }
147                self.config
148                    .write(|config| match requested_vector_params(&create.config) {
149                        RequestedVectorParams::Dense(params) => {
150                            config.vectors.insert(create.vector_name.clone(), params);
151                        }
152                        RequestedVectorParams::Sparse(params) => {
153                            config
154                                .sparse_vectors
155                                .insert(create.vector_name.clone(), params);
156                        }
157                    })
158                    .map_err(service_error)?;
159            }
160            VectorNameOperations::DeleteVectorName(delete) => {
161                self.config
162                    .write(|config| {
163                        config.vectors.remove(&delete.vector_name);
164                        config.sparse_vectors.remove(&delete.vector_name);
165                    })
166                    .map_err(service_error)?;
167            }
168        }
169        Ok(())
170    }
171}
172
173/// Dense- or sparse-vector params requested by a `CreateVectorName` op,
174/// projected onto the edge config's user-facing shape.
175enum RequestedVectorParams {
176    Dense(EdgeVectorParams),
177    Sparse(EdgeSparseVectorParams),
178}
179
180fn requested_vector_params(config: &VectorNameConfig) -> RequestedVectorParams {
181    match config {
182        VectorNameConfig::Dense(wrapper) => RequestedVectorParams::Dense(EdgeVectorParams {
183            size: wrapper.dense.size,
184            distance: wrapper.dense.distance,
185            on_disk: None,
186            multivector_config: wrapper.dense.multivector_config,
187            datatype: wrapper.dense.datatype,
188            quantization_config: None,
189            hnsw_config: None,
190        }),
191        VectorNameConfig::Sparse(wrapper) => {
192            RequestedVectorParams::Sparse(EdgeSparseVectorParams {
193                full_scan_threshold: None,
194                on_disk: None,
195                modifier: wrapper.sparse.modifier,
196                datatype: wrapper.sparse.datatype,
197            })
198        }
199    }
200}
201
202/// Whether two dense vectors share the identity a `CreateVectorName` op
203/// defines. Excludes storage/tuning fields (`on_disk`, `quantization_config`,
204/// `hnsw_config`) that the op cannot express and that are set elsewhere.
205fn dense_identity_matches(existing: &EdgeVectorParams, requested: &EdgeVectorParams) -> bool {
206    existing.size == requested.size
207        && existing.distance == requested.distance
208        && existing.multivector_config == requested.multivector_config
209        && existing.datatype == requested.datatype
210}
211
212/// Whether two sparse vectors share the identity a `CreateVectorName` op
213/// defines. Excludes `on_disk`/`full_scan_threshold`, which the op cannot
214/// express.
215fn sparse_identity_matches(
216    existing: &EdgeSparseVectorParams,
217    requested: &EdgeSparseVectorParams,
218) -> bool {
219    existing.modifier == requested.modifier && existing.datatype == requested.datatype
220}
221
222fn service_error(err: impl fmt::Display) -> OperationError {
223    OperationError::service_error(err.to_string())
224}