Skip to main content

hermes_core/dsl/sdl/
mod.rs

1//! Schema Definition Language (SDL) for Hermes
2//!
3//! A simple, readable format for defining index schemas using pest parser.
4//!
5//! # Example SDL
6//!
7//! ```text
8//! # Article index schema
9//! index articles {
10//!     # Primary text field for full-text search
11//!     field title: text [indexed, stored]
12//!
13//!     # Body content - indexed but not stored (save space)
14//!     field body: text [indexed]
15//!
16//!     # Author name
17//!     field author: text [indexed, stored]
18//!
19//!     # Publication timestamp
20//!     field published_at: i64 [indexed, stored]
21//!
22//!     # View count
23//!     field views: u64 [indexed, stored]
24//!
25//!     # Rating score
26//!     field rating: f64 [indexed, stored]
27//!
28//!     # Raw content hash (not indexed, just stored)
29//!     field content_hash: bytes [stored]
30//!
31//!     # Dense vector with IVF-RaBitQ index
32//!     field embedding: dense_vector<768> [indexed<rabitq, centroids: "centroids.bin", nprobe: 32>]
33//!
34//! }
35//! ```
36//!
37//! # Dense Vector Index Configuration
38//!
39//! Index-related parameters for dense vectors are specified in `indexed<...>`:
40//! - `rabitq` or `scann` - index type
41//! - `centroids: "path"` - path to pre-trained centroids file
42//! - `codebook: "path"` - path to PQ codebook (ScaNN only)
43//! - `nprobe: N` - number of clusters to probe (default: 32)
44
45use pest::Parser;
46use pest_derive::Parser;
47
48use super::query_field_router::{QueryRouterRule, RoutingMode};
49use super::schema::{DenseVectorQuantization, FieldType, Schema, SchemaBuilder};
50use crate::Result;
51use crate::error::Error;
52
53#[derive(Parser)]
54#[grammar = "dsl/sdl/sdl.pest"]
55pub struct SdlParser;
56
57use super::schema::{BinaryDenseVectorConfig, DenseVectorConfig};
58use crate::structures::{
59    IndexSize, QueryWeighting, SparseFormat, SparseQueryConfig, SparseVectorConfig,
60    WeightQuantization,
61};
62
63/// Parsed field definition
64#[derive(Debug, Clone)]
65pub struct FieldDef {
66    pub name: String,
67    pub field_type: FieldType,
68    pub indexed: bool,
69    pub stored: bool,
70    /// Tokenizer name for text fields (e.g., "simple", "en_stem", "german")
71    pub tokenizer: Option<String>,
72    /// Whether this field can have multiple values (serialized as array in JSON)
73    pub multi: bool,
74    /// Position tracking mode for phrase queries and multi-field element tracking
75    pub positions: Option<super::schema::PositionMode>,
76    /// Configuration for sparse vector fields
77    pub sparse_vector_config: Option<SparseVectorConfig>,
78    /// Configuration for dense vector fields
79    pub dense_vector_config: Option<DenseVectorConfig>,
80    /// Configuration for binary dense vector fields
81    pub binary_dense_vector_config: Option<BinaryDenseVectorConfig>,
82    /// Whether this field has columnar fast-field storage
83    pub fast: bool,
84    /// Whether this field is a primary key (unique constraint)
85    pub primary: bool,
86    /// Whether build-time document reordering (BP) is enabled for BMP fields
87    pub reorder: bool,
88}
89
90/// Parsed index definition
91#[derive(Debug, Clone)]
92pub struct IndexDef {
93    pub name: String,
94    pub fields: Vec<FieldDef>,
95    pub default_fields: Vec<String>,
96    /// Query router rules for routing queries to specific fields
97    pub query_routers: Vec<QueryRouterRule>,
98    /// BP-reorder `reorder`-attributed BMP fields inside merges
99    /// (index-level `reorder_on_merge: true`). Absent = disabled.
100    pub reorder_on_merge: bool,
101}
102
103impl IndexDef {
104    /// Convert to a Schema
105    pub fn to_schema(&self) -> Schema {
106        let mut builder = SchemaBuilder::default();
107
108        for field in &self.fields {
109            let f = match field.field_type {
110                FieldType::Text => {
111                    let tokenizer = field.tokenizer.as_deref().unwrap_or("simple");
112                    builder.add_text_field_with_tokenizer(
113                        &field.name,
114                        field.indexed,
115                        field.stored,
116                        tokenizer,
117                    )
118                }
119                FieldType::U64 => builder.add_u64_field(&field.name, field.indexed, field.stored),
120                FieldType::I64 => builder.add_i64_field(&field.name, field.indexed, field.stored),
121                FieldType::F64 => builder.add_f64_field(&field.name, field.indexed, field.stored),
122                FieldType::Bytes => builder.add_bytes_field(&field.name, field.stored),
123                FieldType::Json => builder.add_json_field(&field.name, field.stored),
124                FieldType::SparseVector => {
125                    if let Some(config) = &field.sparse_vector_config {
126                        builder.add_sparse_vector_field_with_config(
127                            &field.name,
128                            field.indexed,
129                            field.stored,
130                            config.clone(),
131                        )
132                    } else {
133                        builder.add_sparse_vector_field(&field.name, field.indexed, field.stored)
134                    }
135                }
136                FieldType::DenseVector => {
137                    // Dense vector dimension must be specified via config
138                    let config = field
139                        .dense_vector_config
140                        .as_ref()
141                        .expect("DenseVector field requires dimension to be specified");
142                    builder.add_dense_vector_field_with_config(
143                        &field.name,
144                        field.indexed,
145                        field.stored,
146                        config.clone(),
147                    )
148                }
149                FieldType::BinaryDenseVector => {
150                    let config = field
151                        .binary_dense_vector_config
152                        .as_ref()
153                        .expect("BinaryDenseVector field requires dimension to be specified");
154                    builder.add_binary_dense_vector_field_with_config(
155                        &field.name,
156                        field.indexed,
157                        field.stored,
158                        config.clone(),
159                    )
160                }
161            };
162            if field.multi {
163                builder.set_multi(f, true);
164            }
165            if field.fast {
166                builder.set_fast(f, true);
167            }
168            if field.primary {
169                builder.set_primary_key(f);
170            }
171            if field.reorder {
172                builder.set_reorder(f, true);
173            }
174            // Set positions: explicit > auto (ordinal for multi vectors)
175            let positions = field.positions.or({
176                // Auto-set ordinal positions for multi-valued vector fields
177                if field.multi
178                    && matches!(
179                        field.field_type,
180                        FieldType::SparseVector
181                            | FieldType::DenseVector
182                            | FieldType::BinaryDenseVector
183                    )
184                {
185                    Some(super::schema::PositionMode::Ordinal)
186                } else {
187                    None
188                }
189            });
190            if let Some(mode) = positions {
191                builder.set_positions(f, mode);
192            }
193        }
194
195        // Set default fields if specified
196        if !self.default_fields.is_empty() {
197            builder.set_default_fields(self.default_fields.clone());
198        }
199
200        // Set query routers if specified
201        if !self.query_routers.is_empty() {
202            builder.set_query_routers(self.query_routers.clone());
203        }
204
205        builder.set_index_name(self.name.clone());
206
207        if self.reorder_on_merge {
208            if self.fields.iter().any(|f| f.reorder) {
209                builder.set_reorder_on_merge(true);
210            } else {
211                // Fail loud: the option would silently do nothing without at
212                // least one `reorder`-attributed field.
213                log::warn!(
214                    "index '{}': reorder_on_merge is set but no field has the `reorder` attribute — merges will not reorder anything",
215                    self.name,
216                );
217                builder.set_reorder_on_merge(true);
218            }
219        }
220
221        builder.build()
222    }
223
224    /// Create a QueryFieldRouter from the query router rules
225    ///
226    /// Returns None if there are no query router rules defined.
227    /// Returns Err if any regex pattern is invalid.
228    pub fn to_query_router(&self) -> Result<Option<super::query_field_router::QueryFieldRouter>> {
229        if self.query_routers.is_empty() {
230            return Ok(None);
231        }
232
233        super::query_field_router::QueryFieldRouter::from_rules(&self.query_routers)
234            .map(Some)
235            .map_err(Error::Schema)
236    }
237}
238
239/// Parse field type from string
240fn parse_field_type(type_str: &str) -> Result<FieldType> {
241    match type_str {
242        "text" | "string" | "str" => Ok(FieldType::Text),
243        "u64" | "uint" | "unsigned" => Ok(FieldType::U64),
244        "i64" | "int" | "integer" => Ok(FieldType::I64),
245        "f64" | "float" | "double" => Ok(FieldType::F64),
246        "bytes" | "binary" | "blob" => Ok(FieldType::Bytes),
247        "json" => Ok(FieldType::Json),
248        "sparse_vector" => Ok(FieldType::SparseVector),
249        "dense_vector" | "vector" => Ok(FieldType::DenseVector),
250        "binary_dense_vector" | "binary_vector" => Ok(FieldType::BinaryDenseVector),
251        _ => Err(Error::Schema(format!("Unknown field type: {}", type_str))),
252    }
253}
254
255/// Index configuration parsed from indexed<...> attribute
256#[derive(Debug, Clone, Default)]
257struct IndexConfig {
258    index_type: Option<super::schema::VectorIndexType>,
259    num_clusters: Option<usize>,
260    nprobe: Option<usize>,
261    build_threshold: Option<usize>,
262    soar: Option<crate::structures::SoarConfig>,
263    rabitq_bits: Option<u8>,
264    binary_ivf: bool,
265    // Sparse vector index params
266    sparse_format: Option<SparseFormat>,
267    quantization: Option<WeightQuantization>,
268    weight_threshold: Option<f32>,
269    block_size: Option<usize>,
270    pruning: Option<f32>,
271    min_terms: Option<usize>,
272    doc_mass: Option<f32>,
273    // Sparse vector query-time config
274    query_tokenizer: Option<String>,
275    query_weighting: Option<QueryWeighting>,
276    query_weight_threshold: Option<f32>,
277    query_max_dims: Option<usize>,
278    query_pruning: Option<f32>,
279    query_min_query_dims: Option<usize>,
280    // BMP fixed dims (vocabulary size) and max weight scale
281    dims: Option<u32>,
282    max_weight: Option<f32>,
283    // Position tracking mode for phrase queries
284    positions: Option<super::schema::PositionMode>,
285}
286
287/// Parsed attributes from SDL field definition
288struct ParsedAttributes {
289    indexed: bool,
290    stored: bool,
291    multi: bool,
292    fast: bool,
293    primary: bool,
294    reorder: bool,
295    index_config: Option<IndexConfig>,
296}
297
298/// Parse attributes from pest pair
299fn parse_attributes(pair: pest::iterators::Pair<Rule>) -> ParsedAttributes {
300    let mut attrs = ParsedAttributes {
301        indexed: false,
302        stored: false,
303        multi: false,
304        fast: false,
305        primary: false,
306        reorder: false,
307        index_config: None,
308    };
309
310    for attr in pair.into_inner() {
311        if attr.as_rule() == Rule::attribute {
312            let mut found_config = false;
313            for inner in attr.clone().into_inner() {
314                match inner.as_rule() {
315                    Rule::indexed_with_config => {
316                        attrs.indexed = true;
317                        attrs.index_config = Some(parse_index_config(inner));
318                        found_config = true;
319                        break;
320                    }
321                    Rule::stored_with_config => {
322                        attrs.stored = true;
323                        attrs.multi = true; // stored<multi>
324                        found_config = true;
325                        break;
326                    }
327                    _ => {}
328                }
329            }
330            if !found_config {
331                match attr.as_str() {
332                    "indexed" => attrs.indexed = true,
333                    "stored" => attrs.stored = true,
334                    "fast" => attrs.fast = true,
335                    "primary" => attrs.primary = true,
336                    "reorder" => attrs.reorder = true,
337                    _ => {}
338                }
339            }
340        }
341    }
342
343    attrs
344}
345
346/// Parse index configuration from indexed<...> attribute
347fn parse_index_config(pair: pest::iterators::Pair<Rule>) -> IndexConfig {
348    let mut config = IndexConfig::default();
349
350    // indexed_with_config = { "indexed" ~ "<" ~ index_config_params ~ ">" }
351    // index_config_params = { index_config_param ~ ("," ~ index_config_param)* }
352    // index_config_param = { index_type_kwarg | centroids_kwarg | codebook_kwarg | nprobe_kwarg | index_type_spec }
353
354    for inner in pair.into_inner() {
355        if inner.as_rule() == Rule::index_config_params {
356            for param in inner.into_inner() {
357                if param.as_rule() == Rule::index_config_param {
358                    for p in param.into_inner() {
359                        parse_single_index_config_param(&mut config, p);
360                    }
361                }
362            }
363        }
364    }
365
366    config
367}
368
369/// Parse a single index config parameter
370fn parse_single_index_config_param(config: &mut IndexConfig, p: pest::iterators::Pair<Rule>) {
371    use super::schema::VectorIndexType;
372
373    match p.as_rule() {
374        Rule::index_type_spec => {
375            if p.as_str() == "ivf" {
376                // Binary dense vector IVF (Hamming clusters)
377                config.binary_ivf = true;
378                return;
379            }
380            config.index_type = Some(match p.as_str() {
381                "flat" => VectorIndexType::Flat,
382                "rabitq" => VectorIndexType::RaBitQ,
383                "ivf_rabitq" => VectorIndexType::IvfRaBitQ,
384                "scann" => VectorIndexType::ScaNN,
385                _ => VectorIndexType::RaBitQ,
386            });
387        }
388        Rule::index_type_kwarg => {
389            // index_type_kwarg = { "index" ~ ":" ~ index_type_spec }
390            if let Some(t) = p.into_inner().next() {
391                if t.as_str() == "ivf" {
392                    config.binary_ivf = true;
393                    return;
394                }
395                config.index_type = Some(match t.as_str() {
396                    "flat" => VectorIndexType::Flat,
397                    "rabitq" => VectorIndexType::RaBitQ,
398                    "ivf_rabitq" => VectorIndexType::IvfRaBitQ,
399                    "scann" => VectorIndexType::ScaNN,
400                    _ => VectorIndexType::RaBitQ,
401                });
402            }
403        }
404        Rule::num_clusters_kwarg => {
405            // num_clusters_kwarg = { "num_clusters" ~ ":" ~ num_clusters_spec }
406            if let Some(n) = p.into_inner().next() {
407                config.num_clusters = Some(n.as_str().parse().unwrap_or_else(|_| {
408                    log::warn!(
409                        "Invalid num_clusters value '{}', using default 256",
410                        n.as_str()
411                    );
412                    256
413                }));
414            }
415        }
416        Rule::build_threshold_kwarg => {
417            // build_threshold_kwarg = { "build_threshold" ~ ":" ~ build_threshold_spec }
418            if let Some(n) = p.into_inner().next() {
419                config.build_threshold = Some(n.as_str().parse().unwrap_or_else(|_| {
420                    log::warn!(
421                        "Invalid build_threshold value '{}', using default 10000",
422                        n.as_str()
423                    );
424                    10000
425                }));
426            }
427        }
428        Rule::nprobe_kwarg => {
429            // nprobe_kwarg = { "nprobe" ~ ":" ~ nprobe_spec }
430            if let Some(n) = p.into_inner().next() {
431                config.nprobe = Some(n.as_str().parse().unwrap_or_else(|_| {
432                    log::warn!("Invalid nprobe value '{}', using default 32", n.as_str());
433                    32
434                }));
435            }
436        }
437        Rule::bits_kwarg => {
438            // bits_kwarg = { "bits" ~ ":" ~ bits_spec }
439            if let Some(b) = p.into_inner().next() {
440                config.rabitq_bits = Some(b.as_str().parse::<u8>().unwrap_or(1).clamp(1, 8));
441            }
442        }
443        Rule::soar_kwarg => {
444            // soar_kwarg = { "soar" ~ ":" ~ soar_spec }
445            if let Some(s) = p.into_inner().next() {
446                use crate::structures::SoarConfig;
447                config.soar = match s.as_str() {
448                    "selective" => Some(SoarConfig::new()),
449                    "full" => Some(SoarConfig::full()),
450                    "aggressive" => Some(SoarConfig::aggressive()),
451                    _ => None, // "off"
452                };
453            }
454        }
455        Rule::quantization_kwarg => {
456            // quantization_kwarg = { "quantization" ~ ":" ~ quantization_spec }
457            if let Some(q) = p.into_inner().next() {
458                config.quantization = Some(match q.as_str() {
459                    "float32" | "f32" => WeightQuantization::Float32,
460                    "float16" | "f16" => WeightQuantization::Float16,
461                    "uint8" | "u8" => WeightQuantization::UInt8,
462                    "uint4" | "u4" => WeightQuantization::UInt4,
463                    _ => WeightQuantization::default(),
464                });
465            }
466        }
467        Rule::weight_threshold_kwarg => {
468            // weight_threshold_kwarg = { "weight_threshold" ~ ":" ~ weight_threshold_spec }
469            if let Some(t) = p.into_inner().next() {
470                config.weight_threshold = Some(t.as_str().parse().unwrap_or_else(|_| {
471                    log::warn!(
472                        "Invalid weight_threshold value '{}', using default 0.0",
473                        t.as_str()
474                    );
475                    0.0
476                }));
477            }
478        }
479        Rule::block_size_kwarg => {
480            // block_size_kwarg = { "block_size" ~ ":" ~ block_size_spec }
481            if let Some(n) = p.into_inner().next() {
482                config.block_size = Some(n.as_str().parse().unwrap_or_else(|_| {
483                    log::warn!(
484                        "Invalid block_size value '{}', using default 128",
485                        n.as_str()
486                    );
487                    128
488                }));
489            }
490        }
491        Rule::pruning_kwarg => {
492            // pruning_kwarg = { "pruning" ~ ":" ~ pruning_spec }
493            if let Some(f) = p.into_inner().next() {
494                config.pruning = Some(f.as_str().parse().unwrap_or_else(|_| {
495                    log::warn!("Invalid pruning value '{}', using default 1.0", f.as_str());
496                    1.0
497                }));
498            }
499        }
500        Rule::doc_mass_kwarg => {
501            // doc_mass_kwarg = { "doc_mass" ~ ":" ~ pruning_spec }
502            if let Some(f) = p.into_inner().next() {
503                config.doc_mass = Some(f.as_str().parse().unwrap_or_else(|_| {
504                    log::warn!("Invalid doc_mass value '{}', using 1.0 (off)", f.as_str());
505                    1.0
506                }));
507            }
508        }
509        Rule::min_terms_kwarg => {
510            if let Some(n) = p.into_inner().next() {
511                config.min_terms = Some(n.as_str().parse().unwrap_or_else(|_| {
512                    log::warn!("Invalid min_terms value '{}', using default 4", n.as_str());
513                    4
514                }));
515            }
516        }
517        Rule::sparse_format_kwarg => {
518            // sparse_format_kwarg = { "format" ~ ":" ~ sparse_format_spec }
519            if let Some(f) = p.into_inner().next() {
520                config.sparse_format = Some(match f.as_str() {
521                    "bmp" => SparseFormat::Bmp,
522                    "maxscore" => SparseFormat::MaxScore,
523                    _ => SparseFormat::default(),
524                });
525            }
526        }
527        Rule::sparse_dims_kwarg => {
528            if let Some(n) = p.into_inner().next() {
529                config.dims = Some(n.as_str().parse().unwrap_or_else(|_| {
530                    log::warn!("Invalid dims value '{}', using default 105879", n.as_str());
531                    105879
532                }));
533            }
534        }
535        Rule::sparse_max_weight_kwarg => {
536            if let Some(f) = p.into_inner().next() {
537                config.max_weight = Some(f.as_str().parse().unwrap_or_else(|_| {
538                    log::warn!(
539                        "Invalid max_weight value '{}', using default 5.0",
540                        f.as_str()
541                    );
542                    5.0
543                }));
544            }
545        }
546        Rule::query_config_block => {
547            // query_config_block = { "query" ~ "<" ~ query_config_params ~ ">" }
548            parse_query_config_block(config, p);
549        }
550        Rule::positions_kwarg => {
551            // positions_kwarg = { "positions" | "ordinal" | "token_position" }
552            use super::schema::PositionMode;
553            config.positions = Some(match p.as_str() {
554                "ordinal" => PositionMode::Ordinal,
555                "token_position" => PositionMode::TokenPosition,
556                _ => PositionMode::Full, // "positions" or any other value defaults to Full
557            });
558        }
559        _ => {}
560    }
561}
562
563/// Parse query configuration block: query<tokenizer: "...", weighting: idf>
564fn parse_query_config_block(config: &mut IndexConfig, pair: pest::iterators::Pair<Rule>) {
565    for inner in pair.into_inner() {
566        if inner.as_rule() == Rule::query_config_params {
567            for param in inner.into_inner() {
568                if param.as_rule() == Rule::query_config_param {
569                    for p in param.into_inner() {
570                        match p.as_rule() {
571                            Rule::query_tokenizer_kwarg => {
572                                // query_tokenizer_kwarg = { "tokenizer" ~ ":" ~ tokenizer_path }
573                                if let Some(path) = p.into_inner().next()
574                                    && let Some(inner_path) = path.into_inner().next()
575                                {
576                                    config.query_tokenizer = Some(inner_path.as_str().to_string());
577                                }
578                            }
579                            Rule::query_weighting_kwarg => {
580                                // query_weighting_kwarg = { "weighting" ~ ":" ~ weighting_spec }
581                                if let Some(w) = p.into_inner().next() {
582                                    config.query_weighting = Some(match w.as_str() {
583                                        "one" => QueryWeighting::One,
584                                        "idf" => QueryWeighting::Idf,
585                                        "idf_file" => QueryWeighting::IdfFile,
586                                        _ => QueryWeighting::One,
587                                    });
588                                }
589                            }
590                            Rule::query_weight_threshold_kwarg => {
591                                if let Some(t) = p.into_inner().next() {
592                                    config.query_weight_threshold =
593                                        Some(t.as_str().parse().unwrap_or_else(|_| {
594                                            log::warn!(
595                                                "Invalid query weight_threshold '{}', using 0.0",
596                                                t.as_str()
597                                            );
598                                            0.0
599                                        }));
600                                }
601                            }
602                            Rule::query_max_dims_kwarg => {
603                                if let Some(t) = p.into_inner().next() {
604                                    config.query_max_dims =
605                                        Some(t.as_str().parse().unwrap_or_else(|_| {
606                                            log::warn!(
607                                                "Invalid query max_dims '{}', using 0",
608                                                t.as_str()
609                                            );
610                                            0
611                                        }));
612                                }
613                            }
614                            Rule::query_pruning_kwarg => {
615                                if let Some(t) = p.into_inner().next() {
616                                    config.query_pruning =
617                                        Some(t.as_str().parse().unwrap_or_else(|_| {
618                                            log::warn!(
619                                                "Invalid query pruning '{}', using 1.0",
620                                                t.as_str()
621                                            );
622                                            1.0
623                                        }));
624                                }
625                            }
626                            Rule::query_min_query_dims_kwarg => {
627                                if let Some(t) = p.into_inner().next() {
628                                    config.query_min_query_dims =
629                                        Some(t.as_str().parse().unwrap_or_else(|_| {
630                                            log::warn!(
631                                                "Invalid query min_query_dims '{}', using 4",
632                                                t.as_str()
633                                            );
634                                            4
635                                        }));
636                                }
637                            }
638                            _ => {}
639                        }
640                    }
641                }
642            }
643        }
644    }
645}
646
647/// Parse a field definition from pest pair
648fn parse_field_def(pair: pest::iterators::Pair<Rule>) -> Result<FieldDef> {
649    let mut inner = pair.into_inner();
650
651    let name = inner
652        .next()
653        .ok_or_else(|| Error::Schema("Missing field name".to_string()))?
654        .as_str()
655        .to_string();
656
657    let field_type_str = inner
658        .next()
659        .ok_or_else(|| Error::Schema("Missing field type".to_string()))?
660        .as_str();
661
662    let field_type = parse_field_type(field_type_str)?;
663
664    // Parse optional tokenizer spec, sparse_vector_config, dense_vector_config, and attributes
665    let mut tokenizer = None;
666    let mut sparse_vector_config = None;
667    let mut dense_vector_config = None;
668    let mut binary_dense_vector_config = None;
669    let mut indexed = true;
670    let mut stored = true;
671    let mut multi = false;
672    let mut fast = false;
673    let mut primary = false;
674    let mut reorder = false;
675    let mut index_config: Option<IndexConfig> = None;
676
677    for item in inner {
678        match item.as_rule() {
679            Rule::tokenizer_spec => {
680                // Extract tokenizer name from <name>
681                if let Some(tok_name) = item.into_inner().next() {
682                    tokenizer = Some(tok_name.as_str().to_string());
683                }
684            }
685            Rule::sparse_vector_config => {
686                // Parse named parameters: <index_size: u16, quantization: uint8, weight_threshold: 0.1>
687                sparse_vector_config = Some(parse_sparse_vector_config(item));
688            }
689            Rule::dense_vector_config => {
690                // Parse dense_vector_params (keyword or positional) - only dims
691                dense_vector_config = Some(parse_dense_vector_config(item));
692            }
693            Rule::binary_dense_vector_config => {
694                // Parse binary dense vector config - just dimension (number of bits)
695                let dim: usize = item
696                    .into_inner()
697                    .next()
698                    .map(|d| d.as_str().parse().unwrap_or(0))
699                    .unwrap_or(0);
700                if dim == 0 || !dim.is_multiple_of(8) {
701                    return Err(Error::Schema(format!(
702                        "BinaryDenseVector dimension must be a positive multiple of 8, got {dim}"
703                    )));
704                }
705                binary_dense_vector_config = Some(BinaryDenseVectorConfig::new(dim));
706            }
707            Rule::attributes => {
708                let attrs = parse_attributes(item);
709                indexed = attrs.indexed;
710                stored = attrs.stored;
711                multi = attrs.multi;
712                fast = attrs.fast;
713                primary = attrs.primary;
714                reorder = attrs.reorder;
715                index_config = attrs.index_config;
716            }
717            _ => {}
718        }
719    }
720
721    // PEG grammar ambiguity: both dense_vector_config and binary_dense_vector_config
722    // match `<N>`, and dense_vector_config comes first in the ordered choice. When the
723    // field_type is BinaryDenseVector, remap the matched dense_vector_config.
724    if field_type == FieldType::BinaryDenseVector
725        && binary_dense_vector_config.is_none()
726        && let Some(ref dv_config) = dense_vector_config
727    {
728        let dim = dv_config.dim;
729        if dim == 0 || !dim.is_multiple_of(8) {
730            return Err(Error::Schema(format!(
731                "BinaryDenseVector dimension must be a positive multiple of 8, got {dim}"
732            )));
733        }
734        binary_dense_vector_config = Some(BinaryDenseVectorConfig::new(dim));
735        dense_vector_config = None;
736    }
737
738    // Primary key implies fast + indexed (needed for dedup lookups)
739    if primary {
740        fast = true;
741        indexed = true;
742    }
743
744    // Merge index config into vector configs if both exist
745    let mut positions = None;
746    if let Some(idx_cfg) = index_config {
747        positions = idx_cfg.positions;
748        if let Some(ref mut bv_config) = binary_dense_vector_config {
749            apply_index_config_to_binary_dense_vector(bv_config, idx_cfg);
750        } else if let Some(ref mut dv_config) = dense_vector_config {
751            apply_index_config_to_dense_vector(dv_config, idx_cfg);
752        } else if field_type == FieldType::SparseVector {
753            // For sparse vectors, create default config if not present and apply index params
754            let sv_config = sparse_vector_config.get_or_insert(SparseVectorConfig::default());
755            apply_index_config_to_sparse_vector(sv_config, idx_cfg);
756        }
757    }
758
759    Ok(FieldDef {
760        name,
761        field_type,
762        indexed,
763        stored,
764        tokenizer,
765        multi,
766        positions,
767        sparse_vector_config,
768        dense_vector_config,
769        binary_dense_vector_config,
770        fast,
771        primary,
772        reorder,
773    })
774}
775
776/// Apply index configuration from indexed<...> to BinaryDenseVectorConfig
777fn apply_index_config_to_binary_dense_vector(
778    config: &mut BinaryDenseVectorConfig,
779    idx_cfg: IndexConfig,
780) {
781    if idx_cfg.binary_ivf {
782        config.index_type = super::schema::BinaryIndexType::Ivf;
783    }
784    if idx_cfg.num_clusters.is_some() {
785        config.num_clusters = idx_cfg.num_clusters;
786    }
787    if let Some(nprobe) = idx_cfg.nprobe {
788        config.nprobe = nprobe;
789    }
790    if idx_cfg.build_threshold.is_some() {
791        config.build_threshold = idx_cfg.build_threshold;
792    }
793}
794
795/// Apply index configuration from indexed<...> to DenseVectorConfig
796fn apply_index_config_to_dense_vector(config: &mut DenseVectorConfig, idx_cfg: IndexConfig) {
797    // Apply index type if specified
798    if let Some(index_type) = idx_cfg.index_type {
799        config.index_type = index_type;
800    }
801
802    // Apply num_clusters for IVF-based indexes
803    if idx_cfg.num_clusters.is_some() {
804        config.num_clusters = idx_cfg.num_clusters;
805    }
806
807    // Apply nprobe if specified
808    if let Some(nprobe) = idx_cfg.nprobe {
809        config.nprobe = nprobe;
810    }
811
812    // Apply build_threshold if specified
813    if idx_cfg.build_threshold.is_some() {
814        config.build_threshold = idx_cfg.build_threshold;
815    }
816
817    // Apply RaBitQ bit width if specified (IVF-RaBitQ only)
818    if let Some(bits) = idx_cfg.rabitq_bits {
819        if config.index_type == super::schema::VectorIndexType::IvfRaBitQ {
820            config.rabitq_bits = Some(bits);
821        } else {
822            log::warn!(
823                "'bits' requires an ivf_rabitq index; ignoring for index type {:?}",
824                config.index_type
825            );
826        }
827    }
828
829    // Apply SOAR spilling if specified (IVF-based indexes only)
830    if idx_cfg.soar.is_some() {
831        if config.uses_ivf() {
832            config.soar = idx_cfg.soar;
833        } else {
834            log::warn!(
835                "'soar' requires an IVF-based index (ivf_rabitq, scann); \
836                 ignoring for index type {:?}",
837                config.index_type
838            );
839        }
840    }
841}
842
843/// Parse sparse_vector_config - only index_size (positional)
844/// Example: <u16> or <u32>
845fn parse_sparse_vector_config(pair: pest::iterators::Pair<Rule>) -> SparseVectorConfig {
846    let mut index_size = IndexSize::default();
847
848    // Parse positional index_size_spec
849    for inner in pair.into_inner() {
850        if inner.as_rule() == Rule::index_size_spec {
851            index_size = match inner.as_str() {
852                "u16" => IndexSize::U16,
853                "u32" => IndexSize::U32,
854                _ => IndexSize::default(),
855            };
856        }
857    }
858
859    SparseVectorConfig {
860        format: SparseFormat::default(),
861        index_size,
862        weight_quantization: WeightQuantization::default(),
863        weight_threshold: 0.0,
864        doc_mass: None,
865        block_size: 128,
866        bmp_block_size: 64,
867        max_bmp_grid_bytes: 0,
868        bmp_superblock_size: 64,
869        pruning: None,
870        query_config: None,
871        dims: None,
872        max_weight: None,
873        min_terms: 4,
874    }
875}
876
877/// Apply index configuration from indexed<...> to SparseVectorConfig
878fn apply_index_config_to_sparse_vector(config: &mut SparseVectorConfig, idx_cfg: IndexConfig) {
879    if let Some(f) = idx_cfg.sparse_format {
880        config.format = f;
881    }
882    if let Some(q) = idx_cfg.quantization {
883        config.weight_quantization = q;
884    }
885    if let Some(t) = idx_cfg.weight_threshold {
886        config.weight_threshold = t;
887    }
888    if let Some(bs) = idx_cfg.block_size {
889        let adjusted = bs.next_power_of_two();
890        if adjusted != bs {
891            log::warn!(
892                "block_size {} adjusted to next power of two: {}",
893                bs,
894                adjusted
895            );
896        }
897        config.block_size = adjusted;
898    }
899    if let Some(p) = idx_cfg.pruning {
900        let clamped = p.clamp(0.0, 1.0);
901        if (clamped - p).abs() > f32::EPSILON {
902            log::warn!(
903                "pruning {} clamped to valid range [0.0, 1.0]: {}",
904                p,
905                clamped
906            );
907        }
908        config.pruning = Some(clamped);
909    }
910    if let Some(mt) = idx_cfg.min_terms {
911        config.min_terms = mt;
912    }
913    if let Some(dm) = idx_cfg.doc_mass {
914        let clamped = dm.clamp(0.0, 1.0);
915        if (clamped - dm).abs() > f32::EPSILON {
916            log::warn!(
917                "doc_mass {} clamped to valid range [0.0, 1.0]: {}",
918                dm,
919                clamped
920            );
921        }
922        config.doc_mass = Some(clamped);
923    }
924    if let Some(d) = idx_cfg.dims {
925        config.dims = Some(d);
926    }
927    if let Some(mw) = idx_cfg.max_weight {
928        config.max_weight = Some(mw);
929    }
930    // Apply query-time configuration if present
931    if idx_cfg.query_tokenizer.is_some()
932        || idx_cfg.query_weighting.is_some()
933        || idx_cfg.query_weight_threshold.is_some()
934        || idx_cfg.query_max_dims.is_some()
935        || idx_cfg.query_pruning.is_some()
936        || idx_cfg.query_min_query_dims.is_some()
937    {
938        let query_config = config
939            .query_config
940            .get_or_insert(SparseQueryConfig::default());
941        if let Some(tokenizer) = idx_cfg.query_tokenizer {
942            query_config.tokenizer = Some(tokenizer);
943        }
944        if let Some(weighting) = idx_cfg.query_weighting {
945            query_config.weighting = weighting;
946        }
947        if let Some(t) = idx_cfg.query_weight_threshold {
948            query_config.weight_threshold = t;
949        }
950        if let Some(d) = idx_cfg.query_max_dims {
951            query_config.max_query_dims = Some(d);
952        }
953        if let Some(p) = idx_cfg.query_pruning {
954            query_config.pruning = Some(p);
955        }
956        if let Some(m) = idx_cfg.query_min_query_dims {
957            query_config.min_query_dims = m;
958        }
959    }
960}
961
962/// Parse dense_vector_config - dims and optional quantization type
963/// All index-related params are in indexed<...> attribute
964fn parse_dense_vector_config(pair: pest::iterators::Pair<Rule>) -> DenseVectorConfig {
965    let mut dim: usize = 0;
966    let mut quantization = DenseVectorQuantization::F32;
967
968    // Navigate to dense_vector_params
969    for params in pair.into_inner() {
970        if params.as_rule() == Rule::dense_vector_params {
971            for inner in params.into_inner() {
972                match inner.as_rule() {
973                    Rule::dense_vector_keyword_params => {
974                        for kwarg in inner.into_inner() {
975                            match kwarg.as_rule() {
976                                Rule::dims_kwarg => {
977                                    if let Some(d) = kwarg.into_inner().next() {
978                                        dim = d.as_str().parse().unwrap_or(0);
979                                    }
980                                }
981                                Rule::quant_type_spec => {
982                                    quantization = parse_quant_type(kwarg.as_str());
983                                }
984                                _ => {}
985                            }
986                        }
987                    }
988                    Rule::dense_vector_positional_params => {
989                        for item in inner.into_inner() {
990                            match item.as_rule() {
991                                Rule::dimension_spec => {
992                                    dim = item.as_str().parse().unwrap_or(0);
993                                }
994                                Rule::quant_type_spec => {
995                                    quantization = parse_quant_type(item.as_str());
996                                }
997                                _ => {}
998                            }
999                        }
1000                    }
1001                    _ => {}
1002                }
1003            }
1004        }
1005    }
1006
1007    DenseVectorConfig::new(dim).with_quantization(quantization)
1008}
1009
1010fn parse_quant_type(s: &str) -> DenseVectorQuantization {
1011    match s.trim() {
1012        "f16" => DenseVectorQuantization::F16,
1013        "uint8" | "u8" => DenseVectorQuantization::UInt8,
1014        _ => DenseVectorQuantization::F32,
1015    }
1016}
1017
1018/// Parse default_fields definition
1019fn parse_default_fields_def(pair: pest::iterators::Pair<Rule>) -> Vec<String> {
1020    pair.into_inner().map(|p| p.as_str().to_string()).collect()
1021}
1022
1023/// Parse a query router definition
1024fn parse_query_router_def(pair: pest::iterators::Pair<Rule>) -> Result<QueryRouterRule> {
1025    let mut pattern = String::new();
1026    let mut substitution = String::new();
1027    let mut target_field = String::new();
1028    let mut mode = RoutingMode::Additional;
1029
1030    for prop in pair.into_inner() {
1031        if prop.as_rule() != Rule::query_router_prop {
1032            continue;
1033        }
1034
1035        for inner in prop.into_inner() {
1036            match inner.as_rule() {
1037                Rule::query_router_pattern => {
1038                    if let Some(regex_str) = inner.into_inner().next() {
1039                        pattern = parse_string_value(regex_str);
1040                    }
1041                }
1042                Rule::query_router_substitution => {
1043                    if let Some(quoted) = inner.into_inner().next() {
1044                        substitution = parse_string_value(quoted);
1045                    }
1046                }
1047                Rule::query_router_target => {
1048                    if let Some(ident) = inner.into_inner().next() {
1049                        target_field = ident.as_str().to_string();
1050                    }
1051                }
1052                Rule::query_router_mode => {
1053                    if let Some(mode_val) = inner.into_inner().next() {
1054                        mode = match mode_val.as_str() {
1055                            "exclusive" => RoutingMode::Exclusive,
1056                            "additional" => RoutingMode::Additional,
1057                            _ => RoutingMode::Additional,
1058                        };
1059                    }
1060                }
1061                _ => {}
1062            }
1063        }
1064    }
1065
1066    if pattern.is_empty() {
1067        return Err(Error::Schema("query_router missing 'pattern'".to_string()));
1068    }
1069    if substitution.is_empty() {
1070        return Err(Error::Schema(
1071            "query_router missing 'substitution'".to_string(),
1072        ));
1073    }
1074    if target_field.is_empty() {
1075        return Err(Error::Schema(
1076            "query_router missing 'target_field'".to_string(),
1077        ));
1078    }
1079
1080    Ok(QueryRouterRule {
1081        pattern,
1082        substitution,
1083        target_field,
1084        mode,
1085    })
1086}
1087
1088/// Parse a string value from quoted_string, raw_string, or regex_string
1089fn parse_string_value(pair: pest::iterators::Pair<Rule>) -> String {
1090    let s = pair.as_str();
1091    match pair.as_rule() {
1092        Rule::regex_string => {
1093            // regex_string contains either raw_string or quoted_string
1094            if let Some(inner) = pair.into_inner().next() {
1095                parse_string_value(inner)
1096            } else {
1097                s.to_string()
1098            }
1099        }
1100        Rule::raw_string => {
1101            // r"..." - strip r" prefix and " suffix
1102            s[2..s.len() - 1].to_string()
1103        }
1104        Rule::quoted_string => {
1105            // "..." - strip quotes and handle escapes
1106            let inner = &s[1..s.len() - 1];
1107            // Simple escape handling
1108            inner
1109                .replace("\\n", "\n")
1110                .replace("\\t", "\t")
1111                .replace("\\\"", "\"")
1112                .replace("\\\\", "\\")
1113        }
1114        _ => s.to_string(),
1115    }
1116}
1117
1118/// Parse an index definition from pest pair
1119fn parse_index_def(pair: pest::iterators::Pair<Rule>) -> Result<IndexDef> {
1120    let mut inner = pair.into_inner();
1121
1122    let name = inner
1123        .next()
1124        .ok_or_else(|| Error::Schema("Missing index name".to_string()))?
1125        .as_str()
1126        .to_string();
1127
1128    let mut fields = Vec::new();
1129    let mut default_fields = Vec::new();
1130    let mut query_routers = Vec::new();
1131    let mut reorder_on_merge = false;
1132
1133    for item in inner {
1134        match item.as_rule() {
1135            Rule::field_def => {
1136                fields.push(parse_field_def(item)?);
1137            }
1138            Rule::default_fields_def => {
1139                default_fields = parse_default_fields_def(item);
1140            }
1141            Rule::query_router_def => {
1142                query_routers.push(parse_query_router_def(item)?);
1143            }
1144            Rule::reorder_on_merge_def => {
1145                let value = item
1146                    .into_inner()
1147                    .next()
1148                    .map(|b| b.as_str() == "true")
1149                    .unwrap_or(false);
1150                reorder_on_merge = value;
1151            }
1152            _ => {}
1153        }
1154    }
1155
1156    // Validate primary key constraints
1157    let primary_fields: Vec<&FieldDef> = fields.iter().filter(|f| f.primary).collect();
1158    if primary_fields.len() > 1 {
1159        return Err(Error::Schema(format!(
1160            "Index '{}' has {} primary key fields, but at most one is allowed",
1161            name,
1162            primary_fields.len()
1163        )));
1164    }
1165    if let Some(pk) = primary_fields.first() {
1166        if pk.field_type != FieldType::Text {
1167            return Err(Error::Schema(format!(
1168                "Primary key field '{}' must be of type text, got {:?}",
1169                pk.name, pk.field_type
1170            )));
1171        }
1172        if pk.multi {
1173            return Err(Error::Schema(format!(
1174                "Primary key field '{}' cannot be multi-valued",
1175                pk.name
1176            )));
1177        }
1178    }
1179
1180    Ok(IndexDef {
1181        name,
1182        fields,
1183        default_fields,
1184        query_routers,
1185        reorder_on_merge,
1186    })
1187}
1188
1189/// Parse SDL from a string
1190pub fn parse_sdl(input: &str) -> Result<Vec<IndexDef>> {
1191    let pairs = SdlParser::parse(Rule::file, input)
1192        .map_err(|e| Error::Schema(format!("Parse error: {}", e)))?;
1193
1194    let mut indexes = Vec::new();
1195
1196    for pair in pairs {
1197        if pair.as_rule() == Rule::file {
1198            for inner in pair.into_inner() {
1199                if inner.as_rule() == Rule::index_def {
1200                    indexes.push(parse_index_def(inner)?);
1201                }
1202            }
1203        }
1204    }
1205
1206    Ok(indexes)
1207}
1208
1209/// Parse SDL and return a single index definition
1210pub fn parse_single_index(input: &str) -> Result<IndexDef> {
1211    let indexes = parse_sdl(input)?;
1212
1213    if indexes.is_empty() {
1214        return Err(Error::Schema("No index definition found".to_string()));
1215    }
1216
1217    if indexes.len() > 1 {
1218        return Err(Error::Schema(
1219            "Multiple index definitions found, expected one".to_string(),
1220        ));
1221    }
1222
1223    Ok(indexes.into_iter().next().unwrap())
1224}
1225
1226#[cfg(test)]
1227mod tests {
1228    use super::*;
1229
1230    #[test]
1231    fn test_parse_simple_schema() {
1232        let sdl = r#"
1233            index articles {
1234                field title: text [indexed, stored]
1235                field body: text [indexed]
1236            }
1237        "#;
1238
1239        let indexes = parse_sdl(sdl).unwrap();
1240        assert_eq!(indexes.len(), 1);
1241
1242        let index = &indexes[0];
1243        assert_eq!(index.name, "articles");
1244        assert_eq!(index.fields.len(), 2);
1245
1246        assert_eq!(index.fields[0].name, "title");
1247        assert!(matches!(index.fields[0].field_type, FieldType::Text));
1248        assert!(index.fields[0].indexed);
1249        assert!(index.fields[0].stored);
1250
1251        assert_eq!(index.fields[1].name, "body");
1252        assert!(matches!(index.fields[1].field_type, FieldType::Text));
1253        assert!(index.fields[1].indexed);
1254        assert!(!index.fields[1].stored);
1255    }
1256
1257    #[test]
1258    fn test_parse_all_field_types() {
1259        let sdl = r#"
1260            index test {
1261                field text_field: text [indexed, stored]
1262                field u64_field: u64 [indexed, stored]
1263                field i64_field: i64 [indexed, stored]
1264                field f64_field: f64 [indexed, stored]
1265                field bytes_field: bytes [stored]
1266            }
1267        "#;
1268
1269        let indexes = parse_sdl(sdl).unwrap();
1270        let index = &indexes[0];
1271
1272        assert!(matches!(index.fields[0].field_type, FieldType::Text));
1273        assert!(matches!(index.fields[1].field_type, FieldType::U64));
1274        assert!(matches!(index.fields[2].field_type, FieldType::I64));
1275        assert!(matches!(index.fields[3].field_type, FieldType::F64));
1276        assert!(matches!(index.fields[4].field_type, FieldType::Bytes));
1277    }
1278
1279    #[test]
1280    fn test_parse_with_comments() {
1281        let sdl = r#"
1282            # This is a comment
1283            index articles {
1284                # Title field
1285                field title: text [indexed, stored]
1286                field body: text [indexed] # inline comment not supported yet
1287            }
1288        "#;
1289
1290        let indexes = parse_sdl(sdl).unwrap();
1291        assert_eq!(indexes[0].fields.len(), 2);
1292    }
1293
1294    #[test]
1295    fn test_parse_type_aliases() {
1296        let sdl = r#"
1297            index test {
1298                field a: string [indexed]
1299                field b: int [indexed]
1300                field c: uint [indexed]
1301                field d: float [indexed]
1302                field e: binary [stored]
1303            }
1304        "#;
1305
1306        let indexes = parse_sdl(sdl).unwrap();
1307        let index = &indexes[0];
1308
1309        assert!(matches!(index.fields[0].field_type, FieldType::Text));
1310        assert!(matches!(index.fields[1].field_type, FieldType::I64));
1311        assert!(matches!(index.fields[2].field_type, FieldType::U64));
1312        assert!(matches!(index.fields[3].field_type, FieldType::F64));
1313        assert!(matches!(index.fields[4].field_type, FieldType::Bytes));
1314    }
1315
1316    #[test]
1317    fn test_to_schema() {
1318        let sdl = r#"
1319            index articles {
1320                field title: text [indexed, stored]
1321                field views: u64 [indexed, stored]
1322            }
1323        "#;
1324
1325        let indexes = parse_sdl(sdl).unwrap();
1326        let schema = indexes[0].to_schema();
1327
1328        assert!(schema.get_field("title").is_some());
1329        assert!(schema.get_field("views").is_some());
1330        assert!(schema.get_field("nonexistent").is_none());
1331    }
1332
1333    #[test]
1334    fn test_default_attributes() {
1335        let sdl = r#"
1336            index test {
1337                field title: text
1338            }
1339        "#;
1340
1341        let indexes = parse_sdl(sdl).unwrap();
1342        let field = &indexes[0].fields[0];
1343
1344        // Default should be indexed and stored
1345        assert!(field.indexed);
1346        assert!(field.stored);
1347    }
1348
1349    #[test]
1350    fn test_multiple_indexes() {
1351        let sdl = r#"
1352            index articles {
1353                field title: text [indexed, stored]
1354            }
1355
1356            index users {
1357                field name: text [indexed, stored]
1358                field email: text [indexed, stored]
1359            }
1360        "#;
1361
1362        let indexes = parse_sdl(sdl).unwrap();
1363        assert_eq!(indexes.len(), 2);
1364        assert_eq!(indexes[0].name, "articles");
1365        assert_eq!(indexes[1].name, "users");
1366    }
1367
1368    #[test]
1369    fn test_tokenizer_spec() {
1370        let sdl = r#"
1371            index articles {
1372                field title: text<en_stem> [indexed, stored]
1373                field body: text<simple> [indexed]
1374                field author: text [indexed, stored]
1375            }
1376        "#;
1377
1378        let indexes = parse_sdl(sdl).unwrap();
1379        let index = &indexes[0];
1380
1381        assert_eq!(index.fields[0].name, "title");
1382        assert_eq!(index.fields[0].tokenizer, Some("en_stem".to_string()));
1383
1384        assert_eq!(index.fields[1].name, "body");
1385        assert_eq!(index.fields[1].tokenizer, Some("simple".to_string()));
1386
1387        assert_eq!(index.fields[2].name, "author");
1388        assert_eq!(index.fields[2].tokenizer, None); // No tokenizer specified
1389    }
1390
1391    #[test]
1392    fn test_tokenizer_in_schema() {
1393        let sdl = r#"
1394            index articles {
1395                field title: text<german> [indexed, stored]
1396                field body: text<en_stem> [indexed]
1397            }
1398        "#;
1399
1400        let indexes = parse_sdl(sdl).unwrap();
1401        let schema = indexes[0].to_schema();
1402
1403        let title_field = schema.get_field("title").unwrap();
1404        let title_entry = schema.get_field_entry(title_field).unwrap();
1405        assert_eq!(title_entry.tokenizer, Some("german".to_string()));
1406
1407        let body_field = schema.get_field("body").unwrap();
1408        let body_entry = schema.get_field_entry(body_field).unwrap();
1409        assert_eq!(body_entry.tokenizer, Some("en_stem".to_string()));
1410    }
1411
1412    #[test]
1413    fn test_query_router_basic() {
1414        let sdl = r#"
1415            index documents {
1416                field title: text [indexed, stored]
1417                field uri: text [indexed, stored]
1418
1419                query_router {
1420                    pattern: "10\\.\\d{4,}/[^\\s]+"
1421                    substitution: "doi://{0}"
1422                    target_field: uris
1423                    mode: exclusive
1424                }
1425            }
1426        "#;
1427
1428        let indexes = parse_sdl(sdl).unwrap();
1429        let index = &indexes[0];
1430
1431        assert_eq!(index.query_routers.len(), 1);
1432        let router = &index.query_routers[0];
1433        assert_eq!(router.pattern, r"10\.\d{4,}/[^\s]+");
1434        assert_eq!(router.substitution, "doi://{0}");
1435        assert_eq!(router.target_field, "uris");
1436        assert_eq!(router.mode, RoutingMode::Exclusive);
1437    }
1438
1439    #[test]
1440    fn test_query_router_raw_string() {
1441        let sdl = r#"
1442            index documents {
1443                field uris: text [indexed, stored]
1444
1445                query_router {
1446                    pattern: r"^pmid:(\d+)$"
1447                    substitution: "pubmed://{1}"
1448                    target_field: uris
1449                    mode: additional
1450                }
1451            }
1452        "#;
1453
1454        let indexes = parse_sdl(sdl).unwrap();
1455        let router = &indexes[0].query_routers[0];
1456
1457        assert_eq!(router.pattern, r"^pmid:(\d+)$");
1458        assert_eq!(router.substitution, "pubmed://{1}");
1459        assert_eq!(router.mode, RoutingMode::Additional);
1460    }
1461
1462    #[test]
1463    fn test_multiple_query_routers() {
1464        let sdl = r#"
1465            index documents {
1466                field uris: text [indexed, stored]
1467
1468                query_router {
1469                    pattern: r"^doi:(10\.\d{4,}/[^\s]+)$"
1470                    substitution: "doi://{1}"
1471                    target_field: uris
1472                    mode: exclusive
1473                }
1474
1475                query_router {
1476                    pattern: r"^pmid:(\d+)$"
1477                    substitution: "pubmed://{1}"
1478                    target_field: uris
1479                    mode: exclusive
1480                }
1481
1482                query_router {
1483                    pattern: r"^arxiv:(\d+\.\d+)$"
1484                    substitution: "arxiv://{1}"
1485                    target_field: uris
1486                    mode: additional
1487                }
1488            }
1489        "#;
1490
1491        let indexes = parse_sdl(sdl).unwrap();
1492        assert_eq!(indexes[0].query_routers.len(), 3);
1493    }
1494
1495    #[test]
1496    fn test_query_router_default_mode() {
1497        let sdl = r#"
1498            index documents {
1499                field uris: text [indexed, stored]
1500
1501                query_router {
1502                    pattern: r"test"
1503                    substitution: "{0}"
1504                    target_field: uris
1505                }
1506            }
1507        "#;
1508
1509        let indexes = parse_sdl(sdl).unwrap();
1510        // Default mode should be Additional
1511        assert_eq!(indexes[0].query_routers[0].mode, RoutingMode::Additional);
1512    }
1513
1514    #[test]
1515    fn test_multi_attribute() {
1516        let sdl = r#"
1517            index documents {
1518                field uris: text [indexed, stored<multi>]
1519                field title: text [indexed, stored]
1520            }
1521        "#;
1522
1523        let indexes = parse_sdl(sdl).unwrap();
1524        assert_eq!(indexes.len(), 1);
1525
1526        let fields = &indexes[0].fields;
1527        assert_eq!(fields.len(), 2);
1528
1529        // uris should have multi=true
1530        assert_eq!(fields[0].name, "uris");
1531        assert!(fields[0].multi, "uris field should have multi=true");
1532
1533        // title should have multi=false
1534        assert_eq!(fields[1].name, "title");
1535        assert!(!fields[1].multi, "title field should have multi=false");
1536
1537        // Verify schema conversion preserves multi attribute
1538        let schema = indexes[0].to_schema();
1539        let uris_field = schema.get_field("uris").unwrap();
1540        let title_field = schema.get_field("title").unwrap();
1541
1542        assert!(schema.get_field_entry(uris_field).unwrap().multi);
1543        assert!(!schema.get_field_entry(title_field).unwrap().multi);
1544    }
1545
1546    #[test]
1547    fn test_sparse_vector_field() {
1548        let sdl = r#"
1549            index documents {
1550                field embedding: sparse_vector [indexed, stored]
1551            }
1552        "#;
1553
1554        let indexes = parse_sdl(sdl).unwrap();
1555        assert_eq!(indexes.len(), 1);
1556        assert_eq!(indexes[0].fields.len(), 1);
1557        assert_eq!(indexes[0].fields[0].name, "embedding");
1558        assert_eq!(indexes[0].fields[0].field_type, FieldType::SparseVector);
1559        assert!(indexes[0].fields[0].sparse_vector_config.is_none());
1560    }
1561
1562    #[test]
1563    fn test_sparse_vector_with_config() {
1564        let sdl = r#"
1565            index documents {
1566                field embedding: sparse_vector<u16> [indexed<quantization: uint8>, stored]
1567                field dense: sparse_vector<u32> [indexed<quantization: float32>]
1568            }
1569        "#;
1570
1571        let indexes = parse_sdl(sdl).unwrap();
1572        assert_eq!(indexes[0].fields.len(), 2);
1573
1574        // First field: u16 indices, uint8 quantization
1575        let f1 = &indexes[0].fields[0];
1576        assert_eq!(f1.name, "embedding");
1577        let config1 = f1.sparse_vector_config.as_ref().unwrap();
1578        assert_eq!(config1.index_size, IndexSize::U16);
1579        assert_eq!(config1.weight_quantization, WeightQuantization::UInt8);
1580
1581        // Second field: u32 indices, float32 quantization
1582        let f2 = &indexes[0].fields[1];
1583        assert_eq!(f2.name, "dense");
1584        let config2 = f2.sparse_vector_config.as_ref().unwrap();
1585        assert_eq!(config2.index_size, IndexSize::U32);
1586        assert_eq!(config2.weight_quantization, WeightQuantization::Float32);
1587    }
1588
1589    #[test]
1590    fn test_sparse_vector_with_weight_threshold() {
1591        let sdl = r#"
1592            index documents {
1593                field embedding: sparse_vector<u16> [indexed<quantization: uint8, weight_threshold: 0.1>, stored]
1594                field embedding2: sparse_vector<u32> [indexed<quantization: float16, weight_threshold: 0.05>]
1595            }
1596        "#;
1597
1598        let indexes = parse_sdl(sdl).unwrap();
1599        assert_eq!(indexes[0].fields.len(), 2);
1600
1601        // First field: u16 indices, uint8 quantization, threshold 0.1
1602        let f1 = &indexes[0].fields[0];
1603        assert_eq!(f1.name, "embedding");
1604        let config1 = f1.sparse_vector_config.as_ref().unwrap();
1605        assert_eq!(config1.index_size, IndexSize::U16);
1606        assert_eq!(config1.weight_quantization, WeightQuantization::UInt8);
1607        assert!((config1.weight_threshold - 0.1).abs() < 0.001);
1608
1609        // Second field: u32 indices, float16 quantization, threshold 0.05
1610        let f2 = &indexes[0].fields[1];
1611        assert_eq!(f2.name, "embedding2");
1612        let config2 = f2.sparse_vector_config.as_ref().unwrap();
1613        assert_eq!(config2.index_size, IndexSize::U32);
1614        assert_eq!(config2.weight_quantization, WeightQuantization::Float16);
1615        assert!((config2.weight_threshold - 0.05).abs() < 0.001);
1616    }
1617
1618    #[test]
1619    fn test_sparse_vector_with_pruning() {
1620        let sdl = r#"
1621            index documents {
1622                field embedding: sparse_vector [indexed<quantization: uint8, pruning: 0.1>, stored]
1623            }
1624        "#;
1625
1626        let indexes = parse_sdl(sdl).unwrap();
1627        let f = &indexes[0].fields[0];
1628        assert_eq!(f.name, "embedding");
1629        let config = f.sparse_vector_config.as_ref().unwrap();
1630        assert_eq!(config.weight_quantization, WeightQuantization::UInt8);
1631        assert_eq!(config.pruning, Some(0.1));
1632    }
1633
1634    #[test]
1635    fn test_sparse_vector_with_doc_mass() {
1636        let sdl = r#"
1637            index documents {
1638                field embedding: sparse_vector [indexed<quantization: uint8, doc_mass: 0.9>, stored]
1639            }
1640        "#;
1641
1642        let indexes = parse_sdl(sdl).unwrap();
1643        let config = indexes[0].fields[0].sparse_vector_config.as_ref().unwrap();
1644        assert_eq!(config.doc_mass, Some(0.9));
1645
1646        // Not specified → off
1647        let sdl = r#"
1648            index documents {
1649                field embedding: sparse_vector [indexed<quantization: uint8>]
1650            }
1651        "#;
1652        let indexes = parse_sdl(sdl).unwrap();
1653        let config = indexes[0].fields[0].sparse_vector_config.as_ref().unwrap();
1654        assert_eq!(config.doc_mass, None);
1655    }
1656
1657    #[test]
1658    fn test_dense_vector_field() {
1659        let sdl = r#"
1660            index documents {
1661                field embedding: dense_vector<768> [indexed, stored]
1662            }
1663        "#;
1664
1665        let indexes = parse_sdl(sdl).unwrap();
1666        assert_eq!(indexes.len(), 1);
1667        assert_eq!(indexes[0].fields.len(), 1);
1668
1669        let f = &indexes[0].fields[0];
1670        assert_eq!(f.name, "embedding");
1671        assert_eq!(f.field_type, FieldType::DenseVector);
1672
1673        let config = f.dense_vector_config.as_ref().unwrap();
1674        assert_eq!(config.dim, 768);
1675    }
1676
1677    #[test]
1678    fn test_dense_vector_alias() {
1679        let sdl = r#"
1680            index documents {
1681                field embedding: vector<1536> [indexed]
1682            }
1683        "#;
1684
1685        let indexes = parse_sdl(sdl).unwrap();
1686        assert_eq!(indexes[0].fields[0].field_type, FieldType::DenseVector);
1687        assert_eq!(
1688            indexes[0].fields[0]
1689                .dense_vector_config
1690                .as_ref()
1691                .unwrap()
1692                .dim,
1693            1536
1694        );
1695    }
1696
1697    #[test]
1698    fn test_dense_vector_with_num_clusters() {
1699        let sdl = r#"
1700            index documents {
1701                field embedding: dense_vector<768> [indexed<ivf_rabitq, num_clusters: 256>, stored]
1702            }
1703        "#;
1704
1705        let indexes = parse_sdl(sdl).unwrap();
1706        assert_eq!(indexes.len(), 1);
1707
1708        let f = &indexes[0].fields[0];
1709        assert_eq!(f.name, "embedding");
1710        assert_eq!(f.field_type, FieldType::DenseVector);
1711
1712        let config = f.dense_vector_config.as_ref().unwrap();
1713        assert_eq!(config.dim, 768);
1714        assert_eq!(config.num_clusters, Some(256));
1715        assert_eq!(config.nprobe, 32); // default
1716    }
1717
1718    #[test]
1719    fn test_dense_vector_with_soar() {
1720        let sdl = r#"
1721            index documents {
1722                field embedding: dense_vector<768> [indexed<ivf_rabitq, num_clusters: 256, soar: selective>, stored]
1723            }
1724        "#;
1725
1726        let indexes = parse_sdl(sdl).unwrap();
1727        let config = indexes[0].fields[0].dense_vector_config.as_ref().unwrap();
1728
1729        let soar = config.soar.as_ref().expect("soar should be enabled");
1730        assert_eq!(soar.num_secondary, 1);
1731        assert!(soar.selective);
1732
1733        // aggressive preset: 2 secondary clusters, no selectivity
1734        let sdl = r#"
1735            index documents {
1736                field embedding: dense_vector<768> [indexed<scann, soar: aggressive>]
1737            }
1738        "#;
1739        let indexes = parse_sdl(sdl).unwrap();
1740        let config = indexes[0].fields[0].dense_vector_config.as_ref().unwrap();
1741        let soar = config.soar.as_ref().expect("soar should be enabled");
1742        assert_eq!(soar.num_secondary, 2);
1743        assert!(!soar.selective);
1744
1745        // off keeps soar disabled
1746        let sdl = r#"
1747            index documents {
1748                field embedding: dense_vector<768> [indexed<ivf_rabitq, soar: off>]
1749            }
1750        "#;
1751        let indexes = parse_sdl(sdl).unwrap();
1752        let config = indexes[0].fields[0].dense_vector_config.as_ref().unwrap();
1753        assert!(config.soar.is_none());
1754    }
1755
1756    #[test]
1757    fn test_binary_dense_vector_with_ivf() {
1758        let sdl = r#"
1759            index documents {
1760                field hash: binary_dense_vector<512> [indexed<ivf, num_clusters: 128, nprobe: 16>, stored]
1761            }
1762        "#;
1763
1764        let indexes = parse_sdl(sdl).unwrap();
1765        let config = indexes[0].fields[0]
1766            .binary_dense_vector_config
1767            .as_ref()
1768            .unwrap();
1769        assert_eq!(config.dim, 512);
1770        assert_eq!(
1771            config.index_type,
1772            super::super::schema::BinaryIndexType::Ivf
1773        );
1774        assert_eq!(config.num_clusters, Some(128));
1775        assert_eq!(config.nprobe, 16);
1776
1777        // Default stays flat
1778        let sdl = r#"
1779            index documents {
1780                field hash: binary_dense_vector<512> [indexed]
1781            }
1782        "#;
1783        let indexes = parse_sdl(sdl).unwrap();
1784        let config = indexes[0].fields[0]
1785            .binary_dense_vector_config
1786            .as_ref()
1787            .unwrap();
1788        assert_eq!(
1789            config.index_type,
1790            super::super::schema::BinaryIndexType::Flat
1791        );
1792    }
1793
1794    #[test]
1795    fn test_dense_vector_with_rabitq_bits() {
1796        let sdl = r#"
1797            index documents {
1798                field embedding: dense_vector<768> [indexed<ivf_rabitq, bits: 4>, stored]
1799            }
1800        "#;
1801
1802        let indexes = parse_sdl(sdl).unwrap();
1803        let config = indexes[0].fields[0].dense_vector_config.as_ref().unwrap();
1804        assert_eq!(config.rabitq_bits, Some(4));
1805
1806        // Not specified -> classic 1-bit
1807        let sdl = r#"
1808            index documents {
1809                field embedding: dense_vector<768> [indexed<ivf_rabitq>]
1810            }
1811        "#;
1812        let indexes = parse_sdl(sdl).unwrap();
1813        let config = indexes[0].fields[0].dense_vector_config.as_ref().unwrap();
1814        assert_eq!(config.rabitq_bits, None);
1815    }
1816
1817    #[test]
1818    fn test_dense_vector_with_num_clusters_and_nprobe() {
1819        let sdl = r#"
1820            index documents {
1821                field embedding: dense_vector<1536> [indexed<ivf_rabitq, num_clusters: 512, nprobe: 64>]
1822            }
1823        "#;
1824
1825        let indexes = parse_sdl(sdl).unwrap();
1826        let config = indexes[0].fields[0].dense_vector_config.as_ref().unwrap();
1827
1828        assert_eq!(config.dim, 1536);
1829        assert_eq!(config.num_clusters, Some(512));
1830        assert_eq!(config.nprobe, 64);
1831    }
1832
1833    #[test]
1834    fn test_dense_vector_keyword_syntax() {
1835        let sdl = r#"
1836            index documents {
1837                field embedding: dense_vector<dims: 1536> [indexed, stored]
1838            }
1839        "#;
1840
1841        let indexes = parse_sdl(sdl).unwrap();
1842        let config = indexes[0].fields[0].dense_vector_config.as_ref().unwrap();
1843
1844        assert_eq!(config.dim, 1536);
1845        assert!(config.num_clusters.is_none());
1846    }
1847
1848    #[test]
1849    fn test_dense_vector_keyword_syntax_full() {
1850        let sdl = r#"
1851            index documents {
1852                field embedding: dense_vector<dims: 1536> [indexed<ivf_rabitq, num_clusters: 256, nprobe: 64>]
1853            }
1854        "#;
1855
1856        let indexes = parse_sdl(sdl).unwrap();
1857        let config = indexes[0].fields[0].dense_vector_config.as_ref().unwrap();
1858
1859        assert_eq!(config.dim, 1536);
1860        assert_eq!(config.num_clusters, Some(256));
1861        assert_eq!(config.nprobe, 64);
1862    }
1863
1864    #[test]
1865    fn test_dense_vector_keyword_syntax_partial() {
1866        let sdl = r#"
1867            index documents {
1868                field embedding: dense_vector<dims: 768> [indexed<ivf_rabitq, num_clusters: 128>]
1869            }
1870        "#;
1871
1872        let indexes = parse_sdl(sdl).unwrap();
1873        let config = indexes[0].fields[0].dense_vector_config.as_ref().unwrap();
1874
1875        assert_eq!(config.dim, 768);
1876        assert_eq!(config.num_clusters, Some(128));
1877        assert_eq!(config.nprobe, 32); // default
1878    }
1879
1880    #[test]
1881    fn test_dense_vector_scann_index() {
1882        use crate::dsl::schema::VectorIndexType;
1883
1884        let sdl = r#"
1885            index documents {
1886                field embedding: dense_vector<dims: 768> [indexed<scann, num_clusters: 256, nprobe: 64>]
1887            }
1888        "#;
1889
1890        let indexes = parse_sdl(sdl).unwrap();
1891        let config = indexes[0].fields[0].dense_vector_config.as_ref().unwrap();
1892
1893        assert_eq!(config.dim, 768);
1894        assert_eq!(config.index_type, VectorIndexType::ScaNN);
1895        assert_eq!(config.num_clusters, Some(256));
1896        assert_eq!(config.nprobe, 64);
1897    }
1898
1899    #[test]
1900    fn test_dense_vector_ivf_rabitq_index() {
1901        use crate::dsl::schema::VectorIndexType;
1902
1903        let sdl = r#"
1904            index documents {
1905                field embedding: dense_vector<dims: 1536> [indexed<ivf_rabitq, num_clusters: 512>]
1906            }
1907        "#;
1908
1909        let indexes = parse_sdl(sdl).unwrap();
1910        let config = indexes[0].fields[0].dense_vector_config.as_ref().unwrap();
1911
1912        assert_eq!(config.dim, 1536);
1913        assert_eq!(config.index_type, VectorIndexType::IvfRaBitQ);
1914        assert_eq!(config.num_clusters, Some(512));
1915    }
1916
1917    #[test]
1918    fn test_dense_vector_rabitq_no_clusters() {
1919        use crate::dsl::schema::VectorIndexType;
1920
1921        let sdl = r#"
1922            index documents {
1923                field embedding: dense_vector<dims: 768> [indexed<rabitq>]
1924            }
1925        "#;
1926
1927        let indexes = parse_sdl(sdl).unwrap();
1928        let config = indexes[0].fields[0].dense_vector_config.as_ref().unwrap();
1929
1930        assert_eq!(config.dim, 768);
1931        assert_eq!(config.index_type, VectorIndexType::RaBitQ);
1932        assert!(config.num_clusters.is_none());
1933    }
1934
1935    #[test]
1936    fn test_dense_vector_flat_index() {
1937        use crate::dsl::schema::VectorIndexType;
1938
1939        let sdl = r#"
1940            index documents {
1941                field embedding: dense_vector<dims: 768> [indexed<flat>]
1942            }
1943        "#;
1944
1945        let indexes = parse_sdl(sdl).unwrap();
1946        let config = indexes[0].fields[0].dense_vector_config.as_ref().unwrap();
1947
1948        assert_eq!(config.dim, 768);
1949        assert_eq!(config.index_type, VectorIndexType::Flat);
1950    }
1951
1952    #[test]
1953    fn test_dense_vector_default_index_type() {
1954        use crate::dsl::schema::VectorIndexType;
1955
1956        // When no index type specified, should default to RaBitQ (basic)
1957        let sdl = r#"
1958            index documents {
1959                field embedding: dense_vector<dims: 768> [indexed]
1960            }
1961        "#;
1962
1963        let indexes = parse_sdl(sdl).unwrap();
1964        let config = indexes[0].fields[0].dense_vector_config.as_ref().unwrap();
1965
1966        assert_eq!(config.dim, 768);
1967        assert_eq!(config.index_type, VectorIndexType::RaBitQ);
1968    }
1969
1970    #[test]
1971    fn test_dense_vector_f16_quantization() {
1972        use crate::dsl::schema::{DenseVectorQuantization, VectorIndexType};
1973
1974        let sdl = r#"
1975            index documents {
1976                field embedding: dense_vector<768, f16> [indexed]
1977            }
1978        "#;
1979
1980        let indexes = parse_sdl(sdl).unwrap();
1981        let config = indexes[0].fields[0].dense_vector_config.as_ref().unwrap();
1982
1983        assert_eq!(config.dim, 768);
1984        assert_eq!(config.quantization, DenseVectorQuantization::F16);
1985        assert_eq!(config.index_type, VectorIndexType::RaBitQ);
1986    }
1987
1988    #[test]
1989    fn test_dense_vector_uint8_quantization() {
1990        use crate::dsl::schema::DenseVectorQuantization;
1991
1992        let sdl = r#"
1993            index documents {
1994                field embedding: dense_vector<1024, uint8> [indexed<ivf_rabitq>]
1995            }
1996        "#;
1997
1998        let indexes = parse_sdl(sdl).unwrap();
1999        let config = indexes[0].fields[0].dense_vector_config.as_ref().unwrap();
2000
2001        assert_eq!(config.dim, 1024);
2002        assert_eq!(config.quantization, DenseVectorQuantization::UInt8);
2003    }
2004
2005    #[test]
2006    fn test_dense_vector_u8_alias() {
2007        use crate::dsl::schema::DenseVectorQuantization;
2008
2009        let sdl = r#"
2010            index documents {
2011                field embedding: dense_vector<512, u8> [indexed]
2012            }
2013        "#;
2014
2015        let indexes = parse_sdl(sdl).unwrap();
2016        let config = indexes[0].fields[0].dense_vector_config.as_ref().unwrap();
2017
2018        assert_eq!(config.dim, 512);
2019        assert_eq!(config.quantization, DenseVectorQuantization::UInt8);
2020    }
2021
2022    #[test]
2023    fn test_dense_vector_default_f32_quantization() {
2024        use crate::dsl::schema::DenseVectorQuantization;
2025
2026        // No quantization type → default f32
2027        let sdl = r#"
2028            index documents {
2029                field embedding: dense_vector<768> [indexed]
2030            }
2031        "#;
2032
2033        let indexes = parse_sdl(sdl).unwrap();
2034        let config = indexes[0].fields[0].dense_vector_config.as_ref().unwrap();
2035
2036        assert_eq!(config.dim, 768);
2037        assert_eq!(config.quantization, DenseVectorQuantization::F32);
2038    }
2039
2040    #[test]
2041    fn test_dense_vector_keyword_with_quantization() {
2042        use crate::dsl::schema::DenseVectorQuantization;
2043
2044        let sdl = r#"
2045            index documents {
2046                field embedding: dense_vector<dims: 768, f16> [indexed]
2047            }
2048        "#;
2049
2050        let indexes = parse_sdl(sdl).unwrap();
2051        let config = indexes[0].fields[0].dense_vector_config.as_ref().unwrap();
2052
2053        assert_eq!(config.dim, 768);
2054        assert_eq!(config.quantization, DenseVectorQuantization::F16);
2055    }
2056
2057    #[test]
2058    fn test_json_field_type() {
2059        let sdl = r#"
2060            index documents {
2061                field title: text [indexed, stored]
2062                field metadata: json [stored]
2063                field extra: json
2064            }
2065        "#;
2066
2067        let indexes = parse_sdl(sdl).unwrap();
2068        let index = &indexes[0];
2069
2070        assert_eq!(index.fields.len(), 3);
2071
2072        // Check JSON field
2073        assert_eq!(index.fields[1].name, "metadata");
2074        assert!(matches!(index.fields[1].field_type, FieldType::Json));
2075        assert!(index.fields[1].stored);
2076        // JSON fields should not be indexed (enforced by add_json_field)
2077
2078        // Check default attributes for JSON field
2079        assert_eq!(index.fields[2].name, "extra");
2080        assert!(matches!(index.fields[2].field_type, FieldType::Json));
2081
2082        // Verify schema conversion
2083        let schema = index.to_schema();
2084        let metadata_field = schema.get_field("metadata").unwrap();
2085        let entry = schema.get_field_entry(metadata_field).unwrap();
2086        assert_eq!(entry.field_type, FieldType::Json);
2087        assert!(!entry.indexed); // JSON fields are never indexed
2088        assert!(entry.stored);
2089    }
2090
2091    #[test]
2092    fn test_sparse_vector_query_config() {
2093        use crate::structures::QueryWeighting;
2094
2095        let sdl = r#"
2096            index documents {
2097                field embedding: sparse_vector<u16> [indexed<quantization: uint8, query<tokenizer: "Alibaba-NLP/gte-Qwen2-1.5B-instruct", weighting: idf>>]
2098            }
2099        "#;
2100
2101        let indexes = parse_sdl(sdl).unwrap();
2102        let index = &indexes[0];
2103
2104        assert_eq!(index.fields.len(), 1);
2105        assert_eq!(index.fields[0].name, "embedding");
2106        assert!(matches!(
2107            index.fields[0].field_type,
2108            FieldType::SparseVector
2109        ));
2110
2111        let config = index.fields[0].sparse_vector_config.as_ref().unwrap();
2112        assert_eq!(config.index_size, IndexSize::U16);
2113        assert_eq!(config.weight_quantization, WeightQuantization::UInt8);
2114
2115        // Check query config
2116        let query_config = config.query_config.as_ref().unwrap();
2117        assert_eq!(
2118            query_config.tokenizer.as_deref(),
2119            Some("Alibaba-NLP/gte-Qwen2-1.5B-instruct")
2120        );
2121        assert_eq!(query_config.weighting, QueryWeighting::Idf);
2122
2123        // Verify schema conversion preserves query config
2124        let schema = index.to_schema();
2125        let embedding_field = schema.get_field("embedding").unwrap();
2126        let entry = schema.get_field_entry(embedding_field).unwrap();
2127        let sv_config = entry.sparse_vector_config.as_ref().unwrap();
2128        let qc = sv_config.query_config.as_ref().unwrap();
2129        assert_eq!(
2130            qc.tokenizer.as_deref(),
2131            Some("Alibaba-NLP/gte-Qwen2-1.5B-instruct")
2132        );
2133        assert_eq!(qc.weighting, QueryWeighting::Idf);
2134    }
2135
2136    #[test]
2137    fn test_sparse_vector_query_config_weighting_one() {
2138        use crate::structures::QueryWeighting;
2139
2140        let sdl = r#"
2141            index documents {
2142                field embedding: sparse_vector [indexed<query<weighting: one>>]
2143            }
2144        "#;
2145
2146        let indexes = parse_sdl(sdl).unwrap();
2147        let config = indexes[0].fields[0].sparse_vector_config.as_ref().unwrap();
2148
2149        let query_config = config.query_config.as_ref().unwrap();
2150        assert!(query_config.tokenizer.is_none());
2151        assert_eq!(query_config.weighting, QueryWeighting::One);
2152    }
2153
2154    #[test]
2155    fn test_sparse_vector_query_config_weighting_idf_file() {
2156        use crate::structures::QueryWeighting;
2157
2158        let sdl = r#"
2159            index documents {
2160                field embedding: sparse_vector<u16> [indexed<quantization: uint8, query<tokenizer: "opensearch-neural-sparse-encoding-v1", weighting: idf_file>>]
2161            }
2162        "#;
2163
2164        let indexes = parse_sdl(sdl).unwrap();
2165        let config = indexes[0].fields[0].sparse_vector_config.as_ref().unwrap();
2166
2167        let query_config = config.query_config.as_ref().unwrap();
2168        assert_eq!(
2169            query_config.tokenizer.as_deref(),
2170            Some("opensearch-neural-sparse-encoding-v1")
2171        );
2172        assert_eq!(query_config.weighting, QueryWeighting::IdfFile);
2173
2174        // Verify schema conversion preserves idf_file
2175        let schema = indexes[0].to_schema();
2176        let field = schema.get_field("embedding").unwrap();
2177        let entry = schema.get_field_entry(field).unwrap();
2178        let sc = entry.sparse_vector_config.as_ref().unwrap();
2179        let qc = sc.query_config.as_ref().unwrap();
2180        assert_eq!(qc.weighting, QueryWeighting::IdfFile);
2181    }
2182
2183    #[test]
2184    fn test_sparse_vector_query_config_pruning_params() {
2185        let sdl = r#"
2186            index documents {
2187                field embedding: sparse_vector<u16> [indexed<quantization: uint8, query<weighting: idf, weight_threshold: 0.03, max_dims: 25, pruning: 0.2>>]
2188            }
2189        "#;
2190
2191        let indexes = parse_sdl(sdl).unwrap();
2192        let config = indexes[0].fields[0].sparse_vector_config.as_ref().unwrap();
2193
2194        let qc = config.query_config.as_ref().unwrap();
2195        assert_eq!(qc.weighting, QueryWeighting::Idf);
2196        assert!((qc.weight_threshold - 0.03).abs() < 0.001);
2197        assert_eq!(qc.max_query_dims, Some(25));
2198        assert!((qc.pruning.unwrap() - 0.2).abs() < 0.001);
2199
2200        // Verify schema roundtrip
2201        let schema = indexes[0].to_schema();
2202        let field = schema.get_field("embedding").unwrap();
2203        let entry = schema.get_field_entry(field).unwrap();
2204        let sc = entry.sparse_vector_config.as_ref().unwrap();
2205        let rqc = sc.query_config.as_ref().unwrap();
2206        assert!((rqc.weight_threshold - 0.03).abs() < 0.001);
2207        assert_eq!(rqc.max_query_dims, Some(25));
2208        assert!((rqc.pruning.unwrap() - 0.2).abs() < 0.001);
2209    }
2210
2211    #[test]
2212    fn test_sparse_vector_format_maxscore() {
2213        let sdl = r#"
2214            index documents {
2215                field embedding: sparse_vector<u16> [indexed<format: maxscore, quantization: uint8>]
2216            }
2217        "#;
2218
2219        let indexes = parse_sdl(sdl).unwrap();
2220        let config = indexes[0].fields[0].sparse_vector_config.as_ref().unwrap();
2221        assert_eq!(config.format, SparseFormat::MaxScore);
2222        assert_eq!(config.weight_quantization, WeightQuantization::UInt8);
2223
2224        // Verify schema roundtrip
2225        let schema = indexes[0].to_schema();
2226        let field = schema.get_field("embedding").unwrap();
2227        let entry = schema.get_field_entry(field).unwrap();
2228        let sc = entry.sparse_vector_config.as_ref().unwrap();
2229        assert_eq!(sc.format, SparseFormat::MaxScore);
2230    }
2231
2232    #[test]
2233    fn test_sparse_vector_format_bmp() {
2234        let sdl = r#"
2235            index documents {
2236                field embedding: sparse_vector<u16> [indexed<format: bmp, quantization: uint8>]
2237            }
2238        "#;
2239
2240        let indexes = parse_sdl(sdl).unwrap();
2241        let config = indexes[0].fields[0].sparse_vector_config.as_ref().unwrap();
2242        assert_eq!(config.format, SparseFormat::Bmp);
2243    }
2244
2245    #[test]
2246    fn test_fast_attribute() {
2247        let sdl = r#"
2248            index products {
2249                field name: text [indexed, stored]
2250                field price: f64 [indexed, fast]
2251                field category: text [indexed, stored, fast]
2252                field count: u64 [fast]
2253                field score: i64 [indexed, stored, fast]
2254            }
2255        "#;
2256
2257        let indexes = parse_sdl(sdl).unwrap();
2258        assert_eq!(indexes.len(), 1);
2259        let index = &indexes[0];
2260        assert_eq!(index.fields.len(), 5);
2261
2262        // name: no fast
2263        assert!(!index.fields[0].fast);
2264        // price: fast
2265        assert!(index.fields[1].fast);
2266        assert!(matches!(index.fields[1].field_type, FieldType::F64));
2267        // category: fast text
2268        assert!(index.fields[2].fast);
2269        assert!(matches!(index.fields[2].field_type, FieldType::Text));
2270        // count: fast only
2271        assert!(index.fields[3].fast);
2272        assert!(matches!(index.fields[3].field_type, FieldType::U64));
2273        // score: fast i64
2274        assert!(index.fields[4].fast);
2275        assert!(matches!(index.fields[4].field_type, FieldType::I64));
2276
2277        // Verify schema roundtrip preserves fast flag
2278        let schema = index.to_schema();
2279        let price_field = schema.get_field("price").unwrap();
2280        assert!(schema.get_field_entry(price_field).unwrap().fast);
2281
2282        let category_field = schema.get_field("category").unwrap();
2283        assert!(schema.get_field_entry(category_field).unwrap().fast);
2284
2285        let name_field = schema.get_field("name").unwrap();
2286        assert!(!schema.get_field_entry(name_field).unwrap().fast);
2287    }
2288
2289    #[test]
2290    fn test_primary_attribute() {
2291        let sdl = r#"
2292            index documents {
2293                field id: text [primary, stored]
2294                field title: text [indexed, stored]
2295            }
2296        "#;
2297
2298        let indexes = parse_sdl(sdl).unwrap();
2299        assert_eq!(indexes.len(), 1);
2300        let index = &indexes[0];
2301        assert_eq!(index.fields.len(), 2);
2302
2303        // id should be primary, and auto-set fast + indexed
2304        let id_field = &index.fields[0];
2305        assert!(id_field.primary, "id should be primary");
2306        assert!(id_field.fast, "primary implies fast");
2307        assert!(id_field.indexed, "primary implies indexed");
2308
2309        // title should NOT be primary
2310        assert!(!index.fields[1].primary);
2311
2312        // Verify schema conversion preserves primary_key
2313        let schema = index.to_schema();
2314        let id = schema.get_field("id").unwrap();
2315        let id_entry = schema.get_field_entry(id).unwrap();
2316        assert!(id_entry.primary_key);
2317        assert!(id_entry.fast);
2318        assert!(id_entry.indexed);
2319
2320        let title = schema.get_field("title").unwrap();
2321        assert!(!schema.get_field_entry(title).unwrap().primary_key);
2322
2323        // primary_field() should return the primary field
2324        assert_eq!(schema.primary_field(), Some(id));
2325    }
2326
2327    #[test]
2328    fn test_primary_with_other_attributes() {
2329        let sdl = r#"
2330            index documents {
2331                field id: text<simple> [primary, indexed, stored]
2332                field body: text [indexed]
2333            }
2334        "#;
2335
2336        let indexes = parse_sdl(sdl).unwrap();
2337        let id_field = &indexes[0].fields[0];
2338        assert!(id_field.primary);
2339        assert!(id_field.indexed);
2340        assert!(id_field.stored);
2341        assert!(id_field.fast);
2342        assert_eq!(id_field.tokenizer, Some("simple".to_string()));
2343    }
2344
2345    #[test]
2346    fn test_primary_only_one_allowed() {
2347        let sdl = r#"
2348            index documents {
2349                field id: text [primary]
2350                field alt_id: text [primary]
2351            }
2352        "#;
2353
2354        let result = parse_sdl(sdl);
2355        assert!(result.is_err());
2356        let err = result.unwrap_err().to_string();
2357        assert!(
2358            err.contains("primary key"),
2359            "Error should mention primary key: {}",
2360            err
2361        );
2362    }
2363
2364    #[test]
2365    fn test_primary_must_be_text() {
2366        let sdl = r#"
2367            index documents {
2368                field id: u64 [primary]
2369            }
2370        "#;
2371
2372        let result = parse_sdl(sdl);
2373        assert!(result.is_err());
2374        let err = result.unwrap_err().to_string();
2375        assert!(
2376            err.contains("text"),
2377            "Error should mention text type: {}",
2378            err
2379        );
2380    }
2381
2382    #[test]
2383    fn test_primary_cannot_be_multi() {
2384        let sdl = r#"
2385            index documents {
2386                field id: text [primary, stored<multi>]
2387            }
2388        "#;
2389
2390        let result = parse_sdl(sdl);
2391        assert!(result.is_err());
2392        let err = result.unwrap_err().to_string();
2393        assert!(err.contains("multi"), "Error should mention multi: {}", err);
2394    }
2395
2396    #[test]
2397    fn test_no_primary_field() {
2398        // Schema without primary field should work fine
2399        let sdl = r#"
2400            index documents {
2401                field title: text [indexed, stored]
2402            }
2403        "#;
2404
2405        let indexes = parse_sdl(sdl).unwrap();
2406        let schema = indexes[0].to_schema();
2407        assert!(schema.primary_field().is_none());
2408    }
2409
2410    #[test]
2411    fn test_reorder_attribute() {
2412        let sdl = r#"
2413            index documents {
2414                field embedding: sparse_vector<u16> [indexed<format: bmp, quantization: uint8>, reorder]
2415                field embedding2: sparse_vector [indexed<format: bmp>]
2416            }
2417        "#;
2418
2419        let indexes = parse_sdl(sdl).unwrap();
2420        assert_eq!(indexes[0].fields.len(), 2);
2421
2422        // First field should have reorder=true
2423        assert!(indexes[0].fields[0].reorder);
2424        // Second field should have reorder=false
2425        assert!(!indexes[0].fields[1].reorder);
2426
2427        // Verify schema roundtrip
2428        let schema = indexes[0].to_schema();
2429        let f1 = schema.get_field("embedding").unwrap();
2430        assert!(schema.get_field_entry(f1).unwrap().reorder);
2431
2432        let f2 = schema.get_field("embedding2").unwrap();
2433        assert!(!schema.get_field_entry(f2).unwrap().reorder);
2434
2435        // Index-level reorder_on_merge absent → disabled (current behaviour)
2436        assert!(!schema.reorder_on_merge());
2437    }
2438
2439    #[test]
2440    fn test_reorder_on_merge_index_option() {
2441        let sdl = r#"
2442            index documents {
2443                reorder_on_merge: true
2444                field embedding: sparse_vector<u16> [indexed<format: bmp>, reorder]
2445            }
2446        "#;
2447
2448        let indexes = parse_sdl(sdl).unwrap();
2449        assert!(indexes[0].reorder_on_merge);
2450        let schema = indexes[0].to_schema();
2451        assert!(schema.reorder_on_merge());
2452
2453        // Explicit false parses and stays disabled
2454        let sdl_off = r#"
2455            index documents {
2456                reorder_on_merge: false
2457                field embedding: sparse_vector<u16> [indexed<format: bmp>, reorder]
2458            }
2459        "#;
2460        let indexes = parse_sdl(sdl_off).unwrap();
2461        assert!(!indexes[0].reorder_on_merge);
2462        assert!(!indexes[0].to_schema().reorder_on_merge());
2463
2464        // Schema serde roundtrip preserves the flag (persisted in metadata)
2465        let schema_on = parse_sdl(sdl).unwrap()[0].to_schema();
2466        let json = serde_json::to_string(&schema_on).unwrap();
2467        let back: crate::dsl::Schema = serde_json::from_str(&json).unwrap();
2468        assert!(back.reorder_on_merge());
2469    }
2470}