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