Skip to main content

nodedb_physical/physical_plan/
vector.rs

1// SPDX-License-Identifier: Apache-2.0
2
3//! Vector engine operations dispatched to the Data Plane.
4
5use nodedb_types::{Surrogate, SurrogateBitmap, vector_distance::DistanceMetric};
6
7use crate::physical_plan::PhysicalPlan;
8
9/// Vector engine physical operations.
10#[derive(
11    Debug,
12    Clone,
13    PartialEq,
14    serde::Serialize,
15    serde::Deserialize,
16    zerompk::ToMessagePack,
17    zerompk::FromMessagePack,
18)]
19pub enum VectorOp {
20    /// Vector similarity search.
21    Search {
22        collection: String,
23        query_vector: Vec<f32>,
24        top_k: usize,
25        /// Optional search beam width override. If 0, uses default `4 * top_k`.
26        ef_search: usize,
27        /// Per-query distance metric override. Overrides the collection-default
28        /// metric configured at index creation time.
29        metric: DistanceMetric,
30        /// Pre-computed bitmap of eligible surrogates (from filter evaluation).
31        filter_bitmap: Option<SurrogateBitmap>,
32        /// Named vector field to search. Empty string = default field.
33        field_name: String,
34        /// RLS post-candidate filters (serialized `Vec<ScanFilter>`).
35        /// Applied after HNSW returns candidates, before returning to client.
36        /// Result count may be less than requested `top_k`.
37        rls_filters: Vec<u8>,
38        /// Optional sub-plan whose output rows materialize a `SurrogateBitmap`
39        /// used as an additional prefilter (intersected with `filter_bitmap`).
40        ///
41        /// Set by the planner when fusing `Vector ORDER BY ... + JOIN
42        /// ARRAY_SLICE(...) ON v.id = s.surrogate` into a single op:
43        /// the array slice runs first, its surrogate column becomes the
44        /// vector engine's pre-filter, then HNSW search runs against the
45        /// reduced candidate set. Mirrors `HashJoin`'s `inline_*_bitmap`
46        /// mechanism so the Data Plane composition is uniform.
47        inline_prefilter_plan: Option<Box<PhysicalPlan>>,
48        /// ANN tuning knobs from the SQL caller. Defaults to no overrides.
49        ann_options: nodedb_types::VectorAnnOptions,
50        /// When `true`, the SELECT projection contains only `id` and/or
51        /// `vector_distance(...)` — no payload fields. The handler skips
52        /// the document-body fetch. Ignored (treated as `false`) when RLS
53        /// filters are active, as body fetch is required for RLS evaluation.
54        skip_payload_fetch: bool,
55        /// Optional payload bitmap pre-filter for vector-primary collections.
56        /// Each atom is `Eq` / `In` / `Range` — the handler ANDs all atoms
57        /// and intersects the resulting bitmap with the HNSW candidate set
58        /// before walking. The planner only emits atoms whose field has a
59        /// registered payload index of the matching kind. Empty means no
60        /// payload pre-filter.
61        payload_filters: Vec<nodedb_types::PayloadAtom>,
62    },
63
64    /// Insert a vector into the HNSW index (write path).
65    Insert {
66        collection: String,
67        vector: Vec<f32>,
68        dim: usize,
69        /// Named vector field. Empty string = default (unnamed) field.
70        field_name: String,
71        /// Global surrogate identifying this row. Allocated by the
72        /// Control Plane via `SurrogateAssigner` before dispatch; the
73        /// engine binds the HNSW node id to this surrogate.
74        surrogate: Surrogate,
75    },
76
77    /// Batch insert vectors into the HNSW index.
78    BatchInsert {
79        collection: String,
80        vectors: Vec<Vec<f32>>,
81        dim: usize,
82        /// One surrogate per inserted vector, parallel to `vectors`.
83        /// Empty vector = headless batch (no PK binding).
84        surrogates: Vec<Surrogate>,
85    },
86
87    /// Multi-vector search: query across all named vector fields, fuse via RRF.
88    MultiSearch {
89        collection: String,
90        query_vector: Vec<f32>,
91        top_k: usize,
92        ef_search: usize,
93        filter_bitmap: Option<SurrogateBitmap>,
94        /// RLS post-candidate filters.
95        rls_filters: Vec<u8>,
96    },
97
98    /// Soft-delete a vector by internal node ID.
99    Delete { collection: String, vector_id: u32 },
100
101    /// Soft-delete a vector by surrogate (sync inbound path).
102    ///
103    /// The Data Plane resolves `surrogate → HNSW node_id` via the
104    /// `surrogate_to_local` map and tombstones the node.  If the surrogate
105    /// is not found the op is a no-op (idempotent).
106    DeleteBySurrogate {
107        collection: String,
108        surrogate: nodedb_types::Surrogate,
109        /// Named vector field; empty = default field.
110        field_name: String,
111    },
112
113    /// Set vector index parameters for a collection.
114    SetParams {
115        collection: String,
116        /// Named vector field this config applies to. Empty string = the
117        /// default (unnamed) vector field. Lets one collection carry
118        /// multiple vector indexes, one per `VECTOR` / embedding column.
119        field_name: String,
120        m: usize,
121        ef_construction: usize,
122        metric: String,
123        /// Index type: "hnsw" (default), "hnsw_pq", or "ivf_pq".
124        index_type: String,
125        /// PQ subvectors (for hnsw_pq and ivf_pq). Default: 8.
126        pq_m: usize,
127        /// IVF cells (for ivf_pq only). Default: 256.
128        ivf_cells: usize,
129        /// IVF probe count (for ivf_pq only). Default: 16.
130        ivf_nprobe: usize,
131    },
132
133    /// Query live vector index statistics. Returns `VectorIndexStats` as payload.
134    QueryStats {
135        collection: String,
136        /// Named vector field. Empty string = default (unnamed) field.
137        field_name: String,
138    },
139
140    /// Force-seal the growing segment, triggering background HNSW build.
141    Seal {
142        collection: String,
143        /// Named vector field. Empty string = default field.
144        field_name: String,
145    },
146
147    /// Force tombstone compaction on sealed segments.
148    CompactIndex {
149        collection: String,
150        /// Named vector field. Empty string = default field.
151        field_name: String,
152    },
153
154    /// Rebuild sealed segments with new HNSW parameters.
155    /// Old index serves queries until rebuild completes, then swaps atomically.
156    Rebuild {
157        collection: String,
158        /// Named vector field. Empty string = default field.
159        field_name: String,
160        /// New M parameter. 0 = keep current.
161        m: usize,
162        /// New M0 parameter. 0 = keep current.
163        m0: usize,
164        /// New ef_construction. 0 = keep current.
165        ef_construction: usize,
166    },
167
168    /// Insert a sparse vector into the inverted index.
169    SparseInsert {
170        collection: String,
171        /// Named sparse vector field.
172        field_name: String,
173        /// Document ID to associate with this sparse vector.
174        doc_id: String,
175        /// Sparse vector entries as `(dimension, weight)` pairs.
176        entries: Vec<(u32, f32)>,
177    },
178
179    /// Search the sparse inverted index via dot-product scoring.
180    SparseSearch {
181        collection: String,
182        /// Named sparse vector field.
183        field_name: String,
184        /// Query sparse vector entries.
185        query_entries: Vec<(u32, f32)>,
186        /// Maximum results to return.
187        top_k: usize,
188    },
189
190    /// Delete a document from the sparse inverted index.
191    SparseDelete {
192        collection: String,
193        /// Named sparse vector field.
194        field_name: String,
195        /// Document ID to remove.
196        doc_id: String,
197    },
198
199    /// Insert multiple vectors for a single document (ColBERT-style).
200    /// All vectors are inserted as separate HNSW nodes sharing the
201    /// same `document_surrogate`.
202    MultiVectorInsert {
203        collection: String,
204        /// Named vector field. Empty = default.
205        field_name: String,
206        /// Surrogate shared by all vectors of the document.
207        document_surrogate: Surrogate,
208        /// Flat vector data: count × dim f32 values.
209        vectors: Vec<f32>,
210        /// Number of vectors.
211        count: usize,
212        /// Dimensionality of each vector.
213        dim: usize,
214    },
215
216    /// Delete all vectors for a document from the multi-vector index.
217    MultiVectorDelete {
218        collection: String,
219        /// Named vector field. Empty = default.
220        field_name: String,
221        /// Document surrogate whose vectors should be tombstoned.
222        document_surrogate: Surrogate,
223    },
224
225    /// Search with multi-vector aggregated scoring (MaxSim, AvgSim, SumSim).
226    /// Over-fetches from HNSW, groups by doc_id, aggregates, deduplicates.
227    MultiVectorScoreSearch {
228        collection: String,
229        /// Named vector field. Empty = default.
230        field_name: String,
231        /// Query vector.
232        query_vector: Vec<f32>,
233        /// Maximum documents to return.
234        top_k: usize,
235        /// HNSW ef_search override. 0 = auto.
236        ef_search: usize,
237        /// Aggregation mode: "max_sim", "avg_sim", "sum_sim".
238        mode: String,
239    },
240
241    /// Direct vector upsert for vector-primary collections.
242    ///
243    /// Bypasses MessagePack document encoding — the Data Plane inserts the
244    /// vector into HNSW directly and updates payload bitmap indexes from
245    /// `payload`. No full-document blob is written.
246    ///
247    /// Ordering invariant (enforced by the handler):
248    ///   1. Validate dim.
249    ///   2. Decode `payload` bytes.
250    ///   3. Insert into HNSW (surrogate bound).
251    ///   4. Update payload bitmap indexes.
252    ///
253    /// If step 3 fails, step 4 is not reached — no partial state.
254    /// If step 4 fails (should not happen — pure in-memory), the handler
255    /// attempts to delete the just-inserted HNSW node and returns an error.
256    DirectUpsert {
257        collection: String,
258        /// Vector column name. Used to compute the vector index key so the
259        /// SELECT path (which keys by `(tid, collection, field)`) finds the
260        /// same index this insert wrote into.
261        field: String,
262        /// Global surrogate allocated by the Control Plane.
263        surrogate: Surrogate,
264        /// FP32 vector values.
265        vector: Vec<f32>,
266        /// Pre-encoded MessagePack of only the payload-indexed fields.
267        /// Empty when the collection has no payload indexes configured.
268        payload: Vec<u8>,
269        /// Collection-level quantization. Applied via `set_quantization` on
270        /// first insert so subsequent seals trigger codec-dispatch rebuilds
271        /// against the configured codec (RaBitQ / BBQ).
272        quantization: nodedb_types::VectorQuantization,
273        /// Native storage dtype for vector values (F32 / F16 / BF16). The
274        /// executor uses this to decide whether to transcode incoming f32
275        /// components before writing them into the HNSW index backing store.
276        storage_dtype: nodedb_types::VectorStorageDtype,
277        /// Payload field bitmap indexes (name + kind). Registered via
278        /// `payload.add_index` on the first insert into a new collection.
279        payload_indexes: Vec<(String, nodedb_types::PayloadIndexKind)>,
280    },
281}