1use super::api::TableVersion;
4pub use super::api::{
5 SearchVectorsRequest, VectorIndexArtifact, VectorIndexOrigin, VectorMetric, VectorSearchInfo,
6};
7use super::error::ScandalError;
8
9pub const ROW_ID_COLUMN: &str = "scandal.row_id";
11pub const DISTANCE_COLUMN: &str = "scandal.distance";
13pub const RANK_COLUMN: &str = "scandal.rank";
15pub const INFO_KEY: &str = "scandal.vector_search";
17pub const MAX_RESULTS: u32 = 1_000;
19pub const MAX_CANDIDATES: u32 = 100_000;
21
22const MAX_PARTITIONS: u32 = 4096;
23const MAX_METADATA_BYTES: usize = 512;
24const MAX_COLUMNS: usize = 64;
25const MAX_COLUMN_NAME_BYTES: usize = 1024;
26
27impl VectorIndexOrigin {
28 pub fn source(&self) -> TableVersion {
30 TableVersion {
31 resource_id: self.source_resource_id,
32 version_id: self.source_version_id,
33 }
34 }
35}
36
37impl SearchVectorsRequest {
38 pub fn validate(&self) -> Result<(), ScandalError> {
40 if self.limit == 0 || self.limit > MAX_RESULTS {
41 return Err(invalid(format!(
42 "Vector search limit must be between 1 and {MAX_RESULTS}"
43 )));
44 }
45 if self.probes == Some(0)
46 || self
47 .rerank
48 .is_some_and(|count| count < self.limit || count > MAX_CANDIDATES)
49 {
50 return Err(invalid(format!(
51 "Vector search probes must be positive and rerank must be between limit and {MAX_CANDIDATES}"
52 )));
53 }
54 if (self.index.partitions == 0 && self.index.indexed_rows.0 != 0)
55 || self.index.partitions > MAX_PARTITIONS
56 {
57 return Err(invalid(format!(
58 "Vector Index partitions must be between 1 and {MAX_PARTITIONS}"
59 )));
60 }
61 if self.index.dimensions == 0
62 || self.query.len() != self.index.dimensions as usize
63 || self.query.iter().any(|value| !value.is_finite())
64 {
65 return Err(invalid(
66 "Vector search requires a finite query with the index dimensions",
67 ));
68 }
69 if !matches!(self.index.codec_version, 3 | 4) {
70 return Err(invalid(
71 "Vector search requires a resource refresh with exact artifact identities",
72 ));
73 }
74 if self.index.metadata.len() > MAX_METADATA_BYTES {
75 return Err(invalid("Vector Index checksum metadata exceeds its limit"));
76 }
77 let columns = self.columns.as_deref().unwrap_or_default();
78 if columns.len() > MAX_COLUMNS
79 || columns.iter().any(|name| {
80 name.len() > MAX_COLUMN_NAME_BYTES
81 || [ROW_ID_COLUMN, DISTANCE_COLUMN, RANK_COLUMN].contains(&name.as_str())
82 })
83 {
84 return Err(invalid(
85 "Vector search columns exceed their bounds or use a reserved result name",
86 ));
87 }
88 if self
89 .exclude_row_ids
90 .as_ref()
91 .is_some_and(|ids| ids.len() > MAX_RESULTS as usize)
92 {
93 return Err(invalid(format!(
94 "Vector search permits at most {MAX_RESULTS} excluded RowIDs"
95 )));
96 }
97 Ok(())
98 }
99}
100
101fn invalid(message: impl Into<String>) -> ScandalError {
102 ScandalError::InvalidArgument {
103 message: message.into(),
104 }
105}