1use std::fmt;
4
5use serde_json::{Map, Value, json};
6
7use crate::plan::{
8 GpuOptionDefinition, GpuOptionKind, GpuOptionValidationError, GpuOutputFieldDefinition, VectorElementType,
9 gpu_function_options::{
10 canonicalize_options, dense_relation_binding_schema, option_schema as gpu_option_schema,
11 validate_dense_relation_binding_value,
12 },
13};
14
15pub const CUVS_BRUTE_FORCE_KNN_FUNCTION: &str = "cuvs_brute_force_knn";
17pub const CUVS_IVF_FLAT_FUNCTION: &str = "cuvs_ivf_flat";
19pub const CUVS_IVF_PQ_FUNCTION: &str = "cuvs_ivf_pq";
21pub const CUVS_CAGRA_FUNCTION: &str = "cuvs_cagra";
23pub const CUVS_KMEANS_FUNCTION: &str = "cuvs_kmeans";
25pub const CUVS_PCA_FUNCTION: &str = "cuvs_pca";
27pub const CUVS_PAIRWISE_DISTANCE_FUNCTION: &str = "cuvs_pairwise_distance";
29pub const CUVS_TOP_K_FUNCTION: &str = "cuvs_top_k";
31
32#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
34pub enum CuvsRelationRole {
35 Dataset,
37 Queries,
39 Input,
41 X,
43 Y,
45}
46
47impl CuvsRelationRole {
48 pub const fn as_str(self) -> &'static str {
50 match self {
51 Self::Dataset => "dataset",
52 Self::Queries => "queries",
53 Self::Input => "input",
54 Self::X => "x",
55 Self::Y => "y",
56 }
57 }
58
59 pub const fn description(self) -> &'static str {
61 match self {
62 Self::Dataset => "Dense-vector rows indexed for nearest-neighbor search.",
63 Self::Queries => "Dense-vector query rows matched against the evaluated dataset.",
64 Self::Input => "Dense-vector rows consumed by this operation.",
65 Self::X => "Dense-vector rows on the first axis of the pairwise distance result.",
66 Self::Y => "Dense-vector rows on the second axis of the pairwise distance result.",
67 }
68 }
69}
70
71#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
73pub enum CuvsOutputContract {
74 KnnNeighbors,
76 KMeansAssignments,
78 PcaComponents,
80 PairwiseDistances,
82 TopKValues,
84}
85
86#[derive(Debug, Clone, Copy, PartialEq, Eq)]
88pub struct CuvsVectorElementSupport {
89 pub element: VectorElementType,
91 pub metrics: &'static [&'static str],
93}
94
95#[derive(Debug, Clone, Copy, PartialEq)]
98pub struct CuvsOperationDescriptor {
99 pub operation: CuvsOperation,
101 pub sql_function_name: &'static str,
103 pub display_name: &'static str,
105 pub summary: &'static str,
107 pub use_this_when: &'static str,
109 pub relation_roles: &'static [CuvsRelationRole],
112 pub vector_element_support: &'static [CuvsVectorElementSupport],
114 pub output_contract: CuvsOutputContract,
116 pub options: &'static [GpuOptionDefinition],
118 pub output_fields: &'static [GpuOutputFieldDefinition],
120 pub quickstart_sql: &'static str,
123 pub limitations: &'static [&'static str],
126}
127
128const FLOAT32_VECTOR_SUPPORT: &[CuvsVectorElementSupport] =
129 &[CuvsVectorElementSupport { element: VectorElementType::Float32, metrics: &[] }];
130const KNN_RELATIONS: &[CuvsRelationRole] = &[CuvsRelationRole::Dataset, CuvsRelationRole::Queries];
131const INPUT_RELATION: &[CuvsRelationRole] = &[CuvsRelationRole::Input];
132const PAIRWISE_RELATIONS: &[CuvsRelationRole] = &[CuvsRelationRole::X, CuvsRelationRole::Y];
133const TOP_K_SELECTIONS: &[&str] = &["min", "max"];
134const KNN_METRICS: &[&str] = &["l2_expanded", "l2_sqrt_expanded", "cosine", "inner_product"];
135const KMEANS_METRICS: &[&str] = &["l2_expanded", "l2_sqrt_expanded"];
136const IVF_PQ_INTEGER_METRICS: &[&str] = &["l2_expanded", "l2_sqrt_expanded", "inner_product"];
137const BRUTE_FORCE_SUPPORT: &[CuvsVectorElementSupport] =
138 &[CuvsVectorElementSupport { element: VectorElementType::Float32, metrics: KNN_METRICS }];
139const IVF_SUPPORT: &[CuvsVectorElementSupport] = &[
140 CuvsVectorElementSupport { element: VectorElementType::Float32, metrics: KNN_METRICS },
141 CuvsVectorElementSupport { element: VectorElementType::Int8, metrics: KNN_METRICS },
142 CuvsVectorElementSupport { element: VectorElementType::UInt8, metrics: KNN_METRICS },
143];
144const IVF_PQ_SUPPORT: &[CuvsVectorElementSupport] = &[
145 CuvsVectorElementSupport { element: VectorElementType::Float32, metrics: KNN_METRICS },
146 CuvsVectorElementSupport { element: VectorElementType::Int8, metrics: IVF_PQ_INTEGER_METRICS },
147 CuvsVectorElementSupport { element: VectorElementType::UInt8, metrics: IVF_PQ_INTEGER_METRICS },
148];
149const CAGRA_SUPPORT: &[CuvsVectorElementSupport] = &[
150 CuvsVectorElementSupport { element: VectorElementType::Float32, metrics: CAGRA_METRICS },
151 CuvsVectorElementSupport { element: VectorElementType::Int8, metrics: CAGRA_METRICS },
152 CuvsVectorElementSupport { element: VectorElementType::UInt8, metrics: CAGRA_METRICS },
153];
154const PAIRWISE_SUPPORT: &[CuvsVectorElementSupport] = &[
155 CuvsVectorElementSupport { element: VectorElementType::Float32, metrics: KNN_METRICS },
156 CuvsVectorElementSupport { element: VectorElementType::Float64, metrics: KNN_METRICS },
157];
158const KMEANS_SUPPORT: &[CuvsVectorElementSupport] = &[
159 CuvsVectorElementSupport { element: VectorElementType::Float32, metrics: KMEANS_METRICS },
160 CuvsVectorElementSupport { element: VectorElementType::Float64, metrics: KMEANS_METRICS },
161];
162const KMEANS_INIT: &[&str] = &["kmeans++", "random"];
163const PCA_SOLVERS: &[&str] = &["cov_eig_dq", "cov_eig_jacobi"];
164
165const KNN_OPTIONS: &[GpuOptionDefinition] = &[
166 GpuOptionDefinition {
167 name: "dataset",
168 required: true,
169 kind: GpuOptionKind::RelationBinding,
170 description: "Names the dataset ID column and one dense-vector binding shape.",
171 },
172 GpuOptionDefinition {
173 name: "queries",
174 required: true,
175 kind: GpuOptionKind::RelationBinding,
176 description: "Names the query ID column and one dense-vector binding shape.",
177 },
178 GpuOptionDefinition {
179 name: "k",
180 required: true,
181 kind: GpuOptionKind::Integer { minimum: 1, maximum: u32::MAX as u64, default: None },
182 description: "Number of neighbors returned for each evaluated query row.",
183 },
184 GpuOptionDefinition {
185 name: "metric",
186 required: true,
187 kind: GpuOptionKind::Enum { values: KNN_METRICS, default: None },
188 description: "Distance or score metric. Distance metrics rank lower values first; inner product ranks higher values first.",
189 },
190];
191const CAGRA_METRICS: &[&str] = &["l2_expanded", "inner_product", "cosine"];
192const CAGRA_BUILD_ALGOS: &[&str] = &["ivf_pq", "nn_descent"];
193const CAGRA_SEARCH_ALGOS: &[&str] = &["auto", "single_cta", "multi_cta", "multi_kernel"];
194const CAGRA_OPTIONS: &[GpuOptionDefinition] = &[
195 KNN_OPTIONS[0],
196 KNN_OPTIONS[1],
197 KNN_OPTIONS[2],
198 GpuOptionDefinition {
199 name: "metric",
200 required: false,
201 kind: GpuOptionKind::Enum { values: CAGRA_METRICS, default: Some("l2_expanded") },
202 description: "Distance or score metric; native default l2_expanded. inner_product ranks higher scores first.",
203 },
204 GpuOptionDefinition {
205 name: "graph_degree",
206 required: false,
207 kind: GpuOptionKind::Integer { minimum: 1, maximum: u32::MAX as u64, default: Some(64) },
208 description: "Output graph degree; native default 64. Must be less than dataset rows.",
209 },
210 GpuOptionDefinition {
211 name: "intermediate_graph_degree",
212 required: false,
213 kind: GpuOptionKind::Integer { minimum: 1, maximum: u32::MAX as u64, default: Some(128) },
214 description: "Build graph degree; native default 128. Must exceed graph_degree and be less than dataset rows.",
215 },
216 GpuOptionDefinition {
217 name: "build_algo",
218 required: false,
219 kind: GpuOptionKind::Enum { values: CAGRA_BUILD_ALGOS, default: Some("ivf_pq") },
220 description: "Graph construction algorithm; native default ivf_pq. nn_descent does not support cosine.",
221 },
222 GpuOptionDefinition {
223 name: "itopk_size",
224 required: false,
225 kind: GpuOptionKind::Integer { minimum: 1, maximum: u32::MAX as u64, default: Some(64) },
226 description: "Intermediate search results; native default 64. Must be at least k.",
227 },
228 GpuOptionDefinition {
229 name: "search_width",
230 required: false,
231 kind: GpuOptionKind::Integer { minimum: 1, maximum: u32::MAX as u64, default: Some(1) },
232 description: "Starting graph nodes per iteration; native default 1.",
233 },
234 GpuOptionDefinition {
235 name: "max_iterations",
236 required: false,
237 kind: GpuOptionKind::Integer { minimum: 0, maximum: u32::MAX as u64, default: Some(0) },
238 description: "Maximum search iterations; native default 0 selects automatically.",
239 },
240 GpuOptionDefinition {
241 name: "search_algo",
242 required: false,
243 kind: GpuOptionKind::Enum { values: CAGRA_SEARCH_ALGOS, default: Some("auto") },
244 description: "Search kernel; auto is set explicitly (the native zero-initialized default is single_cta).",
245 },
246];
247const IVF_FLAT_OPTIONS: &[GpuOptionDefinition] = &[
248 KNN_OPTIONS[0],
249 KNN_OPTIONS[1],
250 KNN_OPTIONS[2],
251 GpuOptionDefinition {
252 name: "metric",
253 required: false,
254 kind: GpuOptionKind::Enum { values: KNN_METRICS, default: Some("l2_expanded") },
255 description: "Distance or score metric; native default l2_expanded. Distance metrics rank lower values first; inner product ranks higher values first.",
256 },
257 GpuOptionDefinition {
258 name: "n_lists",
259 required: true,
260 kind: GpuOptionKind::Integer { minimum: 1, maximum: u32::MAX as u64, default: None },
261 description: "Number of inverted lists; must not exceed the evaluated dataset row count.",
262 },
263 GpuOptionDefinition {
264 name: "n_probes",
265 required: false,
266 kind: GpuOptionKind::Integer { minimum: 1, maximum: u32::MAX as u64, default: Some(20) },
267 description: "Lists probed per query; native default 20, and must not exceed n_lists.",
268 },
269 GpuOptionDefinition {
270 name: "kmeans_n_iters",
271 required: false,
272 kind: GpuOptionKind::Integer { minimum: 1, maximum: u32::MAX as u64, default: Some(20) },
273 description: "Iterations for training list centers; native default 20.",
274 },
275 GpuOptionDefinition {
276 name: "kmeans_trainset_fraction",
277 required: false,
278 kind: GpuOptionKind::Fraction { default: 0.5 },
279 description: "Fraction of the dataset used for training centers; native default 0.5.",
280 },
281];
282const IVF_PQ_CODEBOOK_KINDS: &[&str] = &["per_subspace", "per_cluster"];
283const IVF_PQ_OPTIONS: &[GpuOptionDefinition] = &[
284 IVF_FLAT_OPTIONS[0],
285 IVF_FLAT_OPTIONS[1],
286 IVF_FLAT_OPTIONS[2],
287 IVF_FLAT_OPTIONS[3],
288 IVF_FLAT_OPTIONS[4],
289 IVF_FLAT_OPTIONS[5],
290 IVF_FLAT_OPTIONS[6],
291 IVF_FLAT_OPTIONS[7],
292 GpuOptionDefinition {
293 name: "pq_dim",
294 required: false,
295 kind: GpuOptionKind::Integer { minimum: 0, maximum: u32::MAX as u64, default: Some(0) },
296 description: "Compressed dimensions; native default 0 selects the dimension automatically. pq_dim * pq_bits must be divisible by 8.",
297 },
298 GpuOptionDefinition {
299 name: "pq_bits",
300 required: false,
301 kind: GpuOptionKind::Integer { minimum: 4, maximum: 8, default: Some(8) },
302 description: "Bits per PQ code; native default 8.",
303 },
304 GpuOptionDefinition {
305 name: "codebook_kind",
306 required: false,
307 kind: GpuOptionKind::Enum { values: IVF_PQ_CODEBOOK_KINDS, default: Some("per_subspace") },
308 description: "PQ codebook training scope; native default per_subspace.",
309 },
310];
311const PAIRWISE_OPTIONS: &[GpuOptionDefinition] = &[
312 GpuOptionDefinition {
313 name: "x",
314 required: true,
315 kind: GpuOptionKind::RelationBinding,
316 description: "First dense-vector relation.",
317 },
318 GpuOptionDefinition {
319 name: "y",
320 required: true,
321 kind: GpuOptionKind::RelationBinding,
322 description: "Second dense-vector relation.",
323 },
324 GpuOptionDefinition {
325 name: "metric",
326 required: true,
327 kind: GpuOptionKind::Enum { values: KNN_METRICS, default: None },
328 description: "Distance metric; inner_product is a similarity where larger is closer.",
329 },
330];
331const TOP_K_OPTIONS: &[GpuOptionDefinition] = &[
332 GpuOptionDefinition {
333 name: "input",
334 required: true,
335 kind: GpuOptionKind::RelationBinding,
336 description: "Dense-vector rows to select from.",
337 },
338 GpuOptionDefinition {
339 name: "k",
340 required: true,
341 kind: GpuOptionKind::Integer { minimum: 1, maximum: u32::MAX as u64, default: None },
342 description: "Number of values selected from each row.",
343 },
344 GpuOptionDefinition {
345 name: "select",
346 required: true,
347 kind: GpuOptionKind::Enum { values: TOP_K_SELECTIONS, default: None },
348 description: "Select the minimum or maximum values.",
349 },
350];
351const DENSE_INPUT_OPTION: GpuOptionDefinition = GpuOptionDefinition {
352 name: "input",
353 required: true,
354 kind: GpuOptionKind::RelationBinding,
355 description: "Names the input ID column and one dense-vector binding shape.",
356};
357const KMEANS_OPTIONS: &[GpuOptionDefinition] = &[
358 DENSE_INPUT_OPTION,
359 GpuOptionDefinition {
360 name: "n_clusters",
361 required: true,
362 kind: GpuOptionKind::Integer { minimum: 1, maximum: i32::MAX as u64, default: None },
363 description: "Number of clusters to fit.",
364 },
365 GpuOptionDefinition {
366 name: "metric",
367 required: false,
368 kind: GpuOptionKind::Enum { values: KMEANS_METRICS, default: Some("l2_expanded") },
369 description: "L2 distance form used while fitting and assigning the current input relation.",
370 },
371 GpuOptionDefinition {
372 name: "max_iter",
373 required: false,
374 kind: GpuOptionKind::Integer { minimum: 1, maximum: i32::MAX as u64, default: Some(100) },
375 description: "Maximum fitting iterations for this invocation.",
376 },
377 GpuOptionDefinition {
378 name: "tol",
379 required: false,
380 kind: GpuOptionKind::NonnegativeNumber { default: 0.0001 },
381 description: "Non-negative convergence tolerance used by the fitted model.",
382 },
383 GpuOptionDefinition {
384 name: "n_init",
385 required: false,
386 kind: GpuOptionKind::Integer { minimum: 1, maximum: i32::MAX as u64, default: Some(1) },
387 description: "Number of initialization attempts performed within this call.",
388 },
389 GpuOptionDefinition {
390 name: "init",
391 required: false,
392 kind: GpuOptionKind::Enum { values: KMEANS_INIT, default: Some("kmeans++") },
393 description: "Centroid initialization strategy for this fitted model.",
394 },
395];
396const PCA_OPTIONS: &[GpuOptionDefinition] = &[
397 DENSE_INPUT_OPTION,
398 GpuOptionDefinition {
399 name: "n_components",
400 required: true,
401 kind: GpuOptionKind::Integer { minimum: 1, maximum: i32::MAX as u64, default: None },
402 description: "Number of principal-component columns emitted as pc_0 through pc_<n_components - 1>.",
403 },
404 GpuOptionDefinition {
405 name: "whiten",
406 required: false,
407 kind: GpuOptionKind::Boolean { default: Some(false) },
408 description: "Whether the query-local transformed components are whitened.",
409 },
410 GpuOptionDefinition {
411 name: "solver",
412 required: false,
413 kind: GpuOptionKind::Enum { values: PCA_SOLVERS, default: Some("cov_eig_dq") },
414 description: "Covariance eigensolver used for this fit-transform call.",
415 },
416 GpuOptionDefinition {
417 name: "tol",
418 required: false,
419 kind: GpuOptionKind::NonnegativeNumber { default: 0.0 },
420 description: "Non-negative eigensolver tolerance.",
421 },
422 GpuOptionDefinition {
423 name: "n_iterations",
424 required: false,
425 kind: GpuOptionKind::Integer { minimum: 1, maximum: i32::MAX as u64, default: Some(15) },
426 description: "Maximum eigensolver iterations for this invocation.",
427 },
428 GpuOptionDefinition {
429 name: "flip_signs_based_on_u",
430 required: false,
431 kind: GpuOptionKind::Boolean { default: Some(false) },
432 description: "Whether cuVS applies its U-based component-sign convention.",
433 },
434];
435
436const KNN_OUTPUT_FIELDS: &[GpuOutputFieldDefinition] = &[
437 GpuOutputFieldDefinition {
438 name: "query_ordinal",
439 data_type: "UInt64",
440 nullable: false,
441 repeats: None,
442 description: "Zero-based ordinal of the evaluated query row; it disambiguates duplicate query IDs.",
443 },
444 GpuOutputFieldDefinition {
445 name: "query_id",
446 data_type: "same_as_queries.id",
447 nullable: false,
448 repeats: None,
449 description: "Logical ID copied from the queries relation.",
450 },
451 GpuOutputFieldDefinition {
452 name: "neighbor_ordinal",
453 data_type: "UInt64",
454 nullable: false,
455 repeats: None,
456 description: "Zero-based ordinal of the matched dataset row; it disambiguates duplicate dataset IDs.",
457 },
458 GpuOutputFieldDefinition {
459 name: "neighbor_id",
460 data_type: "same_as_dataset.id",
461 nullable: false,
462 repeats: None,
463 description: "Logical ID copied from the matched dataset row.",
464 },
465 GpuOutputFieldDefinition {
466 name: "rank",
467 data_type: "UInt32",
468 nullable: false,
469 repeats: None,
470 description: "One-based neighbor rank within a query. Order consumers explicitly by query_ordinal, rank.",
471 },
472 GpuOutputFieldDefinition {
473 name: "distance",
474 data_type: "Float32",
475 nullable: false,
476 repeats: None,
477 description: "Metric value; smaller is better for distance metrics, while inner_product prefers larger values.",
478 },
479];
480
481const PAIRWISE_OUTPUT_FIELDS: &[GpuOutputFieldDefinition] = &[
482 GpuOutputFieldDefinition {
483 name: "x_ordinal",
484 data_type: "UInt64",
485 nullable: false,
486 repeats: None,
487 description: "Zero-based physical ordinal of the evaluated x row; it disambiguates duplicate x IDs.",
488 },
489 GpuOutputFieldDefinition {
490 name: "x_id",
491 data_type: "same_as_x.id",
492 nullable: false,
493 repeats: None,
494 description: "Logical ID copied from the x relation.",
495 },
496 GpuOutputFieldDefinition {
497 name: "y_ordinal",
498 data_type: "UInt64",
499 nullable: false,
500 repeats: None,
501 description: "Zero-based physical ordinal of the evaluated y row; it disambiguates duplicate y IDs.",
502 },
503 GpuOutputFieldDefinition {
504 name: "y_id",
505 data_type: "same_as_y.id",
506 nullable: false,
507 repeats: None,
508 description: "Logical ID copied from the y relation.",
509 },
510 GpuOutputFieldDefinition {
511 name: "distance",
512 data_type: "same_as_x.vector_element",
513 nullable: false,
514 repeats: None,
515 description: "Float32 for Float32 input, Float64 for Float64 input. Smaller is closer for distance metrics; inner_product prefers larger values.",
516 },
517];
518const TOP_K_OUTPUT_FIELDS: &[GpuOutputFieldDefinition] = &[
519 GpuOutputFieldDefinition {
520 name: "row_ordinal",
521 data_type: "UInt64",
522 nullable: false,
523 repeats: None,
524 description: "Zero-based physical ordinal of the evaluated input row; it disambiguates duplicate input IDs. Follows the input's top-level ORDER BY on output columns; unspecified without one.",
525 },
526 GpuOutputFieldDefinition {
527 name: "id",
528 data_type: "same_as_input.id",
529 nullable: false,
530 repeats: None,
531 description: "Logical ID copied from the input relation.",
532 },
533 GpuOutputFieldDefinition {
534 name: "rank",
535 data_type: "UInt32",
536 nullable: false,
537 repeats: None,
538 description: "One-based value rank within an input row. Order consumers explicitly by row_ordinal, rank.",
539 },
540 GpuOutputFieldDefinition {
541 name: "position",
542 data_type: "Int64",
543 nullable: false,
544 repeats: None,
545 description: "Zero-based dimension index of the selected value within the input vector.",
546 },
547 GpuOutputFieldDefinition {
548 name: "value",
549 data_type: "Float32",
550 nullable: false,
551 repeats: None,
552 description: "Selected Float32 input vector value; rank follows ascending values for min and descending values for max.",
553 },
554];
555const KMEANS_OUTPUT_FIELDS: &[GpuOutputFieldDefinition] = &[
556 GpuOutputFieldDefinition {
557 name: "row_ordinal",
558 data_type: "UInt64",
559 nullable: false,
560 repeats: None,
561 description: "Zero-based ordinal of the evaluated input row; it disambiguates duplicate IDs. Follows the input's top-level ORDER BY on output columns; unspecified without one.",
562 },
563 GpuOutputFieldDefinition {
564 name: "id",
565 data_type: "same_as_input.id",
566 nullable: false,
567 repeats: None,
568 description: "Logical ID copied from the input relation.",
569 },
570 GpuOutputFieldDefinition {
571 name: "cluster_id",
572 data_type: "Int32",
573 nullable: false,
574 repeats: None,
575 description: "Query-local numeric assignment label, not a stable business or topic identifier.",
576 },
577];
578
579const PCA_OUTPUT_FIELDS: &[GpuOutputFieldDefinition] = &[
580 GpuOutputFieldDefinition {
581 name: "row_ordinal",
582 data_type: "UInt64",
583 nullable: false,
584 repeats: None,
585 description: "Zero-based ordinal of the evaluated input row; it disambiguates duplicate IDs. Follows the input's top-level ORDER BY on output columns; unspecified without one.",
586 },
587 GpuOutputFieldDefinition {
588 name: "id",
589 data_type: "same_as_input.id",
590 nullable: false,
591 repeats: None,
592 description: "Logical ID copied from the input relation.",
593 },
594 GpuOutputFieldDefinition {
595 name: "pc_<index>",
596 data_type: "Float32",
597 nullable: false,
598 repeats: Some("n_components"),
599 description: "One transformed principal-component value; the field count is set by the literal n_components option.",
600 },
601];
602
603#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
605pub enum CuvsOperation {
606 BruteForceKnn,
608 IvfFlat,
610 IvfPq,
612 Cagra,
614 PairwiseDistance,
616 TopK,
618 KMeans,
620 Pca,
622}
623
624impl CuvsOperation {
625 pub fn all() -> impl ExactSizeIterator<Item = Self> + Clone {
627 CUVS_OPERATION_REGISTRY.iter().map(|descriptor| descriptor.operation)
628 }
629
630 pub fn descriptor(self) -> &'static CuvsOperationDescriptor {
632 CUVS_OPERATION_REGISTRY
633 .iter()
634 .find(|descriptor| descriptor.operation == self)
635 .expect("cuVS operation registry is missing an operation variant")
636 }
637
638 pub fn from_sql_function_name(name: &str) -> Option<Self> {
642 CUVS_OPERATION_REGISTRY.iter().find(|descriptor| descriptor.sql_function_name == name).map(|d| d.operation)
643 }
644}
645
646pub const CUVS_OPERATION_REGISTRY: &[CuvsOperationDescriptor] = &[
648 CuvsOperationDescriptor {
649 operation: CuvsOperation::BruteForceKnn,
650 sql_function_name: CUVS_BRUTE_FORCE_KNN_FUNCTION,
651 display_name: "Brute-force kNN",
652 summary: "Exact brute-force nearest-neighbor search over dense vectors.",
653 use_this_when: "Use this when SQL can first filter a candidate relation and you need exact batch reranking, not a persistent ANN index.",
654 relation_roles: KNN_RELATIONS,
655 vector_element_support: BRUTE_FORCE_SUPPORT,
656 output_contract: CuvsOutputContract::KnnNeighbors,
657 options: KNN_OPTIONS,
658 output_fields: KNN_OUTPUT_FIELDS,
659 quickstart_sql: r#"SELECT *
660FROM cuvs_brute_force_knn(
661 dataset => (SELECT id, d0, d1 FROM dataset_vectors),
662 queries => (SELECT id, d0, d1 FROM query_vectors),
663 k => 8,
664 metric => 'l2_expanded'
665);"#,
666 limitations: &[
667 "Builds an exact query-local index over the evaluated dataset relation; it does not persist an ANN index or replace a vector database.",
668 "The evaluated dataset must be non-empty and k must not exceed its row count. An empty query relation may return an empty result with the stable schema.",
669 "Dataset and query vector dimensions must match after both relation children are evaluated.",
670 "Distances repeat between runs; neighbors that tie at the k-th distance can differ, because candidate merge order is not fixed.",
671 ],
672 },
673 CuvsOperationDescriptor {
674 operation: CuvsOperation::IvfFlat,
675 sql_function_name: CUVS_IVF_FLAT_FUNCTION,
676 display_name: "IVF-Flat",
677 summary: "Query-local inverted-file approximate nearest-neighbor search.",
678 use_this_when: "Use this when a query-local approximate index reduces search work over a large evaluated dataset.",
679 relation_roles: KNN_RELATIONS,
680 vector_element_support: IVF_SUPPORT,
681 output_contract: CuvsOutputContract::KnnNeighbors,
682 options: IVF_FLAT_OPTIONS,
683 output_fields: KNN_OUTPUT_FIELDS,
684 quickstart_sql: r#"SELECT *
685FROM cuvs_ivf_flat(
686 dataset => (SELECT id, d0, d1 FROM dataset_vectors),
687 queries => (SELECT id, d0, d1 FROM query_vectors),
688 k => 8,
689 metric => 'l2_expanded',
690 n_lists => 16,
691 n_probes => 16
692)
693ORDER BY query_ordinal, rank;"#,
694 limitations: &[
695 "Builds and frees its index during each query execution; executions never share an index.",
696 "Results are approximate unless all lists are probed. Fewer than k neighbors may be returned for a query; missing (query, rank) rows are omitted and valid ranks remain contiguous.",
697 "The evaluated dataset must be non-empty, k and n_lists must not exceed its row count, and n_probes must not exceed n_lists.",
698 "Dataset and query dimensions and element types must match. Cosine requires at least two dimensions.",
699 "Neighbors and distances can differ slightly between runs, because index construction accumulates k-means centroid sums with floating-point atomics; there is no seed option.",
700 ],
701 },
702 CuvsOperationDescriptor {
703 operation: CuvsOperation::IvfPq,
704 sql_function_name: CUVS_IVF_PQ_FUNCTION,
705 display_name: "IVF-PQ",
706 summary: "Query-local product-quantized inverted-file nearest-neighbor search.",
707 use_this_when: "Use this when a query-local compressed index can reduce vector search memory, subject to lossy recall.",
708 relation_roles: KNN_RELATIONS,
709 vector_element_support: IVF_PQ_SUPPORT,
710 output_contract: CuvsOutputContract::KnnNeighbors,
711 options: IVF_PQ_OPTIONS,
712 output_fields: KNN_OUTPUT_FIELDS,
713 quickstart_sql: r#"SELECT *
714FROM cuvs_ivf_pq(
715 dataset => (SELECT id, d0, d1 FROM dataset_vectors),
716 queries => (SELECT id, d0, d1 FROM query_vectors),
717 k => 8,
718 metric => 'l2_expanded',
719 n_lists => 16,
720 n_probes => 16,
721 pq_dim => 2
722)
723ORDER BY query_ordinal, rank;"#,
724 limitations: &[
725 "Builds and frees its index during each query execution; executions never share an index.",
726 "Product quantization is lossy even when all lists are probed. Missing neighbors are omitted and valid ranks remain contiguous.",
727 "The dataset must be non-empty; k and n_lists must not exceed its row count, and n_probes must not exceed n_lists.",
728 "Dataset and query dimensions and element types must match. Cosine requires at least two dimensions. Nonzero pq_dim * pq_bits must be divisible by 8; dimensions are padded when needed.",
729 "Neighbors and distances can differ slightly between runs, because index training accumulates k-means centroid sums with floating-point atomics; there is no seed option.",
730 ],
731 },
732 CuvsOperationDescriptor {
733 operation: CuvsOperation::Cagra,
734 sql_function_name: CUVS_CAGRA_FUNCTION,
735 display_name: "CAGRA",
736 summary: "Query-local graph approximate nearest-neighbor search.",
737 use_this_when: "Use this for graph-based approximate search over an evaluated dataset.",
738 relation_roles: KNN_RELATIONS,
739 vector_element_support: CAGRA_SUPPORT,
740 output_contract: CuvsOutputContract::KnnNeighbors,
741 options: CAGRA_OPTIONS,
742 output_fields: KNN_OUTPUT_FIELDS,
743 quickstart_sql: r#"SELECT *
744FROM cuvs_cagra(
745 dataset => (SELECT id, d0, d1 FROM dataset_vectors),
746 queries => (SELECT id, d0, d1 FROM query_vectors),
747 k => 8,
748 metric => 'l2_expanded',
749 graph_degree => 32,
750 intermediate_graph_degree => 64
751)
752ORDER BY query_ordinal, rank;"#,
753 limitations: &[
754 "Builds and frees its graph and padded dataset during each query execution; executions never share an index.",
755 "Approximate search may return fewer than k neighbors; missing (query, rank) rows are omitted and valid ranks remain contiguous.",
756 "The dataset must be non-empty, k and intermediate_graph_degree must be less than or equal to its row count, and graph_degree must be less than intermediate_graph_degree.",
757 "k must not exceed itopk_size. single_cta requires itopk_size no greater than 1024. cosine cannot use nn_descent.",
758 "Dataset and query dimensions and element types must match.",
759 "Neighbors and distances can differ slightly between runs, because graph construction orders edges by atomic arrival; there is no seed option.",
760 ],
761 },
762 CuvsOperationDescriptor {
763 operation: CuvsOperation::PairwiseDistance,
764 sql_function_name: CUVS_PAIRWISE_DISTANCE_FUNCTION,
765 display_name: "Pairwise Distance",
766 summary: "Pairwise distance or similarity for every x and y vector row.",
767 use_this_when: "Use this when every pair between two evaluated relations needs a metric value.",
768 relation_roles: PAIRWISE_RELATIONS,
769 vector_element_support: PAIRWISE_SUPPORT,
770 output_contract: CuvsOutputContract::PairwiseDistances,
771 options: PAIRWISE_OPTIONS,
772 output_fields: PAIRWISE_OUTPUT_FIELDS,
773 quickstart_sql: r#"SELECT *
774FROM cuvs_pairwise_distance(
775 x => (SELECT id, d0, d1 FROM x_vectors),
776 y => (SELECT id, d0, d1 FROM y_vectors),
777 metric => 'l2_expanded'
778)
779ORDER BY x_ordinal, y_ordinal;"#,
780 limitations: &[
781 "Output rows equal x rows multiplied by y rows; the query allocation cap limits the result size.",
782 "Empty x or y returns an empty result with the stable schema.",
783 "x and y must share the same vector dimension and element type. inner_product is a similarity where larger is closer.",
784 ],
785 },
786 CuvsOperationDescriptor {
787 operation: CuvsOperation::TopK,
788 sql_function_name: CUVS_TOP_K_FUNCTION,
789 display_name: "Vector Top-K",
790 summary: "Select the smallest or largest values in every dense vector row.",
791 use_this_when: "Use this when each row needs its own ranked scalar values and vector positions.",
792 relation_roles: INPUT_RELATION,
793 vector_element_support: FLOAT32_VECTOR_SUPPORT,
794 output_contract: CuvsOutputContract::TopKValues,
795 options: TOP_K_OPTIONS,
796 output_fields: TOP_K_OUTPUT_FIELDS,
797 quickstart_sql: r#"SELECT *
798FROM cuvs_top_k(
799 input => (SELECT id, d0, d1 FROM input_vectors),
800 k => 1,
801 select => 'max'
802)
803ORDER BY row_ordinal, rank;"#,
804 limitations: &[
805 "k must not exceed the vector dimension. Empty input returns an empty result with the stable schema.",
806 "Equal selected values are ordered by position; when ties straddle rank k, the selected positions among them are unspecified.",
807 ],
808 },
809 CuvsOperationDescriptor {
810 operation: CuvsOperation::KMeans,
811 sql_function_name: CUVS_KMEANS_FUNCTION,
812 display_name: "KMeans",
813 summary: "K-means clustering assignments for dense vector rows.",
814 use_this_when: "Use this when one query should fit clusters over its evaluated relation and immediately join or aggregate the assignments.",
815 relation_roles: INPUT_RELATION,
816 vector_element_support: KMEANS_SUPPORT,
817 output_contract: CuvsOutputContract::KMeansAssignments,
818 options: KMEANS_OPTIONS,
819 output_fields: KMEANS_OUTPUT_FIELDS,
820 quickstart_sql: r#"SELECT id, cluster_id
821FROM cuvs_kmeans(
822 input => (SELECT id, d0, d1 FROM input_vectors),
823 n_clusters => 8
824)
825ORDER BY row_ordinal;"#,
826 limitations: &[
827 "Fits a model and returns assignments within one call; it does not return a reusable model, centroids, or inertia.",
828 "The evaluated input must be non-empty and n_clusters must not exceed its row count.",
829 "cluster_id values are query-local labels. Do not attach permanent business meaning to their numeric values.",
830 "Cluster sizes can differ slightly between runs, because centroid sums accumulate with floating-point atomics; the native seed is fixed at 0 and cannot be set.",
831 ],
832 },
833 CuvsOperationDescriptor {
834 operation: CuvsOperation::Pca,
835 sql_function_name: CUVS_PCA_FUNCTION,
836 display_name: "PCA",
837 summary: "Principal-component analysis transform for dense vector rows.",
838 use_this_when: "Use this when one query should fit and transform its evaluated input relation for relational export or downstream visualization.",
839 relation_roles: INPUT_RELATION,
840 vector_element_support: FLOAT32_VECTOR_SUPPORT,
841 output_contract: CuvsOutputContract::PcaComponents,
842 options: PCA_OPTIONS,
843 output_fields: PCA_OUTPUT_FIELDS,
844 quickstart_sql: r#"SELECT id, pc_0, pc_1
845FROM cuvs_pca(
846 input => (SELECT id, d0, d1 FROM input_vectors),
847 n_components => 2
848)
849ORDER BY row_ordinal;"#,
850 limitations: &[
851 "Fits and transforms the evaluated input in one call; it does not return a reusable transform, components, or explained-variance metadata.",
852 "The evaluated input needs at least two rows and two dimensions, and n_components must not exceed the evaluated dimension.",
853 "Component sign orientation is not a semantic label. The output schema changes with the literal n_components option.",
854 ],
855 },
856];
857
858impl CuvsOperationDescriptor {
859 pub fn element_support(&self, element: VectorElementType) -> Option<&CuvsVectorElementSupport> {
861 self.vector_element_support.iter().find(|support| support.element == element)
862 }
863
864 pub fn option(&self, name: &str) -> Option<&'static GpuOptionDefinition> {
866 self.options.iter().find(|option| option.name == name)
867 }
868
869 pub fn options_json_schema(&self) -> Value {
875 let mut properties = serde_json::Map::new();
876 let mut required = Vec::new();
877 for option in self.options {
878 let mut schema = gpu_option_schema(
879 option.kind,
880 &dense_relation_binding_schema(
881 &self.vector_element_support.iter().map(|support| support.element).collect::<Vec<_>>(),
882 ),
883 );
884 schema["description"] = Value::String(option.description.to_string());
885 properties.insert(option.name.to_string(), schema);
886 if option.required {
887 required.push(Value::String(option.name.to_string()));
888 }
889 }
890 if let Some(metric) = properties.get_mut("metric") {
891 let combinations = self
892 .vector_element_support
893 .iter()
894 .map(|support| format!("{}: {}", support.element.as_str(), support.metrics.join(", ")))
895 .collect::<Vec<_>>()
896 .join("; ");
897 metric["description"] = json!(format!(
898 "{} Supported element/metric combinations: {combinations}.",
899 self.option("metric").expect("metric option").description
900 ));
901 }
902 json!({
903 "$schema": "https://json-schema.org/draft/2020-12/schema",
904 "title": format!("{} options", self.sql_function_name),
905 "type": "object",
906 "additionalProperties": false,
907 "required": required,
908 "properties": properties,
909 })
910 }
911}
912
913#[cfg(test)]
914fn option_schema(kind: GpuOptionKind, element_support: &[CuvsVectorElementSupport]) -> Value {
915 gpu_option_schema(
916 kind,
917 &dense_relation_binding_schema(&element_support.iter().map(|support| support.element).collect::<Vec<_>>()),
918 )
919}
920
921pub fn canonicalize_cuvs_options(
925 operation: CuvsOperation,
926 raw: &str,
927) -> Result<Map<String, Value>, GpuOptionValidationError> {
928 let descriptor = operation.descriptor();
929 let object = canonicalize_options(descriptor.options, raw, "cuVS", |option, value| {
930 validate_dense_relation_binding_value("cuVS", option, value, false)
931 })?;
932 if matches!(operation, CuvsOperation::IvfFlat | CuvsOperation::IvfPq) {
933 let n_probes = object.get("n_probes").and_then(Value::as_u64).expect("validated n_probes");
934 let n_lists = object.get("n_lists").and_then(Value::as_u64).expect("validated n_lists");
935 if n_probes > n_lists {
936 return Err(
937 option_error("missing_or_invalid_option", "n_probes must not exceed n_lists").for_argument("n_probes")
938 );
939 }
940 }
941 if operation == CuvsOperation::Cagra {
942 let integer = |name| object.get(name).and_then(Value::as_u64).expect("validated CAGRA integer");
943 if integer("intermediate_graph_degree") <= integer("graph_degree") {
944 return Err(option_error(
945 "missing_or_invalid_option",
946 "intermediate_graph_degree must exceed graph_degree",
947 )
948 .for_argument("intermediate_graph_degree"));
949 }
950 if integer("k") > integer("itopk_size") {
951 return Err(
952 option_error("missing_or_invalid_option", "k must not exceed itopk_size").for_argument("itopk_size")
953 );
954 }
955 if object["search_algo"] == "single_cta" && integer("itopk_size") > 1024 {
956 return Err(option_error("missing_or_invalid_option", "single_cta requires itopk_size <= 1024")
957 .for_argument("itopk_size"));
958 }
959 if object["metric"] == "cosine" && object["build_algo"] == "nn_descent" {
960 return Err(option_error("missing_or_invalid_option", "nn_descent cannot build a cosine graph")
961 .for_argument("build_algo"));
962 }
963 }
964 if operation == CuvsOperation::IvfPq {
965 let pq_dim = object.get("pq_dim").and_then(Value::as_u64).expect("validated pq_dim");
966 let pq_bits = object.get("pq_bits").and_then(Value::as_u64).expect("validated pq_bits");
967 if pq_dim != 0 && (pq_dim * pq_bits) % 8 != 0 {
968 return Err(option_error("missing_or_invalid_option", "pq_dim * pq_bits must be divisible by 8")
969 .for_argument("pq_dim"));
970 }
971 }
972 Ok(object)
973}
974
975fn option_error(code: &'static str, message: impl Into<String>) -> GpuOptionValidationError {
976 GpuOptionValidationError::new(code, message)
977}
978
979#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
983pub enum VectorAlgorithm {
984 BruteForceKnn,
987 IvfFlat,
990 IvfPq,
993 Cagra,
996 PairwiseDistance,
999 TopK,
1002 KMeans,
1005 Pca,
1008}
1009
1010impl VectorAlgorithm {
1011 pub const fn as_str(self) -> &'static str {
1013 match self {
1014 Self::BruteForceKnn => "brute_force_knn",
1015 Self::IvfFlat => "ivf_flat",
1016 Self::IvfPq => "ivf_pq",
1017 Self::Cagra => "cagra",
1018 Self::PairwiseDistance => "pairwise_distance",
1019 Self::TopK => "top_k",
1020 Self::KMeans => "kmeans",
1021 Self::Pca => "pca",
1022 }
1023 }
1024}
1025
1026impl fmt::Display for VectorAlgorithm {
1027 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1028 f.write_str(self.as_str())
1029 }
1030}
1031
1032#[cfg(test)]
1033mod tests {
1034 use serde_json::json;
1035
1036 use super::{
1037 CuvsOperation, GpuOptionDefinition, GpuOptionKind, canonicalize_cuvs_options, option_schema,
1038 validate_dense_relation_binding_value,
1039 };
1040 use crate::plan::{
1041 GpuOptionValidationError, gpu_function_options::validate_option_value as gpu_validate_option_value,
1042 };
1043
1044 fn validate_option_value(
1045 option: &GpuOptionDefinition,
1046 value: &serde_json::Value,
1047 ) -> Result<(), GpuOptionValidationError> {
1048 gpu_validate_option_value(option, value, "cuVS", &|option, value| {
1049 validate_dense_relation_binding_value("cuVS", option, value, false)
1050 })
1051 }
1052
1053 #[test]
1054 fn ivf_flat_rejects_probes_above_lists_with_argument_attribution() {
1055 let raw = json!({
1056 "dataset": {"id": "id", "vector": {"columns": ["d0", "d1"]}},
1057 "queries": {"id": "id", "vector": {"columns": ["d0", "d1"]}},
1058 "k": 1, "metric": "l2_expanded", "n_lists": 2, "n_probes": 3
1059 });
1060 let error = canonicalize_cuvs_options(CuvsOperation::IvfFlat, &raw.to_string()).unwrap_err();
1061 assert_eq!(error.argument(), Some("n_probes"));
1062 assert!(error.message().contains("n_probes must not exceed n_lists"));
1063 for fraction in [0.0, 1.01] {
1064 let mut invalid = raw.clone();
1065 invalid["n_probes"] = json!(2);
1066 invalid["kmeans_trainset_fraction"] = json!(fraction);
1067 let error = canonicalize_cuvs_options(CuvsOperation::IvfFlat, &invalid.to_string()).unwrap_err();
1068 assert_eq!(error.argument(), Some("kmeans_trainset_fraction"));
1069 }
1070 }
1071
1072 #[test]
1073 fn ivf_pq_rejects_invalid_bit_packing_and_probe_count_with_argument_attribution() {
1074 let mut raw = json!({
1075 "dataset": {"id": "id", "vector": {"columns": ["d0", "d1"]}},
1076 "queries": {"id": "id", "vector": {"columns": ["d0", "d1"]}},
1077 "k": 1, "metric": "l2_expanded", "n_lists": 2,
1078 "n_probes": 2, "pq_dim": 1, "pq_bits": 5
1079 });
1080 let error = canonicalize_cuvs_options(CuvsOperation::IvfPq, &raw.to_string()).unwrap_err();
1081 assert_eq!(error.argument(), Some("pq_dim"));
1082 assert!(error.message().contains("divisible by 8"));
1083 raw["pq_dim"] = json!(8);
1084 raw["n_probes"] = json!(3);
1085 assert_eq!(
1086 canonicalize_cuvs_options(CuvsOperation::IvfPq, &raw.to_string()).unwrap_err().argument(),
1087 Some("n_probes")
1088 );
1089 raw["n_probes"] = json!(2);
1090 raw["pq_bits"] = json!(9);
1091 assert_eq!(
1092 canonicalize_cuvs_options(CuvsOperation::IvfPq, &raw.to_string()).unwrap_err().argument(),
1093 Some("pq_bits")
1094 );
1095 }
1096
1097 #[test]
1098 fn cagra_rejects_incompatible_graph_and_search_parameters() {
1099 let mut raw = json!({
1100 "dataset": {"id": "id", "vector": {"columns": ["d0", "d1"]}},
1101 "queries": {"id": "id", "vector": {"columns": ["d0", "d1"]}},
1102 "k": 8, "metric": "l2_expanded", "graph_degree": 32,
1103 "intermediate_graph_degree": 16
1104 });
1105 assert_eq!(
1106 canonicalize_cuvs_options(CuvsOperation::Cagra, &raw.to_string()).unwrap_err().argument(),
1107 Some("intermediate_graph_degree")
1108 );
1109 raw["intermediate_graph_degree"] = json!(64);
1110 raw["itopk_size"] = json!(7);
1111 assert_eq!(
1112 canonicalize_cuvs_options(CuvsOperation::Cagra, &raw.to_string()).unwrap_err().argument(),
1113 Some("itopk_size")
1114 );
1115 raw["itopk_size"] = json!(8);
1116 raw["metric"] = json!("cosine");
1117 raw["build_algo"] = json!("nn_descent");
1118 assert_eq!(
1119 canonicalize_cuvs_options(CuvsOperation::Cagra, &raw.to_string()).unwrap_err().argument(),
1120 Some("build_algo")
1121 );
1122 raw["metric"] = json!("inner_product");
1123 let normalized = canonicalize_cuvs_options(CuvsOperation::Cagra, &raw.to_string()).unwrap();
1124 assert_eq!(normalized["search_algo"], "auto");
1125 }
1126
1127 #[test]
1128 fn integer_options_honor_zero_and_bounded_minima() {
1129 for (minimum, maximum, accepted, rejected) in [(0, 10, [0, 10], [11, 12]), (4, 8, [4, 8], [0, 9])] {
1130 let option = GpuOptionDefinition {
1131 name: "integer",
1132 required: false,
1133 kind: GpuOptionKind::Integer { minimum, maximum, default: Some(minimum) },
1134 description: "Integer range.",
1135 };
1136 assert_eq!(option_schema(option.kind, &[])["minimum"], json!(minimum));
1137 for value in accepted {
1138 validate_option_value(&option, &json!(value)).expect("value inside range");
1139 }
1140 for value in rejected {
1141 assert!(validate_option_value(&option, &json!(value)).is_err());
1142 }
1143 }
1144 }
1145
1146 #[test]
1147 fn fraction_options_exclude_zero_and_values_above_one() {
1148 let option = GpuOptionDefinition {
1149 name: "fraction",
1150 required: false,
1151 kind: GpuOptionKind::Fraction { default: 0.5 },
1152 description: "Training fraction.",
1153 };
1154 assert_eq!(option_schema(option.kind, &[])["exclusiveMinimum"], json!(0));
1155 for value in [0.5, 1.0] {
1156 validate_option_value(&option, &json!(value)).expect("fraction inside range");
1157 }
1158 for value in [0.0, -0.1, 1.1] {
1159 assert!(validate_option_value(&option, &json!(value)).is_err());
1160 }
1161 }
1162}