Skip to main content

hermes_core/dsl/
schema.rs

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