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