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}