Skip to main content

hermes_core/dsl/sdl/
mod.rs

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