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