Skip to main content

hermes_core/dsl/
schema.rs

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