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