Skip to main content

hermes_core/dsl/sdl/
mod.rs

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