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