Skip to main content

hermes_core/dsl/sdl/
mod.rs

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