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