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