Skip to main content

a3s_vec/
schema.rs

1//! Collection and index schemas.
2
3mod index_contract;
4
5use crate::error::{Error, Result};
6use crate::types::{DataType, IndexType, MetricType, QuantizeType};
7use serde::{Deserialize, Serialize};
8use serde_json::{json, Map, Value};
9use std::collections::BTreeSet;
10
11use index_contract::validate_index_configuration;
12
13/// HNSW build parameters.
14#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
15pub struct HnswIndexParam {
16    pub metric: MetricType,
17    pub m: u32,
18    pub ef_construction: u32,
19    pub quantize: QuantizeType,
20}
21
22/// IVF build parameters.
23#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
24pub struct IVFIndexParam {
25    pub metric: MetricType,
26    pub n_list: u32,
27    pub n_iters: u32,
28    pub use_soar: bool,
29}
30
31pub type IvfIndexParam = IVFIndexParam;
32
33/// IVF `RaBitQ` build parameters.
34#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
35pub struct IvfRabitqIndexParam {
36    pub metric: MetricType,
37    pub n_list: u32,
38    pub total_bits: u32,
39    pub sample_count: u32,
40}
41
42/// Exact flat index parameters.
43#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
44pub struct FlatIndexParam {
45    pub metric: MetricType,
46}
47
48/// DiskANN/Vamana build parameters.
49#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
50pub struct DiskAnnIndexParam {
51    pub metric: MetricType,
52    pub max_degree: u32,
53    pub list_size: u32,
54    pub pq_chunk_num: u32,
55    pub alpha: f64,
56}
57
58pub type DiskANNIndexParam = DiskAnnIndexParam;
59
60/// Vamana graph parameters.  `DiskANN` uses the same graph with a disk layout.
61#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
62pub struct VamanaIndexParam {
63    pub metric: MetricType,
64    pub max_degree: u32,
65    pub search_list_size: u32,
66    pub alpha: f64,
67    pub max_occlusion: u32,
68    pub saturate: bool,
69}
70
71/// Scalar inverted-index parameters.
72#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
73pub struct InvertIndexParam {
74    pub enable_range_optimization: bool,
75    pub enable_wildcard: bool,
76}
77
78/// Full-text index parameters.
79#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
80pub struct FtsIndexParam {
81    pub tokenizer_name: String,
82    pub filters: Vec<String>,
83    pub extra_params: Option<String>,
84}
85
86/// Serializable index configuration.
87///
88/// The open `params` map is retained for adapter compatibility, but collection
89/// schemas reject entries that do not have an execution consumer. Live Flat,
90/// HNSW, IVF, HNSW/IVF `RaBitQ`, Vamana, DiskANN/PQ, scalar-inverted, and
91/// scan-FTS configurations are validated at the field boundary; attaching a
92/// future descriptor returns [`crate::ErrorCode::NotSupported`].
93#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
94pub struct IndexParams {
95    pub index_type: IndexType,
96    pub metric_type: MetricType,
97    pub quantize_type: QuantizeType,
98    #[serde(default)]
99    pub params: Map<String, Value>,
100}
101
102impl IndexParams {
103    pub fn hnsw(metric: MetricType, m: i32, ef_construction: i32) -> Result<Self> {
104        Self::hnsw_with_quantize(metric, m, ef_construction, QuantizeType::Undefined)
105    }
106
107    pub fn hnsw_with_quantize(
108        metric: MetricType,
109        m: i32,
110        ef_construction: i32,
111        quantize: QuantizeType,
112    ) -> Result<Self> {
113        if m <= 0 || ef_construction <= 0 {
114            return Err(Error::invalid_argument(
115                "HNSW m and ef_construction must be positive",
116            ));
117        }
118        let mut out = Self::new(IndexType::Hnsw, metric);
119        out.quantize_type = quantize;
120        out.params.insert("m".into(), json!(m));
121        out.params
122            .insert("ef_construction".into(), json!(ef_construction));
123        out.params.insert("quantize_type".into(), json!(quantize));
124        Ok(out)
125    }
126
127    pub fn ivf(metric: MetricType, n_list: i32, n_iters: i32, use_soar: bool) -> Result<Self> {
128        if n_list <= 0 || n_iters < 0 {
129            return Err(Error::invalid_argument(
130                "IVF n_list must be positive and n_iters non-negative",
131            ));
132        }
133        let mut out = Self::new(IndexType::Ivf, metric);
134        out.params.insert("n_list".into(), json!(n_list));
135        out.params.insert("n_iters".into(), json!(n_iters));
136        out.params.insert("use_soar".into(), json!(use_soar));
137        Ok(out)
138    }
139
140    pub fn ivf_rabitq(
141        metric: MetricType,
142        nlist: i32,
143        total_bits: i32,
144        sample_count: i32,
145    ) -> Result<Self> {
146        if nlist <= 0 || !(1..=9).contains(&total_bits) || sample_count < 0 {
147            return Err(Error::invalid_argument(
148                "IVF RaBitQ requires positive nlist, total_bits in 1..=9, and non-negative sample_count",
149            ));
150        }
151        let mut out = Self::new(IndexType::IvfRabitq, metric);
152        out.quantize_type = QuantizeType::Rabitq;
153        out.params.insert("n_list".into(), json!(nlist));
154        out.params.insert("total_bits".into(), json!(total_bits));
155        out.params
156            .insert("sample_count".into(), json!(sample_count));
157        out.params
158            .insert("quantize_type".into(), json!(QuantizeType::Rabitq));
159        Ok(out)
160    }
161
162    pub fn flat(metric: MetricType) -> Result<Self> {
163        Ok(Self::new(IndexType::Flat, metric))
164    }
165
166    pub fn diskann(
167        metric: MetricType,
168        max_degree: i32,
169        list_size: i32,
170        pq_chunk_num: i32,
171    ) -> Result<Self> {
172        if max_degree <= 0 || list_size <= 0 || pq_chunk_num < 0 {
173            return Err(Error::invalid_argument(
174                "DiskANN parameters are out of range",
175            ));
176        }
177        let mut out = Self::new(IndexType::Diskann, metric);
178        out.params.insert("max_degree".into(), json!(max_degree));
179        out.params.insert("list_size".into(), json!(list_size));
180        out.params
181            .insert("pq_chunk_num".into(), json!(pq_chunk_num));
182        out.params.insert("alpha".into(), json!(1.2));
183        Ok(out)
184    }
185
186    pub fn vamana(
187        metric: MetricType,
188        max_degree: i32,
189        search_list_size: i32,
190        alpha: f64,
191    ) -> Result<Self> {
192        Self::vamana_with_options(metric, max_degree, search_list_size, alpha, 0, false)
193    }
194
195    /// Creates a Vamana graph descriptor with explicit `RobustPrune` controls.
196    ///
197    /// `max_occlusion` bounds the candidate set considered by `RobustPrune`;
198    /// zero keeps the historical unbounded behavior. `saturate` fills any
199    /// remaining out-edge slots after diversity pruning.
200    pub fn vamana_with_options(
201        metric: MetricType,
202        max_degree: i32,
203        search_list_size: i32,
204        alpha: f64,
205        max_occlusion: i32,
206        saturate: bool,
207    ) -> Result<Self> {
208        if max_degree <= 0
209            || search_list_size <= 0
210            || max_occlusion < 0
211            || !alpha.is_finite()
212            || alpha < 1.0
213        {
214            return Err(Error::invalid_argument(
215                "Vamana parameters are out of range",
216            ));
217        }
218        let mut out = Self::new(IndexType::Vamana, metric);
219        out.params.insert("max_degree".into(), json!(max_degree));
220        out.params
221            .insert("search_list_size".into(), json!(search_list_size));
222        out.params.insert("alpha".into(), json!(alpha));
223        out.params
224            .insert("max_occlusion".into(), json!(max_occlusion));
225        out.params.insert("saturate".into(), json!(saturate));
226        Ok(out)
227    }
228
229    pub fn hnsw_rabitq(metric: MetricType, m: i32, ef_construction: i32) -> Result<Self> {
230        Self::hnsw_rabitq_with_options(metric, m, ef_construction, 7, 16, 0)
231    }
232
233    pub fn hnsw_rabitq_with_options(
234        metric: MetricType,
235        m: i32,
236        ef_construction: i32,
237        total_bits: i32,
238        num_clusters: i32,
239        sample_count: i32,
240    ) -> Result<Self> {
241        if !(1..=9).contains(&total_bits) || num_clusters <= 0 || sample_count < 0 {
242            return Err(Error::invalid_argument(
243                "HNSW RaBitQ requires total_bits in 1..=9, positive num_clusters, and non-negative sample_count",
244            ));
245        }
246        let mut out = Self::hnsw_with_quantize(metric, m, ef_construction, QuantizeType::Rabitq)?;
247        out.index_type = IndexType::HnswRabitq;
248        out.params.insert("total_bits".into(), json!(total_bits));
249        out.params
250            .insert("num_clusters".into(), json!(num_clusters));
251        out.params
252            .insert("sample_count".into(), json!(sample_count));
253        Ok(out)
254    }
255
256    pub fn invert(enable_range_opt: bool, enable_wildcard: bool) -> Result<Self> {
257        let mut out = Self::new(IndexType::Invert, MetricType::Undefined);
258        out.params
259            .insert("enable_range_optimization".into(), json!(enable_range_opt));
260        out.params
261            .insert("enable_wildcard".into(), json!(enable_wildcard));
262        Ok(out)
263    }
264
265    /// Creates full-text index parameters.
266    ///
267    /// Omitting `filters` selects the zvec-compatible `lowercase` default;
268    /// passing an explicit empty slice preserves tokenizer case. Supported
269    /// filters are `lowercase`, `ascii_folding`, and `stemmer`.
270    ///
271    /// The `ngram` tokenizer accepts `ngram_min`, `ngram_max`, and
272    /// `token_chars` in `extra_params`. The standard tokenizer accepts
273    /// `max_token_length`, while the stemmer filter accepts `stemmer_lang`.
274    pub fn fts(
275        tokenizer_name: Option<&str>,
276        filters: Option<&[&str]>,
277        extra_params: Option<&str>,
278    ) -> Result<Self> {
279        let tokenizer = tokenizer_name.unwrap_or("standard").trim();
280        if tokenizer.is_empty() {
281            return Err(Error::invalid_argument(
282                "FTS tokenizer name must not be empty",
283            ));
284        }
285        let mut out = Self::new(IndexType::Fts, MetricType::Undefined);
286        out.params.insert("tokenizer_name".into(), json!(tokenizer));
287        out.params.insert(
288            "filters".into(),
289            json!(filters.map_or_else(|| vec!["lowercase"], <[_]>::to_vec)),
290        );
291        if let Some(extra) = extra_params {
292            out.params.insert("extra_params".into(), json!(extra));
293        }
294        Ok(out)
295    }
296
297    pub fn new(index_type: IndexType, metric_type: MetricType) -> Self {
298        Self {
299            index_type,
300            metric_type,
301            quantize_type: QuantizeType::Undefined,
302            params: Map::new(),
303        }
304    }
305
306    pub fn index_type(&self) -> IndexType {
307        self.index_type
308    }
309    pub fn metric_type(&self) -> MetricType {
310        self.metric_type
311    }
312    pub fn quantize_type(&self) -> QuantizeType {
313        self.quantize_type
314    }
315    pub fn set_metric_type(&mut self, metric: MetricType) -> Result<()> {
316        if metric == MetricType::Undefined && self.index_type.is_vector_index() {
317            return Err(Error::invalid_argument("vector index requires a metric"));
318        }
319        self.metric_type = metric;
320        Ok(())
321    }
322    pub fn set_quantize_type(&mut self, quantize: QuantizeType) -> Result<()> {
323        self.quantize_type = quantize;
324        self.params.insert("quantize_type".into(), json!(quantize));
325        Ok(())
326    }
327    pub fn parameter(&self, name: &str) -> Option<&Value> {
328        self.params.get(name)
329    }
330    pub fn with_parameter(mut self, name: impl Into<String>, value: Value) -> Self {
331        self.params.insert(name.into(), value);
332        self
333    }
334}
335
336impl IndexType {
337    pub fn is_vector_index(self) -> bool {
338        matches!(
339            self,
340            Self::Hnsw
341                | Self::HnswRabitq
342                | Self::Ivf
343                | Self::IvfRabitq
344                | Self::Flat
345                | Self::Diskann
346                | Self::Vamana
347        )
348    }
349}
350
351/// Fluent index-parameter builder for callers that prefer typed construction.
352#[derive(Debug, Clone)]
353pub struct IndexParamsBuilder {
354    params: IndexParams,
355}
356
357impl IndexParamsBuilder {
358    pub fn new(index_type: IndexType) -> Self {
359        Self {
360            params: IndexParams::new(index_type, MetricType::Undefined),
361        }
362    }
363    pub fn metric_type(mut self, metric: MetricType) -> Self {
364        self.params.metric_type = metric;
365        self
366    }
367    pub fn metric(self, metric: MetricType) -> Self {
368        self.metric_type(metric)
369    }
370    pub fn quantize_type(mut self, quantize: QuantizeType) -> Self {
371        self.params.quantize_type = quantize;
372        self
373    }
374    pub fn m(mut self, value: u32) -> Self {
375        self.params.params.insert("m".into(), json!(value));
376        self
377    }
378    pub fn ef_construction(mut self, value: u32) -> Self {
379        self.params
380            .params
381            .insert("ef_construction".into(), json!(value));
382        self
383    }
384    pub fn n_list(mut self, value: u32) -> Self {
385        self.params.params.insert("n_list".into(), json!(value));
386        self
387    }
388    pub fn n_iters(mut self, value: u32) -> Self {
389        self.params.params.insert("n_iters".into(), json!(value));
390        self
391    }
392    pub fn use_soar(mut self, value: bool) -> Self {
393        self.params.params.insert("use_soar".into(), json!(value));
394        self
395    }
396    pub fn max_degree(mut self, value: u32) -> Self {
397        self.params.params.insert("max_degree".into(), json!(value));
398        self
399    }
400    pub fn list_size(mut self, value: u32) -> Self {
401        self.params.params.insert("list_size".into(), json!(value));
402        self
403    }
404    pub fn search_list_size(mut self, value: u32) -> Self {
405        self.params
406            .params
407            .insert("search_list_size".into(), json!(value));
408        self
409    }
410    pub fn alpha(mut self, value: f64) -> Self {
411        self.params.params.insert("alpha".into(), json!(value));
412        self
413    }
414    pub fn max_occlusion(mut self, value: u32) -> Self {
415        self.params
416            .params
417            .insert("max_occlusion".into(), json!(value));
418        self
419    }
420    pub fn saturate(mut self, value: bool) -> Self {
421        self.params.params.insert("saturate".into(), json!(value));
422        self
423    }
424    pub fn pq_chunk_num(mut self, value: u32) -> Self {
425        self.params
426            .params
427            .insert("pq_chunk_num".into(), json!(value));
428        self
429    }
430    pub fn tokenizer(mut self, value: impl Into<String>) -> Self {
431        self.params
432            .params
433            .insert("tokenizer_name".into(), json!(value.into()));
434        self
435    }
436    pub fn parameter(mut self, name: impl Into<String>, value: Value) -> Self {
437        self.params.params.insert(name.into(), value);
438        self
439    }
440    pub fn build(self) -> Result<IndexParams> {
441        if self.params.index_type.is_vector_index()
442            && self.params.metric_type == MetricType::Undefined
443        {
444            return Err(Error::invalid_argument("vector index requires a metric"));
445        }
446        Ok(self.params)
447    }
448}
449
450/// Schema for one field.
451#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
452pub struct FieldSchema {
453    pub name: String,
454    pub data_type: DataType,
455    pub nullable: bool,
456    pub dimension: u32,
457    pub index_params: Option<IndexParams>,
458}
459
460impl FieldSchema {
461    pub fn new(name: &str, data_type: DataType, nullable: bool, dimension: u32) -> Result<Self> {
462        validate_field_shape(name, data_type, dimension)?;
463        Ok(Self {
464            name: name.to_string(),
465            data_type,
466            nullable,
467            dimension,
468            index_params: None,
469        })
470    }
471    pub fn set_index_params(&mut self, params: &IndexParams) -> Result<()> {
472        validate_field_shape(&self.name, self.data_type, self.dimension)?;
473        validate_index_configuration(&self.name, self.data_type, self.dimension, params)?;
474        self.index_params = Some(params.clone());
475        Ok(())
476    }
477    pub fn name(&self) -> &str {
478        &self.name
479    }
480    pub fn data_type(&self) -> DataType {
481        self.data_type
482    }
483    pub fn dimension(&self) -> u32 {
484        self.dimension
485    }
486    pub fn is_nullable(&self) -> bool {
487        self.nullable
488    }
489    pub fn is_vector_field(&self) -> bool {
490        self.data_type.is_vector()
491    }
492    pub fn is_dense_vector(&self) -> bool {
493        self.data_type.is_dense_vector()
494    }
495    pub fn is_sparse_vector(&self) -> bool {
496        self.data_type.is_sparse_vector()
497    }
498    pub fn has_index(&self) -> bool {
499        self.index_params.is_some()
500    }
501    pub fn index_type(&self) -> IndexType {
502        self.index_params
503            .as_ref()
504            .map_or(IndexType::Undefined, IndexParams::index_type)
505    }
506    pub fn is_array_type(&self) -> bool {
507        self.data_type.is_array()
508    }
509    pub fn index_params(&self) -> Option<&IndexParams> {
510        self.index_params.as_ref()
511    }
512}
513
514/// Explicit vector schema (also useful to adapters that keep vectors separate).
515#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
516pub struct VectorSchema {
517    pub name: String,
518    pub data_type: DataType,
519    pub dimension: u32,
520    pub index_params: Option<IndexParams>,
521}
522
523impl VectorSchema {
524    pub fn new(name: &str, data_type: DataType, dimension: u32) -> Result<Self> {
525        let field = FieldSchema::new(name, data_type, false, dimension)?;
526        if !field.is_vector_field() {
527            return Err(Error::invalid_argument(
528                "vector schema requires a vector data type",
529            ));
530        }
531        Ok(Self {
532            name: field.name,
533            data_type,
534            dimension,
535            index_params: None,
536        })
537    }
538    pub fn set_index_params(&mut self, params: &IndexParams) -> Result<()> {
539        let mut field = FieldSchema::new(&self.name, self.data_type, false, self.dimension)?;
540        field.set_index_params(params)?;
541        self.index_params = field.index_params;
542        Ok(())
543    }
544    pub fn name(&self) -> &str {
545        &self.name
546    }
547    pub fn dimension(&self) -> u32 {
548        self.dimension
549    }
550    pub fn data_type(&self) -> DataType {
551        self.data_type
552    }
553    pub fn has_index(&self) -> bool {
554        self.index_params.is_some()
555    }
556    pub fn index_type(&self) -> IndexType {
557        self.index_params
558            .as_ref()
559            .map_or(IndexType::Undefined, IndexParams::index_type)
560    }
561}
562
563/// Collection schema containing scalar and vector fields.
564#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
565pub struct CollectionSchema {
566    pub name: String,
567    pub fields: Vec<FieldSchema>,
568    pub vectors: Vec<VectorSchema>,
569    pub max_doc_count_per_segment: u64,
570}
571
572impl CollectionSchema {
573    pub fn new(name: &str) -> Result<Self> {
574        validate_name(name)?;
575        Ok(Self {
576            name: name.to_string(),
577            fields: Vec::new(),
578            vectors: Vec::new(),
579            max_doc_count_per_segment: 0,
580        })
581    }
582    pub fn builder(name: &str) -> CollectionSchemaBuilder {
583        CollectionSchemaBuilder::new(name)
584    }
585    pub fn name(&self) -> &str {
586        &self.name
587    }
588    pub fn add_field(&mut self, field: &FieldSchema) -> Result<()> {
589        validate_field_shape(&field.name, field.data_type, field.dimension)?;
590        self.ensure_unique(&field.name)?;
591        if let Some(params) = &field.index_params {
592            validate_index_configuration(&field.name, field.data_type, field.dimension, params)?;
593        }
594        if field.data_type.is_vector() {
595            self.vectors.push(VectorSchema {
596                name: field.name.clone(),
597                data_type: field.data_type,
598                dimension: field.dimension,
599                index_params: field.index_params.clone(),
600            });
601        } else {
602            self.fields.push(field.clone());
603        }
604        Ok(())
605    }
606    pub fn add_vector_field(&mut self, field: &VectorSchema) -> Result<()> {
607        validate_field_shape(&field.name, field.data_type, field.dimension)?;
608        self.ensure_unique(&field.name)?;
609        if !field.data_type.is_vector() {
610            return Err(Error::invalid_argument(
611                "vector field requires vector data type",
612            ));
613        }
614        if let Some(params) = &field.index_params {
615            validate_index_configuration(&field.name, field.data_type, field.dimension, params)?;
616        }
617        self.vectors.push(field.clone());
618        Ok(())
619    }
620    pub fn has_field(&self, name: &str) -> bool {
621        self.fields.iter().any(|f| f.name == name) || self.vectors.iter().any(|f| f.name == name)
622    }
623    pub fn field(&self, name: &str) -> Option<&FieldSchema> {
624        self.fields.iter().find(|f| f.name == name)
625    }
626    pub fn vector(&self, name: &str) -> Option<&VectorSchema> {
627        self.vectors.iter().find(|f| f.name == name)
628    }
629    pub fn has_index(&self, name: &str) -> bool {
630        self.fields
631            .iter()
632            .find(|f| f.name == name)
633            .and_then(|f| f.index_params.as_ref())
634            .is_some()
635            || self
636                .vectors
637                .iter()
638                .find(|f| f.name == name)
639                .and_then(|f| f.index_params.as_ref())
640                .is_some()
641    }
642    pub fn drop_field(&mut self, name: &str) -> Result<()> {
643        let before = self.fields.len() + self.vectors.len();
644        self.fields.retain(|f| f.name != name);
645        self.vectors.retain(|f| f.name != name);
646        if before == self.fields.len() + self.vectors.len() {
647            return Err(Error::not_found(format!("field '{name}' not found")));
648        }
649        Ok(())
650    }
651    pub fn add_index(&mut self, field_name: &str, params: &IndexParams) -> Result<()> {
652        if let Some(field) = self.fields.iter_mut().find(|f| f.name == field_name) {
653            return field.set_index_params(params);
654        }
655        if let Some(field) = self.vectors.iter_mut().find(|f| f.name == field_name) {
656            return field.set_index_params(params);
657        }
658        Err(Error::not_found(format!("field '{field_name}' not found")))
659    }
660    pub(crate) fn check_index_configuration(
661        &self,
662        field_name: &str,
663        params: &IndexParams,
664    ) -> Result<()> {
665        if let Some(field) = self.fields.iter().find(|field| field.name == field_name) {
666            return validate_index_configuration(
667                &field.name,
668                field.data_type,
669                field.dimension,
670                params,
671            );
672        }
673        if let Some(field) = self.vectors.iter().find(|field| field.name == field_name) {
674            return validate_index_configuration(
675                &field.name,
676                field.data_type,
677                field.dimension,
678                params,
679            );
680        }
681        Err(Error::not_found(format!("field '{field_name}' not found")))
682    }
683    pub fn drop_index(&mut self, field_name: &str) -> Result<()> {
684        if let Some(field) = self.fields.iter_mut().find(|f| f.name == field_name) {
685            field.index_params = None;
686            return Ok(());
687        }
688        if let Some(field) = self.vectors.iter_mut().find(|f| f.name == field_name) {
689            field.index_params = None;
690            return Ok(());
691        }
692        Err(Error::not_found(format!("field '{field_name}' not found")))
693    }
694    pub fn set_max_doc_count_per_segment(&mut self, count: u64) -> Result<()> {
695        if count == 0 {
696            self.max_doc_count_per_segment = 0;
697            Ok(())
698        } else {
699            Err(Error::not_supported(
700                "max_doc_count_per_segment requires a segmented storage executor",
701            ))
702        }
703    }
704    pub fn max_doc_count_per_segment(&self) -> u64 {
705        self.max_doc_count_per_segment
706    }
707    pub fn validate(&self) -> Result<()> {
708        validate_name(&self.name)?;
709        if self.max_doc_count_per_segment != 0 {
710            return Err(Error::not_supported(
711                "max_doc_count_per_segment requires a segmented storage executor",
712            ));
713        }
714        if self.fields.len() + self.vectors.len() == 0 {
715            return Err(Error::invalid_argument(
716                "collection schema must contain at least one field",
717            ));
718        }
719        let mut names = BTreeSet::new();
720        for field in &self.fields {
721            validate_field_shape(&field.name, field.data_type, field.dimension)?;
722            if field.data_type.is_vector() {
723                return Err(Error::invalid_argument(format!(
724                    "vector field '{}' must be stored in the vector schema list",
725                    field.name
726                )));
727            }
728            if !names.insert(&field.name) {
729                return Err(Error::invalid_argument(format!(
730                    "duplicate field name '{}'",
731                    field.name
732                )));
733            }
734            if let Some(params) = &field.index_params {
735                validate_index_configuration(
736                    &field.name,
737                    field.data_type,
738                    field.dimension,
739                    params,
740                )?;
741            }
742        }
743        for field in &self.vectors {
744            validate_field_shape(&field.name, field.data_type, field.dimension)?;
745            if !field.data_type.is_vector() {
746                return Err(Error::invalid_argument(format!(
747                    "non-vector field '{}' must be stored in the scalar schema list",
748                    field.name
749                )));
750            }
751            if !names.insert(&field.name) {
752                return Err(Error::invalid_argument(format!(
753                    "duplicate field name '{}'",
754                    field.name
755                )));
756            }
757            if let Some(params) = &field.index_params {
758                validate_index_configuration(
759                    &field.name,
760                    field.data_type,
761                    field.dimension,
762                    params,
763                )?;
764            }
765        }
766        Ok(())
767    }
768    pub fn digest(&self) -> String {
769        let bytes = serde_json::to_vec(self).unwrap_or_default();
770        format!("{:08x}", crc32fast::hash(&bytes))
771    }
772    pub fn fields(&self) -> &[FieldSchema] {
773        &self.fields
774    }
775    pub fn vectors(&self) -> &[VectorSchema] {
776        &self.vectors
777    }
778    fn ensure_unique(&self, name: &str) -> Result<()> {
779        if self.has_field(name) {
780            Err(Error::already_exists(format!(
781                "field '{name}' already exists"
782            )))
783        } else {
784            Ok(())
785        }
786    }
787}
788
789/// Fluent collection schema builder.
790#[derive(Debug, Clone)]
791pub struct CollectionSchemaBuilder {
792    name: String,
793    fields: Vec<(FieldSchema, Option<IndexParams>)>,
794    max_doc_count_per_segment: Option<u64>,
795    deferred_error: Option<Error>,
796}
797
798impl CollectionSchemaBuilder {
799    pub fn new(name: &str) -> Self {
800        Self {
801            name: name.to_string(),
802            fields: Vec::new(),
803            max_doc_count_per_segment: None,
804            deferred_error: None,
805        }
806    }
807    pub fn add_field(mut self, field: FieldSchema) -> Self {
808        self.fields.push((field, None));
809        self
810    }
811    pub fn add_vector_field(
812        mut self,
813        name: &str,
814        data_type: DataType,
815        dimension: u32,
816        index_params: IndexParams,
817    ) -> Self {
818        match FieldSchema::new(name, data_type, false, dimension) {
819            Ok(field) => self.fields.push((field, Some(index_params))),
820            Err(error) => self.deferred_error = Some(error),
821        }
822        self
823    }
824    pub fn add_indexed_field(
825        mut self,
826        name: &str,
827        data_type: DataType,
828        index_params: IndexParams,
829    ) -> Self {
830        match FieldSchema::new(name, data_type, false, 0) {
831            Ok(field) => self.fields.push((field, Some(index_params))),
832            Err(error) => self.deferred_error = Some(error),
833        }
834        self
835    }
836    pub fn max_doc_count_per_segment(mut self, count: u64) -> Self {
837        self.max_doc_count_per_segment = Some(count);
838        self
839    }
840    pub fn build(self) -> Result<CollectionSchema> {
841        if let Some(error) = self.deferred_error {
842            return Err(error);
843        }
844        let mut schema = CollectionSchema::new(&self.name)?;
845        for (mut field, params) in self.fields {
846            if let Some(params) = params {
847                field.set_index_params(&params)?;
848            }
849            schema.add_field(&field)?;
850        }
851        if let Some(count) = self.max_doc_count_per_segment {
852            schema.set_max_doc_count_per_segment(count)?;
853        }
854        schema.validate()?;
855        Ok(schema)
856    }
857}
858
859#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
860pub struct AddColumnOption {
861    /// Requested schema-backfill workers. Zero keeps the deterministic serial
862    /// path; positive values are bounded by work size, host parallelism, and
863    /// the engine's worker ceiling.
864    pub concurrency: u32,
865}
866
867#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
868pub struct AlterColumnOption {
869    /// Requested candidate-schema validation workers. Zero keeps the
870    /// deterministic serial path; positive values use the same bounded pool
871    /// as [`AddColumnOption::concurrency`].
872    pub concurrency: u32,
873}
874
875fn validate_name(name: &str) -> Result<()> {
876    if name.trim().is_empty() || name.contains('\0') {
877        return Err(Error::invalid_argument(
878            "name must be non-empty and contain no NUL byte",
879        ));
880    }
881    Ok(())
882}
883
884/// Validates the portion of a public field descriptor that must remain sound
885/// even when callers construct or deserialize the descriptor and then mutate
886/// its public fields directly.
887fn validate_field_shape(name: &str, data_type: DataType, dimension: u32) -> Result<()> {
888    validate_name(name)?;
889    if data_type == DataType::Undefined {
890        return Err(Error::invalid_argument("field data type must be defined"));
891    }
892    if data_type.is_vector() && !data_type.is_sparse_vector() && dimension == 0 {
893        return Err(Error::invalid_argument(
894            "dense vector dimension must be positive",
895        ));
896    }
897    let binary_alignment = match data_type {
898        DataType::VectorBinary32 => Some(32),
899        DataType::VectorBinary64 => Some(64),
900        _ => None,
901    };
902    if let Some(alignment) = binary_alignment {
903        if dimension % alignment != 0 {
904            return Err(Error::invalid_argument(format!(
905                "{data_type} dimension must be a multiple of {alignment}"
906            )));
907        }
908    }
909    if !data_type.is_vector() && dimension != 0 {
910        return Err(Error::invalid_argument(
911            "non-vector field dimension must be zero",
912        ));
913    }
914    Ok(())
915}
916
917#[cfg(test)]
918mod tests {
919    use super::*;
920
921    #[test]
922    fn check_index_configuration_resolves_scalar_and_vector_fields() {
923        let mut schema = CollectionSchema::new("check-index").expect("schema");
924        schema
925            .add_field(&FieldSchema::new("title", DataType::String, false, 0).expect("title"))
926            .expect("add title");
927        schema
928            .add_vector_field(
929                &VectorSchema::new("embedding", DataType::VectorFp32, 2).expect("embedding"),
930            )
931            .expect("add embedding");
932        assert!(schema
933            .check_index_configuration("missing", &IndexParams::invert(false, false).unwrap())
934            .is_err());
935        schema
936            .check_index_configuration("title", &IndexParams::invert(false, false).unwrap())
937            .expect("scalar invert");
938        schema
939            .check_index_configuration("embedding", &IndexParams::flat(MetricType::L2).unwrap())
940            .expect("vector flat");
941    }
942}