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