Skip to main content

spiral/scandal/
vector.rs

1//! Fixed-budget vector search over exact resource versions.
2
3use super::api::TableVersion;
4pub use super::api::{
5    SearchVectorsRequest, VectorIndexArtifact, VectorIndexOrigin, VectorMetric, VectorSearchInfo,
6};
7use super::error::ScandalError;
8
9/// This column contains the stable source RowID.
10pub const ROW_ID_COLUMN: &str = "scandal.row_id";
11/// This column contains the exact float64 distance.
12pub const DISTANCE_COLUMN: &str = "scandal.distance";
13/// This column contains the one-based rank.
14pub const RANK_COLUMN: &str = "scandal.rank";
15/// The Arrow schema uses this key for a JSON `VectorSearchInfo` value.
16pub const INFO_KEY: &str = "scandal.vector_search";
17/// This limit caps the final rows in one request.
18pub const MAX_RESULTS: u32 = 1_000;
19/// This limit caps the index candidates in one request.
20pub 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    /// Return the exact source Table version for this artifact.
29    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    /// Check query shape, recall controls, and projected field names.
39    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}