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 using RaBitQ binary quantization for ANN search
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
80/// Position tracking mode for text fields
81#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
82#[serde(rename_all = "snake_case")]
83pub enum PositionMode {
84    /// Track only element ordinal for multi-valued fields (which array element)
85    /// Useful for returning which element matched without full phrase query support
86    Ordinal,
87    /// Track only token position within text (for phrase queries)
88    /// Does not track element ordinal - all positions are relative to concatenated text
89    TokenPosition,
90    /// Track both element ordinal and token position (full support)
91    /// Position format: (element_ordinal << 20) | token_position
92    Full,
93}
94
95impl PositionMode {
96    /// Whether this mode tracks element ordinals
97    pub fn tracks_ordinal(&self) -> bool {
98        matches!(self, PositionMode::Ordinal | PositionMode::Full)
99    }
100
101    /// Whether this mode tracks token positions
102    pub fn tracks_token_position(&self) -> bool {
103        matches!(self, PositionMode::TokenPosition | PositionMode::Full)
104    }
105}
106
107/// Vector index algorithm type
108#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
109#[serde(rename_all = "snake_case")]
110pub enum VectorIndexType {
111    /// Flat - brute-force search over raw vectors (accumulating state)
112    Flat,
113    /// RaBitQ - binary quantization, good for small datasets (<100K)
114    #[default]
115    RaBitQ,
116    /// IVF-RaBitQ - inverted file with RaBitQ, good for medium datasets
117    IvfRaBitQ,
118    /// ScaNN - product quantization with OPQ and anisotropic loss, best for large datasets
119    ScaNN,
120}
121
122/// Storage quantization for dense vector elements
123///
124/// Controls the precision of each vector coordinate in `.vectors` files.
125/// Lower precision reduces storage and memory bandwidth; scoring uses
126/// native-precision SIMD (no dequantization on the hot path).
127#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
128#[serde(rename_all = "snake_case")]
129pub enum DenseVectorQuantization {
130    /// 32-bit IEEE 754 float (4 bytes/dim) — full precision, baseline
131    #[default]
132    F32,
133    /// 16-bit IEEE 754 half-float (2 bytes/dim) — <0.1% recall loss for normalized embeddings
134    F16,
135    /// 8-bit unsigned scalar quantization (1 byte/dim) — maps [-1,1] → [0,255]
136    UInt8,
137    /// Binary packed-bit storage (1 bit per dimension, ceil(dim/8) bytes per vector).
138    /// Used internally by BinaryDenseVector fields. Not selectable for DenseVector fields.
139    Binary,
140}
141
142impl DenseVectorQuantization {
143    /// Bytes per element for non-binary quantization types.
144    /// Panics for Binary — use `dim.div_ceil(8)` for binary vector byte size.
145    pub fn element_size(self) -> usize {
146        match self {
147            Self::F32 => 4,
148            Self::F16 => 2,
149            Self::UInt8 => 1,
150            Self::Binary => panic!("element_size() not valid for Binary; use dim.div_ceil(8)"),
151        }
152    }
153
154    /// Wire format tag (stored in .vectors header)
155    pub fn tag(self) -> u8 {
156        match self {
157            Self::F32 => 0,
158            Self::F16 => 1,
159            Self::UInt8 => 2,
160            Self::Binary => 3,
161        }
162    }
163
164    /// Decode wire format tag
165    pub fn from_tag(tag: u8) -> Option<Self> {
166        match tag {
167            0 => Some(Self::F32),
168            1 => Some(Self::F16),
169            2 => Some(Self::UInt8),
170            3 => Some(Self::Binary),
171            _ => None,
172        }
173    }
174}
175
176/// Configuration for dense vector fields using Flat, RaBitQ, IVF-RaBitQ, or ScaNN
177///
178/// Indexes operate in two states:
179/// - **Flat (accumulating)**: Brute-force search over raw vectors. Used when vector count
180///   is below `build_threshold` or before `build_index` is called.
181/// - **Built (ANN)**: Fast approximate nearest neighbor search using trained structures.
182///   Centroids and codebooks are trained from data and stored within the segment.
183#[derive(Debug, Clone, Serialize, Deserialize)]
184pub struct DenseVectorConfig {
185    /// Dimensionality of vectors
186    pub dim: usize,
187    /// Target vector index algorithm (Flat, RaBitQ, IVF-RaBitQ, or ScaNN)
188    /// When in accumulating state, search uses brute-force regardless of this setting.
189    #[serde(default)]
190    pub index_type: VectorIndexType,
191    /// Storage quantization for vector elements (f32, f16, uint8)
192    #[serde(default)]
193    pub quantization: DenseVectorQuantization,
194    /// Number of IVF clusters for IVF-RaBitQ and ScaNN (default: sqrt(n) capped at 4096)
195    /// If None, automatically determined based on dataset size.
196    #[serde(default, skip_serializing_if = "Option::is_none")]
197    pub num_clusters: Option<usize>,
198    /// Number of clusters to probe during search (default: 32)
199    #[serde(default = "default_nprobe")]
200    pub nprobe: usize,
201    /// Minimum number of vectors required before building ANN index.
202    /// Below this threshold, brute-force (Flat) search is used.
203    /// Default: 1000 for RaBitQ, 10000 for IVF-RaBitQ/ScaNN.
204    #[serde(default, skip_serializing_if = "Option::is_none")]
205    pub build_threshold: Option<usize>,
206    /// Whether stored vectors are pre-normalized to unit L2 norm.
207    /// When true, scoring skips per-vector norm computation (cosine = dot / ||q||),
208    /// reducing compute by ~40%. Common for embedding models (e.g. OpenAI, Cohere).
209    /// Default: true (most embedding models produce L2-normalized vectors).
210    #[serde(default = "default_unit_norm")]
211    pub unit_norm: bool,
212    /// Total RaBitQ bits per dimension for IVF-RaBitQ indexes.
213    /// 1 = classic binary RaBitQ (default). 2-8 = extended multi-bit codes
214    /// with much tighter distance estimates — allows lowering rerank_factor
215    /// (fewer raw-vector reads) at the same recall. Recommended: 4-5 for
216    /// disk-resident indexes.
217    #[serde(default, skip_serializing_if = "Option::is_none")]
218    pub rabitq_bits: Option<u8>,
219    /// SOAR spilled cluster assignments for IVF-based indexes (IVF-RaBitQ, ScaNN).
220    /// Assigns vectors to a secondary cluster with an orthogonality-amplified
221    /// residual, improving recall at the same nprobe for ~1.2-2x assignment storage.
222    /// Default: None (disabled). Ignored for Flat/RaBitQ index types.
223    #[serde(default, skip_serializing_if = "Option::is_none")]
224    pub soar: Option<crate::structures::SoarConfig>,
225}
226
227fn default_nprobe() -> usize {
228    32
229}
230
231fn default_unit_norm() -> bool {
232    true
233}
234
235impl DenseVectorConfig {
236    pub fn new(dim: usize) -> Self {
237        Self {
238            dim,
239            index_type: VectorIndexType::RaBitQ,
240            quantization: DenseVectorQuantization::F32,
241            num_clusters: None,
242            nprobe: 32,
243            build_threshold: None,
244            unit_norm: true,
245            soar: None,
246            rabitq_bits: None,
247        }
248    }
249
250    /// Create IVF-RaBitQ configuration
251    pub fn with_ivf(dim: usize, num_clusters: Option<usize>, nprobe: usize) -> Self {
252        Self {
253            dim,
254            index_type: VectorIndexType::IvfRaBitQ,
255            quantization: DenseVectorQuantization::F32,
256            num_clusters,
257            nprobe,
258            build_threshold: None,
259            unit_norm: true,
260            soar: None,
261            rabitq_bits: None,
262        }
263    }
264
265    /// Create ScaNN configuration
266    pub fn with_scann(dim: usize, num_clusters: Option<usize>, nprobe: usize) -> Self {
267        Self {
268            dim,
269            index_type: VectorIndexType::ScaNN,
270            quantization: DenseVectorQuantization::F32,
271            num_clusters,
272            nprobe,
273            build_threshold: None,
274            unit_norm: true,
275            soar: None,
276            rabitq_bits: None,
277        }
278    }
279
280    /// Create Flat (brute-force) configuration - no ANN index
281    pub fn flat(dim: usize) -> Self {
282        Self {
283            dim,
284            index_type: VectorIndexType::Flat,
285            quantization: DenseVectorQuantization::F32,
286            num_clusters: None,
287            nprobe: 0,
288            build_threshold: None,
289            unit_norm: true,
290            soar: None,
291            rabitq_bits: None,
292        }
293    }
294
295    /// Set storage quantization
296    pub fn with_quantization(mut self, quantization: DenseVectorQuantization) -> Self {
297        self.quantization = quantization;
298        self
299    }
300
301    /// Set build threshold for auto-building ANN index
302    pub fn with_build_threshold(mut self, threshold: usize) -> Self {
303        self.build_threshold = Some(threshold);
304        self
305    }
306
307    /// Mark vectors as pre-normalized to unit L2 norm
308    pub fn with_unit_norm(mut self) -> Self {
309        self.unit_norm = true;
310        self
311    }
312
313    /// Set number of IVF clusters
314    pub fn with_num_clusters(mut self, num_clusters: usize) -> Self {
315        self.num_clusters = Some(num_clusters);
316        self
317    }
318
319    /// Set RaBitQ total bits per dimension (1 = classic, 2-8 = extended)
320    pub fn with_rabitq_bits(mut self, bits: u8) -> Self {
321        self.rabitq_bits = Some(bits.clamp(1, 8));
322        self
323    }
324
325    /// Enable SOAR spilled secondary cluster assignments (IVF-based indexes only)
326    pub fn with_soar(mut self, soar: crate::structures::SoarConfig) -> Self {
327        self.soar = Some(soar);
328        self
329    }
330
331    /// Check if this config uses IVF
332    pub fn uses_ivf(&self) -> bool {
333        matches!(
334            self.index_type,
335            VectorIndexType::IvfRaBitQ | VectorIndexType::ScaNN
336        )
337    }
338
339    /// Check if this config uses ScaNN
340    pub fn uses_scann(&self) -> bool {
341        self.index_type == VectorIndexType::ScaNN
342    }
343
344    /// Check if this config is flat (brute-force)
345    pub fn is_flat(&self) -> bool {
346        self.index_type == VectorIndexType::Flat
347    }
348
349    /// Get the default build threshold for this index type
350    pub fn default_build_threshold(&self) -> usize {
351        self.build_threshold.unwrap_or(match self.index_type {
352            VectorIndexType::Flat => usize::MAX, // Never auto-build
353            VectorIndexType::RaBitQ => 1000,
354            VectorIndexType::IvfRaBitQ | VectorIndexType::ScaNN => 10000,
355        })
356    }
357
358    /// Calculate optimal number of clusters for given vector count
359    pub fn optimal_num_clusters(&self, num_vectors: usize) -> usize {
360        self.num_clusters.unwrap_or_else(|| {
361            // sqrt(n) heuristic, capped at 4096
362            let optimal = (num_vectors as f64).sqrt() as usize;
363            optimal.clamp(16, 4096)
364        })
365    }
366}
367
368/// Configuration for binary dense vector fields
369///
370/// Binary dense vectors store packed bits (1 bit per dimension) and use
371/// Hamming distance for scoring. Always uses brute-force flat search
372/// (Hamming popcount is ~10ns/vec for 768-bit, ANN indexes don't help).
373#[derive(Debug, Clone, Serialize, Deserialize)]
374pub struct BinaryDenseVectorConfig {
375    /// Number of bits (dimensions). Storage is ceil(dim/8) bytes per vector.
376    pub dim: usize,
377    /// ANN index type: Flat (brute-force SIMD Hamming, default) or Ivf
378    /// (k-majority Hamming clusters — probe `nprobe` clusters at query time).
379    /// IVF pays off for segments past a few million vectors.
380    #[serde(default)]
381    pub index_type: BinaryIndexType,
382    /// Number of IVF clusters (default: sqrt(n) capped at 4096)
383    #[serde(default, skip_serializing_if = "Option::is_none")]
384    pub num_clusters: Option<usize>,
385    /// Clusters to probe during search (default: 32)
386    #[serde(default = "default_nprobe")]
387    pub nprobe: usize,
388    /// Minimum vectors before building the IVF index (default: 100_000 —
389    /// below that brute-force SIMD Hamming is faster than probing).
390    #[serde(default, skip_serializing_if = "Option::is_none")]
391    pub build_threshold: Option<usize>,
392}
393
394/// ANN index type for binary dense vector fields
395#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
396#[serde(rename_all = "snake_case")]
397pub enum BinaryIndexType {
398    /// Brute-force SIMD Hamming scan (default)
399    #[default]
400    Flat,
401    /// IVF with k-majority Hamming clustering
402    Ivf,
403}
404
405impl BinaryDenseVectorConfig {
406    pub fn new(dim: usize) -> Self {
407        assert!(
408            dim.is_multiple_of(8),
409            "BinaryDenseVector dimension must be a multiple of 8, got {dim}"
410        );
411        Self {
412            dim,
413            index_type: BinaryIndexType::Flat,
414            num_clusters: None,
415            nprobe: 32,
416            build_threshold: None,
417        }
418    }
419
420    /// Enable the IVF index (builder pattern)
421    pub fn with_ivf(mut self, num_clusters: Option<usize>, nprobe: usize) -> Self {
422        self.index_type = BinaryIndexType::Ivf;
423        self.num_clusters = num_clusters;
424        self.nprobe = nprobe;
425        self
426    }
427
428    /// Set the build threshold (builder pattern)
429    pub fn with_build_threshold(mut self, threshold: usize) -> Self {
430        self.build_threshold = Some(threshold);
431        self
432    }
433
434    /// Default build threshold: brute-force wins below ~100K vectors.
435    pub fn default_build_threshold(&self) -> usize {
436        self.build_threshold.unwrap_or(100_000)
437    }
438
439    /// Optimal cluster count for a given vector count (sqrt(n), capped)
440    pub fn optimal_num_clusters(&self, num_vectors: usize) -> usize {
441        self.num_clusters.unwrap_or_else(|| {
442            let optimal = (num_vectors as f64).sqrt() as usize;
443            optimal.clamp(16, 4096)
444        })
445    }
446
447    /// Number of bytes needed to store one vector
448    pub fn byte_len(&self) -> usize {
449        self.dim.div_ceil(8)
450    }
451}
452
453use super::query_field_router::QueryRouterRule;
454
455/// Schema defining document structure
456#[derive(Debug, Clone, Default, Serialize, Deserialize)]
457pub struct Schema {
458    fields: Vec<FieldEntry>,
459    name_to_field: HashMap<String, Field>,
460    /// Default fields for query parsing (when no field is specified)
461    #[serde(default)]
462    default_fields: Vec<Field>,
463    /// Query router rules for routing queries to specific fields based on regex patterns
464    #[serde(default)]
465    query_routers: Vec<QueryRouterRule>,
466    /// Run BP (graph bisection) reordering of `reorder`-attributed BMP fields
467    /// inside segment merges. SDL: `reorder_on_merge: true` at index level.
468    /// Absent = disabled (merges block-copy; the standalone reorder pass
469    /// handles ordering).
470    #[serde(default)]
471    reorder_on_merge: bool,
472    /// Index name used as the `index` label on metrics. Set from the SDL
473    /// index name at parse time and overridden with the registry name at
474    /// server-side index creation. Empty on old metadata → "unknown".
475    #[serde(default)]
476    index_name: String,
477}
478
479impl Schema {
480    pub fn builder() -> SchemaBuilder {
481        SchemaBuilder::default()
482    }
483
484    pub fn get_field(&self, name: &str) -> Option<Field> {
485        self.name_to_field.get(name).copied()
486    }
487
488    pub fn get_field_entry(&self, field: Field) -> Option<&FieldEntry> {
489        self.fields.get(field.0 as usize)
490    }
491
492    pub fn get_field_name(&self, field: Field) -> Option<&str> {
493        self.fields.get(field.0 as usize).map(|e| e.name.as_str())
494    }
495
496    pub fn fields(&self) -> impl Iterator<Item = (Field, &FieldEntry)> {
497        self.fields
498            .iter()
499            .enumerate()
500            .map(|(i, e)| (Field(i as u32), e))
501    }
502
503    pub fn num_fields(&self) -> usize {
504        self.fields.len()
505    }
506
507    /// Whether any field has the `reorder` attribute set.
508    /// Used by the background optimizer to determine which indexes need BP reordering.
509    pub fn has_reorder_fields(&self) -> bool {
510        self.fields.iter().any(|e| e.reorder)
511    }
512
513    /// Whether merges BP-reorder `reorder`-attributed BMP fields while writing
514    /// the merged segment (index-level SDL option `reorder_on_merge: true`).
515    pub fn reorder_on_merge(&self) -> bool {
516        self.reorder_on_merge
517    }
518
519    /// Index name for metric labels ("unknown" when not set — pre-existing
520    /// metadata or programmatic schemas without a name).
521    pub fn index_label(&self) -> &str {
522        if self.index_name.is_empty() {
523            "unknown"
524        } else {
525            &self.index_name
526        }
527    }
528
529    /// Set the index name used as the metrics `index` label.
530    pub fn set_index_name(&mut self, name: impl Into<String>) {
531        self.index_name = name.into();
532    }
533
534    /// Get the default fields for query parsing
535    pub fn default_fields(&self) -> &[Field] {
536        &self.default_fields
537    }
538
539    /// Set default fields (used by builder)
540    pub fn set_default_fields(&mut self, fields: Vec<Field>) {
541        self.default_fields = fields;
542    }
543
544    /// Get the query router rules
545    pub fn query_routers(&self) -> &[QueryRouterRule] {
546        &self.query_routers
547    }
548
549    /// Set query router rules
550    pub fn set_query_routers(&mut self, rules: Vec<QueryRouterRule>) {
551        self.query_routers = rules;
552    }
553
554    /// Get the primary key field, if one is defined
555    pub fn primary_field(&self) -> Option<Field> {
556        self.fields
557            .iter()
558            .enumerate()
559            .find(|(_, e)| e.primary_key)
560            .map(|(i, _)| Field(i as u32))
561    }
562}
563
564/// Builder for Schema
565#[derive(Debug, Default)]
566pub struct SchemaBuilder {
567    fields: Vec<FieldEntry>,
568    default_fields: Vec<String>,
569    query_routers: Vec<QueryRouterRule>,
570    reorder_on_merge: bool,
571    index_name: String,
572}
573
574impl SchemaBuilder {
575    pub fn add_text_field(&mut self, name: &str, indexed: bool, stored: bool) -> Field {
576        self.add_field_with_tokenizer(
577            name,
578            FieldType::Text,
579            indexed,
580            stored,
581            Some("simple".to_string()),
582        )
583    }
584
585    pub fn add_text_field_with_tokenizer(
586        &mut self,
587        name: &str,
588        indexed: bool,
589        stored: bool,
590        tokenizer: &str,
591    ) -> Field {
592        self.add_field_with_tokenizer(
593            name,
594            FieldType::Text,
595            indexed,
596            stored,
597            Some(tokenizer.to_string()),
598        )
599    }
600
601    pub fn add_u64_field(&mut self, name: &str, indexed: bool, stored: bool) -> Field {
602        self.add_field(name, FieldType::U64, indexed, stored)
603    }
604
605    pub fn add_i64_field(&mut self, name: &str, indexed: bool, stored: bool) -> Field {
606        self.add_field(name, FieldType::I64, indexed, stored)
607    }
608
609    pub fn add_f64_field(&mut self, name: &str, indexed: bool, stored: bool) -> Field {
610        self.add_field(name, FieldType::F64, indexed, stored)
611    }
612
613    pub fn add_bytes_field(&mut self, name: &str, stored: bool) -> Field {
614        self.add_field(name, FieldType::Bytes, false, stored)
615    }
616
617    /// Add a JSON field for storing arbitrary JSON data
618    ///
619    /// JSON fields are never indexed, only stored. They can hold any valid JSON value
620    /// (objects, arrays, strings, numbers, booleans, null).
621    pub fn add_json_field(&mut self, name: &str, stored: bool) -> Field {
622        self.add_field(name, FieldType::Json, false, stored)
623    }
624
625    /// Add a sparse vector field with default configuration
626    ///
627    /// Sparse vectors are indexed as inverted posting lists where each dimension
628    /// becomes a "term" and documents have quantized weights for each dimension.
629    pub fn add_sparse_vector_field(&mut self, name: &str, indexed: bool, stored: bool) -> Field {
630        self.add_sparse_vector_field_with_config(
631            name,
632            indexed,
633            stored,
634            crate::structures::SparseVectorConfig::default(),
635        )
636    }
637
638    /// Add a sparse vector field with custom configuration
639    ///
640    /// Use `SparseVectorConfig::splade()` for SPLADE models (u16 indices, uint8 weights).
641    /// Use `SparseVectorConfig::compact()` for maximum compression (u16 indices, uint4 weights).
642    pub fn add_sparse_vector_field_with_config(
643        &mut self,
644        name: &str,
645        indexed: bool,
646        stored: bool,
647        config: crate::structures::SparseVectorConfig,
648    ) -> Field {
649        let field = Field(self.fields.len() as u32);
650        self.fields.push(FieldEntry {
651            name: name.to_string(),
652            field_type: FieldType::SparseVector,
653            indexed,
654            stored,
655            tokenizer: None,
656            multi: false,
657            positions: None,
658            sparse_vector_config: Some(config),
659            dense_vector_config: None,
660            binary_dense_vector_config: None,
661            fast: false,
662            primary_key: false,
663            reorder: false,
664        });
665        field
666    }
667
668    /// Set sparse vector configuration for an existing field
669    pub fn set_sparse_vector_config(
670        &mut self,
671        field: Field,
672        config: crate::structures::SparseVectorConfig,
673    ) {
674        if let Some(entry) = self.fields.get_mut(field.0 as usize) {
675            entry.sparse_vector_config = Some(config);
676        }
677    }
678
679    /// Add a dense vector field with default configuration
680    ///
681    /// Dense vectors are indexed using RaBitQ binary quantization for fast ANN search.
682    /// The dimension must be specified as it determines the quantization structure.
683    pub fn add_dense_vector_field(
684        &mut self,
685        name: &str,
686        dim: usize,
687        indexed: bool,
688        stored: bool,
689    ) -> Field {
690        self.add_dense_vector_field_with_config(name, indexed, stored, DenseVectorConfig::new(dim))
691    }
692
693    /// Add a dense vector field with custom configuration
694    pub fn add_dense_vector_field_with_config(
695        &mut self,
696        name: &str,
697        indexed: bool,
698        stored: bool,
699        config: DenseVectorConfig,
700    ) -> Field {
701        let field = Field(self.fields.len() as u32);
702        self.fields.push(FieldEntry {
703            name: name.to_string(),
704            field_type: FieldType::DenseVector,
705            indexed,
706            stored,
707            tokenizer: None,
708            multi: false,
709            positions: None,
710            sparse_vector_config: None,
711            dense_vector_config: Some(config),
712            binary_dense_vector_config: None,
713            fast: false,
714            primary_key: false,
715            reorder: false,
716        });
717        field
718    }
719
720    /// Add a binary dense vector field
721    ///
722    /// Binary dense vectors use packed-bit storage (1 bit per dimension)
723    /// and Hamming distance scoring. Always brute-force flat search.
724    pub fn add_binary_dense_vector_field(
725        &mut self,
726        name: &str,
727        dim: usize,
728        indexed: bool,
729        stored: bool,
730    ) -> Field {
731        self.add_binary_dense_vector_field_with_config(
732            name,
733            indexed,
734            stored,
735            BinaryDenseVectorConfig::new(dim),
736        )
737    }
738
739    /// Add a binary dense vector field with custom configuration
740    pub fn add_binary_dense_vector_field_with_config(
741        &mut self,
742        name: &str,
743        indexed: bool,
744        stored: bool,
745        config: BinaryDenseVectorConfig,
746    ) -> Field {
747        let field = Field(self.fields.len() as u32);
748        self.fields.push(FieldEntry {
749            name: name.to_string(),
750            field_type: FieldType::BinaryDenseVector,
751            indexed,
752            stored,
753            tokenizer: None,
754            multi: false,
755            positions: None,
756            sparse_vector_config: None,
757            dense_vector_config: None,
758            binary_dense_vector_config: Some(config),
759            fast: false,
760            primary_key: false,
761            reorder: false,
762        });
763        field
764    }
765
766    fn add_field(
767        &mut self,
768        name: &str,
769        field_type: FieldType,
770        indexed: bool,
771        stored: bool,
772    ) -> Field {
773        self.add_field_with_tokenizer(name, field_type, indexed, stored, None)
774    }
775
776    fn add_field_with_tokenizer(
777        &mut self,
778        name: &str,
779        field_type: FieldType,
780        indexed: bool,
781        stored: bool,
782        tokenizer: Option<String>,
783    ) -> Field {
784        self.add_field_full(name, field_type, indexed, stored, tokenizer, false)
785    }
786
787    fn add_field_full(
788        &mut self,
789        name: &str,
790        field_type: FieldType,
791        indexed: bool,
792        stored: bool,
793        tokenizer: Option<String>,
794        multi: bool,
795    ) -> Field {
796        let field = Field(self.fields.len() as u32);
797        self.fields.push(FieldEntry {
798            name: name.to_string(),
799            field_type,
800            indexed,
801            stored,
802            tokenizer,
803            multi,
804            positions: None,
805            sparse_vector_config: None,
806            dense_vector_config: None,
807            binary_dense_vector_config: None,
808            fast: false,
809            primary_key: false,
810            reorder: false,
811        });
812        field
813    }
814
815    /// Set the multi attribute on the last added field
816    pub fn set_multi(&mut self, field: Field, multi: bool) {
817        if let Some(entry) = self.fields.get_mut(field.0 as usize) {
818            entry.multi = multi;
819        }
820    }
821
822    /// Set fast-field columnar storage for O(1) doc→value access.
823    /// Valid for u64, i64, f64, and text fields.
824    pub fn set_fast(&mut self, field: Field, fast: bool) {
825        if let Some(entry) = self.fields.get_mut(field.0 as usize) {
826            entry.fast = fast;
827        }
828    }
829
830    /// Mark a field as the primary key (unique constraint).
831    ///
832    /// Primary key implies fast + indexed (dedup looks committed keys up in
833    /// the fast-field text dictionary) — kept in sync with the SDL path,
834    /// which forces the same attributes.
835    pub fn set_primary_key(&mut self, field: Field) {
836        if let Some(entry) = self.fields.get_mut(field.0 as usize) {
837            entry.primary_key = true;
838            entry.fast = true;
839            entry.indexed = true;
840        }
841    }
842
843    /// Enable build-time document reordering (Recursive Graph Bisection) for BMP fields
844    pub fn set_reorder(&mut self, field: Field, reorder: bool) {
845        if let Some(entry) = self.fields.get_mut(field.0 as usize) {
846            entry.reorder = reorder;
847        }
848    }
849
850    /// Enable BP reordering of `reorder`-attributed BMP fields inside merges
851    /// (index-level; SDL `reorder_on_merge: true`). Default: disabled.
852    pub fn set_reorder_on_merge(&mut self, on: bool) {
853        self.reorder_on_merge = on;
854    }
855
856    /// Set the index name used as the metrics `index` label.
857    pub fn set_index_name(&mut self, name: impl Into<String>) {
858        self.index_name = name.into();
859    }
860
861    /// Set position tracking mode for phrase queries and multi-field element tracking
862    pub fn set_positions(&mut self, field: Field, mode: PositionMode) {
863        if let Some(entry) = self.fields.get_mut(field.0 as usize) {
864            entry.positions = Some(mode);
865        }
866    }
867
868    /// Set default fields by name
869    pub fn set_default_fields(&mut self, field_names: Vec<String>) {
870        self.default_fields = field_names;
871    }
872
873    /// Set query router rules
874    pub fn set_query_routers(&mut self, rules: Vec<QueryRouterRule>) {
875        self.query_routers = rules;
876    }
877
878    pub fn build(self) -> Schema {
879        let mut name_to_field = HashMap::new();
880        for (i, entry) in self.fields.iter().enumerate() {
881            name_to_field.insert(entry.name.clone(), Field(i as u32));
882        }
883
884        // Resolve default field names to Field IDs
885        let default_fields: Vec<Field> = self
886            .default_fields
887            .iter()
888            .filter_map(|name| name_to_field.get(name).copied())
889            .collect();
890
891        Schema {
892            fields: self.fields,
893            name_to_field,
894            default_fields,
895            query_routers: self.query_routers,
896            reorder_on_merge: self.reorder_on_merge,
897            index_name: self.index_name,
898        }
899    }
900}
901
902/// Value that can be stored in a field
903#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
904pub enum FieldValue {
905    #[serde(rename = "text")]
906    Text(String),
907    #[serde(rename = "u64")]
908    U64(u64),
909    #[serde(rename = "i64")]
910    I64(i64),
911    #[serde(rename = "f64")]
912    F64(f64),
913    #[serde(rename = "bytes")]
914    Bytes(Vec<u8>),
915    /// Sparse vector: list of (dimension_id, weight) pairs
916    #[serde(rename = "sparse_vector")]
917    SparseVector(Vec<(u32, f32)>),
918    /// Dense vector: float32 values
919    #[serde(rename = "dense_vector")]
920    DenseVector(Vec<f32>),
921    /// Arbitrary JSON value
922    #[serde(rename = "json")]
923    Json(serde_json::Value),
924    /// Binary dense vector: packed bits (ceil(dim/8) bytes)
925    #[serde(rename = "binary_dense_vector")]
926    BinaryDenseVector(Vec<u8>),
927}
928
929impl FieldValue {
930    pub fn as_text(&self) -> Option<&str> {
931        match self {
932            FieldValue::Text(s) => Some(s),
933            _ => None,
934        }
935    }
936
937    pub fn as_u64(&self) -> Option<u64> {
938        match self {
939            FieldValue::U64(v) => Some(*v),
940            _ => None,
941        }
942    }
943
944    pub fn as_i64(&self) -> Option<i64> {
945        match self {
946            FieldValue::I64(v) => Some(*v),
947            _ => None,
948        }
949    }
950
951    pub fn as_f64(&self) -> Option<f64> {
952        match self {
953            FieldValue::F64(v) => Some(*v),
954            _ => None,
955        }
956    }
957
958    pub fn as_bytes(&self) -> Option<&[u8]> {
959        match self {
960            FieldValue::Bytes(b) => Some(b),
961            _ => None,
962        }
963    }
964
965    pub fn as_sparse_vector(&self) -> Option<&[(u32, f32)]> {
966        match self {
967            FieldValue::SparseVector(entries) => Some(entries),
968            _ => None,
969        }
970    }
971
972    pub fn as_dense_vector(&self) -> Option<&[f32]> {
973        match self {
974            FieldValue::DenseVector(v) => Some(v),
975            _ => None,
976        }
977    }
978
979    pub fn as_json(&self) -> Option<&serde_json::Value> {
980        match self {
981            FieldValue::Json(v) => Some(v),
982            _ => None,
983        }
984    }
985
986    pub fn as_binary_dense_vector(&self) -> Option<&[u8]> {
987        match self {
988            FieldValue::BinaryDenseVector(v) => Some(v),
989            _ => None,
990        }
991    }
992}
993
994/// A document to be indexed
995#[derive(Debug, Clone, Default, Serialize, Deserialize)]
996pub struct Document {
997    field_values: Vec<(Field, FieldValue)>,
998}
999
1000impl Document {
1001    pub fn new() -> Self {
1002        Self::default()
1003    }
1004
1005    pub fn add_text(&mut self, field: Field, value: impl Into<String>) {
1006        self.field_values
1007            .push((field, FieldValue::Text(value.into())));
1008    }
1009
1010    pub fn add_u64(&mut self, field: Field, value: u64) {
1011        self.field_values.push((field, FieldValue::U64(value)));
1012    }
1013
1014    pub fn add_i64(&mut self, field: Field, value: i64) {
1015        self.field_values.push((field, FieldValue::I64(value)));
1016    }
1017
1018    pub fn add_f64(&mut self, field: Field, value: f64) {
1019        self.field_values.push((field, FieldValue::F64(value)));
1020    }
1021
1022    pub fn add_bytes(&mut self, field: Field, value: Vec<u8>) {
1023        self.field_values.push((field, FieldValue::Bytes(value)));
1024    }
1025
1026    pub fn add_sparse_vector(&mut self, field: Field, entries: Vec<(u32, f32)>) {
1027        self.field_values
1028            .push((field, FieldValue::SparseVector(entries)));
1029    }
1030
1031    pub fn add_dense_vector(&mut self, field: Field, values: Vec<f32>) {
1032        self.field_values
1033            .push((field, FieldValue::DenseVector(values)));
1034    }
1035
1036    pub fn add_json(&mut self, field: Field, value: serde_json::Value) {
1037        self.field_values.push((field, FieldValue::Json(value)));
1038    }
1039
1040    pub fn add_binary_dense_vector(&mut self, field: Field, values: Vec<u8>) {
1041        self.field_values
1042            .push((field, FieldValue::BinaryDenseVector(values)));
1043    }
1044
1045    pub fn get_first(&self, field: Field) -> Option<&FieldValue> {
1046        self.field_values
1047            .iter()
1048            .find(|(f, _)| *f == field)
1049            .map(|(_, v)| v)
1050    }
1051
1052    pub fn get_all(&self, field: Field) -> impl Iterator<Item = &FieldValue> {
1053        self.field_values
1054            .iter()
1055            .filter(move |(f, _)| *f == field)
1056            .map(|(_, v)| v)
1057    }
1058
1059    pub fn field_values(&self) -> &[(Field, FieldValue)] {
1060        &self.field_values
1061    }
1062
1063    /// Return a new Document containing only fields marked as `stored` in the schema
1064    pub fn filter_stored(&self, schema: &Schema) -> Document {
1065        Document {
1066            field_values: self
1067                .field_values
1068                .iter()
1069                .filter(|(field, _)| {
1070                    schema
1071                        .get_field_entry(*field)
1072                        .is_some_and(|entry| entry.stored)
1073                })
1074                .cloned()
1075                .collect(),
1076        }
1077    }
1078
1079    /// Convert document to a JSON object using field names from schema
1080    ///
1081    /// Fields marked as `multi` in the schema are always returned as JSON arrays.
1082    /// Other fields with multiple values are also returned as arrays.
1083    /// Fields with a single value (and not marked multi) are returned as scalar values.
1084    pub fn to_json(&self, schema: &Schema) -> serde_json::Value {
1085        use std::collections::HashMap;
1086
1087        // Group values by field, keeping track of field entry for multi check
1088        let mut field_values_map: HashMap<Field, (String, bool, Vec<serde_json::Value>)> =
1089            HashMap::new();
1090
1091        for (field, value) in &self.field_values {
1092            if let Some(entry) = schema.get_field_entry(*field) {
1093                let json_value = match value {
1094                    FieldValue::Text(s) => serde_json::Value::String(s.clone()),
1095                    FieldValue::U64(n) => serde_json::Value::Number((*n).into()),
1096                    FieldValue::I64(n) => serde_json::Value::Number((*n).into()),
1097                    FieldValue::F64(n) => serde_json::json!(n),
1098                    FieldValue::Bytes(b) => {
1099                        use base64::Engine;
1100                        serde_json::Value::String(
1101                            base64::engine::general_purpose::STANDARD.encode(b),
1102                        )
1103                    }
1104                    FieldValue::SparseVector(entries) => {
1105                        let indices: Vec<u32> = entries.iter().map(|(i, _)| *i).collect();
1106                        let values: Vec<f32> = entries.iter().map(|(_, v)| *v).collect();
1107                        serde_json::json!({
1108                            "indices": indices,
1109                            "values": values
1110                        })
1111                    }
1112                    FieldValue::DenseVector(values) => {
1113                        serde_json::json!(values)
1114                    }
1115                    FieldValue::Json(v) => v.clone(),
1116                    FieldValue::BinaryDenseVector(b) => {
1117                        use base64::Engine;
1118                        serde_json::Value::String(
1119                            base64::engine::general_purpose::STANDARD.encode(b),
1120                        )
1121                    }
1122                };
1123                field_values_map
1124                    .entry(*field)
1125                    .or_insert_with(|| (entry.name.clone(), entry.multi, Vec::new()))
1126                    .2
1127                    .push(json_value);
1128            }
1129        }
1130
1131        // Convert to JSON object, using arrays for multi fields or when multiple values exist
1132        let mut map = serde_json::Map::new();
1133        for (_field, (name, is_multi, values)) in field_values_map {
1134            let json_value = if is_multi || values.len() > 1 {
1135                serde_json::Value::Array(values)
1136            } else {
1137                values.into_iter().next().unwrap()
1138            };
1139            map.insert(name, json_value);
1140        }
1141
1142        serde_json::Value::Object(map)
1143    }
1144
1145    /// Create a Document from a JSON object using field names from schema
1146    ///
1147    /// Supports:
1148    /// - String values -> Text fields
1149    /// - Number values -> U64/I64/F64 fields (based on schema type)
1150    /// - Array values -> Multiple values for the same field (multifields)
1151    ///
1152    /// Unknown fields (not in schema) are silently ignored.
1153    pub fn from_json(json: &serde_json::Value, schema: &Schema) -> Option<Self> {
1154        let obj = json.as_object()?;
1155        let mut doc = Document::new();
1156
1157        for (key, value) in obj {
1158            if let Some(field) = schema.get_field(key) {
1159                let field_entry = schema.get_field_entry(field)?;
1160                Self::add_json_value(&mut doc, field, &field_entry.field_type, value);
1161            }
1162        }
1163
1164        Some(doc)
1165    }
1166
1167    /// Helper to add a JSON value to a document, handling type conversion
1168    fn add_json_value(
1169        doc: &mut Document,
1170        field: Field,
1171        field_type: &FieldType,
1172        value: &serde_json::Value,
1173    ) {
1174        match value {
1175            serde_json::Value::String(s) => {
1176                if matches!(field_type, FieldType::Text) {
1177                    doc.add_text(field, s.clone());
1178                }
1179            }
1180            serde_json::Value::Number(n) => {
1181                match field_type {
1182                    FieldType::I64 => {
1183                        if let Some(i) = n.as_i64() {
1184                            doc.add_i64(field, i);
1185                        }
1186                    }
1187                    FieldType::U64 => {
1188                        if let Some(u) = n.as_u64() {
1189                            doc.add_u64(field, u);
1190                        } else if let Some(i) = n.as_i64() {
1191                            // Allow positive i64 as u64
1192                            if i >= 0 {
1193                                doc.add_u64(field, i as u64);
1194                            }
1195                        }
1196                    }
1197                    FieldType::F64 => {
1198                        if let Some(f) = n.as_f64() {
1199                            doc.add_f64(field, f);
1200                        }
1201                    }
1202                    _ => {}
1203                }
1204            }
1205            // Handle arrays (multifields) - add each element separately
1206            serde_json::Value::Array(arr) => {
1207                for item in arr {
1208                    Self::add_json_value(doc, field, field_type, item);
1209                }
1210            }
1211            // Handle sparse vector objects
1212            serde_json::Value::Object(obj) if matches!(field_type, FieldType::SparseVector) => {
1213                if let (Some(indices_val), Some(values_val)) =
1214                    (obj.get("indices"), obj.get("values"))
1215                {
1216                    let indices: Vec<u32> = indices_val
1217                        .as_array()
1218                        .map(|arr| {
1219                            arr.iter()
1220                                .filter_map(|v| v.as_u64().map(|n| n as u32))
1221                                .collect()
1222                        })
1223                        .unwrap_or_default();
1224                    let values: Vec<f32> = values_val
1225                        .as_array()
1226                        .map(|arr| {
1227                            arr.iter()
1228                                .filter_map(|v| v.as_f64().map(|n| n as f32))
1229                                .collect()
1230                        })
1231                        .unwrap_or_default();
1232                    if indices.len() == values.len() {
1233                        let entries: Vec<(u32, f32)> = indices.into_iter().zip(values).collect();
1234                        doc.add_sparse_vector(field, entries);
1235                    }
1236                }
1237            }
1238            // Handle JSON fields - accept any value directly
1239            _ if matches!(field_type, FieldType::Json) => {
1240                doc.add_json(field, value.clone());
1241            }
1242            serde_json::Value::Object(_) => {}
1243            _ => {}
1244        }
1245    }
1246}
1247
1248#[cfg(test)]
1249mod tests {
1250    use super::*;
1251
1252    #[test]
1253    fn test_schema_builder() {
1254        let mut builder = Schema::builder();
1255        let title = builder.add_text_field("title", true, true);
1256        let body = builder.add_text_field("body", true, false);
1257        let count = builder.add_u64_field("count", true, true);
1258        let schema = builder.build();
1259
1260        assert_eq!(schema.get_field("title"), Some(title));
1261        assert_eq!(schema.get_field("body"), Some(body));
1262        assert_eq!(schema.get_field("count"), Some(count));
1263        assert_eq!(schema.get_field("nonexistent"), None);
1264    }
1265
1266    #[test]
1267    fn test_set_primary_key_forces_fast_and_indexed() {
1268        // Regression: the SDL path forces fast + indexed on primary-key fields
1269        // (needed for dedup lookups against the fast-field text dict). The
1270        // programmatic builder must do the same, otherwise committed-key dedup
1271        // is silently inert after every commit.
1272        let mut builder = Schema::builder();
1273        let id = builder.add_text_field("id", false, true);
1274        builder.set_primary_key(id);
1275        let schema = builder.build();
1276
1277        let entry = schema.get_field_entry(id).unwrap();
1278        assert!(entry.primary_key);
1279        assert!(
1280            entry.fast,
1281            "primary key must imply fast (dedup reads the fast-field text dict)"
1282        );
1283        assert!(entry.indexed, "primary key must imply indexed");
1284    }
1285
1286    #[test]
1287    fn test_document() {
1288        let mut builder = Schema::builder();
1289        let title = builder.add_text_field("title", true, true);
1290        let count = builder.add_u64_field("count", true, true);
1291        let _schema = builder.build();
1292
1293        let mut doc = Document::new();
1294        doc.add_text(title, "Hello World");
1295        doc.add_u64(count, 42);
1296
1297        assert_eq!(doc.get_first(title).unwrap().as_text(), Some("Hello World"));
1298        assert_eq!(doc.get_first(count).unwrap().as_u64(), Some(42));
1299    }
1300
1301    #[test]
1302    fn test_document_serialization() {
1303        let mut builder = Schema::builder();
1304        let title = builder.add_text_field("title", true, true);
1305        let count = builder.add_u64_field("count", true, true);
1306        let _schema = builder.build();
1307
1308        let mut doc = Document::new();
1309        doc.add_text(title, "Hello World");
1310        doc.add_u64(count, 42);
1311
1312        // Serialize
1313        let json = serde_json::to_string(&doc).unwrap();
1314        println!("Serialized doc: {}", json);
1315
1316        // Deserialize
1317        let doc2: Document = serde_json::from_str(&json).unwrap();
1318        assert_eq!(
1319            doc2.field_values().len(),
1320            2,
1321            "Should have 2 field values after deserialization"
1322        );
1323        assert_eq!(
1324            doc2.get_first(title).unwrap().as_text(),
1325            Some("Hello World")
1326        );
1327        assert_eq!(doc2.get_first(count).unwrap().as_u64(), Some(42));
1328    }
1329
1330    #[test]
1331    fn test_multivalue_field() {
1332        let mut builder = Schema::builder();
1333        let uris = builder.add_text_field("uris", true, true);
1334        let title = builder.add_text_field("title", true, true);
1335        let schema = builder.build();
1336
1337        // Create document with multiple values for the same field
1338        let mut doc = Document::new();
1339        doc.add_text(uris, "one");
1340        doc.add_text(uris, "two");
1341        doc.add_text(title, "Test Document");
1342
1343        // Verify get_first returns the first value
1344        assert_eq!(doc.get_first(uris).unwrap().as_text(), Some("one"));
1345
1346        // Verify get_all returns all values
1347        let all_uris: Vec<_> = doc.get_all(uris).collect();
1348        assert_eq!(all_uris.len(), 2);
1349        assert_eq!(all_uris[0].as_text(), Some("one"));
1350        assert_eq!(all_uris[1].as_text(), Some("two"));
1351
1352        // Verify to_json returns array for multi-value field
1353        let json = doc.to_json(&schema);
1354        let uris_json = json.get("uris").unwrap();
1355        assert!(uris_json.is_array(), "Multi-value field should be an array");
1356        let uris_arr = uris_json.as_array().unwrap();
1357        assert_eq!(uris_arr.len(), 2);
1358        assert_eq!(uris_arr[0].as_str(), Some("one"));
1359        assert_eq!(uris_arr[1].as_str(), Some("two"));
1360
1361        // Verify single-value field is NOT an array
1362        let title_json = json.get("title").unwrap();
1363        assert!(
1364            title_json.is_string(),
1365            "Single-value field should be a string"
1366        );
1367        assert_eq!(title_json.as_str(), Some("Test Document"));
1368    }
1369
1370    #[test]
1371    fn test_multivalue_from_json() {
1372        let mut builder = Schema::builder();
1373        let uris = builder.add_text_field("uris", true, true);
1374        let title = builder.add_text_field("title", true, true);
1375        let schema = builder.build();
1376
1377        // Create JSON with array value
1378        let json = serde_json::json!({
1379            "uris": ["one", "two"],
1380            "title": "Test Document"
1381        });
1382
1383        // Parse from JSON
1384        let doc = Document::from_json(&json, &schema).unwrap();
1385
1386        // Verify all values are present
1387        let all_uris: Vec<_> = doc.get_all(uris).collect();
1388        assert_eq!(all_uris.len(), 2);
1389        assert_eq!(all_uris[0].as_text(), Some("one"));
1390        assert_eq!(all_uris[1].as_text(), Some("two"));
1391
1392        // Verify single value
1393        assert_eq!(
1394            doc.get_first(title).unwrap().as_text(),
1395            Some("Test Document")
1396        );
1397
1398        // Verify roundtrip: to_json should produce equivalent JSON
1399        let json_out = doc.to_json(&schema);
1400        let uris_out = json_out.get("uris").unwrap().as_array().unwrap();
1401        assert_eq!(uris_out.len(), 2);
1402        assert_eq!(uris_out[0].as_str(), Some("one"));
1403        assert_eq!(uris_out[1].as_str(), Some("two"));
1404    }
1405
1406    #[test]
1407    fn test_multi_attribute_forces_array() {
1408        // Test that fields marked as 'multi' are always serialized as arrays,
1409        // even when they have only one value
1410        let mut builder = Schema::builder();
1411        let uris = builder.add_text_field("uris", true, true);
1412        builder.set_multi(uris, true); // Mark as multi
1413        let title = builder.add_text_field("title", true, true);
1414        let schema = builder.build();
1415
1416        // Verify the multi attribute is set
1417        assert!(schema.get_field_entry(uris).unwrap().multi);
1418        assert!(!schema.get_field_entry(title).unwrap().multi);
1419
1420        // Create document with single value for multi field
1421        let mut doc = Document::new();
1422        doc.add_text(uris, "only_one");
1423        doc.add_text(title, "Test Document");
1424
1425        // Verify to_json returns array for multi field even with single value
1426        let json = doc.to_json(&schema);
1427
1428        let uris_json = json.get("uris").unwrap();
1429        assert!(
1430            uris_json.is_array(),
1431            "Multi field should be array even with single value"
1432        );
1433        let uris_arr = uris_json.as_array().unwrap();
1434        assert_eq!(uris_arr.len(), 1);
1435        assert_eq!(uris_arr[0].as_str(), Some("only_one"));
1436
1437        // Verify non-multi field with single value is NOT an array
1438        let title_json = json.get("title").unwrap();
1439        assert!(
1440            title_json.is_string(),
1441            "Non-multi single-value field should be a string"
1442        );
1443        assert_eq!(title_json.as_str(), Some("Test Document"));
1444    }
1445
1446    #[test]
1447    fn test_sparse_vector_field() {
1448        let mut builder = Schema::builder();
1449        let embedding = builder.add_sparse_vector_field("embedding", true, true);
1450        let title = builder.add_text_field("title", true, true);
1451        let schema = builder.build();
1452
1453        assert_eq!(schema.get_field("embedding"), Some(embedding));
1454        assert_eq!(
1455            schema.get_field_entry(embedding).unwrap().field_type,
1456            FieldType::SparseVector
1457        );
1458
1459        // Create document with sparse vector
1460        let mut doc = Document::new();
1461        doc.add_sparse_vector(embedding, vec![(0, 1.0), (5, 2.5), (10, 0.5)]);
1462        doc.add_text(title, "Test Document");
1463
1464        // Verify accessor
1465        let entries = doc
1466            .get_first(embedding)
1467            .unwrap()
1468            .as_sparse_vector()
1469            .unwrap();
1470        assert_eq!(entries, &[(0, 1.0), (5, 2.5), (10, 0.5)]);
1471
1472        // Verify JSON roundtrip
1473        let json = doc.to_json(&schema);
1474        let embedding_json = json.get("embedding").unwrap();
1475        assert!(embedding_json.is_object());
1476        assert_eq!(
1477            embedding_json
1478                .get("indices")
1479                .unwrap()
1480                .as_array()
1481                .unwrap()
1482                .len(),
1483            3
1484        );
1485
1486        // Parse back from JSON
1487        let doc2 = Document::from_json(&json, &schema).unwrap();
1488        let entries2 = doc2
1489            .get_first(embedding)
1490            .unwrap()
1491            .as_sparse_vector()
1492            .unwrap();
1493        assert_eq!(entries2[0].0, 0);
1494        assert!((entries2[0].1 - 1.0).abs() < 1e-6);
1495        assert_eq!(entries2[1].0, 5);
1496        assert!((entries2[1].1 - 2.5).abs() < 1e-6);
1497        assert_eq!(entries2[2].0, 10);
1498        assert!((entries2[2].1 - 0.5).abs() < 1e-6);
1499    }
1500
1501    #[test]
1502    fn test_json_field() {
1503        let mut builder = Schema::builder();
1504        let metadata = builder.add_json_field("metadata", true);
1505        let title = builder.add_text_field("title", true, true);
1506        let schema = builder.build();
1507
1508        assert_eq!(schema.get_field("metadata"), Some(metadata));
1509        assert_eq!(
1510            schema.get_field_entry(metadata).unwrap().field_type,
1511            FieldType::Json
1512        );
1513        // JSON fields are never indexed
1514        assert!(!schema.get_field_entry(metadata).unwrap().indexed);
1515        assert!(schema.get_field_entry(metadata).unwrap().stored);
1516
1517        // Create document with JSON value (object)
1518        let json_value = serde_json::json!({
1519            "author": "John Doe",
1520            "tags": ["rust", "search"],
1521            "nested": {"key": "value"}
1522        });
1523        let mut doc = Document::new();
1524        doc.add_json(metadata, json_value.clone());
1525        doc.add_text(title, "Test Document");
1526
1527        // Verify accessor
1528        let stored_json = doc.get_first(metadata).unwrap().as_json().unwrap();
1529        assert_eq!(stored_json, &json_value);
1530        assert_eq!(
1531            stored_json.get("author").unwrap().as_str(),
1532            Some("John Doe")
1533        );
1534
1535        // Verify JSON roundtrip via to_json/from_json
1536        let doc_json = doc.to_json(&schema);
1537        let metadata_out = doc_json.get("metadata").unwrap();
1538        assert_eq!(metadata_out, &json_value);
1539
1540        // Parse back from JSON
1541        let doc2 = Document::from_json(&doc_json, &schema).unwrap();
1542        let stored_json2 = doc2.get_first(metadata).unwrap().as_json().unwrap();
1543        assert_eq!(stored_json2, &json_value);
1544    }
1545
1546    #[test]
1547    fn test_json_field_various_types() {
1548        let mut builder = Schema::builder();
1549        let data = builder.add_json_field("data", true);
1550        let _schema = builder.build();
1551
1552        // Test with array
1553        let arr_value = serde_json::json!([1, 2, 3, "four", null]);
1554        let mut doc = Document::new();
1555        doc.add_json(data, arr_value.clone());
1556        assert_eq!(doc.get_first(data).unwrap().as_json().unwrap(), &arr_value);
1557
1558        // Test with string
1559        let str_value = serde_json::json!("just a string");
1560        let mut doc2 = Document::new();
1561        doc2.add_json(data, str_value.clone());
1562        assert_eq!(doc2.get_first(data).unwrap().as_json().unwrap(), &str_value);
1563
1564        // Test with number
1565        let num_value = serde_json::json!(42.5);
1566        let mut doc3 = Document::new();
1567        doc3.add_json(data, num_value.clone());
1568        assert_eq!(doc3.get_first(data).unwrap().as_json().unwrap(), &num_value);
1569
1570        // Test with null
1571        let null_value = serde_json::Value::Null;
1572        let mut doc4 = Document::new();
1573        doc4.add_json(data, null_value.clone());
1574        assert_eq!(
1575            doc4.get_first(data).unwrap().as_json().unwrap(),
1576            &null_value
1577        );
1578
1579        // Test with boolean
1580        let bool_value = serde_json::json!(true);
1581        let mut doc5 = Document::new();
1582        doc5.add_json(data, bool_value.clone());
1583        assert_eq!(
1584            doc5.get_first(data).unwrap().as_json().unwrap(),
1585            &bool_value
1586        );
1587    }
1588}