Skip to main content

summa_core/dsl/
schema.rs

1//! Schema definitions for documents and fields
2
3use serde::{Deserialize, Serialize};
4use std::collections::HashMap;
5use std::num::NonZeroU32;
6
7/// Field identifier
8#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
9pub struct Field(pub u32);
10
11/// Types of fields supported
12#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
13pub enum FieldType {
14    /// Text field - tokenized and indexed
15    #[serde(rename = "text")]
16    Text,
17    /// Unsigned 64-bit integer
18    #[serde(rename = "u64")]
19    U64,
20    /// Signed 64-bit integer
21    #[serde(rename = "i64")]
22    I64,
23    /// 64-bit floating point
24    #[serde(rename = "f64")]
25    F64,
26    /// Raw bytes (not tokenized)
27    #[serde(rename = "bytes")]
28    Bytes,
29    /// Sparse vector field - indexed as inverted posting lists with quantized weights
30    #[serde(rename = "sparse_vector")]
31    SparseVector,
32    /// Dense vector field indexed with the global IVF-PQ ANN implementation.
33    #[serde(rename = "dense_vector")]
34    DenseVector,
35    /// JSON field - arbitrary JSON data, stored but not indexed
36    #[serde(rename = "json")]
37    Json,
38    /// Binary dense vector field - packed-bit storage with Hamming distance scoring
39    #[serde(rename = "binary_dense_vector")]
40    BinaryDenseVector,
41}
42
43/// Field options
44#[derive(Debug, Clone, Serialize, Deserialize)]
45pub struct FieldEntry {
46    pub name: String,
47    pub field_type: FieldType,
48    pub indexed: bool,
49    pub stored: bool,
50    /// Name of the tokenizer to use for this field (for text fields)
51    pub tokenizer: Option<String>,
52    /// Whether this field can have multiple values (serialized as array in JSON)
53    #[serde(default)]
54    pub multi: bool,
55    /// Position tracking mode for phrase queries and multi-field element tracking
56    #[serde(default, skip_serializing_if = "Option::is_none")]
57    pub positions: Option<PositionMode>,
58    /// Configuration for sparse vector fields (index size, weight quantization)
59    #[serde(default, skip_serializing_if = "Option::is_none")]
60    pub sparse_vector_config: Option<crate::structures::SparseVectorConfig>,
61    /// Configuration for dense vector fields (dimension, quantization)
62    #[serde(default, skip_serializing_if = "Option::is_none")]
63    pub dense_vector_config: Option<DenseVectorConfig>,
64    /// Configuration for binary dense vector fields (dimension in bits)
65    #[serde(default, skip_serializing_if = "Option::is_none")]
66    pub binary_dense_vector_config: Option<BinaryDenseVectorConfig>,
67    /// Whether this field has columnar fast-field storage for O(1) doc→value access.
68    /// Valid for u64, i64, f64, and text fields.
69    #[serde(default)]
70    pub fast: bool,
71    /// Whether this field is a primary key (unique constraint, at most one per schema)
72    #[serde(default)]
73    pub primary_key: bool,
74    /// Stored fingerprint used to skip unchanged committed upserts.
75    #[serde(default, skip_serializing_if = "std::ops::Not::not")]
76    pub content_hash: bool,
77    /// Whether BP reordering is enabled for this indexed text or BMP field.
78    /// Plain text and chunked text both retain logical document/ordinal mappings.
79    #[serde(default)]
80    pub reorder: bool,
81    /// Chunked text field: every value is its own BM25 scoring unit with a
82    /// per-chunk ordinal in results (`docs/chunked-text-fields.md`). Text only.
83    #[serde(default)]
84    pub chunked: bool,
85    /// BM25 k1 of a text field; `None` = `BM25_K1`.
86    #[serde(default, skip_serializing_if = "Option::is_none")]
87    pub bm25_k1: Option<f32>,
88    /// BM25 b of a text field; `None` = `BM25_B`.
89    #[serde(default, skip_serializing_if = "Option::is_none")]
90    pub bm25_b: Option<f32>,
91}
92
93impl FieldEntry {
94    /// Parsed tokenizer spec of a text field (`None` for non-text fields,
95    /// fields without a tokenizer, or unparsable names).
96    pub fn tokenizer_spec(&self) -> Option<crate::tokenizer::TokenizerSpec> {
97        if self.field_type != FieldType::Text {
98            return None;
99        }
100        crate::tokenizer::TokenizerSpec::parse(self.tokenizer.as_deref()?).ok()
101    }
102
103    fn supports_reorder(&self) -> bool {
104        self.indexed
105            && (self.field_type == FieldType::Text
106                || self
107                    .sparse_vector_config
108                    .as_ref()
109                    .is_some_and(|config| config.format == crate::structures::SparseFormat::Bmp))
110    }
111}
112
113/// Position tracking mode for text fields
114#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
115#[serde(rename_all = "snake_case")]
116pub enum PositionMode {
117    /// Track only element ordinal for multi-valued fields (which array element)
118    /// Useful for returning which element matched without full phrase query support
119    Ordinal,
120    /// Track only token position within text (for phrase queries)
121    /// Does not track element ordinal - all positions are relative to concatenated text
122    TokenPosition,
123    /// Track both element ordinal and token position (full support)
124    /// Position format: (element_ordinal << 20) | token_position
125    Full,
126}
127
128impl PositionMode {
129    /// Whether this mode tracks element ordinals
130    pub fn tracks_ordinal(&self) -> bool {
131        matches!(self, PositionMode::Ordinal | PositionMode::Full)
132    }
133
134    /// Whether this mode tracks token positions
135    pub fn tracks_token_position(&self) -> bool {
136        matches!(self, PositionMode::TokenPosition | PositionMode::Full)
137    }
138}
139
140/// Vector index algorithm type
141#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
142#[serde(rename_all = "snake_case")]
143pub enum VectorIndexType {
144    /// Flat - brute-force search over raw vectors (accumulating state)
145    Flat,
146    /// Removed: global IVF with residual product quantization. The variant is
147    /// kept only so schemas from older indexes deserialize into an actionable
148    /// error instead of an unknown-variant failure. See
149    /// `docs/turboquant-quantization.md` for the IVF-TQ replacement.
150    IvfPq,
151    /// TurboQuant: training-free per-segment compressed flat scan
152    /// (`docs/turboquant-quantization.md`). Available from the first segment
153    /// build with no global artifacts.
154    Tq,
155    /// Trained global IVF router with TurboQuant-coded centroid residuals:
156    /// sub-linear probing with a derived (never trained) leaf codec. The
157    /// default trained float ANN format.
158    #[default]
159    IvfTq,
160    /// ScaNN: a shared, index-wide hierarchical partitioner with
161    /// asymmetric-hashing leaf codes. Immutable segments reference one
162    /// trained generation and can therefore be merged without retraining.
163    Scann,
164}
165
166/// Reject schemas that reference removed index types. Called on every schema
167/// entry point (index create, metadata load), so SDL/JSON/programmatic
168/// construction all fail loudly with the same actionable message.
169pub(crate) fn reject_removed_vector_index_types(schema: &Schema) -> Result<(), String> {
170    for (_, entry) in schema.fields() {
171        if let Some(config) = &entry.sparse_vector_config
172            && config.format == crate::structures::SparseFormat::Seismic
173        {
174            config
175                .seismic
176                .validate()
177                .map_err(|error| format!("sparse field '{}': {error}", entry.name))?;
178            if let Some(query) = &config.query_config
179                && (query.seismic_cut == 0
180                    || query.seismic_cut > crate::query::MAX_QUERY_TERMS
181                    || !query.seismic_factor.is_finite()
182                    || !(0.0..=1.0).contains(&query.seismic_factor))
183            {
184                return Err(format!(
185                    "sparse field '{}': invalid Seismic query cut/factor",
186                    entry.name
187                ));
188            }
189        }
190        if let Some(config) = entry.dense_vector_config.as_ref() {
191            validate_target_vectors(
192                &entry.name,
193                config.target_vectors,
194                !matches!(
195                    config.index_type,
196                    VectorIndexType::Flat | VectorIndexType::Tq
197                ),
198            )?;
199            if config.index_type == VectorIndexType::IvfPq {
200                return Err(format!(
201                    "dense field '{}' uses index_type `ivf_pq`, which was removed; \
202                     recreate the index with `ivf_tq` (trained router, training-free \
203                     TurboQuant leaves) and reindex — see docs/turboquant-quantization.md",
204                    entry.name,
205                ));
206            }
207            if config.index_type == VectorIndexType::Scann && config.soar.is_some() {
208                return Err(format!(
209                    "dense field '{}' enables SOAR for ScaNN, but ScaNN SOAR secondary assignments are not implemented; set soar to null/off",
210                    entry.name,
211                ));
212            }
213            validate_persisted_scann_options(
214                &entry.name,
215                config.index_type == VectorIndexType::Scann,
216                config.num_clusters,
217                config.tree_levels,
218                config.nprobe,
219                config.ivf_routing,
220            )?;
221        }
222        if let Some(config) = entry.binary_dense_vector_config.as_ref() {
223            validate_target_vectors(
224                &entry.name,
225                config.target_vectors,
226                config.index_type != BinaryIndexType::Flat,
227            )?;
228            if config.soar.is_some() && config.index_type != BinaryIndexType::Scann {
229                return Err(format!(
230                    "binary dense field '{}' enables binary SOAR spilling, but it requires the ScaNN index",
231                    entry.name,
232                ));
233            }
234            if config.index_type == BinaryIndexType::Scann && !config.dim.is_multiple_of(8) {
235                return Err(format!(
236                    "binary dense field '{}' uses ScaNN with dimension {}; binary ScaNN dimensions must be a multiple of 8 bits",
237                    entry.name, config.dim,
238                ));
239            }
240            validate_persisted_scann_options(
241                &entry.name,
242                config.index_type == BinaryIndexType::Scann,
243                config.num_clusters,
244                config.tree_levels,
245                config.nprobe,
246                config.ivf_routing,
247            )?;
248        }
249    }
250    Ok(())
251}
252
253fn validate_target_vectors(
254    field_name: &str,
255    target_vectors: Option<u64>,
256    topology_is_automatic: bool,
257) -> Result<(), String> {
258    if target_vectors == Some(0) {
259        return Err(format!(
260            "field '{field_name}' has target_vectors 0; expected a positive steady-state vector count"
261        ));
262    }
263    if target_vectors.is_some() && !topology_is_automatic {
264        return Err(format!(
265            "field '{field_name}' sets target_vectors for a flat/training-free index; the hint is only valid for IVF or ScaNN automatic topology"
266        ));
267    }
268    Ok(())
269}
270
271fn validate_persisted_scann_options(
272    field_name: &str,
273    is_scann: bool,
274    num_clusters: Option<usize>,
275    tree_levels: Option<u8>,
276    nprobe: usize,
277    routing: IvfRoutingMode,
278) -> Result<(), String> {
279    if !is_scann {
280        if tree_levels.is_some() {
281            return Err(format!(
282                "field '{field_name}' sets tree_levels but does not use the ScaNN index"
283            ));
284        }
285        return Ok(());
286    }
287    if routing != IvfRoutingMode::Auto {
288        return Err(format!(
289            "field '{field_name}' sets routing {routing:?} for ScaNN, but ScaNN owns its hierarchical routing; remove the routing option"
290        ));
291    }
292
293    if let Some(levels) = tree_levels
294        && !(1..=3).contains(&levels)
295    {
296        return Err(format!(
297            "field '{field_name}' has ScaNN tree_levels {levels}; expected 1..=3"
298        ));
299    }
300    if let Some(leaves) = num_clusters {
301        if !(2..=30_000_000).contains(&leaves) {
302            return Err(format!(
303                "field '{field_name}' has ScaNN num_clusters {leaves}; expected 2..=30000000"
304            ));
305        }
306        if nprobe > leaves {
307            return Err(format!(
308                "field '{field_name}' has ScaNN nprobe {nprobe} greater than num_clusters {leaves}"
309            ));
310        }
311    }
312    if nprobe == 0 {
313        return Err(format!(
314            "field '{field_name}' has ScaNN nprobe 0; expected a positive probe count"
315        ));
316    }
317    Ok(())
318}
319
320/// How an IVF coarse codebook is searched.
321///
322/// This is shared by floating-point and packed-binary dense fields. It only
323/// controls centroid routing; vector encoding and the distance metric remain
324/// properties of the concrete dense index.
325#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
326#[serde(rename_all = "snake_case")]
327pub enum IvfRoutingMode {
328    /// Select flat routing for small codebooks and HNSW routing for large
329    /// codebooks where scanning every centroid would dominate query latency.
330    #[default]
331    Auto,
332    /// Score every leaf centroid exactly.
333    Flat,
334    /// Use a two-level, beam-routed hierarchy over the leaf centroids.
335    TwoLevel,
336    /// Use an HNSW graph over the global leaf centroids.
337    Hnsw,
338}
339
340/// Storage quantization for dense vector elements
341///
342/// Controls the precision of each vector coordinate in `.vectors` files.
343/// Lower precision reduces storage and memory bandwidth; scoring uses
344/// native-precision SIMD (no dequantization on the hot path).
345#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
346#[serde(rename_all = "snake_case")]
347pub enum DenseVectorQuantization {
348    /// 32-bit IEEE 754 float (4 bytes/dim) — full precision, baseline
349    #[default]
350    F32,
351    /// 16-bit IEEE 754 half-float (2 bytes/dim) — <0.1% recall loss for normalized embeddings
352    F16,
353    /// 8-bit unsigned scalar quantization (1 byte/dim) — maps `[-1, 1]` to `[0, 255]`
354    UInt8,
355    /// Binary packed-bit storage (1 bit per dimension, ceil(dim/8) bytes per vector).
356    /// Used internally by BinaryDenseVector fields. Not selectable for DenseVector fields.
357    Binary,
358}
359
360impl DenseVectorQuantization {
361    /// Bytes per element for non-binary quantization types.
362    /// Panics for Binary — use `dim.div_ceil(8)` for binary vector byte size.
363    pub fn element_size(self) -> usize {
364        match self {
365            Self::F32 => 4,
366            Self::F16 => 2,
367            Self::UInt8 => 1,
368            Self::Binary => panic!("element_size() not valid for Binary; use dim.div_ceil(8)"),
369        }
370    }
371
372    /// Wire format tag (stored in .vectors header)
373    pub fn tag(self) -> u8 {
374        match self {
375            Self::F32 => 0,
376            Self::F16 => 1,
377            Self::UInt8 => 2,
378            Self::Binary => 3,
379        }
380    }
381
382    /// Decode wire format tag
383    pub fn from_tag(tag: u8) -> Option<Self> {
384        match tag {
385            0 => Some(Self::F32),
386            1 => Some(Self::F16),
387            2 => Some(Self::UInt8),
388            3 => Some(Self::Binary),
389            _ => None,
390        }
391    }
392}
393
394/// Configuration for dense vector fields using exact Flat accumulation or the
395/// single production IVF-PQ ANN format.
396///
397/// Indexes operate in two states:
398/// - **Flat (accumulating)**: Brute-force search over raw vectors before
399///   `build_vector_index` is called.
400/// - **Built (ANN)**: Fast approximate nearest neighbor search using trained structures.
401///   Centroids and codebooks are trained from index-wide data and shared by
402///   every segment; segment payloads contain only assignments and PQ codes.
403#[derive(Debug, Clone, Serialize)]
404#[serde(deny_unknown_fields)]
405pub struct DenseVectorConfig {
406    /// Dimensionality of vectors
407    pub dim: usize,
408    /// Target vector index algorithm (Flat or IVF-PQ).
409    /// When in accumulating state, search uses brute-force regardless of this setting.
410    #[serde(default)]
411    pub index_type: VectorIndexType,
412    /// Storage quantization for vector elements (f32, f16, uint8)
413    #[serde(default)]
414    pub quantization: DenseVectorQuantization,
415    /// Number of IVF leaf clusters. If omitted, the selected index algorithm's
416    /// corpus-size cost model determines the value.
417    /// If None, automatically determined based on dataset size.
418    #[serde(default, skip_serializing_if = "Option::is_none")]
419    pub num_clusters: Option<usize>,
420    /// Expected steady-state vector count used only for automatic topology
421    /// sizing. Training readiness still depends on the observed live corpus.
422    /// Explicit `num_clusters` takes precedence over this hint.
423    #[serde(default, skip_serializing_if = "Option::is_none")]
424    pub target_vectors: Option<u64>,
425    /// Number of levels in the ScaNN routing tree. When omitted, training
426    /// derives the depth from corpus size. Only meaningful for ScaNN.
427    #[serde(default, skip_serializing_if = "Option::is_none")]
428    pub tree_levels: Option<u8>,
429    /// Coarse-codebook routing strategy. This setting is metric agnostic and
430    /// is applied to every IVF-backed dense index.
431    #[serde(default)]
432    pub ivf_routing: IvfRoutingMode,
433    /// Number of leaf clusters to probe during search (default: 64)
434    #[serde(default = "default_nprobe")]
435    pub nprobe: usize,
436    /// Whether stored vectors are pre-normalized to unit L2 norm.
437    /// When true, scoring skips per-vector norm computation (cosine = dot / ||q||),
438    /// reducing compute by ~40%. Common for embedding models (e.g. OpenAI, Cohere).
439    /// New IVF-TQ generations index a normalized ANN-only copy while retaining
440    /// the original values for exact reranking. Legacy unnormalized IVF-TQ
441    /// generations must be rebuilt before they can be searched.
442    /// Default: true (most embedding models produce L2-normalized vectors).
443    #[serde(default = "default_unit_norm")]
444    pub unit_norm: bool,
445    /// SOAR spilled cluster assignments for IVF-TQ.
446    /// Assigns vectors to a secondary cluster with an orthogonality-amplified
447    /// residual, improving recall at the same nprobe for ~1.2-2x assignment storage.
448    /// Default: selective spilling calibrated to at most 30% of vectors for
449    /// IVF-TQ. Set this to `None` to disable SOAR. Ignored by non-IVF formats.
450    ///
451    /// Unlike optional fields whose `None` value is omitted on serialization,
452    /// this field serializes `None` as `null`: omission means "use the new
453    /// selective default", while an explicit `null` must continue to mean off
454    /// across a schema round trip.
455    #[serde(default = "default_soar")]
456    pub soar: Option<crate::structures::SoarConfig>,
457}
458
459#[derive(Default)]
460enum PersistedSoar {
461    #[default]
462    Unspecified,
463    Specified(Option<crate::structures::SoarConfig>),
464}
465
466impl<'de> Deserialize<'de> for PersistedSoar {
467    fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
468        Option::<crate::structures::SoarConfig>::deserialize(deserializer).map(Self::Specified)
469    }
470}
471
472#[derive(Deserialize)]
473#[serde(deny_unknown_fields)]
474struct DenseVectorConfigWire {
475    dim: usize,
476    #[serde(default)]
477    index_type: VectorIndexType,
478    #[serde(default)]
479    quantization: DenseVectorQuantization,
480    #[serde(default)]
481    num_clusters: Option<usize>,
482    #[serde(default)]
483    target_vectors: Option<u64>,
484    #[serde(default)]
485    tree_levels: Option<u8>,
486    #[serde(default)]
487    ivf_routing: IvfRoutingMode,
488    #[serde(default = "default_nprobe")]
489    nprobe: usize,
490    #[serde(default = "default_unit_norm")]
491    unit_norm: bool,
492    #[serde(default)]
493    soar: PersistedSoar,
494}
495
496impl<'de> Deserialize<'de> for DenseVectorConfig {
497    fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
498        let wire = DenseVectorConfigWire::deserialize(deserializer)?;
499        let soar = match wire.soar {
500            PersistedSoar::Specified(soar) => soar,
501            PersistedSoar::Unspecified if wire.index_type == VectorIndexType::IvfTq => {
502                default_soar()
503            }
504            PersistedSoar::Unspecified => None,
505        };
506        Ok(Self {
507            dim: wire.dim,
508            index_type: wire.index_type,
509            quantization: wire.quantization,
510            num_clusters: wire.num_clusters,
511            target_vectors: wire.target_vectors,
512            tree_levels: wire.tree_levels,
513            ivf_routing: wire.ivf_routing,
514            nprobe: wire.nprobe,
515            unit_norm: wire.unit_norm,
516            soar,
517        })
518    }
519}
520
521fn default_nprobe() -> usize {
522    64
523}
524
525fn default_unit_norm() -> bool {
526    true
527}
528
529fn default_soar() -> Option<crate::structures::SoarConfig> {
530    Some(crate::structures::SoarConfig::default())
531}
532
533impl DenseVectorConfig {
534    pub fn new(dim: usize) -> Self {
535        Self {
536            dim,
537            index_type: VectorIndexType::IvfTq,
538            quantization: DenseVectorQuantization::F32,
539            num_clusters: None,
540            target_vectors: None,
541            tree_levels: None,
542            ivf_routing: IvfRoutingMode::Auto,
543            nprobe: 64,
544            unit_norm: true,
545            soar: Some(crate::structures::SoarConfig::default()),
546        }
547    }
548
549    /// Create Flat (brute-force) configuration - no ANN index
550    pub fn flat(dim: usize) -> Self {
551        Self {
552            dim,
553            index_type: VectorIndexType::Flat,
554            quantization: DenseVectorQuantization::F32,
555            num_clusters: None,
556            target_vectors: None,
557            tree_levels: None,
558            ivf_routing: IvfRoutingMode::Auto,
559            nprobe: 0,
560            unit_norm: true,
561            soar: None,
562        }
563    }
564
565    /// Create TurboQuant configuration: training-free compressed flat scan.
566    pub fn tq(dim: usize) -> Self {
567        Self {
568            dim,
569            index_type: VectorIndexType::Tq,
570            quantization: DenseVectorQuantization::F32,
571            num_clusters: None,
572            target_vectors: None,
573            tree_levels: None,
574            ivf_routing: IvfRoutingMode::Flat,
575            nprobe: 0,
576            unit_norm: true,
577            soar: None,
578        }
579    }
580
581    /// Create IVF-TQ configuration: trained coarse router, TurboQuant leaves.
582    pub fn ivf_tq(dim: usize, num_clusters: Option<usize>, nprobe: usize) -> Self {
583        Self {
584            dim,
585            index_type: VectorIndexType::IvfTq,
586            quantization: DenseVectorQuantization::F32,
587            num_clusters,
588            target_vectors: None,
589            tree_levels: None,
590            ivf_routing: IvfRoutingMode::Auto,
591            nprobe,
592            unit_norm: true,
593            soar: Some(crate::structures::SoarConfig::default()),
594        }
595    }
596
597    /// Set storage quantization
598    pub fn with_quantization(mut self, quantization: DenseVectorQuantization) -> Self {
599        self.quantization = quantization;
600        self
601    }
602
603    /// Mark vectors as pre-normalized to unit L2 norm
604    pub fn with_unit_norm(mut self) -> Self {
605        self.unit_norm = true;
606        self
607    }
608
609    /// Set number of IVF clusters
610    pub fn with_num_clusters(mut self, num_clusters: usize) -> Self {
611        self.num_clusters = Some(num_clusters);
612        self
613    }
614
615    /// Hint the expected steady-state corpus size for automatic topology.
616    pub fn with_target_vectors(mut self, target_vectors: u64) -> Self {
617        self.target_vectors = Some(target_vectors);
618        self
619    }
620
621    /// Set flat, two-level, or HNSW IVF centroid routing explicitly.
622    pub fn with_ivf_routing(mut self, routing: IvfRoutingMode) -> Self {
623        self.ivf_routing = routing;
624        self
625    }
626    /// Enable SOAR spilled secondary cluster assignments (IVF-based indexes only)
627    pub fn with_soar(mut self, soar: crate::structures::SoarConfig) -> Self {
628        self.soar = Some(soar);
629        self
630    }
631
632    /// Explicitly disable SOAR secondary assignments.
633    pub fn without_soar(mut self) -> Self {
634        self.soar = None;
635        self
636    }
637
638    /// Check if this config uses IVF
639    pub fn uses_ivf(&self) -> bool {
640        self.index_type == VectorIndexType::IvfTq
641    }
642
643    /// Whether the partitioner supports SOAR secondary assignments.
644    pub fn supports_soar(&self) -> bool {
645        self.index_type == VectorIndexType::IvfTq
646    }
647
648    /// Check if this config is flat (brute-force)
649    pub fn is_flat(&self) -> bool {
650        self.index_type == VectorIndexType::Flat
651    }
652
653    /// Calculate optimal number of clusters for given vector count
654    pub fn optimal_num_clusters(&self, num_vectors: usize) -> usize {
655        self.num_clusters.unwrap_or_else(|| {
656            let num_vectors = self.target_vectors.map_or(num_vectors, |target| {
657                usize::try_from(target)
658                    .unwrap_or(usize::MAX)
659                    .max(num_vectors)
660            });
661            // Balanced IVF cost model: practical values are commonly in the
662            // 4-16×sqrt(N) range. Eight is a conservative midpoint; training
663            // quality and artifact memory impose the final bounds.
664            let optimal = 8.0 * (num_vectors as f64).sqrt();
665            (optimal as usize).clamp(16, 1_048_576)
666        })
667    }
668}
669
670/// Configuration for binary dense vector fields
671///
672/// Binary dense vectors store packed bits (1 bit per dimension) and use
673/// Hamming distance for scoring. Segments accumulate exact packed codes and
674/// use the same global IVF router after `build_vector_index`.
675#[derive(Debug, Clone, Serialize, Deserialize)]
676#[serde(deny_unknown_fields)]
677pub struct BinaryDenseVectorConfig {
678    /// Number of bits (dimensions). Storage is ceil(dim/8) bytes per vector.
679    pub dim: usize,
680    /// ANN index type: Flat (brute-force SIMD Hamming) or Ivf (default)
681    /// (k-majority Hamming clusters — probe `nprobe` clusters at query time).
682    /// IVF pays off for segments past a few million vectors.
683    #[serde(default)]
684    pub index_type: BinaryIndexType,
685    /// Number of IVF leaf clusters, selected from corpus and sample size by default.
686    #[serde(default, skip_serializing_if = "Option::is_none")]
687    pub num_clusters: Option<usize>,
688    /// Expected steady-state vector count used only for automatic topology
689    /// sizing. Training readiness still depends on the observed live corpus.
690    /// Explicit `num_clusters` takes precedence over this hint.
691    #[serde(default, skip_serializing_if = "Option::is_none")]
692    pub target_vectors: Option<u64>,
693    /// Number of levels in the ScaNN Hamming routing tree. When omitted,
694    /// training derives the depth from corpus size. Only meaningful for ScaNN.
695    #[serde(default, skip_serializing_if = "Option::is_none")]
696    pub tree_levels: Option<u8>,
697    /// Coarse-codebook routing strategy. Uses the same routing planner as
698    /// floating-point IVF indexes.
699    #[serde(default)]
700    pub ivf_routing: IvfRoutingMode,
701    /// Clusters to probe during search (default: 64)
702    #[serde(default = "default_nprobe")]
703    pub nprobe: usize,
704    /// Optional one-secondary selective spilling for binary ScaNN. The
705    /// alternate leaf is chosen by exact centroid Hamming distance. Unlike
706    /// float SOAR, packed bits have no continuous residual geometry.
707    #[serde(default, skip_serializing_if = "Option::is_none")]
708    pub soar: Option<crate::structures::SoarConfig>,
709}
710
711/// ANN index type for binary dense vector fields
712#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
713#[serde(rename_all = "snake_case")]
714pub enum BinaryIndexType {
715    /// Brute-force SIMD Hamming scan
716    Flat,
717    /// IVF with a global k-majority Hamming quantizer
718    #[default]
719    Ivf,
720    /// Hierarchical Hamming partitioning with exact packed-code leaf scoring.
721    Scann,
722}
723
724/// Complete target ANN configuration for an atomic vector-index ALTER.
725/// Storage shape is deliberately included for validation but cannot change:
726/// ALTER rewrites ANN payloads from retained flat vectors, not stored vectors.
727#[derive(Debug, Clone)]
728pub enum VectorIndexAlter {
729    Dense(DenseVectorConfig),
730    Binary(BinaryDenseVectorConfig),
731}
732
733impl BinaryDenseVectorConfig {
734    pub fn new(dim: usize) -> Self {
735        assert!(
736            dim.is_multiple_of(8),
737            "BinaryDenseVector dimension must be a multiple of 8, got {dim}"
738        );
739        Self {
740            dim,
741            index_type: BinaryIndexType::Ivf,
742            num_clusters: None,
743            target_vectors: None,
744            tree_levels: None,
745            ivf_routing: IvfRoutingMode::Auto,
746            nprobe: 64,
747            soar: None,
748        }
749    }
750
751    /// Enable the IVF index (builder pattern)
752    pub fn with_ivf(mut self, num_clusters: Option<usize>, nprobe: usize) -> Self {
753        self.index_type = BinaryIndexType::Ivf;
754        self.num_clusters = num_clusters;
755        self.nprobe = nprobe;
756        self
757    }
758
759    /// Hint the expected steady-state corpus size for automatic topology.
760    pub fn with_target_vectors(mut self, target_vectors: u64) -> Self {
761        self.target_vectors = Some(target_vectors);
762        self
763    }
764
765    /// Set flat, two-level, or HNSW IVF centroid routing explicitly.
766    pub fn with_ivf_routing(mut self, routing: IvfRoutingMode) -> Self {
767        self.ivf_routing = routing;
768        self
769    }
770
771    /// Enable selective secondary-leaf spilling for binary ScaNN.
772    pub fn with_soar(mut self, soar: crate::structures::SoarConfig) -> Self {
773        self.soar = Some(soar);
774        self
775    }
776
777    /// Disable binary ScaNN secondary-leaf spilling.
778    pub fn without_soar(mut self) -> Self {
779        self.soar = None;
780        self
781    }
782
783    /// Balanced binary IVF cluster count for a given vector count.
784    pub fn optimal_num_clusters(&self, num_vectors: usize) -> usize {
785        self.num_clusters.unwrap_or_else(|| {
786            let num_vectors = self.target_vectors.map_or(num_vectors, |target| {
787                usize::try_from(target)
788                    .unwrap_or(usize::MAX)
789                    .max(num_vectors)
790            });
791            // The 15M-row packed-Hamming sweep found the balanced sqrt(N)
792            // geometry Pareto-optimal for practical recall/latency targets.
793            // Larger, search-quality geometries remain available explicitly.
794            let balanced = (num_vectors as f64).sqrt().ceil() as usize;
795            balanced.clamp(16, 1_048_576)
796        })
797    }
798
799    /// Number of bytes needed to store one vector
800    pub fn byte_len(&self) -> usize {
801        self.dim.div_ceil(8)
802    }
803}
804
805use super::query_field_router::QueryRouterRule;
806
807/// Schema defining document structure
808#[derive(Debug, Clone, Default, Serialize, Deserialize)]
809pub struct Schema {
810    #[serde(skip)]
811    content_hash_field: std::sync::OnceLock<Option<Field>>,
812    fields: Vec<FieldEntry>,
813    name_to_field: HashMap<String, Field>,
814    /// Default fields for query parsing (when no field is specified)
815    #[serde(default)]
816    default_fields: Vec<Field>,
817    /// Query router rules for routing queries to specific fields based on regex patterns
818    #[serde(default)]
819    query_routers: Vec<QueryRouterRule>,
820    /// Run BP (graph bisection) reordering of `reorder`-attributed text or BMP fields
821    /// inside segment merges. SDL: `reorder_on_merge: true` at index level.
822    /// Absent = disabled (merges block-copy; the standalone reorder pass
823    /// handles ordering).
824    #[serde(default)]
825    reorder_on_merge: bool,
826    /// Creation-time cap on retained tokens in each L1 phrase feature.
827    /// Absent (including legacy metadata) means 64. Nonzero u32 keeps the
828    /// serialized range identical on native and WASM targets.
829    #[serde(default, skip_serializing_if = "Option::is_none")]
830    max_l1_phrase_terms: Option<NonZeroU32>,
831    /// Index name used as the `index` label on metrics. Set from the SDL
832    /// index name at parse time and overridden with the registry name at
833    /// server-side index creation. Empty on old metadata → "unknown".
834    #[serde(default)]
835    index_name: String,
836}
837
838impl Schema {
839    pub fn builder() -> SchemaBuilder {
840        SchemaBuilder::default()
841    }
842
843    pub fn get_field(&self, name: &str) -> Option<Field> {
844        self.name_to_field.get(name).copied()
845    }
846
847    pub fn get_field_entry(&self, field: Field) -> Option<&FieldEntry> {
848        self.fields.get(field.0 as usize)
849    }
850
851    /// Field whose values hint the dynamic tokenizer of `field`
852    /// (`text<lex(by: <hint field>, ...)>`), if any.
853    pub fn tokenizer_hint_field(&self, field: Field) -> Option<Field> {
854        let spec = self.get_field_entry(field)?.tokenizer_spec()?;
855        self.get_field(spec.hint_field()?)
856    }
857
858    /// Clone this schema with one vector field's ANN parameters replaced.
859    /// Field type, dimension, and storage quantization are immutable.
860    pub fn with_vector_index_alter(
861        &self,
862        field: Field,
863        alter: VectorIndexAlter,
864    ) -> Result<Self, String> {
865        let mut next = self.clone();
866        let entry = next
867            .fields
868            .get_mut(field.0 as usize)
869            .ok_or_else(|| format!("vector ALTER references unknown field {}", field.0))?;
870        match alter {
871            VectorIndexAlter::Dense(config) => {
872                let current = entry
873                    .dense_vector_config
874                    .as_ref()
875                    .ok_or_else(|| format!("field '{}' is not a dense vector field", entry.name))?;
876                if config.dim != current.dim || config.quantization != current.quantization {
877                    return Err(format!(
878                        "field '{}' ALTER cannot change dimension or storage quantization",
879                        entry.name
880                    ));
881                }
882                if matches!(
883                    config.index_type,
884                    VectorIndexType::Flat | VectorIndexType::Tq
885                ) {
886                    return Err(format!(
887                        "field '{}' ALTER target must be `ivf_tq` or `scann`",
888                        entry.name
889                    ));
890                }
891                entry.dense_vector_config = Some(config);
892            }
893            VectorIndexAlter::Binary(config) => {
894                let current = entry.binary_dense_vector_config.as_ref().ok_or_else(|| {
895                    format!("field '{}' is not a binary dense vector field", entry.name)
896                })?;
897                if config.dim != current.dim {
898                    return Err(format!(
899                        "field '{}' ALTER cannot change binary dimension",
900                        entry.name
901                    ));
902                }
903                if config.index_type == BinaryIndexType::Flat {
904                    return Err(format!(
905                        "field '{}' ALTER target must be `ivf` or `scann`",
906                        entry.name
907                    ));
908                }
909                entry.binary_dense_vector_config = Some(config);
910            }
911        }
912        reject_removed_vector_index_types(&next)?;
913        Ok(next)
914    }
915
916    pub fn get_field_name(&self, field: Field) -> Option<&str> {
917        self.fields.get(field.0 as usize).map(|e| e.name.as_str())
918    }
919
920    pub fn fields(&self) -> impl Iterator<Item = (Field, &FieldEntry)> {
921        self.fields
922            .iter()
923            .enumerate()
924            .map(|(i, e)| (Field(i as u32), e))
925    }
926
927    pub fn num_fields(&self) -> usize {
928        self.fields.len()
929    }
930
931    /// Whether indexed text or BMP fields opt into BP reordering.
932    pub fn has_reorder_fields(&self) -> bool {
933        self.fields
934            .iter()
935            .any(|entry| entry.reorder && entry.supports_reorder())
936    }
937
938    /// Whether this index has fields serviced by the bounded background optimizer.
939    pub fn has_background_maintenance_fields(&self) -> bool {
940        self.fields.iter().any(|entry| {
941            (entry.reorder && entry.supports_reorder())
942                || (entry.indexed
943                    && (entry
944                        .binary_dense_vector_config
945                        .as_ref()
946                        .is_some_and(|config| {
947                            matches!(
948                                config.index_type,
949                                BinaryIndexType::Ivf | BinaryIndexType::Scann
950                            )
951                        })
952                        || entry.sparse_vector_config.as_ref().is_some_and(|config| {
953                            config.format == crate::structures::SparseFormat::Seismic
954                        })))
955        })
956    }
957
958    /// Whether merges BP-reorder `reorder`-attributed text or BMP fields while writing
959    /// the merged segment (index-level SDL option `reorder_on_merge: true`).
960    pub fn reorder_on_merge(&self) -> bool {
961        self.reorder_on_merge
962    }
963
964    /// Maximum retained tokens per L1 phrase, for ranking and feature export.
965    /// This index policy is independent of nomination and server token limits.
966    pub fn max_l1_phrase_terms(&self) -> usize {
967        self.max_l1_phrase_terms
968            .map_or(64, |limit| limit.get() as usize)
969    }
970
971    /// Index name for metric labels ("unknown" when not set — pre-existing
972    /// metadata or programmatic schemas without a name).
973    pub fn index_label(&self) -> &str {
974        if self.index_name.is_empty() {
975            "unknown"
976        } else {
977            &self.index_name
978        }
979    }
980
981    /// Set the index name used as the metrics `index` label.
982    pub fn set_index_name(&mut self, name: impl Into<String>) {
983        self.index_name = name.into();
984    }
985
986    /// Get the default fields for query parsing
987    pub fn default_fields(&self) -> &[Field] {
988        &self.default_fields
989    }
990
991    /// Set default fields (used by builder)
992    pub fn set_default_fields(&mut self, fields: Vec<Field>) {
993        self.default_fields = fields;
994    }
995
996    /// Get the query router rules
997    pub fn query_routers(&self) -> &[QueryRouterRule] {
998        &self.query_routers
999    }
1000
1001    /// Set query router rules
1002    pub fn set_query_routers(&mut self, rules: Vec<QueryRouterRule>) {
1003        self.query_routers = rules;
1004    }
1005
1006    /// Get the optional stored content fingerprint field.
1007    pub fn content_hash_field(&self) -> Option<Field> {
1008        *self.content_hash_field.get_or_init(|| {
1009            self.fields
1010                .iter()
1011                .position(|entry| entry.content_hash)
1012                .map(|id| Field(id as u32))
1013        })
1014    }
1015
1016    /// Shared admission for SDL, programmatic schemas, native/WASM creation and reopen.
1017    pub(crate) fn validate(&self) -> crate::Result<()> {
1018        for entry in &self.fields {
1019            if entry.reorder && !entry.supports_reorder() {
1020                return Err(crate::Error::Schema(format!(
1021                    "field '{}' uses reorder, which requires indexed text or BMP; Seismic and binary ANN maintenance is automatic",
1022                    entry.name
1023                )));
1024            }
1025        }
1026        self.validate_content_hash()
1027    }
1028
1029    fn validate_content_hash(&self) -> crate::Result<()> {
1030        let hashes: Vec<_> = self
1031            .fields
1032            .iter()
1033            .filter(|entry| entry.content_hash)
1034            .collect();
1035        if hashes.is_empty() {
1036            return Ok(());
1037        }
1038        if hashes.len() != 1 || self.fields.iter().filter(|entry| entry.primary_key).count() != 1 {
1039            return Err(crate::Error::Schema(
1040                "content_hash requires exactly one hash field and one primary key".into(),
1041            ));
1042        }
1043        let primary = self.get_field_entry(self.primary_field().unwrap()).unwrap();
1044        if primary.field_type != FieldType::Text
1045            || primary.multi
1046            || !primary.fast
1047            || !primary.indexed
1048        {
1049            return Err(crate::Error::Schema("content_hash requires a single-valued text primary key with fast and indexed enabled".into()));
1050        }
1051        let entry = hashes[0];
1052        if !entry.stored
1053            || entry.multi
1054            || !matches!(
1055                entry.field_type,
1056                FieldType::Text | FieldType::Bytes | FieldType::U64
1057            )
1058        {
1059            return Err(crate::Error::Schema(
1060                "content_hash must be a stored, single-valued text, bytes, or u64 field".into(),
1061            ));
1062        }
1063        Ok(())
1064    }
1065
1066    /// Get the primary key field, if one is defined
1067    pub fn primary_field(&self) -> Option<Field> {
1068        self.fields
1069            .iter()
1070            .enumerate()
1071            .find(|(_, e)| e.primary_key)
1072            .map(|(i, _)| Field(i as u32))
1073    }
1074}
1075
1076/// Builder for Schema
1077#[derive(Debug, Default)]
1078pub struct SchemaBuilder {
1079    fields: Vec<FieldEntry>,
1080    default_fields: Vec<String>,
1081    query_routers: Vec<QueryRouterRule>,
1082    reorder_on_merge: bool,
1083    max_l1_phrase_terms: Option<NonZeroU32>,
1084    index_name: String,
1085}
1086
1087impl SchemaBuilder {
1088    pub fn add_text_field(&mut self, name: &str, indexed: bool, stored: bool) -> Field {
1089        self.add_field_with_tokenizer(
1090            name,
1091            FieldType::Text,
1092            indexed,
1093            stored,
1094            Some("simple".to_string()),
1095        )
1096    }
1097
1098    pub fn add_text_field_with_tokenizer(
1099        &mut self,
1100        name: &str,
1101        indexed: bool,
1102        stored: bool,
1103        tokenizer: &str,
1104    ) -> Field {
1105        self.add_field_with_tokenizer(
1106            name,
1107            FieldType::Text,
1108            indexed,
1109            stored,
1110            Some(tokenizer.to_string()),
1111        )
1112    }
1113
1114    pub fn add_u64_field(&mut self, name: &str, indexed: bool, stored: bool) -> Field {
1115        self.add_field(name, FieldType::U64, indexed, stored)
1116    }
1117
1118    pub fn add_i64_field(&mut self, name: &str, indexed: bool, stored: bool) -> Field {
1119        self.add_field(name, FieldType::I64, indexed, stored)
1120    }
1121
1122    pub fn add_f64_field(&mut self, name: &str, indexed: bool, stored: bool) -> Field {
1123        self.add_field(name, FieldType::F64, indexed, stored)
1124    }
1125
1126    pub fn add_bytes_field(&mut self, name: &str, stored: bool) -> Field {
1127        self.add_field(name, FieldType::Bytes, false, stored)
1128    }
1129
1130    /// Add a JSON field for storing arbitrary JSON data
1131    ///
1132    /// JSON fields are never indexed, only stored. They can hold any valid JSON value
1133    /// (objects, arrays, strings, numbers, booleans, null).
1134    pub fn add_json_field(&mut self, name: &str, stored: bool) -> Field {
1135        self.add_field(name, FieldType::Json, false, stored)
1136    }
1137
1138    /// Add a sparse vector field with default configuration
1139    ///
1140    /// Sparse vectors are indexed as inverted posting lists where each dimension
1141    /// becomes a "term" and documents have quantized weights for each dimension.
1142    pub fn add_sparse_vector_field(&mut self, name: &str, indexed: bool, stored: bool) -> Field {
1143        self.add_sparse_vector_field_with_config(
1144            name,
1145            indexed,
1146            stored,
1147            crate::structures::SparseVectorConfig::default(),
1148        )
1149    }
1150
1151    /// Add a sparse vector field with custom configuration
1152    ///
1153    /// Use `SparseVectorConfig::splade()` for SPLADE models (u16 indices, uint8 weights).
1154    /// Use `SparseVectorConfig::compact()` for maximum compression (u16 indices, uint4 weights).
1155    pub fn add_sparse_vector_field_with_config(
1156        &mut self,
1157        name: &str,
1158        indexed: bool,
1159        stored: bool,
1160        config: crate::structures::SparseVectorConfig,
1161    ) -> Field {
1162        let field = Field(self.fields.len() as u32);
1163        self.fields.push(FieldEntry {
1164            name: name.to_string(),
1165            field_type: FieldType::SparseVector,
1166            indexed,
1167            stored,
1168            tokenizer: None,
1169            multi: false,
1170            positions: None,
1171            sparse_vector_config: Some(config),
1172            dense_vector_config: None,
1173            binary_dense_vector_config: None,
1174            fast: false,
1175            primary_key: false,
1176            content_hash: false,
1177            reorder: false,
1178            chunked: false,
1179            bm25_k1: None,
1180            bm25_b: None,
1181        });
1182        field
1183    }
1184
1185    /// Set sparse vector configuration for an existing field
1186    pub fn set_sparse_vector_config(
1187        &mut self,
1188        field: Field,
1189        config: crate::structures::SparseVectorConfig,
1190    ) {
1191        if let Some(entry) = self.fields.get_mut(field.0 as usize) {
1192            entry.sparse_vector_config = Some(config);
1193        }
1194    }
1195
1196    /// Add a dense vector field with default configuration
1197    ///
1198    /// Dense vectors use the global IVF-PQ ANN implementation. The dimension
1199    /// determines both the stored vector shape and PQ structure.
1200    pub fn add_dense_vector_field(
1201        &mut self,
1202        name: &str,
1203        dim: usize,
1204        indexed: bool,
1205        stored: bool,
1206    ) -> Field {
1207        self.add_dense_vector_field_with_config(name, indexed, stored, DenseVectorConfig::new(dim))
1208    }
1209
1210    /// Add a dense vector field with custom configuration
1211    pub fn add_dense_vector_field_with_config(
1212        &mut self,
1213        name: &str,
1214        indexed: bool,
1215        stored: bool,
1216        config: DenseVectorConfig,
1217    ) -> Field {
1218        let field = Field(self.fields.len() as u32);
1219        self.fields.push(FieldEntry {
1220            name: name.to_string(),
1221            field_type: FieldType::DenseVector,
1222            indexed,
1223            stored,
1224            tokenizer: None,
1225            multi: false,
1226            positions: None,
1227            sparse_vector_config: None,
1228            dense_vector_config: Some(config),
1229            binary_dense_vector_config: None,
1230            fast: false,
1231            primary_key: false,
1232            content_hash: false,
1233            reorder: false,
1234            chunked: false,
1235            bm25_k1: None,
1236            bm25_b: None,
1237        });
1238        field
1239    }
1240
1241    /// Add a binary dense vector field
1242    ///
1243    /// Binary dense vectors use packed-bit storage (1 bit per dimension),
1244    /// exact Hamming scoring inside globally routed IVF leaves, and a flat
1245    /// SIMD fallback while the index is accumulating.
1246    pub fn add_binary_dense_vector_field(
1247        &mut self,
1248        name: &str,
1249        dim: usize,
1250        indexed: bool,
1251        stored: bool,
1252    ) -> Field {
1253        self.add_binary_dense_vector_field_with_config(
1254            name,
1255            indexed,
1256            stored,
1257            BinaryDenseVectorConfig::new(dim),
1258        )
1259    }
1260
1261    /// Add a binary dense vector field with custom configuration
1262    pub fn add_binary_dense_vector_field_with_config(
1263        &mut self,
1264        name: &str,
1265        indexed: bool,
1266        stored: bool,
1267        config: BinaryDenseVectorConfig,
1268    ) -> Field {
1269        let field = Field(self.fields.len() as u32);
1270        self.fields.push(FieldEntry {
1271            name: name.to_string(),
1272            field_type: FieldType::BinaryDenseVector,
1273            indexed,
1274            stored,
1275            tokenizer: None,
1276            multi: false,
1277            positions: None,
1278            sparse_vector_config: None,
1279            dense_vector_config: None,
1280            binary_dense_vector_config: Some(config),
1281            fast: false,
1282            primary_key: false,
1283            content_hash: false,
1284            reorder: false,
1285            chunked: false,
1286            bm25_k1: None,
1287            bm25_b: None,
1288        });
1289        field
1290    }
1291
1292    fn add_field(
1293        &mut self,
1294        name: &str,
1295        field_type: FieldType,
1296        indexed: bool,
1297        stored: bool,
1298    ) -> Field {
1299        self.add_field_with_tokenizer(name, field_type, indexed, stored, None)
1300    }
1301
1302    fn add_field_with_tokenizer(
1303        &mut self,
1304        name: &str,
1305        field_type: FieldType,
1306        indexed: bool,
1307        stored: bool,
1308        tokenizer: Option<String>,
1309    ) -> Field {
1310        self.add_field_full(name, field_type, indexed, stored, tokenizer, false)
1311    }
1312
1313    fn add_field_full(
1314        &mut self,
1315        name: &str,
1316        field_type: FieldType,
1317        indexed: bool,
1318        stored: bool,
1319        tokenizer: Option<String>,
1320        multi: bool,
1321    ) -> Field {
1322        let field = Field(self.fields.len() as u32);
1323        self.fields.push(FieldEntry {
1324            name: name.to_string(),
1325            field_type,
1326            indexed,
1327            stored,
1328            tokenizer,
1329            multi,
1330            positions: None,
1331            sparse_vector_config: None,
1332            dense_vector_config: None,
1333            binary_dense_vector_config: None,
1334            fast: false,
1335            primary_key: false,
1336            content_hash: false,
1337            reorder: false,
1338            chunked: false,
1339            bm25_k1: None,
1340            bm25_b: None,
1341        });
1342        field
1343    }
1344
1345    /// Set the multi attribute on the last added field
1346    pub fn set_multi(&mut self, field: Field, multi: bool) {
1347        if let Some(entry) = self.fields.get_mut(field.0 as usize) {
1348            entry.multi = multi;
1349        }
1350    }
1351
1352    /// Set fast-field columnar storage for O(1) doc→value access.
1353    /// Valid for u64, i64, f64, and text fields.
1354    pub fn set_fast(&mut self, field: Field, fast: bool) {
1355        if let Some(entry) = self.fields.get_mut(field.0 as usize) {
1356            entry.fast = fast;
1357        }
1358    }
1359
1360    /// Mark a field as the primary key (unique constraint).
1361    ///
1362    /// Primary key implies fast + indexed (dedup looks committed keys up in
1363    /// the fast-field text dictionary) — kept in sync with the SDL path,
1364    /// which forces the same attributes.
1365    pub fn set_primary_key(&mut self, field: Field) {
1366        if let Some(entry) = self.fields.get_mut(field.0 as usize) {
1367            entry.primary_key = true;
1368            entry.fast = true;
1369            entry.indexed = true;
1370        }
1371    }
1372
1373    /// Mark a stored scalar field as the caller-supplied content fingerprint.
1374    /// Invalid configurations are rejected when creating an index.
1375    pub fn set_content_hash(&mut self, field: Field) {
1376        self.fields
1377            .get_mut(field.0 as usize)
1378            .expect("unknown content hash field")
1379            .content_hash = true;
1380    }
1381
1382    /// Enable BP reordering for an indexed text or BMP field.
1383    /// Invalid field types are rejected at schema admission.
1384    pub fn set_reorder(&mut self, field: Field, reorder: bool) {
1385        if let Some(entry) = self.fields.get_mut(field.0 as usize) {
1386            entry.reorder = reorder;
1387        }
1388    }
1389
1390    /// Set the BM25 parameters of a text field (`None` keeps the default).
1391    pub fn set_bm25_params(&mut self, field: Field, k1: Option<f32>, b: Option<f32>) {
1392        if let Some(entry) = self.fields.get_mut(field.0 as usize) {
1393            entry.bm25_k1 = k1;
1394            entry.bm25_b = b;
1395        }
1396    }
1397
1398    /// Mark a text field as chunked: each value is indexed as its own BM25
1399    /// unit and results carry per-chunk ordinals. Implies `multi`.
1400    pub fn set_chunked(&mut self, field: Field, chunked: bool) {
1401        if let Some(entry) = self.fields.get_mut(field.0 as usize) {
1402            entry.chunked = chunked;
1403            if chunked {
1404                entry.multi = true;
1405            }
1406        }
1407    }
1408
1409    /// Enable BP reordering of `reorder`-attributed text or BMP fields inside merges
1410    /// (index-level; SDL `reorder_on_merge: true`). Default: disabled.
1411    pub fn set_reorder_on_merge(&mut self, on: bool) {
1412        self.reorder_on_merge = on;
1413    }
1414
1415    /// Set the persisted L1 phrase-term cap at index creation (default: 64).
1416    /// Higher limits allow more per-phrase cursors and posting/position reads;
1417    /// the shared candidate probe budgets and server token limit still apply.
1418    pub fn set_max_l1_phrase_terms(&mut self, limit: NonZeroU32) {
1419        self.max_l1_phrase_terms = Some(limit);
1420    }
1421
1422    /// Set the index name used as the metrics `index` label.
1423    pub fn set_index_name(&mut self, name: impl Into<String>) {
1424        self.index_name = name.into();
1425    }
1426
1427    /// Set position tracking mode for phrase queries and multi-field element tracking
1428    pub fn set_positions(&mut self, field: Field, mode: PositionMode) {
1429        if let Some(entry) = self.fields.get_mut(field.0 as usize) {
1430            entry.positions = Some(mode);
1431        }
1432    }
1433
1434    /// Set default fields by name
1435    pub fn set_default_fields(&mut self, field_names: Vec<String>) {
1436        self.default_fields = field_names;
1437    }
1438
1439    /// Set query router rules
1440    pub fn set_query_routers(&mut self, rules: Vec<QueryRouterRule>) {
1441        self.query_routers = rules;
1442    }
1443
1444    pub fn build(self) -> Schema {
1445        let mut name_to_field = HashMap::new();
1446        for (i, entry) in self.fields.iter().enumerate() {
1447            name_to_field.insert(entry.name.clone(), Field(i as u32));
1448        }
1449
1450        // Resolve default field names to Field IDs
1451        let default_fields: Vec<Field> = self
1452            .default_fields
1453            .iter()
1454            .filter_map(|name| name_to_field.get(name).copied())
1455            .collect();
1456
1457        Schema {
1458            content_hash_field: Default::default(),
1459            fields: self.fields,
1460            name_to_field,
1461            default_fields,
1462            query_routers: self.query_routers,
1463            reorder_on_merge: self.reorder_on_merge,
1464            max_l1_phrase_terms: self.max_l1_phrase_terms,
1465            index_name: self.index_name,
1466        }
1467    }
1468}
1469
1470/// Value that can be stored in a field
1471#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1472pub enum FieldValue {
1473    #[serde(rename = "text")]
1474    Text(String),
1475    #[serde(rename = "u64")]
1476    U64(u64),
1477    #[serde(rename = "i64")]
1478    I64(i64),
1479    #[serde(rename = "f64")]
1480    F64(f64),
1481    #[serde(rename = "bytes")]
1482    Bytes(Vec<u8>),
1483    /// Sparse vector: list of (dimension_id, weight) pairs
1484    #[serde(rename = "sparse_vector")]
1485    SparseVector(Vec<(u32, f32)>),
1486    /// Dense vector: float32 values
1487    #[serde(rename = "dense_vector")]
1488    DenseVector(Vec<f32>),
1489    /// Arbitrary JSON value
1490    #[serde(rename = "json")]
1491    Json(serde_json::Value),
1492    /// Binary dense vector: packed bits (ceil(dim/8) bytes)
1493    #[serde(rename = "binary_dense_vector")]
1494    BinaryDenseVector(Vec<u8>),
1495}
1496
1497impl FieldValue {
1498    pub fn as_text(&self) -> Option<&str> {
1499        match self {
1500            FieldValue::Text(s) => Some(s),
1501            _ => None,
1502        }
1503    }
1504
1505    pub fn as_u64(&self) -> Option<u64> {
1506        match self {
1507            FieldValue::U64(v) => Some(*v),
1508            _ => None,
1509        }
1510    }
1511
1512    pub fn as_i64(&self) -> Option<i64> {
1513        match self {
1514            FieldValue::I64(v) => Some(*v),
1515            _ => None,
1516        }
1517    }
1518
1519    pub fn as_f64(&self) -> Option<f64> {
1520        match self {
1521            FieldValue::F64(v) => Some(*v),
1522            _ => None,
1523        }
1524    }
1525
1526    pub fn as_bytes(&self) -> Option<&[u8]> {
1527        match self {
1528            FieldValue::Bytes(b) => Some(b),
1529            _ => None,
1530        }
1531    }
1532
1533    pub fn as_sparse_vector(&self) -> Option<&[(u32, f32)]> {
1534        match self {
1535            FieldValue::SparseVector(entries) => Some(entries),
1536            _ => None,
1537        }
1538    }
1539
1540    pub fn as_dense_vector(&self) -> Option<&[f32]> {
1541        match self {
1542            FieldValue::DenseVector(v) => Some(v),
1543            _ => None,
1544        }
1545    }
1546
1547    pub fn as_json(&self) -> Option<&serde_json::Value> {
1548        match self {
1549            FieldValue::Json(v) => Some(v),
1550            _ => None,
1551        }
1552    }
1553
1554    pub fn as_binary_dense_vector(&self) -> Option<&[u8]> {
1555        match self {
1556            FieldValue::BinaryDenseVector(v) => Some(v),
1557            _ => None,
1558        }
1559    }
1560}
1561
1562/// A document to be indexed
1563#[derive(Debug, Clone, Default, Serialize, Deserialize)]
1564pub struct Document {
1565    field_values: Vec<(Field, FieldValue)>,
1566}
1567
1568impl Document {
1569    pub fn new() -> Self {
1570        Self::default()
1571    }
1572
1573    pub fn add_text(&mut self, field: Field, value: impl Into<String>) {
1574        self.field_values
1575            .push((field, FieldValue::Text(value.into())));
1576    }
1577
1578    pub fn add_u64(&mut self, field: Field, value: u64) {
1579        self.field_values.push((field, FieldValue::U64(value)));
1580    }
1581
1582    pub fn add_i64(&mut self, field: Field, value: i64) {
1583        self.field_values.push((field, FieldValue::I64(value)));
1584    }
1585
1586    pub fn add_f64(&mut self, field: Field, value: f64) {
1587        self.field_values.push((field, FieldValue::F64(value)));
1588    }
1589
1590    pub fn add_bytes(&mut self, field: Field, value: Vec<u8>) {
1591        self.field_values.push((field, FieldValue::Bytes(value)));
1592    }
1593
1594    pub fn add_sparse_vector(&mut self, field: Field, entries: Vec<(u32, f32)>) {
1595        self.field_values
1596            .push((field, FieldValue::SparseVector(entries)));
1597    }
1598
1599    pub fn add_dense_vector(&mut self, field: Field, values: Vec<f32>) {
1600        self.field_values
1601            .push((field, FieldValue::DenseVector(values)));
1602    }
1603
1604    pub fn add_json(&mut self, field: Field, value: serde_json::Value) {
1605        self.field_values.push((field, FieldValue::Json(value)));
1606    }
1607
1608    pub fn add_binary_dense_vector(&mut self, field: Field, values: Vec<u8>) {
1609        self.field_values
1610            .push((field, FieldValue::BinaryDenseVector(values)));
1611    }
1612
1613    pub fn get_first(&self, field: Field) -> Option<&FieldValue> {
1614        self.field_values
1615            .iter()
1616            .find(|(f, _)| *f == field)
1617            .map(|(_, v)| v)
1618    }
1619
1620    pub fn get_all(&self, field: Field) -> impl Iterator<Item = &FieldValue> {
1621        self.field_values
1622            .iter()
1623            .filter(move |(f, _)| *f == field)
1624            .map(|(_, v)| v)
1625    }
1626
1627    pub fn field_values(&self) -> &[(Field, FieldValue)] {
1628        &self.field_values
1629    }
1630
1631    /// Return a new Document containing only fields marked as `stored` in the schema
1632    pub fn filter_stored(&self, schema: &Schema) -> Document {
1633        Document {
1634            field_values: self
1635                .field_values
1636                .iter()
1637                .filter(|(field, _)| {
1638                    schema
1639                        .get_field_entry(*field)
1640                        .is_some_and(|entry| entry.stored)
1641                })
1642                .cloned()
1643                .collect(),
1644        }
1645    }
1646
1647    /// Convert document to a JSON object using field names from schema
1648    ///
1649    /// Fields marked as `multi` in the schema are always returned as JSON arrays.
1650    /// Other fields with multiple values are also returned as arrays.
1651    /// Fields with a single value (and not marked multi) are returned as scalar values.
1652    pub fn to_json(&self, schema: &Schema) -> serde_json::Value {
1653        use std::collections::HashMap;
1654
1655        // Group values by field, keeping track of field entry for multi check
1656        let mut field_values_map: HashMap<Field, (String, bool, Vec<serde_json::Value>)> =
1657            HashMap::new();
1658
1659        for (field, value) in &self.field_values {
1660            if let Some(entry) = schema.get_field_entry(*field) {
1661                let json_value = match value {
1662                    FieldValue::Text(s) => serde_json::Value::String(s.clone()),
1663                    FieldValue::U64(n) => serde_json::Value::Number((*n).into()),
1664                    FieldValue::I64(n) => serde_json::Value::Number((*n).into()),
1665                    FieldValue::F64(n) => serde_json::json!(n),
1666                    FieldValue::Bytes(b) => {
1667                        use base64::Engine;
1668                        serde_json::Value::String(
1669                            base64::engine::general_purpose::STANDARD.encode(b),
1670                        )
1671                    }
1672                    FieldValue::SparseVector(entries) => {
1673                        let indices: Vec<u32> = entries.iter().map(|(i, _)| *i).collect();
1674                        let values: Vec<f32> = entries.iter().map(|(_, v)| *v).collect();
1675                        serde_json::json!({
1676                            "indices": indices,
1677                            "values": values
1678                        })
1679                    }
1680                    FieldValue::DenseVector(values) => {
1681                        serde_json::json!(values)
1682                    }
1683                    FieldValue::Json(v) => v.clone(),
1684                    FieldValue::BinaryDenseVector(b) => {
1685                        use base64::Engine;
1686                        serde_json::Value::String(
1687                            base64::engine::general_purpose::STANDARD.encode(b),
1688                        )
1689                    }
1690                };
1691                field_values_map
1692                    .entry(*field)
1693                    .or_insert_with(|| (entry.name.clone(), entry.multi, Vec::new()))
1694                    .2
1695                    .push(json_value);
1696            }
1697        }
1698
1699        // Convert to JSON object, using arrays for multi fields or when multiple values exist
1700        let mut map = serde_json::Map::new();
1701        for (_field, (name, is_multi, values)) in field_values_map {
1702            let json_value = if is_multi || values.len() > 1 {
1703                serde_json::Value::Array(values)
1704            } else {
1705                values.into_iter().next().unwrap()
1706            };
1707            map.insert(name, json_value);
1708        }
1709
1710        serde_json::Value::Object(map)
1711    }
1712
1713    /// Create a Document from a JSON object using field names from schema
1714    ///
1715    /// Supports:
1716    /// - String values -> Text fields
1717    /// - Number values -> U64/I64/F64 fields (based on schema type)
1718    /// - Array values -> Multiple values for the same field (multifields)
1719    ///
1720    /// Unknown fields (not in schema) are silently ignored.
1721    pub fn from_json(json: &serde_json::Value, schema: &Schema) -> Option<Self> {
1722        let obj = json.as_object()?;
1723        let mut doc = Document::new();
1724
1725        for (key, value) in obj {
1726            if let Some(field) = schema.get_field(key) {
1727                let field_entry = schema.get_field_entry(field)?;
1728                // Fingerprints must never disappear through permissive JSON conversion:
1729                // that would turn a malformed hash into an unconditional replacement.
1730                if field_entry.content_hash {
1731                    match (&field_entry.field_type, value) {
1732                        (FieldType::Text, serde_json::Value::String(text)) => {
1733                            doc.add_text(field, text)
1734                        }
1735                        (FieldType::U64, serde_json::Value::Number(number)) => {
1736                            doc.add_u64(field, number.as_u64()?)
1737                        }
1738                        (FieldType::Bytes, serde_json::Value::String(encoded)) => {
1739                            use base64::Engine;
1740                            doc.add_bytes(
1741                                field,
1742                                base64::engine::general_purpose::STANDARD
1743                                    .decode(encoded)
1744                                    .ok()?,
1745                            );
1746                        }
1747                        _ => return None,
1748                    }
1749                    continue;
1750                }
1751                Self::add_json_value(&mut doc, field, &field_entry.field_type, value);
1752            }
1753        }
1754
1755        Some(doc)
1756    }
1757
1758    /// Helper to add a JSON value to a document, handling type conversion
1759    fn add_json_value(
1760        doc: &mut Document,
1761        field: Field,
1762        field_type: &FieldType,
1763        value: &serde_json::Value,
1764    ) {
1765        match value {
1766            serde_json::Value::String(s) => {
1767                if matches!(field_type, FieldType::Text) {
1768                    doc.add_text(field, s.clone());
1769                }
1770            }
1771            serde_json::Value::Number(n) => {
1772                match field_type {
1773                    FieldType::I64 => {
1774                        if let Some(i) = n.as_i64() {
1775                            doc.add_i64(field, i);
1776                        }
1777                    }
1778                    FieldType::U64 => {
1779                        if let Some(u) = n.as_u64() {
1780                            doc.add_u64(field, u);
1781                        } else if let Some(i) = n.as_i64() {
1782                            // Allow positive i64 as u64
1783                            if i >= 0 {
1784                                doc.add_u64(field, i as u64);
1785                            }
1786                        }
1787                    }
1788                    FieldType::F64 => {
1789                        if let Some(f) = n.as_f64() {
1790                            doc.add_f64(field, f);
1791                        }
1792                    }
1793                    _ => {}
1794                }
1795            }
1796            // Handle arrays (multifields) - add each element separately
1797            serde_json::Value::Array(arr) => {
1798                for item in arr {
1799                    Self::add_json_value(doc, field, field_type, item);
1800                }
1801            }
1802            // Handle sparse vector objects
1803            serde_json::Value::Object(obj) if matches!(field_type, FieldType::SparseVector) => {
1804                if let (Some(indices_val), Some(values_val)) =
1805                    (obj.get("indices"), obj.get("values"))
1806                {
1807                    let indices: Vec<u32> = indices_val
1808                        .as_array()
1809                        .map(|arr| {
1810                            arr.iter()
1811                                .filter_map(|v| v.as_u64().map(|n| n as u32))
1812                                .collect()
1813                        })
1814                        .unwrap_or_default();
1815                    let values: Vec<f32> = values_val
1816                        .as_array()
1817                        .map(|arr| {
1818                            arr.iter()
1819                                .filter_map(|v| v.as_f64().map(|n| n as f32))
1820                                .collect()
1821                        })
1822                        .unwrap_or_default();
1823                    if indices.len() == values.len() {
1824                        let entries: Vec<(u32, f32)> = indices.into_iter().zip(values).collect();
1825                        doc.add_sparse_vector(field, entries);
1826                    }
1827                }
1828            }
1829            // Handle JSON fields - accept any value directly
1830            _ if matches!(field_type, FieldType::Json) => {
1831                doc.add_json(field, value.clone());
1832            }
1833            serde_json::Value::Object(_) => {}
1834            _ => {}
1835        }
1836    }
1837}
1838
1839#[cfg(test)]
1840mod tests {
1841    use super::*;
1842
1843    #[test]
1844    fn reorder_accepts_indexed_text_and_bmp_and_rejects_unsupported_fields() {
1845        for field in [
1846            "sparse_vector [indexed<format: seismic>, reorder]",
1847            "sparse_vector [indexed<format: maxscore>, reorder]",
1848            "dense_vector<4> [indexed, reorder]",
1849            "u64 [indexed, reorder]",
1850            "text [stored, reorder]",
1851        ] {
1852            let error =
1853                crate::dsl::sdl::parse_sdl(&format!("index test {{ field value: {field} }}"))
1854                    .unwrap_err();
1855            assert!(error.to_string().contains("reorder"), "{error}");
1856        }
1857        for field in [
1858            "text [indexed, reorder]",
1859            "text [indexed<chunked>, reorder]",
1860            "sparse_vector [indexed, reorder]",
1861        ] {
1862            let definitions =
1863                crate::dsl::sdl::parse_sdl(&format!("index test {{ field value: {field} }}"))
1864                    .unwrap();
1865            assert!(definitions[0].to_schema().has_reorder_fields());
1866        }
1867        let mut builder = Schema::builder();
1868        let sparse = builder.add_sparse_vector_field("sparse", true, false);
1869        builder.set_reorder(sparse, true);
1870        let schema = builder.build();
1871        assert!(schema.has_reorder_fields());
1872        assert!(schema.has_background_maintenance_fields());
1873        assert!(schema.validate().is_ok());
1874    }
1875
1876    #[test]
1877    fn json_content_hashes_roundtrip_and_reject_invalid_values() {
1878        for (kind, valid, invalid) in [
1879            ("text", serde_json::json!("hash"), serde_json::json!(17)),
1880            ("u64", serde_json::json!(17), serde_json::json!(-1)),
1881            (
1882                "bytes",
1883                serde_json::json!("AP8="),
1884                serde_json::json!("not base64!"),
1885            ),
1886        ] {
1887            let schema = crate::dsl::sdl::parse_sdl(&format!("index test {{ field id: text [primary, stored] field hash: {kind} [stored, content_hash] }}")).unwrap()[0].to_schema();
1888            let value = serde_json::json!({"id":"a", "hash":valid});
1889            let doc = Document::from_json(&value, &schema).unwrap();
1890            assert_eq!(doc.to_json(&schema), value);
1891            for invalid in [invalid, serde_json::Value::Null, serde_json::json!([valid])] {
1892                assert!(
1893                    Document::from_json(&serde_json::json!({"id":"a", "hash":invalid}), &schema)
1894                        .is_none()
1895                );
1896            }
1897        }
1898    }
1899
1900    #[test]
1901    fn test_schema_builder() {
1902        let mut builder = Schema::builder();
1903        let title = builder.add_text_field("title", true, true);
1904        let body = builder.add_text_field("body", true, false);
1905        let count = builder.add_u64_field("count", true, true);
1906        let schema = builder.build();
1907
1908        assert_eq!(schema.get_field("title"), Some(title));
1909        assert_eq!(schema.get_field("body"), Some(body));
1910        assert_eq!(schema.get_field("count"), Some(count));
1911        assert_eq!(schema.get_field("nonexistent"), None);
1912    }
1913
1914    #[test]
1915    fn ivf_tq_defaults_to_selective_soar() {
1916        for config in [
1917            DenseVectorConfig::new(8),
1918            DenseVectorConfig::ivf_tq(8, Some(4), 2),
1919        ] {
1920            let soar = config.soar.expect("IVF-TQ should enable SOAR by default");
1921            assert_eq!(soar.num_secondary, 1);
1922            assert!(soar.selective);
1923            assert_eq!(soar.calibration_target(), Some(0.30));
1924        }
1925
1926        assert!(DenseVectorConfig::flat(8).soar.is_none());
1927        assert!(DenseVectorConfig::tq(8).soar.is_none());
1928    }
1929
1930    #[test]
1931    fn binary_ivf_uses_measured_balanced_fifteen_million_geometry() {
1932        let config = BinaryDenseVectorConfig::new(2_560);
1933        assert_eq!(config.optimal_num_clusters(15_000_000), 3_873);
1934
1935        let explicit = config.with_ivf(Some(8_192), 128);
1936        assert_eq!(explicit.optimal_num_clusters(15_000_000), 8_192);
1937    }
1938
1939    #[test]
1940    fn target_vectors_sizes_automatic_topology_but_explicit_clusters_win() {
1941        let hinted = BinaryDenseVectorConfig::new(2_560).with_target_vectors(1_000_000_000);
1942        assert_eq!(hinted.optimal_num_clusters(1_000_000), 31_623);
1943
1944        let lower_hint = BinaryDenseVectorConfig::new(2_560).with_target_vectors(1_000_000);
1945        assert_eq!(
1946            lower_hint.optimal_num_clusters(15_000_000),
1947            BinaryDenseVectorConfig::new(2_560).optimal_num_clusters(15_000_000),
1948            "a steady-state hint is a lower bound and must not shrink live-corpus geometry"
1949        );
1950
1951        let explicit = hinted.with_ivf(Some(8_192), 128);
1952        assert_eq!(explicit.optimal_num_clusters(1_000_000), 8_192);
1953
1954        let float = DenseVectorConfig::ivf_tq(1_024, None, 64).with_target_vectors(1_000_000_000);
1955        assert_eq!(float.optimal_num_clusters(1_000_000), 252_982);
1956    }
1957
1958    #[test]
1959    fn persisted_target_vectors_must_be_positive_and_topology_bearing() {
1960        let mut zero = BinaryDenseVectorConfig::new(256);
1961        zero.target_vectors = Some(0);
1962        let mut builder = Schema::builder();
1963        builder.add_binary_dense_vector_field_with_config("hash", true, false, zero);
1964        let error = reject_removed_vector_index_types(&builder.build()).unwrap_err();
1965        assert!(error.contains("positive steady-state"), "{error}");
1966
1967        let hinted = DenseVectorConfig::ivf_tq(128, None, 64).with_target_vectors(1_000_000_000);
1968        let encoded = serde_json::to_value(&hinted).unwrap();
1969        let decoded: DenseVectorConfig = serde_json::from_value(encoded).unwrap();
1970        assert_eq!(decoded.target_vectors, Some(1_000_000_000));
1971
1972        let binary = BinaryDenseVectorConfig::new(2_560).with_target_vectors(1_000_000_000);
1973        let encoded = serde_json::to_value(&binary).unwrap();
1974        let decoded: BinaryDenseVectorConfig = serde_json::from_value(encoded).unwrap();
1975        assert_eq!(decoded.target_vectors, Some(1_000_000_000));
1976
1977        let old_dense: DenseVectorConfig = serde_json::from_value(serde_json::json!({
1978            "dim": 128,
1979            "index_type": "ivf_tq"
1980        }))
1981        .unwrap();
1982        assert_eq!(old_dense.target_vectors, None);
1983        let old_binary: BinaryDenseVectorConfig = serde_json::from_value(serde_json::json!({
1984            "dim": 256,
1985            "index_type": "ivf"
1986        }))
1987        .unwrap();
1988        assert_eq!(old_binary.target_vectors, None);
1989
1990        let flat = DenseVectorConfig::flat(128).with_target_vectors(1_000_000);
1991        let mut builder = Schema::builder();
1992        builder.add_dense_vector_field_with_config("embedding", true, false, flat);
1993        let error = reject_removed_vector_index_types(&builder.build()).unwrap_err();
1994        assert!(error.contains("flat/training-free"), "{error}");
1995
1996        let mut binary_flat = BinaryDenseVectorConfig::new(256);
1997        binary_flat.index_type = BinaryIndexType::Flat;
1998        binary_flat.target_vectors = Some(1_000_000);
1999        let mut builder = Schema::builder();
2000        builder.add_binary_dense_vector_field_with_config("hash", true, false, binary_flat);
2001        let error = reject_removed_vector_index_types(&builder.build()).unwrap_err();
2002        assert!(error.contains("flat/training-free"), "{error}");
2003    }
2004
2005    #[test]
2006    fn omitted_and_explicitly_disabled_soar_are_distinct_in_serde() {
2007        let omitted: DenseVectorConfig = serde_json::from_value(serde_json::json!({
2008            "dim": 8,
2009            "index_type": "ivf_tq"
2010        }))
2011        .unwrap();
2012        let default_soar = omitted
2013            .soar
2014            .as_ref()
2015            .expect("an omitted SOAR setting should enable the selective default");
2016        assert_eq!(default_soar.num_secondary, 1);
2017        assert!(default_soar.selective);
2018        assert_eq!(default_soar.calibration_target(), Some(0.30));
2019
2020        let disabled: DenseVectorConfig = serde_json::from_value(serde_json::json!({
2021            "dim": 8,
2022            "index_type": "ivf_tq",
2023            "soar": null
2024        }))
2025        .unwrap();
2026        assert!(disabled.soar.is_none());
2027
2028        let encoded = serde_json::to_value(&disabled).unwrap();
2029        assert_eq!(encoded.get("soar"), Some(&serde_json::Value::Null));
2030        let round_trip: DenseVectorConfig = serde_json::from_value(encoded).unwrap();
2031        assert!(
2032            round_trip.soar.is_none(),
2033            "explicit off must survive a schema round trip"
2034        );
2035    }
2036
2037    #[test]
2038    fn scann_config_serde_preserves_old_json_defaults_and_new_parameters() {
2039        let old_dense: DenseVectorConfig = serde_json::from_value(serde_json::json!({
2040            "dim": 768,
2041            "index_type": "ivf_tq"
2042        }))
2043        .unwrap();
2044        assert_eq!(old_dense.tree_levels, None);
2045        let old_json = serde_json::to_value(&old_dense).unwrap();
2046        assert!(old_json.get("tree_levels").is_none());
2047
2048        let scann: DenseVectorConfig = serde_json::from_value(serde_json::json!({
2049            "dim": 1024,
2050            "index_type": "scann",
2051            "num_clusters": 10_000_000,
2052            "tree_levels": 2,
2053            "nprobe": 1024
2054        }))
2055        .unwrap();
2056        assert_eq!(scann.index_type, VectorIndexType::Scann);
2057        assert_eq!(scann.tree_levels, Some(2));
2058        assert!(scann.soar.is_none());
2059
2060        let binary: BinaryDenseVectorConfig = serde_json::from_value(serde_json::json!({
2061            "dim": 1024,
2062            "index_type": "scann",
2063            "tree_levels": 3
2064        }))
2065        .unwrap();
2066        assert_eq!(binary.index_type, BinaryIndexType::Scann);
2067        assert_eq!(binary.tree_levels, Some(3));
2068    }
2069
2070    #[test]
2071    fn persisted_scann_geometry_is_validated_on_schema_load() {
2072        let mut invalid_levels = DenseVectorConfig::new(128);
2073        invalid_levels.index_type = VectorIndexType::Scann;
2074        invalid_levels.tree_levels = Some(4);
2075        invalid_levels.soar = None;
2076        let mut builder = Schema::builder();
2077        builder.add_dense_vector_field_with_config("embedding", true, false, invalid_levels);
2078        let error = reject_removed_vector_index_types(&builder.build())
2079            .expect_err("invalid persisted ScaNN levels must fail at the schema gate");
2080        assert!(error.contains("1..=3"), "{error}");
2081
2082        let mut wrong_algorithm = BinaryDenseVectorConfig::new(256);
2083        wrong_algorithm.tree_levels = Some(2);
2084        let mut builder = Schema::builder();
2085        builder.add_binary_dense_vector_field_with_config("hash", true, false, wrong_algorithm);
2086        let error = reject_removed_vector_index_types(&builder.build())
2087            .expect_err("ScaNN-only persisted options must fail on IVF");
2088        assert!(error.contains("does not use the ScaNN index"), "{error}");
2089
2090        let mut invalid_soar = DenseVectorConfig::flat(128);
2091        invalid_soar.index_type = VectorIndexType::Scann;
2092        invalid_soar.nprobe = 1;
2093        invalid_soar.soar = Some(crate::structures::SoarConfig::default());
2094        let mut builder = Schema::builder();
2095        builder.add_dense_vector_field_with_config("embedding", true, false, invalid_soar);
2096        let error = reject_removed_vector_index_types(&builder.build())
2097            .expect_err("persisted ScaNN SOAR must fail until assignments exist");
2098        assert!(error.contains("not implemented"), "{error}");
2099
2100        let mut one_leaf = DenseVectorConfig::flat(128);
2101        one_leaf.index_type = VectorIndexType::Scann;
2102        one_leaf.num_clusters = Some(1);
2103        one_leaf.nprobe = 1;
2104        let mut builder = Schema::builder();
2105        builder.add_dense_vector_field_with_config("embedding", true, false, one_leaf);
2106        let error = reject_removed_vector_index_types(&builder.build())
2107            .expect_err("one-leaf ScaNN geometry must fail at schema load");
2108        assert!(error.contains("2..=30000000"), "{error}");
2109
2110        let binary = BinaryDenseVectorConfig {
2111            dim: 255,
2112            index_type: BinaryIndexType::Scann,
2113            num_clusters: Some(2),
2114            target_vectors: None,
2115            tree_levels: Some(1),
2116            ivf_routing: IvfRoutingMode::Auto,
2117            nprobe: 1,
2118            soar: None,
2119        };
2120        let mut builder = Schema::builder();
2121        builder.add_binary_dense_vector_field_with_config("hash", true, false, binary);
2122        let error = reject_removed_vector_index_types(&builder.build())
2123            .expect_err("binary ScaNN dimensions must be byte-aligned");
2124        assert!(error.contains("multiple of 8"), "{error}");
2125    }
2126
2127    #[test]
2128    fn test_set_primary_key_forces_fast_and_indexed() {
2129        // Regression: the SDL path forces fast + indexed on primary-key fields
2130        // (needed for dedup lookups against the fast-field text dict). The
2131        // programmatic builder must do the same, otherwise committed-key dedup
2132        // is silently inert after every commit.
2133        let mut builder = Schema::builder();
2134        let id = builder.add_text_field("id", false, true);
2135        builder.set_primary_key(id);
2136        let schema = builder.build();
2137
2138        let entry = schema.get_field_entry(id).unwrap();
2139        assert!(entry.primary_key);
2140        assert!(
2141            entry.fast,
2142            "primary key must imply fast (dedup reads the fast-field text dict)"
2143        );
2144        assert!(entry.indexed, "primary key must imply indexed");
2145    }
2146
2147    #[test]
2148    fn test_document() {
2149        let mut builder = Schema::builder();
2150        let title = builder.add_text_field("title", true, true);
2151        let count = builder.add_u64_field("count", true, true);
2152        let _schema = builder.build();
2153
2154        let mut doc = Document::new();
2155        doc.add_text(title, "Hello World");
2156        doc.add_u64(count, 42);
2157
2158        assert_eq!(doc.get_first(title).unwrap().as_text(), Some("Hello World"));
2159        assert_eq!(doc.get_first(count).unwrap().as_u64(), Some(42));
2160    }
2161
2162    #[test]
2163    fn test_document_serialization() {
2164        let mut builder = Schema::builder();
2165        let title = builder.add_text_field("title", true, true);
2166        let count = builder.add_u64_field("count", true, true);
2167        let _schema = builder.build();
2168
2169        let mut doc = Document::new();
2170        doc.add_text(title, "Hello World");
2171        doc.add_u64(count, 42);
2172
2173        // Serialize
2174        let json = serde_json::to_string(&doc).unwrap();
2175        println!("Serialized doc: {}", json);
2176
2177        // Deserialize
2178        let doc2: Document = serde_json::from_str(&json).unwrap();
2179        assert_eq!(
2180            doc2.field_values().len(),
2181            2,
2182            "Should have 2 field values after deserialization"
2183        );
2184        assert_eq!(
2185            doc2.get_first(title).unwrap().as_text(),
2186            Some("Hello World")
2187        );
2188        assert_eq!(doc2.get_first(count).unwrap().as_u64(), Some(42));
2189    }
2190
2191    #[test]
2192    fn test_multivalue_field() {
2193        let mut builder = Schema::builder();
2194        let uris = builder.add_text_field("uris", true, true);
2195        let title = builder.add_text_field("title", true, true);
2196        let schema = builder.build();
2197
2198        // Create document with multiple values for the same field
2199        let mut doc = Document::new();
2200        doc.add_text(uris, "one");
2201        doc.add_text(uris, "two");
2202        doc.add_text(title, "Test Document");
2203
2204        // Verify get_first returns the first value
2205        assert_eq!(doc.get_first(uris).unwrap().as_text(), Some("one"));
2206
2207        // Verify get_all returns all values
2208        let all_uris: Vec<_> = doc.get_all(uris).collect();
2209        assert_eq!(all_uris.len(), 2);
2210        assert_eq!(all_uris[0].as_text(), Some("one"));
2211        assert_eq!(all_uris[1].as_text(), Some("two"));
2212
2213        // Verify to_json returns array for multi-value field
2214        let json = doc.to_json(&schema);
2215        let uris_json = json.get("uris").unwrap();
2216        assert!(uris_json.is_array(), "Multi-value field should be an array");
2217        let uris_arr = uris_json.as_array().unwrap();
2218        assert_eq!(uris_arr.len(), 2);
2219        assert_eq!(uris_arr[0].as_str(), Some("one"));
2220        assert_eq!(uris_arr[1].as_str(), Some("two"));
2221
2222        // Verify single-value field is NOT an array
2223        let title_json = json.get("title").unwrap();
2224        assert!(
2225            title_json.is_string(),
2226            "Single-value field should be a string"
2227        );
2228        assert_eq!(title_json.as_str(), Some("Test Document"));
2229    }
2230
2231    #[test]
2232    fn test_multivalue_from_json() {
2233        let mut builder = Schema::builder();
2234        let uris = builder.add_text_field("uris", true, true);
2235        let title = builder.add_text_field("title", true, true);
2236        let schema = builder.build();
2237
2238        // Create JSON with array value
2239        let json = serde_json::json!({
2240            "uris": ["one", "two"],
2241            "title": "Test Document"
2242        });
2243
2244        // Parse from JSON
2245        let doc = Document::from_json(&json, &schema).unwrap();
2246
2247        // Verify all values are present
2248        let all_uris: Vec<_> = doc.get_all(uris).collect();
2249        assert_eq!(all_uris.len(), 2);
2250        assert_eq!(all_uris[0].as_text(), Some("one"));
2251        assert_eq!(all_uris[1].as_text(), Some("two"));
2252
2253        // Verify single value
2254        assert_eq!(
2255            doc.get_first(title).unwrap().as_text(),
2256            Some("Test Document")
2257        );
2258
2259        // Verify roundtrip: to_json should produce equivalent JSON
2260        let json_out = doc.to_json(&schema);
2261        let uris_out = json_out.get("uris").unwrap().as_array().unwrap();
2262        assert_eq!(uris_out.len(), 2);
2263        assert_eq!(uris_out[0].as_str(), Some("one"));
2264        assert_eq!(uris_out[1].as_str(), Some("two"));
2265    }
2266
2267    #[test]
2268    fn test_multi_attribute_forces_array() {
2269        // Test that fields marked as 'multi' are always serialized as arrays,
2270        // even when they have only one value
2271        let mut builder = Schema::builder();
2272        let uris = builder.add_text_field("uris", true, true);
2273        builder.set_multi(uris, true); // Mark as multi
2274        let title = builder.add_text_field("title", true, true);
2275        let schema = builder.build();
2276
2277        // Verify the multi attribute is set
2278        assert!(schema.get_field_entry(uris).unwrap().multi);
2279        assert!(!schema.get_field_entry(title).unwrap().multi);
2280
2281        // Create document with single value for multi field
2282        let mut doc = Document::new();
2283        doc.add_text(uris, "only_one");
2284        doc.add_text(title, "Test Document");
2285
2286        // Verify to_json returns array for multi field even with single value
2287        let json = doc.to_json(&schema);
2288
2289        let uris_json = json.get("uris").unwrap();
2290        assert!(
2291            uris_json.is_array(),
2292            "Multi field should be array even with single value"
2293        );
2294        let uris_arr = uris_json.as_array().unwrap();
2295        assert_eq!(uris_arr.len(), 1);
2296        assert_eq!(uris_arr[0].as_str(), Some("only_one"));
2297
2298        // Verify non-multi field with single value is NOT an array
2299        let title_json = json.get("title").unwrap();
2300        assert!(
2301            title_json.is_string(),
2302            "Non-multi single-value field should be a string"
2303        );
2304        assert_eq!(title_json.as_str(), Some("Test Document"));
2305    }
2306
2307    #[test]
2308    fn test_sparse_vector_field() {
2309        let mut builder = Schema::builder();
2310        let embedding = builder.add_sparse_vector_field("embedding", true, true);
2311        let title = builder.add_text_field("title", true, true);
2312        let schema = builder.build();
2313
2314        assert_eq!(schema.get_field("embedding"), Some(embedding));
2315        assert_eq!(
2316            schema.get_field_entry(embedding).unwrap().field_type,
2317            FieldType::SparseVector
2318        );
2319
2320        // Create document with sparse vector
2321        let mut doc = Document::new();
2322        doc.add_sparse_vector(embedding, vec![(0, 1.0), (5, 2.5), (10, 0.5)]);
2323        doc.add_text(title, "Test Document");
2324
2325        // Verify accessor
2326        let entries = doc
2327            .get_first(embedding)
2328            .unwrap()
2329            .as_sparse_vector()
2330            .unwrap();
2331        assert_eq!(entries, &[(0, 1.0), (5, 2.5), (10, 0.5)]);
2332
2333        // Verify JSON roundtrip
2334        let json = doc.to_json(&schema);
2335        let embedding_json = json.get("embedding").unwrap();
2336        assert!(embedding_json.is_object());
2337        assert_eq!(
2338            embedding_json
2339                .get("indices")
2340                .unwrap()
2341                .as_array()
2342                .unwrap()
2343                .len(),
2344            3
2345        );
2346
2347        // Parse back from JSON
2348        let doc2 = Document::from_json(&json, &schema).unwrap();
2349        let entries2 = doc2
2350            .get_first(embedding)
2351            .unwrap()
2352            .as_sparse_vector()
2353            .unwrap();
2354        assert_eq!(entries2[0].0, 0);
2355        assert!((entries2[0].1 - 1.0).abs() < 1e-6);
2356        assert_eq!(entries2[1].0, 5);
2357        assert!((entries2[1].1 - 2.5).abs() < 1e-6);
2358        assert_eq!(entries2[2].0, 10);
2359        assert!((entries2[2].1 - 0.5).abs() < 1e-6);
2360    }
2361
2362    #[test]
2363    fn test_json_field() {
2364        let mut builder = Schema::builder();
2365        let metadata = builder.add_json_field("metadata", true);
2366        let title = builder.add_text_field("title", true, true);
2367        let schema = builder.build();
2368
2369        assert_eq!(schema.get_field("metadata"), Some(metadata));
2370        assert_eq!(
2371            schema.get_field_entry(metadata).unwrap().field_type,
2372            FieldType::Json
2373        );
2374        // JSON fields are never indexed
2375        assert!(!schema.get_field_entry(metadata).unwrap().indexed);
2376        assert!(schema.get_field_entry(metadata).unwrap().stored);
2377
2378        // Create document with JSON value (object)
2379        let json_value = serde_json::json!({
2380            "author": "John Doe",
2381            "tags": ["rust", "search"],
2382            "nested": {"key": "value"}
2383        });
2384        let mut doc = Document::new();
2385        doc.add_json(metadata, json_value.clone());
2386        doc.add_text(title, "Test Document");
2387
2388        // Verify accessor
2389        let stored_json = doc.get_first(metadata).unwrap().as_json().unwrap();
2390        assert_eq!(stored_json, &json_value);
2391        assert_eq!(
2392            stored_json.get("author").unwrap().as_str(),
2393            Some("John Doe")
2394        );
2395
2396        // Verify JSON roundtrip via to_json/from_json
2397        let doc_json = doc.to_json(&schema);
2398        let metadata_out = doc_json.get("metadata").unwrap();
2399        assert_eq!(metadata_out, &json_value);
2400
2401        // Parse back from JSON
2402        let doc2 = Document::from_json(&doc_json, &schema).unwrap();
2403        let stored_json2 = doc2.get_first(metadata).unwrap().as_json().unwrap();
2404        assert_eq!(stored_json2, &json_value);
2405    }
2406
2407    #[test]
2408    fn test_json_field_various_types() {
2409        let mut builder = Schema::builder();
2410        let data = builder.add_json_field("data", true);
2411        let _schema = builder.build();
2412
2413        // Test with array
2414        let arr_value = serde_json::json!([1, 2, 3, "four", null]);
2415        let mut doc = Document::new();
2416        doc.add_json(data, arr_value.clone());
2417        assert_eq!(doc.get_first(data).unwrap().as_json().unwrap(), &arr_value);
2418
2419        // Test with string
2420        let str_value = serde_json::json!("just a string");
2421        let mut doc2 = Document::new();
2422        doc2.add_json(data, str_value.clone());
2423        assert_eq!(doc2.get_first(data).unwrap().as_json().unwrap(), &str_value);
2424
2425        // Test with number
2426        let num_value = serde_json::json!(42.5);
2427        let mut doc3 = Document::new();
2428        doc3.add_json(data, num_value.clone());
2429        assert_eq!(doc3.get_first(data).unwrap().as_json().unwrap(), &num_value);
2430
2431        // Test with null
2432        let null_value = serde_json::Value::Null;
2433        let mut doc4 = Document::new();
2434        doc4.add_json(data, null_value.clone());
2435        assert_eq!(
2436            doc4.get_first(data).unwrap().as_json().unwrap(),
2437            &null_value
2438        );
2439
2440        // Test with boolean
2441        let bool_value = serde_json::json!(true);
2442        let mut doc5 = Document::new();
2443        doc5.add_json(data, bool_value.clone());
2444        assert_eq!(
2445            doc5.get_first(data).unwrap().as_json().unwrap(),
2446            &bool_value
2447        );
2448    }
2449}