1use pest::Parser;
45use pest_derive::Parser;
46
47use super::query_field_router::{QueryRouterRule, RoutingMode};
48use super::schema::{DenseVectorQuantization, FieldType, Schema, SchemaBuilder};
49use crate::Result;
50use crate::error::Error;
51
52#[derive(Parser)]
53#[grammar = "dsl/sdl/sdl.pest"]
54pub struct SdlParser;
55
56use super::schema::{BinaryDenseVectorConfig, DenseVectorConfig};
57use crate::structures::{
58 IndexSize, QueryWeighting, SparseFormat, SparseQueryConfig, SparseVectorConfig,
59 WeightQuantization,
60};
61
62#[derive(Debug, Clone)]
64pub struct FieldDef {
65 pub name: String,
66 pub field_type: FieldType,
67 pub indexed: bool,
68 pub stored: bool,
69 pub tokenizer: Option<String>,
71 pub multi: bool,
73 pub positions: Option<super::schema::PositionMode>,
75 pub sparse_vector_config: Option<SparseVectorConfig>,
77 pub dense_vector_config: Option<DenseVectorConfig>,
79 pub binary_dense_vector_config: Option<BinaryDenseVectorConfig>,
81 pub fast: bool,
83 pub primary: bool,
85 pub reorder: bool,
87 pub bm25_k1: Option<f32>,
89 pub bm25_b: Option<f32>,
91 pub chunked: bool,
93}
94
95#[derive(Debug, Clone)]
97pub struct IndexDef {
98 pub name: String,
99 pub fields: Vec<FieldDef>,
100 pub default_fields: Vec<String>,
101 pub query_routers: Vec<QueryRouterRule>,
103 pub reorder_on_merge: bool,
106}
107
108impl IndexDef {
109 pub fn to_schema(&self) -> Schema {
111 let mut builder = SchemaBuilder::default();
112
113 for field in &self.fields {
114 let f = match field.field_type {
115 FieldType::Text => {
116 let tokenizer = field.tokenizer.as_deref().unwrap_or("simple");
117 builder.add_text_field_with_tokenizer(
118 &field.name,
119 field.indexed,
120 field.stored,
121 tokenizer,
122 )
123 }
124 FieldType::U64 => builder.add_u64_field(&field.name, field.indexed, field.stored),
125 FieldType::I64 => builder.add_i64_field(&field.name, field.indexed, field.stored),
126 FieldType::F64 => builder.add_f64_field(&field.name, field.indexed, field.stored),
127 FieldType::Bytes => builder.add_bytes_field(&field.name, field.stored),
128 FieldType::Json => builder.add_json_field(&field.name, field.stored),
129 FieldType::SparseVector => {
130 if let Some(config) = &field.sparse_vector_config {
131 builder.add_sparse_vector_field_with_config(
132 &field.name,
133 field.indexed,
134 field.stored,
135 config.clone(),
136 )
137 } else {
138 builder.add_sparse_vector_field(&field.name, field.indexed, field.stored)
139 }
140 }
141 FieldType::DenseVector => {
142 let config = field
144 .dense_vector_config
145 .as_ref()
146 .expect("DenseVector field requires dimension to be specified");
147 builder.add_dense_vector_field_with_config(
148 &field.name,
149 field.indexed,
150 field.stored,
151 config.clone(),
152 )
153 }
154 FieldType::BinaryDenseVector => {
155 let config = field
156 .binary_dense_vector_config
157 .as_ref()
158 .expect("BinaryDenseVector field requires dimension to be specified");
159 builder.add_binary_dense_vector_field_with_config(
160 &field.name,
161 field.indexed,
162 field.stored,
163 config.clone(),
164 )
165 }
166 };
167 if field.multi {
168 builder.set_multi(f, true);
169 }
170 if field.fast {
171 builder.set_fast(f, true);
172 }
173 if field.primary {
174 builder.set_primary_key(f);
175 }
176 if field.reorder {
177 builder.set_reorder(f, true);
178 }
179 if field.chunked {
180 builder.set_chunked(f, true);
181 }
182 if field.bm25_k1.is_some() || field.bm25_b.is_some() {
183 builder.set_bm25_params(f, field.bm25_k1, field.bm25_b);
184 }
185 let positions = field.positions.or({
187 if field.multi
189 && matches!(
190 field.field_type,
191 FieldType::SparseVector
192 | FieldType::DenseVector
193 | FieldType::BinaryDenseVector
194 )
195 {
196 Some(super::schema::PositionMode::Ordinal)
197 } else {
198 None
199 }
200 });
201 if let Some(mode) = positions {
202 builder.set_positions(f, mode);
203 }
204 }
205
206 if !self.default_fields.is_empty() {
208 builder.set_default_fields(self.default_fields.clone());
209 }
210
211 if !self.query_routers.is_empty() {
213 builder.set_query_routers(self.query_routers.clone());
214 }
215
216 builder.set_index_name(self.name.clone());
217
218 if self.reorder_on_merge {
219 if self.fields.iter().any(|f| f.reorder) {
220 builder.set_reorder_on_merge(true);
221 } else {
222 log::warn!(
225 "index '{}': reorder_on_merge is set but no field has the `reorder` attribute — merges will not reorder anything",
226 self.name,
227 );
228 builder.set_reorder_on_merge(true);
229 }
230 }
231
232 builder.build()
233 }
234
235 pub fn to_query_router(&self) -> Result<Option<super::query_field_router::QueryFieldRouter>> {
240 if self.query_routers.is_empty() {
241 return Ok(None);
242 }
243
244 super::query_field_router::QueryFieldRouter::from_rules(&self.query_routers)
245 .map(Some)
246 .map_err(Error::Schema)
247 }
248}
249
250fn parse_field_type(type_str: &str) -> Result<FieldType> {
252 match type_str {
253 "text" | "string" | "str" => Ok(FieldType::Text),
254 "u64" | "uint" | "unsigned" => Ok(FieldType::U64),
255 "i64" | "int" | "integer" => Ok(FieldType::I64),
256 "f64" | "float" | "double" => Ok(FieldType::F64),
257 "bytes" | "binary" | "blob" => Ok(FieldType::Bytes),
258 "json" => Ok(FieldType::Json),
259 "sparse_vector" => Ok(FieldType::SparseVector),
260 "dense_vector" | "vector" => Ok(FieldType::DenseVector),
261 "binary_dense_vector" | "binary_vector" => Ok(FieldType::BinaryDenseVector),
262 _ => Err(Error::Schema(format!("Unknown field type: {}", type_str))),
263 }
264}
265
266#[derive(Debug, Clone, Default)]
268enum SoarDirective {
269 #[default]
272 Unspecified,
273 Disabled,
275 Enabled(crate::structures::SoarConfig),
277}
278
279#[derive(Debug, Clone, Default)]
280struct IndexConfig {
281 index_type: Option<super::schema::VectorIndexType>,
282 num_clusters: Option<usize>,
283 target_vectors: Option<u64>,
284 tree_levels: Option<u8>,
285 nprobe: Option<usize>,
286 ivf_routing: Option<super::schema::IvfRoutingMode>,
287 soar: SoarDirective,
288 binary_index_type: Option<super::schema::BinaryIndexType>,
289 sparse_format: Option<SparseFormat>,
291 quantization: Option<WeightQuantization>,
292 weight_threshold: Option<f32>,
293 block_size: Option<usize>,
294 bmp_block_size: Option<u32>,
295 bmp_grid_bits: Option<u8>,
296 pruning: Option<f32>,
297 min_terms: Option<usize>,
298 doc_mass: Option<f32>,
299 query_tokenizer: Option<String>,
301 query_weighting: Option<QueryWeighting>,
302 query_weight_threshold: Option<f32>,
303 query_max_dims: Option<usize>,
304 query_pruning: Option<f32>,
305 query_min_query_dims: Option<usize>,
306 query_lsp_gamma: Option<usize>,
307 dims: Option<u32>,
309 max_weight: Option<f32>,
310 positions: Option<super::schema::PositionMode>,
312 chunked: bool,
314 bm25_k1: Option<f32>,
316 bm25_b: Option<f32>,
317}
318
319struct ParsedAttributes {
321 indexed: bool,
322 stored: bool,
323 multi: bool,
324 fast: bool,
325 primary: bool,
326 reorder: bool,
327 index_config: Option<IndexConfig>,
328}
329
330fn parse_attributes(pair: pest::iterators::Pair<Rule>) -> Result<ParsedAttributes> {
332 let mut attrs = ParsedAttributes {
333 indexed: false,
334 stored: false,
335 multi: false,
336 fast: false,
337 primary: false,
338 reorder: false,
339 index_config: None,
340 };
341
342 for attr in pair.into_inner() {
343 if attr.as_rule() == Rule::attribute {
344 let mut found_config = false;
345 for inner in attr.clone().into_inner() {
346 match inner.as_rule() {
347 Rule::indexed_with_config => {
348 attrs.indexed = true;
349 attrs.index_config = Some(parse_index_config(inner)?);
350 found_config = true;
351 break;
352 }
353 Rule::stored_with_config => {
354 attrs.stored = true;
355 attrs.multi = true; found_config = true;
357 break;
358 }
359 _ => {}
360 }
361 }
362 if !found_config {
363 match attr.as_str() {
364 "indexed" => attrs.indexed = true,
365 "stored" => attrs.stored = true,
366 "fast" => attrs.fast = true,
367 "primary" => attrs.primary = true,
368 "reorder" => attrs.reorder = true,
369 _ => {}
370 }
371 }
372 }
373 }
374
375 Ok(attrs)
376}
377
378fn parse_index_config(pair: pest::iterators::Pair<Rule>) -> Result<IndexConfig> {
380 let mut config = IndexConfig::default();
381
382 for inner in pair.into_inner() {
387 if inner.as_rule() == Rule::index_config_params {
388 for param in inner.into_inner() {
389 if param.as_rule() == Rule::index_config_param {
390 for p in param.into_inner() {
391 parse_single_index_config_param(&mut config, p)?;
392 }
393 }
394 }
395 }
396 }
397
398 Ok(config)
399}
400
401fn parse_single_index_config_param(
403 config: &mut IndexConfig,
404 p: pest::iterators::Pair<Rule>,
405) -> Result<()> {
406 use super::schema::VectorIndexType;
407
408 match p.as_rule() {
409 Rule::index_type_spec => match p.as_str() {
410 "flat" => {
411 config.index_type = Some(VectorIndexType::Flat);
412 config.binary_index_type = Some(super::schema::BinaryIndexType::Flat);
413 }
414 "ivf" => config.binary_index_type = Some(super::schema::BinaryIndexType::Ivf),
415 "ivf_pq" => config.index_type = Some(VectorIndexType::IvfPq),
416 "ivf_tq" => config.index_type = Some(VectorIndexType::IvfTq),
417 "scann" => {
418 config.index_type = Some(VectorIndexType::Scann);
419 config.binary_index_type = Some(super::schema::BinaryIndexType::Scann);
420 }
421 "tq" => config.index_type = Some(VectorIndexType::Tq),
422 _ => {}
423 },
424 Rule::index_type_kwarg => {
425 if let Some(t) = p.into_inner().next() {
427 match t.as_str() {
428 "flat" => {
429 config.index_type = Some(VectorIndexType::Flat);
430 config.binary_index_type = Some(super::schema::BinaryIndexType::Flat);
431 }
432 "ivf" => config.binary_index_type = Some(super::schema::BinaryIndexType::Ivf),
433 "ivf_pq" => config.index_type = Some(VectorIndexType::IvfPq),
434 "ivf_tq" => config.index_type = Some(VectorIndexType::IvfTq),
435 "scann" => {
436 config.index_type = Some(VectorIndexType::Scann);
437 config.binary_index_type = Some(super::schema::BinaryIndexType::Scann);
438 }
439 "tq" => config.index_type = Some(VectorIndexType::Tq),
440 _ => {}
441 }
442 }
443 }
444 Rule::num_clusters_kwarg => {
445 if let Some(n) = p.into_inner().next() {
447 config.num_clusters = Some(n.as_str().parse().map_err(|_| {
448 Error::Schema(format!(
449 "num_clusters '{}' does not fit on this platform",
450 n.as_str()
451 ))
452 })?);
453 }
454 }
455 Rule::target_vectors_kwarg => {
456 if let Some(value) = p.into_inner().next() {
457 config.target_vectors = Some(value.as_str().parse().map_err(|_| {
458 Error::Schema(format!(
459 "target_vectors '{}' does not fit in an unsigned 64-bit integer",
460 value.as_str()
461 ))
462 })?);
463 }
464 }
465 Rule::nprobe_kwarg => {
466 if let Some(n) = p.into_inner().next() {
468 config.nprobe = Some(n.as_str().parse().map_err(|_| {
469 Error::Schema(format!(
470 "nprobe '{}' does not fit on this platform",
471 n.as_str()
472 ))
473 })?);
474 }
475 }
476 Rule::tree_levels_kwarg => {
477 if let Some(value) = p.into_inner().next() {
478 config.tree_levels = Some(value.as_str().parse().map_err(|_| {
479 Error::Schema(format!(
480 "tree_levels '{}' does not fit in an unsigned 8-bit integer",
481 value.as_str()
482 ))
483 })?);
484 }
485 }
486 Rule::routing_kwarg => {
487 if let Some(value) = p.into_inner().next() {
488 config.ivf_routing = Some(match value.as_str() {
489 "flat" => super::schema::IvfRoutingMode::Flat,
490 "two_level" => super::schema::IvfRoutingMode::TwoLevel,
491 "hnsw" => super::schema::IvfRoutingMode::Hnsw,
492 _ => super::schema::IvfRoutingMode::Auto,
493 });
494 }
495 }
496 Rule::soar_kwarg => {
497 if let Some(s) = p.into_inner().next() {
499 use crate::structures::SoarConfig;
500 config.soar = match s.as_str() {
501 "selective" => SoarDirective::Enabled(SoarConfig::new()),
502 "full" => SoarDirective::Enabled(SoarConfig::full()),
503 "aggressive" => SoarDirective::Enabled(SoarConfig::aggressive()),
504 _ => SoarDirective::Disabled, };
506 }
507 }
508 Rule::quantization_kwarg => {
509 if let Some(q) = p.into_inner().next() {
511 config.quantization = Some(match q.as_str() {
512 "float32" | "f32" => WeightQuantization::Float32,
513 "float16" | "f16" => WeightQuantization::Float16,
514 "uint8" | "u8" => WeightQuantization::UInt8,
515 "uint4" | "u4" => WeightQuantization::UInt4,
516 _ => WeightQuantization::default(),
517 });
518 }
519 }
520 Rule::weight_threshold_kwarg => {
521 if let Some(t) = p.into_inner().next() {
523 config.weight_threshold = Some(t.as_str().parse().unwrap_or_else(|_| {
524 log::warn!(
525 "Invalid weight_threshold value '{}', using default 0.0",
526 t.as_str()
527 );
528 0.0
529 }));
530 }
531 }
532 Rule::block_size_kwarg => {
533 if let Some(n) = p.into_inner().next() {
535 config.block_size = Some(n.as_str().parse().unwrap_or_else(|_| {
536 log::warn!(
537 "Invalid block_size value '{}', using default 128",
538 n.as_str()
539 );
540 128
541 }));
542 }
543 }
544 Rule::bmp_grid_bits_kwarg => {
545 if let Some(n) = p.into_inner().next() {
547 config.bmp_grid_bits = Some(n.as_str().parse().unwrap_or_else(|_| {
548 log::warn!(
549 "Invalid bmp_grid_bits value '{}', using default {}",
550 n.as_str(),
551 SparseVectorConfig::DEFAULT_BMP_GRID_BITS,
552 );
553 SparseVectorConfig::DEFAULT_BMP_GRID_BITS
554 }));
555 }
556 }
557 Rule::bmp_block_size_kwarg => {
558 if let Some(n) = p.into_inner().next() {
560 config.bmp_block_size = Some(n.as_str().parse().unwrap_or_else(|_| {
561 log::warn!(
562 "Invalid bmp_block_size value '{}', using default {}",
563 n.as_str(),
564 SparseVectorConfig::DEFAULT_BMP_BLOCK_SIZE,
565 );
566 SparseVectorConfig::DEFAULT_BMP_BLOCK_SIZE
567 }));
568 }
569 }
570 Rule::pruning_kwarg => {
571 if let Some(f) = p.into_inner().next() {
573 config.pruning = Some(f.as_str().parse().unwrap_or_else(|_| {
574 log::warn!("Invalid pruning value '{}', using default 1.0", f.as_str());
575 1.0
576 }));
577 }
578 }
579 Rule::doc_mass_kwarg => {
580 if let Some(f) = p.into_inner().next() {
582 config.doc_mass = Some(f.as_str().parse().unwrap_or_else(|_| {
583 log::warn!("Invalid doc_mass value '{}', using 1.0 (off)", f.as_str());
584 1.0
585 }));
586 }
587 }
588 Rule::min_terms_kwarg => {
589 if let Some(n) = p.into_inner().next() {
590 config.min_terms = Some(n.as_str().parse().unwrap_or_else(|_| {
591 log::warn!("Invalid min_terms value '{}', using default 4", n.as_str());
592 4
593 }));
594 }
595 }
596 Rule::sparse_format_kwarg => {
597 if let Some(f) = p.into_inner().next() {
599 config.sparse_format = Some(match f.as_str() {
600 "bmp" => SparseFormat::Bmp,
601 "maxscore" => SparseFormat::MaxScore,
602 _ => SparseFormat::default(),
603 });
604 }
605 }
606 Rule::sparse_dims_kwarg => {
607 if let Some(n) = p.into_inner().next() {
608 config.dims = Some(n.as_str().parse().unwrap_or_else(|_| {
609 log::warn!("Invalid dims value '{}', using default 105879", n.as_str());
610 105879
611 }));
612 }
613 }
614 Rule::sparse_max_weight_kwarg => {
615 if let Some(f) = p.into_inner().next() {
616 config.max_weight = Some(f.as_str().parse().unwrap_or_else(|_| {
617 log::warn!(
618 "Invalid max_weight value '{}', using default 5.0",
619 f.as_str()
620 );
621 5.0
622 }));
623 }
624 }
625 Rule::query_config_block => {
626 parse_query_config_block(config, p);
628 }
629 Rule::positions_kwarg => {
630 use super::schema::PositionMode;
632 config.positions = Some(match p.as_str() {
633 "ordinal" => PositionMode::Ordinal,
634 "token_position" => PositionMode::TokenPosition,
635 _ => PositionMode::Full, });
637 }
638 Rule::chunked_kwarg => {
639 config.chunked = true;
640 }
641 Rule::bm25_k1_kwarg => {
642 if let Some(v) = p.into_inner().next() {
643 config.bm25_k1 = Some(v.as_str().parse().map_err(|_| {
644 Error::Schema(format!("invalid BM25 k1 value '{}'", v.as_str()))
645 })?);
646 }
647 }
648 Rule::bm25_b_kwarg => {
649 if let Some(v) = p.into_inner().next() {
650 let b: f32 = v
651 .as_str()
652 .parse()
653 .map_err(|_| Error::Schema(format!("invalid BM25 b value '{}'", v.as_str())))?;
654 if !(0.0..=1.0).contains(&b) {
655 return Err(Error::Schema(format!(
656 "BM25 b must be between 0 and 1, got {b}"
657 )));
658 }
659 config.bm25_b = Some(b);
660 }
661 }
662 _ => {}
663 }
664
665 Ok(())
666}
667
668fn parse_query_config_block(config: &mut IndexConfig, pair: pest::iterators::Pair<Rule>) {
670 for inner in pair.into_inner() {
671 if inner.as_rule() == Rule::query_config_params {
672 for param in inner.into_inner() {
673 if param.as_rule() == Rule::query_config_param {
674 for p in param.into_inner() {
675 match p.as_rule() {
676 Rule::query_tokenizer_kwarg => {
677 if let Some(path) = p.into_inner().next()
679 && let Some(inner_path) = path.into_inner().next()
680 {
681 config.query_tokenizer = Some(inner_path.as_str().to_string());
682 }
683 }
684 Rule::query_weighting_kwarg => {
685 if let Some(w) = p.into_inner().next() {
687 config.query_weighting = Some(match w.as_str() {
688 "one" => QueryWeighting::One,
689 "idf" => QueryWeighting::Idf,
690 "idf_file" => QueryWeighting::IdfFile,
691 _ => QueryWeighting::One,
692 });
693 }
694 }
695 Rule::query_weight_threshold_kwarg => {
696 if let Some(t) = p.into_inner().next() {
697 config.query_weight_threshold =
698 Some(t.as_str().parse().unwrap_or_else(|_| {
699 log::warn!(
700 "Invalid query weight_threshold '{}', using 0.0",
701 t.as_str()
702 );
703 0.0
704 }));
705 }
706 }
707 Rule::query_max_dims_kwarg => {
708 if let Some(t) = p.into_inner().next() {
709 config.query_max_dims =
710 Some(t.as_str().parse().unwrap_or_else(|_| {
711 log::warn!(
712 "Invalid query max_dims '{}', using 0",
713 t.as_str()
714 );
715 0
716 }));
717 }
718 }
719 Rule::query_pruning_kwarg => {
720 if let Some(t) = p.into_inner().next() {
721 config.query_pruning =
722 Some(t.as_str().parse().unwrap_or_else(|_| {
723 log::warn!(
724 "Invalid query pruning '{}', using 1.0",
725 t.as_str()
726 );
727 1.0
728 }));
729 }
730 }
731 Rule::query_min_query_dims_kwarg => {
732 if let Some(t) = p.into_inner().next() {
733 config.query_min_query_dims =
734 Some(t.as_str().parse().unwrap_or_else(|_| {
735 log::warn!(
736 "Invalid query min_query_dims '{}', using 4",
737 t.as_str()
738 );
739 4
740 }));
741 }
742 }
743 Rule::query_lsp_gamma_kwarg => {
744 if let Some(value) = p.into_inner().next() {
745 config.query_lsp_gamma =
746 Some(value.as_str().parse().unwrap_or_else(|_| {
747 log::warn!(
748 "Invalid query lsp_gamma '{}', using 0",
749 value.as_str()
750 );
751 0
752 }));
753 }
754 }
755 _ => {}
756 }
757 }
758 }
759 }
760 }
761 }
762}
763
764fn parse_field_def(pair: pest::iterators::Pair<Rule>) -> Result<FieldDef> {
766 let mut inner = pair.into_inner();
767
768 let name = inner
769 .next()
770 .ok_or_else(|| Error::Schema("Missing field name".to_string()))?
771 .as_str()
772 .to_string();
773
774 let field_type_str = inner
775 .next()
776 .ok_or_else(|| Error::Schema("Missing field type".to_string()))?
777 .as_str();
778
779 let field_type = parse_field_type(field_type_str)?;
780
781 let mut tokenizer = None;
783 let mut sparse_vector_config = None;
784 let mut dense_vector_config = None;
785 let mut binary_dense_vector_config = None;
786 let mut indexed = true;
787 let mut stored = true;
788 let mut multi = false;
789 let mut fast = false;
790 let mut primary = false;
791 let mut reorder = false;
792 let mut index_config: Option<IndexConfig> = None;
793
794 for item in inner {
795 match item.as_rule() {
796 Rule::tokenizer_spec => {
797 let raw = item.as_str().trim();
801 let raw = raw
802 .strip_prefix('<')
803 .and_then(|s| s.strip_suffix('>'))
804 .unwrap_or(raw);
805 let spec = crate::tokenizer::TokenizerSpec::parse(raw)
806 .map_err(|e| Error::Schema(format!("Field '{name}': {e}")))?;
807 tokenizer = Some(spec.to_string());
808 }
809 Rule::sparse_vector_config => {
810 sparse_vector_config = Some(parse_sparse_vector_config(item));
812 }
813 Rule::dense_vector_config => {
814 dense_vector_config = Some(parse_dense_vector_config(item));
816 }
817 Rule::binary_dense_vector_config => {
818 let dim: usize = item
820 .into_inner()
821 .next()
822 .map(|d| d.as_str().parse().unwrap_or(0))
823 .unwrap_or(0);
824 if dim == 0 || !dim.is_multiple_of(8) {
825 return Err(Error::Schema(format!(
826 "BinaryDenseVector dimension must be a positive multiple of 8, got {dim}"
827 )));
828 }
829 binary_dense_vector_config = Some(BinaryDenseVectorConfig::new(dim));
830 }
831 Rule::attributes => {
832 let attrs = parse_attributes(item)?;
833 indexed = attrs.indexed;
834 stored = attrs.stored;
835 multi = attrs.multi;
836 fast = attrs.fast;
837 primary = attrs.primary;
838 reorder = attrs.reorder;
839 index_config = attrs.index_config;
840 }
841 _ => {}
842 }
843 }
844
845 if field_type == FieldType::BinaryDenseVector
849 && binary_dense_vector_config.is_none()
850 && let Some(ref dv_config) = dense_vector_config
851 {
852 let dim = dv_config.dim;
853 if dim == 0 || !dim.is_multiple_of(8) {
854 return Err(Error::Schema(format!(
855 "BinaryDenseVector dimension must be a positive multiple of 8, got {dim}"
856 )));
857 }
858 binary_dense_vector_config = Some(BinaryDenseVectorConfig::new(dim));
859 dense_vector_config = None;
860 }
861
862 if primary {
864 fast = true;
865 indexed = true;
866 }
867
868 let mut positions = None;
870 let mut chunked = false;
871 let mut bm25_k1 = None;
872 let mut bm25_b = None;
873 if let Some(idx_cfg) = index_config {
874 positions = idx_cfg.positions;
875 chunked = idx_cfg.chunked;
876 bm25_k1 = idx_cfg.bm25_k1;
877 bm25_b = idx_cfg.bm25_b;
878 if (bm25_k1.is_some() || bm25_b.is_some()) && field_type != FieldType::Text {
879 return Err(Error::Schema(format!(
880 "field '{name}': BM25 `k1`/`b` require a text field, got {field_type:?}"
881 )));
882 }
883 if chunked {
884 if field_type != FieldType::Text {
885 return Err(Error::Schema(format!(
886 "field '{name}': `chunked` requires a text field, got {field_type:?}"
887 )));
888 }
889 if let Some(mode) = positions
890 && mode != super::schema::PositionMode::TokenPosition
891 {
892 return Err(Error::Schema(format!(
893 "field '{name}': a chunked text field may only declare `token_position` \
894 (positions restart in every chunk and the chunk is the ordinal); \
895 `{}` is not allowed",
896 match mode {
897 super::schema::PositionMode::Ordinal => "ordinal",
898 super::schema::PositionMode::Full => "positions",
899 super::schema::PositionMode::TokenPosition => unreachable!(),
900 }
901 )));
902 }
903 multi = true;
905 }
906 if let Some(ref mut bv_config) = binary_dense_vector_config {
907 apply_index_config_to_binary_dense_vector(bv_config, idx_cfg)?;
908 } else if let Some(ref mut dv_config) = dense_vector_config {
909 apply_index_config_to_dense_vector(dv_config, idx_cfg)?;
910 } else if field_type == FieldType::SparseVector {
911 reject_scann_options_for_non_dense_vector(&idx_cfg, "sparse vector")?;
912 let sv_config = sparse_vector_config.get_or_insert(SparseVectorConfig::default());
914 apply_index_config_to_sparse_vector(sv_config, idx_cfg);
915 } else {
916 reject_scann_options_for_non_dense_vector(&idx_cfg, "non-vector field")?;
917 }
918 }
919
920 Ok(FieldDef {
921 name,
922 field_type,
923 indexed,
924 stored,
925 tokenizer,
926 multi,
927 positions,
928 sparse_vector_config,
929 dense_vector_config,
930 binary_dense_vector_config,
931 fast,
932 primary,
933 reorder,
934 chunked,
935 bm25_k1,
936 bm25_b,
937 })
938}
939
940fn reject_scann_options_for_non_dense_vector(config: &IndexConfig, field_kind: &str) -> Result<()> {
941 if config.index_type == Some(super::schema::VectorIndexType::Scann)
942 || config.binary_index_type == Some(super::schema::BinaryIndexType::Scann)
943 || config.tree_levels.is_some()
944 || config.target_vectors.is_some()
945 {
946 return Err(Error::Schema(format!(
947 "vector index options require a dense or binary dense vector field, not a {field_kind}"
948 )));
949 }
950 Ok(())
951}
952
953fn apply_index_config_to_binary_dense_vector(
955 config: &mut BinaryDenseVectorConfig,
956 idx_cfg: IndexConfig,
957) -> Result<()> {
958 if idx_cfg.target_vectors == Some(0) {
959 return Err(Error::Schema(
960 "target_vectors must be greater than zero".to_string(),
961 ));
962 }
963 if idx_cfg.index_type.is_some() && idx_cfg.binary_index_type.is_none() {
964 return Err(Error::Schema(
965 "binary dense vectors support only 'flat', 'ivf', or 'scann' index types".to_string(),
966 ));
967 }
968 if let Some(index_type) = idx_cfg.binary_index_type {
969 config.index_type = index_type;
970 }
971 if idx_cfg.target_vectors.is_some() && config.index_type == super::schema::BinaryIndexType::Flat
972 {
973 return Err(Error::Schema(
974 "'target_vectors' is only valid for binary IVF or ScaNN automatic topology".to_string(),
975 ));
976 }
977 validate_scann_index_options(
978 "binary dense vector",
979 config.index_type == super::schema::BinaryIndexType::Scann,
980 &idx_cfg,
981 )?;
982 match &idx_cfg.soar {
983 SoarDirective::Unspecified | SoarDirective::Disabled => config.soar = None,
984 SoarDirective::Enabled(soar)
985 if config.index_type == super::schema::BinaryIndexType::Scann =>
986 {
987 config.soar = Some(soar.clone());
988 }
989 SoarDirective::Enabled(_) => {
990 return Err(Error::Schema(
991 "'soar' on a binary dense vector requires the ScaNN index".to_string(),
992 ));
993 }
994 }
995 if idx_cfg.num_clusters.is_some() {
996 config.num_clusters = idx_cfg.num_clusters;
997 }
998 if idx_cfg.target_vectors.is_some() {
999 config.target_vectors = idx_cfg.target_vectors;
1000 }
1001 if idx_cfg.tree_levels.is_some() {
1002 config.tree_levels = idx_cfg.tree_levels;
1003 }
1004 if let Some(nprobe) = idx_cfg.nprobe {
1005 config.nprobe = nprobe;
1006 }
1007 if let Some(routing) = idx_cfg.ivf_routing {
1008 config.ivf_routing = routing;
1009 }
1010 Ok(())
1011}
1012
1013fn apply_index_config_to_dense_vector(
1015 config: &mut DenseVectorConfig,
1016 idx_cfg: IndexConfig,
1017) -> Result<()> {
1018 if idx_cfg.target_vectors == Some(0) {
1019 return Err(Error::Schema(
1020 "target_vectors must be greater than zero".to_string(),
1021 ));
1022 }
1023 if idx_cfg.binary_index_type.is_some() && idx_cfg.index_type.is_none() {
1024 return Err(Error::Schema(
1025 "float dense vectors do not support the binary-only 'ivf' index type; use 'ivf_tq' or 'scann'"
1026 .to_string(),
1027 ));
1028 }
1029 if let Some(index_type) = idx_cfg.index_type {
1031 config.index_type = index_type;
1032 }
1033 if idx_cfg.target_vectors.is_some()
1034 && matches!(
1035 config.index_type,
1036 super::schema::VectorIndexType::Flat | super::schema::VectorIndexType::Tq
1037 )
1038 {
1039 return Err(Error::Schema(
1040 "'target_vectors' is only valid for IVF-TQ or ScaNN automatic topology".to_string(),
1041 ));
1042 }
1043
1044 validate_scann_index_options(
1045 "dense vector",
1046 config.index_type == super::schema::VectorIndexType::Scann,
1047 &idx_cfg,
1048 )?;
1049 if idx_cfg.target_vectors.is_some() {
1050 config.target_vectors = idx_cfg.target_vectors;
1051 }
1052
1053 if config.index_type == super::schema::VectorIndexType::Tq {
1056 for (option, present) in [
1057 ("num_clusters", idx_cfg.num_clusters.is_some()),
1058 ("nprobe", idx_cfg.nprobe.is_some()),
1059 ("routing", idx_cfg.ivf_routing.is_some()),
1060 ] {
1061 if present {
1062 log::warn!(
1063 "'{option}' has no effect on the 'tq' index (training-free full \
1064 scan); ignoring"
1065 );
1066 }
1067 }
1068 config.num_clusters = None;
1071 config.nprobe = 0;
1072 config.ivf_routing = super::schema::IvfRoutingMode::Flat;
1073 apply_soar_to_dense_vector(config, idx_cfg)?;
1074 return Ok(());
1075 }
1076
1077 if idx_cfg.num_clusters.is_some() {
1079 config.num_clusters = idx_cfg.num_clusters;
1080 }
1081 if idx_cfg.tree_levels.is_some() {
1082 config.tree_levels = idx_cfg.tree_levels;
1083 }
1084
1085 if let Some(nprobe) = idx_cfg.nprobe {
1087 config.nprobe = nprobe;
1088 }
1089 if let Some(routing) = idx_cfg.ivf_routing {
1090 config.ivf_routing = routing;
1091 }
1092
1093 apply_soar_to_dense_vector(config, idx_cfg)?;
1094 Ok(())
1095}
1096
1097const MAX_SCANN_TREE_LEVELS: u8 = 3;
1098const MAX_SCANN_LEAVES: usize = 30_000_000;
1099
1100fn validate_scann_index_options(
1101 field_kind: &str,
1102 is_scann: bool,
1103 config: &IndexConfig,
1104) -> Result<()> {
1105 if !is_scann {
1106 if config.tree_levels.is_some() {
1107 return Err(Error::Schema(format!(
1108 "'tree_levels' is only valid for a ScaNN {field_kind} index"
1109 )));
1110 }
1111 return Ok(());
1112 }
1113
1114 if config.ivf_routing.is_some() {
1115 return Err(Error::Schema(format!(
1116 "'routing' is not configurable for ScaNN {field_kind} indexes; ScaNN owns its hierarchical routing"
1117 )));
1118 }
1119
1120 if let Some(tree_levels) = config.tree_levels
1121 && !(1..=MAX_SCANN_TREE_LEVELS).contains(&tree_levels)
1122 {
1123 return Err(Error::Schema(format!(
1124 "ScaNN tree_levels must be in 1..={MAX_SCANN_TREE_LEVELS}, got {tree_levels}"
1125 )));
1126 }
1127 if let Some(num_clusters) = config.num_clusters {
1128 if num_clusters < 2 {
1129 return Err(Error::Schema(
1130 "ScaNN num_clusters (terminal leaf count) must be at least 2".to_string(),
1131 ));
1132 }
1133 if num_clusters > MAX_SCANN_LEAVES {
1134 return Err(Error::Schema(format!(
1135 "ScaNN num_clusters cannot exceed {MAX_SCANN_LEAVES}, got {num_clusters}"
1136 )));
1137 }
1138 let nprobe = config.nprobe.unwrap_or(64);
1139 if nprobe > num_clusters {
1140 return Err(Error::Schema(format!(
1141 "ScaNN nprobe ({nprobe}) cannot exceed explicit num_clusters ({num_clusters})"
1142 )));
1143 }
1144 }
1145 if config.nprobe == Some(0) {
1146 return Err(Error::Schema("ScaNN nprobe must be positive".to_string()));
1147 }
1148 Ok(())
1149}
1150
1151fn apply_soar_to_dense_vector(config: &mut DenseVectorConfig, idx_cfg: IndexConfig) -> Result<()> {
1153 match idx_cfg.soar {
1154 SoarDirective::Unspecified => {
1155 config.soar = config
1156 .supports_soar()
1157 .then(crate::structures::SoarConfig::default);
1158 }
1159 SoarDirective::Disabled => {
1160 config.soar = None;
1161 }
1162 SoarDirective::Enabled(soar) => {
1163 if config.supports_soar() {
1164 config.soar = Some(soar);
1165 } else {
1166 config.soar = None;
1167 return Err(Error::Schema(format!(
1168 "'soar' requires the IVF-TQ index and is not implemented for {:?}",
1169 config.index_type
1170 )));
1171 }
1172 }
1173 }
1174 Ok(())
1175}
1176
1177fn parse_sparse_vector_config(pair: pest::iterators::Pair<Rule>) -> SparseVectorConfig {
1180 let mut index_size = IndexSize::default();
1181
1182 for inner in pair.into_inner() {
1184 if inner.as_rule() == Rule::index_size_spec {
1185 index_size = match inner.as_str() {
1186 "u16" => IndexSize::U16,
1187 "u32" => IndexSize::U32,
1188 _ => IndexSize::default(),
1189 };
1190 }
1191 }
1192
1193 SparseVectorConfig {
1194 index_size,
1195 ..SparseVectorConfig::default()
1196 }
1197}
1198
1199fn apply_index_config_to_sparse_vector(config: &mut SparseVectorConfig, idx_cfg: IndexConfig) {
1201 if let Some(f) = idx_cfg.sparse_format {
1202 config.format = f;
1203 }
1204 if let Some(q) = idx_cfg.quantization {
1205 config.weight_quantization = q;
1206 }
1207 if let Some(t) = idx_cfg.weight_threshold {
1208 config.weight_threshold = t;
1209 }
1210 if let Some(bs) = idx_cfg.block_size {
1211 let adjusted = bs.next_power_of_two();
1212 if adjusted != bs {
1213 log::warn!(
1214 "block_size {} adjusted to next power of two: {}",
1215 bs,
1216 adjusted
1217 );
1218 }
1219 config.block_size = adjusted;
1220 }
1221 if let Some(bs) = idx_cfg.bmp_block_size {
1222 let adjusted = bs.next_power_of_two().clamp(1, 256);
1223 if adjusted != bs {
1224 log::warn!(
1225 "bmp_block_size {} adjusted to power of two in 1..=256: {}",
1226 bs,
1227 adjusted
1228 );
1229 }
1230 config.bmp_block_size = adjusted;
1231 }
1232 if let Some(bits) = idx_cfg.bmp_grid_bits {
1233 if bits == 2 || bits == 4 {
1234 config.bmp_grid_bits = bits;
1235 } else {
1236 log::warn!(
1237 "bmp_grid_bits {} unsupported (must be 2 or 4), using {}",
1238 bits,
1239 SparseVectorConfig::DEFAULT_BMP_GRID_BITS,
1240 );
1241 config.bmp_grid_bits = SparseVectorConfig::DEFAULT_BMP_GRID_BITS;
1242 }
1243 }
1244 if let Some(p) = idx_cfg.pruning {
1245 let clamped = p.clamp(0.0, 1.0);
1246 if (clamped - p).abs() > f32::EPSILON {
1247 log::warn!(
1248 "pruning {} clamped to valid range [0.0, 1.0]: {}",
1249 p,
1250 clamped
1251 );
1252 }
1253 config.pruning = Some(clamped);
1254 }
1255 if let Some(mt) = idx_cfg.min_terms {
1256 config.min_terms = mt;
1257 }
1258 if let Some(dm) = idx_cfg.doc_mass {
1259 let clamped = dm.clamp(0.0, 1.0);
1260 if (clamped - dm).abs() > f32::EPSILON {
1261 log::warn!(
1262 "doc_mass {} clamped to valid range [0.0, 1.0]: {}",
1263 dm,
1264 clamped
1265 );
1266 }
1267 config.doc_mass = Some(clamped);
1268 }
1269 if let Some(d) = idx_cfg.dims {
1270 config.dims = Some(d);
1271 }
1272 if let Some(mw) = idx_cfg.max_weight {
1273 config.max_weight = Some(mw);
1274 }
1275 if idx_cfg.query_tokenizer.is_some()
1277 || idx_cfg.query_weighting.is_some()
1278 || idx_cfg.query_weight_threshold.is_some()
1279 || idx_cfg.query_max_dims.is_some()
1280 || idx_cfg.query_pruning.is_some()
1281 || idx_cfg.query_min_query_dims.is_some()
1282 || idx_cfg.query_lsp_gamma.is_some()
1283 {
1284 let query_config = config
1285 .query_config
1286 .get_or_insert(SparseQueryConfig::default());
1287 if let Some(tokenizer) = idx_cfg.query_tokenizer {
1288 query_config.tokenizer = Some(tokenizer);
1289 }
1290 if let Some(weighting) = idx_cfg.query_weighting {
1291 query_config.weighting = weighting;
1292 }
1293 if let Some(t) = idx_cfg.query_weight_threshold {
1294 query_config.weight_threshold = t;
1295 }
1296 if let Some(d) = idx_cfg.query_max_dims {
1297 query_config.max_query_dims = Some(d);
1298 }
1299 if let Some(p) = idx_cfg.query_pruning {
1300 query_config.pruning = Some(p);
1301 }
1302 if let Some(m) = idx_cfg.query_min_query_dims {
1303 query_config.min_query_dims = m;
1304 }
1305 if let Some(gamma) = idx_cfg.query_lsp_gamma {
1306 query_config.lsp_gamma = Some(gamma);
1307 }
1308 }
1309}
1310
1311fn parse_dense_vector_config(pair: pest::iterators::Pair<Rule>) -> DenseVectorConfig {
1314 let mut dim: usize = 0;
1315 let mut quantization = DenseVectorQuantization::F32;
1316
1317 for params in pair.into_inner() {
1319 if params.as_rule() == Rule::dense_vector_params {
1320 for inner in params.into_inner() {
1321 match inner.as_rule() {
1322 Rule::dense_vector_keyword_params => {
1323 for kwarg in inner.into_inner() {
1324 match kwarg.as_rule() {
1325 Rule::dims_kwarg => {
1326 if let Some(d) = kwarg.into_inner().next() {
1327 dim = d.as_str().parse().unwrap_or(0);
1328 }
1329 }
1330 Rule::quant_type_spec => {
1331 quantization = parse_quant_type(kwarg.as_str());
1332 }
1333 _ => {}
1334 }
1335 }
1336 }
1337 Rule::dense_vector_positional_params => {
1338 for item in inner.into_inner() {
1339 match item.as_rule() {
1340 Rule::dimension_spec => {
1341 dim = item.as_str().parse().unwrap_or(0);
1342 }
1343 Rule::quant_type_spec => {
1344 quantization = parse_quant_type(item.as_str());
1345 }
1346 _ => {}
1347 }
1348 }
1349 }
1350 _ => {}
1351 }
1352 }
1353 }
1354 }
1355
1356 DenseVectorConfig::new(dim).with_quantization(quantization)
1357}
1358
1359fn parse_quant_type(s: &str) -> DenseVectorQuantization {
1360 match s.trim() {
1361 "f16" => DenseVectorQuantization::F16,
1362 "uint8" | "u8" => DenseVectorQuantization::UInt8,
1363 _ => DenseVectorQuantization::F32,
1364 }
1365}
1366
1367fn parse_default_fields_def(pair: pest::iterators::Pair<Rule>) -> Vec<String> {
1369 pair.into_inner().map(|p| p.as_str().to_string()).collect()
1370}
1371
1372fn parse_query_router_def(pair: pest::iterators::Pair<Rule>) -> Result<QueryRouterRule> {
1374 let mut pattern = String::new();
1375 let mut substitution = String::new();
1376 let mut target_field = String::new();
1377 let mut mode = RoutingMode::Additional;
1378
1379 for prop in pair.into_inner() {
1380 if prop.as_rule() != Rule::query_router_prop {
1381 continue;
1382 }
1383
1384 for inner in prop.into_inner() {
1385 match inner.as_rule() {
1386 Rule::query_router_pattern => {
1387 if let Some(regex_str) = inner.into_inner().next() {
1388 pattern = parse_string_value(regex_str);
1389 }
1390 }
1391 Rule::query_router_substitution => {
1392 if let Some(quoted) = inner.into_inner().next() {
1393 substitution = parse_string_value(quoted);
1394 }
1395 }
1396 Rule::query_router_target => {
1397 if let Some(ident) = inner.into_inner().next() {
1398 target_field = ident.as_str().to_string();
1399 }
1400 }
1401 Rule::query_router_mode => {
1402 if let Some(mode_val) = inner.into_inner().next() {
1403 mode = match mode_val.as_str() {
1404 "exclusive" => RoutingMode::Exclusive,
1405 "additional" => RoutingMode::Additional,
1406 _ => RoutingMode::Additional,
1407 };
1408 }
1409 }
1410 _ => {}
1411 }
1412 }
1413 }
1414
1415 if pattern.is_empty() {
1416 return Err(Error::Schema("query_router missing 'pattern'".to_string()));
1417 }
1418 if substitution.is_empty() {
1419 return Err(Error::Schema(
1420 "query_router missing 'substitution'".to_string(),
1421 ));
1422 }
1423 if target_field.is_empty() {
1424 return Err(Error::Schema(
1425 "query_router missing 'target_field'".to_string(),
1426 ));
1427 }
1428
1429 Ok(QueryRouterRule {
1430 pattern,
1431 substitution,
1432 target_field,
1433 mode,
1434 })
1435}
1436
1437fn parse_string_value(pair: pest::iterators::Pair<Rule>) -> String {
1439 let s = pair.as_str();
1440 match pair.as_rule() {
1441 Rule::regex_string => {
1442 if let Some(inner) = pair.into_inner().next() {
1444 parse_string_value(inner)
1445 } else {
1446 s.to_string()
1447 }
1448 }
1449 Rule::raw_string => {
1450 s[2..s.len() - 1].to_string()
1452 }
1453 Rule::quoted_string => {
1454 let inner = &s[1..s.len() - 1];
1456 inner
1458 .replace("\\n", "\n")
1459 .replace("\\t", "\t")
1460 .replace("\\\"", "\"")
1461 .replace("\\\\", "\\")
1462 }
1463 _ => s.to_string(),
1464 }
1465}
1466
1467fn parse_index_def(pair: pest::iterators::Pair<Rule>) -> Result<IndexDef> {
1469 let mut inner = pair.into_inner();
1470
1471 let name = inner
1472 .next()
1473 .ok_or_else(|| Error::Schema("Missing index name".to_string()))?
1474 .as_str()
1475 .to_string();
1476
1477 let mut fields = Vec::new();
1478 let mut default_fields = Vec::new();
1479 let mut query_routers = Vec::new();
1480 let mut reorder_on_merge = false;
1481
1482 for item in inner {
1483 match item.as_rule() {
1484 Rule::field_def => {
1485 fields.push(parse_field_def(item)?);
1486 }
1487 Rule::default_fields_def => {
1488 default_fields = parse_default_fields_def(item);
1489 }
1490 Rule::query_router_def => {
1491 query_routers.push(parse_query_router_def(item)?);
1492 }
1493 Rule::reorder_on_merge_def => {
1494 let value = item
1495 .into_inner()
1496 .next()
1497 .map(|b| b.as_str() == "true")
1498 .unwrap_or(false);
1499 reorder_on_merge = value;
1500 }
1501 _ => {}
1502 }
1503 }
1504
1505 validate_tokenizer_specs(&name, &fields)?;
1506
1507 let primary_fields: Vec<&FieldDef> = fields.iter().filter(|f| f.primary).collect();
1509 if primary_fields.len() > 1 {
1510 return Err(Error::Schema(format!(
1511 "Index '{}' has {} primary key fields, but at most one is allowed",
1512 name,
1513 primary_fields.len()
1514 )));
1515 }
1516 if let Some(pk) = primary_fields.first() {
1517 if pk.field_type != FieldType::Text {
1518 return Err(Error::Schema(format!(
1519 "Primary key field '{}' must be of type text, got {:?}",
1520 pk.name, pk.field_type
1521 )));
1522 }
1523 if pk.multi {
1524 return Err(Error::Schema(format!(
1525 "Primary key field '{}' cannot be multi-valued",
1526 pk.name
1527 )));
1528 }
1529 }
1530
1531 Ok(IndexDef {
1532 name,
1533 fields,
1534 default_fields,
1535 query_routers,
1536 reorder_on_merge,
1537 })
1538}
1539
1540fn validate_tokenizer_specs(index_name: &str, fields: &[FieldDef]) -> Result<()> {
1544 use crate::tokenizer::{TokenizerRegistry, TokenizerSpec};
1545 let mut registry: Option<TokenizerRegistry> = None;
1546 for field in fields {
1547 let Some(raw) = field.tokenizer.as_deref() else {
1548 continue;
1549 };
1550 let spec = TokenizerSpec::parse(raw).map_err(|e| {
1551 Error::Schema(format!("Index '{index_name}', field '{}': {e}", field.name))
1552 })?;
1553 match spec {
1554 TokenizerSpec::Named(tokenizer) => {
1555 let registry = registry.get_or_insert_with(TokenizerRegistry::new);
1556 if !registry.contains(&tokenizer) {
1557 return Err(Error::Schema(format!(
1558 "Index '{index_name}', field '{}': unknown tokenizer '{tokenizer}'",
1559 field.name
1560 )));
1561 }
1562 }
1563 TokenizerSpec::DynamicStem { by, .. } => match fields.iter().find(|f| f.name == by) {
1564 None => {
1565 return Err(Error::Schema(format!(
1566 "Index '{index_name}', field '{}': tokenizer hint field '{by}' does not exist",
1567 field.name
1568 )));
1569 }
1570 Some(hint) if hint.field_type != FieldType::Text => {
1571 return Err(Error::Schema(format!(
1572 "Index '{index_name}', field '{}': tokenizer hint field '{by}' must be a text field, got {:?}",
1573 field.name, hint.field_type
1574 )));
1575 }
1576 Some(_) => {}
1577 },
1578 }
1579 }
1580 Ok(())
1581}
1582
1583pub fn parse_sdl(input: &str) -> Result<Vec<IndexDef>> {
1585 let pairs = SdlParser::parse(Rule::file, input)
1586 .map_err(|e| Error::Schema(format!("Parse error: {}", e)))?;
1587
1588 let mut indexes = Vec::new();
1589
1590 for pair in pairs {
1591 if pair.as_rule() == Rule::file {
1592 for inner in pair.into_inner() {
1593 if inner.as_rule() == Rule::index_def {
1594 indexes.push(parse_index_def(inner)?);
1595 }
1596 }
1597 }
1598 }
1599
1600 Ok(indexes)
1601}
1602
1603pub fn parse_single_index(input: &str) -> Result<IndexDef> {
1605 let indexes = parse_sdl(input)?;
1606
1607 if indexes.is_empty() {
1608 return Err(Error::Schema("No index definition found".to_string()));
1609 }
1610
1611 if indexes.len() > 1 {
1612 return Err(Error::Schema(
1613 "Multiple index definitions found, expected one".to_string(),
1614 ));
1615 }
1616
1617 Ok(indexes.into_iter().next().unwrap())
1618}
1619
1620#[cfg(test)]
1621mod tests {
1622 use super::*;
1623
1624 #[test]
1625 fn test_parse_simple_schema() {
1626 let sdl = r#"
1627 index articles {
1628 field title: text [indexed, stored]
1629 field body: text [indexed]
1630 }
1631 "#;
1632
1633 let indexes = parse_sdl(sdl).unwrap();
1634 assert_eq!(indexes.len(), 1);
1635
1636 let index = &indexes[0];
1637 assert_eq!(index.name, "articles");
1638 assert_eq!(index.fields.len(), 2);
1639
1640 assert_eq!(index.fields[0].name, "title");
1641 assert!(matches!(index.fields[0].field_type, FieldType::Text));
1642 assert!(index.fields[0].indexed);
1643 assert!(index.fields[0].stored);
1644
1645 assert_eq!(index.fields[1].name, "body");
1646 assert!(matches!(index.fields[1].field_type, FieldType::Text));
1647 assert!(index.fields[1].indexed);
1648 assert!(!index.fields[1].stored);
1649 }
1650
1651 #[test]
1652 fn test_parse_all_field_types() {
1653 let sdl = r#"
1654 index test {
1655 field text_field: text [indexed, stored]
1656 field u64_field: u64 [indexed, stored]
1657 field i64_field: i64 [indexed, stored]
1658 field f64_field: f64 [indexed, stored]
1659 field bytes_field: bytes [stored]
1660 }
1661 "#;
1662
1663 let indexes = parse_sdl(sdl).unwrap();
1664 let index = &indexes[0];
1665
1666 assert!(matches!(index.fields[0].field_type, FieldType::Text));
1667 assert!(matches!(index.fields[1].field_type, FieldType::U64));
1668 assert!(matches!(index.fields[2].field_type, FieldType::I64));
1669 assert!(matches!(index.fields[3].field_type, FieldType::F64));
1670 assert!(matches!(index.fields[4].field_type, FieldType::Bytes));
1671 }
1672
1673 #[test]
1674 fn test_parse_with_comments() {
1675 let sdl = r#"
1676 # This is a comment
1677 index articles {
1678 # Title field
1679 field title: text [indexed, stored]
1680 field body: text [indexed] # inline comment not supported yet
1681 }
1682 "#;
1683
1684 let indexes = parse_sdl(sdl).unwrap();
1685 assert_eq!(indexes[0].fields.len(), 2);
1686 }
1687
1688 #[test]
1689 fn test_parse_type_aliases() {
1690 let sdl = r#"
1691 index test {
1692 field a: string [indexed]
1693 field b: int [indexed]
1694 field c: uint [indexed]
1695 field d: float [indexed]
1696 field e: binary [stored]
1697 }
1698 "#;
1699
1700 let indexes = parse_sdl(sdl).unwrap();
1701 let index = &indexes[0];
1702
1703 assert!(matches!(index.fields[0].field_type, FieldType::Text));
1704 assert!(matches!(index.fields[1].field_type, FieldType::I64));
1705 assert!(matches!(index.fields[2].field_type, FieldType::U64));
1706 assert!(matches!(index.fields[3].field_type, FieldType::F64));
1707 assert!(matches!(index.fields[4].field_type, FieldType::Bytes));
1708 }
1709
1710 #[test]
1711 fn test_to_schema() {
1712 let sdl = r#"
1713 index articles {
1714 field title: text [indexed, stored]
1715 field views: u64 [indexed, stored]
1716 }
1717 "#;
1718
1719 let indexes = parse_sdl(sdl).unwrap();
1720 let schema = indexes[0].to_schema();
1721
1722 assert!(schema.get_field("title").is_some());
1723 assert!(schema.get_field("views").is_some());
1724 assert!(schema.get_field("nonexistent").is_none());
1725 }
1726
1727 #[test]
1728 fn test_default_attributes() {
1729 let sdl = r#"
1730 index test {
1731 field title: text
1732 }
1733 "#;
1734
1735 let indexes = parse_sdl(sdl).unwrap();
1736 let field = &indexes[0].fields[0];
1737
1738 assert!(field.indexed);
1740 assert!(field.stored);
1741 }
1742
1743 #[test]
1744 fn chunked_text_field_parses_and_implies_multi() {
1745 let sdl = r#"
1746 index documents {
1747 field languages: text<raw_ci> [fast]
1748 field content: text<stem(by: languages, default: simple)> [indexed<chunked, token_position>]
1749 field notes: text<simple> [indexed<chunked>, stored]
1750 }
1751 "#;
1752 let index = parse_single_index(sdl).unwrap();
1753 let content = &index.fields[1];
1754 assert!(content.chunked);
1755 assert!(content.multi, "chunked implies multi-valued storage");
1756 assert_eq!(
1757 content.positions,
1758 Some(crate::dsl::PositionMode::TokenPosition)
1759 );
1760 let notes = &index.fields[2];
1761 assert!(notes.chunked && notes.stored && notes.positions.is_none());
1762
1763 let schema = index.to_schema();
1764 let entry = schema
1765 .get_field_entry(schema.get_field("content").unwrap())
1766 .unwrap();
1767 assert!(entry.chunked && entry.multi);
1768 assert!(
1769 !schema
1770 .get_field_entry(schema.get_field("languages").unwrap())
1771 .unwrap()
1772 .chunked
1773 );
1774 }
1775
1776 #[test]
1777 fn chunked_rejects_non_text_and_ordinal_position_modes() {
1778 let non_text = parse_sdl("index i { field n: u64 [indexed<chunked>] }").unwrap_err();
1779 assert!(
1780 non_text.to_string().contains("requires a text field"),
1781 "{non_text}"
1782 );
1783
1784 for mode in ["positions", "ordinal"] {
1785 let sdl = format!("index i {{ field c: text<simple> [indexed<chunked, {mode}>] }}");
1786 let error = parse_sdl(&sdl).unwrap_err();
1787 assert!(
1788 error.to_string().contains("token_position"),
1789 "{mode}: {error}"
1790 );
1791 }
1792 }
1793
1794 #[test]
1795 fn test_multiple_indexes() {
1796 let sdl = r#"
1797 index articles {
1798 field title: text [indexed, stored]
1799 }
1800
1801 index users {
1802 field name: text [indexed, stored]
1803 field email: text [indexed, stored]
1804 }
1805 "#;
1806
1807 let indexes = parse_sdl(sdl).unwrap();
1808 assert_eq!(indexes.len(), 2);
1809 assert_eq!(indexes[0].name, "articles");
1810 assert_eq!(indexes[1].name, "users");
1811 }
1812
1813 #[test]
1814 fn test_tokenizer_spec() {
1815 let sdl = r#"
1816 index articles {
1817 field title: text<en_stem> [indexed, stored]
1818 field body: text<simple> [indexed]
1819 field author: text [indexed, stored]
1820 }
1821 "#;
1822
1823 let indexes = parse_sdl(sdl).unwrap();
1824 let index = &indexes[0];
1825
1826 assert_eq!(index.fields[0].name, "title");
1827 assert_eq!(index.fields[0].tokenizer, Some("en_stem".to_string()));
1828
1829 assert_eq!(index.fields[1].name, "body");
1830 assert_eq!(index.fields[1].tokenizer, Some("simple".to_string()));
1831
1832 assert_eq!(index.fields[2].name, "author");
1833 assert_eq!(index.fields[2].tokenizer, None); }
1835
1836 #[test]
1837 fn test_dynamic_tokenizer_spec() {
1838 let sdl = r#"
1839 index documents {
1840 field languages: text<raw_ci> [fast]
1841 field content: text<stem(by: languages, default: simple)> [indexed<token_position>]
1842 field title: text<stem(by:languages,default:english)> [indexed]
1843 field embedding: dense_vector<768> [indexed]
1844 field hash: binary_dense_vector<64> [indexed]
1845 }
1846 "#;
1847
1848 let indexes = parse_sdl(sdl).unwrap();
1849 let index = &indexes[0];
1850 assert_eq!(
1851 index.fields[1].tokenizer,
1852 Some("stem(by: languages, default: simple)".to_string())
1853 );
1854 assert_eq!(
1855 index.fields[1].positions,
1856 Some(super::super::schema::PositionMode::TokenPosition)
1857 );
1858 assert_eq!(
1860 index.fields[2].tokenizer,
1861 Some("stem(by: languages, default: en)".to_string())
1862 );
1863 assert_eq!(
1865 index.fields[3].dense_vector_config.as_ref().unwrap().dim,
1866 768
1867 );
1868 assert_eq!(
1869 index.fields[4]
1870 .binary_dense_vector_config
1871 .as_ref()
1872 .unwrap()
1873 .dim,
1874 64
1875 );
1876
1877 let schema = index.to_schema();
1878 let content = schema.get_field("content").unwrap();
1879 let languages = schema.get_field("languages").unwrap();
1880 assert_eq!(schema.tokenizer_hint_field(content), Some(languages));
1881 assert_eq!(schema.tokenizer_hint_field(languages), None);
1882 let entry = schema.get_field_entry(content).unwrap();
1883 assert_eq!(
1884 entry.tokenizer_spec().unwrap().hint_field(),
1885 Some("languages")
1886 );
1887 }
1888
1889 #[test]
1890 fn test_tokenizer_specs_fail_loud() {
1891 let missing_hint_field = r#"
1892 index documents {
1893 field content: text<stem(by: languages, default: simple)> [indexed]
1894 }
1895 "#;
1896 let err = parse_sdl(missing_hint_field).unwrap_err().to_string();
1897 assert!(
1898 err.contains("hint field 'languages' does not exist"),
1899 "{err}"
1900 );
1901
1902 let numeric_hint_field = r#"
1903 index documents {
1904 field languages: u64 [fast]
1905 field content: text<stem(by: languages)> [indexed]
1906 }
1907 "#;
1908 let err = parse_sdl(numeric_hint_field).unwrap_err().to_string();
1909 assert!(err.contains("must be a text field"), "{err}");
1910
1911 let unknown_default = r#"
1912 index documents {
1913 field languages: text [fast]
1914 field content: text<stem(by: languages, default: klingon)> [indexed]
1915 }
1916 "#;
1917 let err = parse_sdl(unknown_default).unwrap_err().to_string();
1918 assert!(err.contains("unknown default language 'klingon'"), "{err}");
1919
1920 let unknown_tokenizer = r#"
1921 index documents {
1922 field content: text<klingon_stem> [indexed]
1923 }
1924 "#;
1925 let err = parse_sdl(unknown_tokenizer).unwrap_err().to_string();
1926 assert!(err.contains("unknown tokenizer 'klingon_stem'"), "{err}");
1927 }
1928
1929 #[test]
1930 fn test_tokenizer_in_schema() {
1931 let sdl = r#"
1932 index articles {
1933 field title: text<german> [indexed, stored]
1934 field body: text<en_stem> [indexed]
1935 }
1936 "#;
1937
1938 let indexes = parse_sdl(sdl).unwrap();
1939 let schema = indexes[0].to_schema();
1940
1941 let title_field = schema.get_field("title").unwrap();
1942 let title_entry = schema.get_field_entry(title_field).unwrap();
1943 assert_eq!(title_entry.tokenizer, Some("german".to_string()));
1944
1945 let body_field = schema.get_field("body").unwrap();
1946 let body_entry = schema.get_field_entry(body_field).unwrap();
1947 assert_eq!(body_entry.tokenizer, Some("en_stem".to_string()));
1948 }
1949
1950 #[test]
1951 fn test_query_router_basic() {
1952 let sdl = r#"
1953 index documents {
1954 field title: text [indexed, stored]
1955 field uri: text [indexed, stored]
1956
1957 query_router {
1958 pattern: "10\\.\\d{4,}/[^\\s]+"
1959 substitution: "doi://{0}"
1960 target_field: uris
1961 mode: exclusive
1962 }
1963 }
1964 "#;
1965
1966 let indexes = parse_sdl(sdl).unwrap();
1967 let index = &indexes[0];
1968
1969 assert_eq!(index.query_routers.len(), 1);
1970 let router = &index.query_routers[0];
1971 assert_eq!(router.pattern, r"10\.\d{4,}/[^\s]+");
1972 assert_eq!(router.substitution, "doi://{0}");
1973 assert_eq!(router.target_field, "uris");
1974 assert_eq!(router.mode, RoutingMode::Exclusive);
1975 }
1976
1977 #[test]
1978 fn test_query_router_raw_string() {
1979 let sdl = r#"
1980 index documents {
1981 field uris: text [indexed, stored]
1982
1983 query_router {
1984 pattern: r"^pmid:(\d+)$"
1985 substitution: "pubmed://{1}"
1986 target_field: uris
1987 mode: additional
1988 }
1989 }
1990 "#;
1991
1992 let indexes = parse_sdl(sdl).unwrap();
1993 let router = &indexes[0].query_routers[0];
1994
1995 assert_eq!(router.pattern, r"^pmid:(\d+)$");
1996 assert_eq!(router.substitution, "pubmed://{1}");
1997 assert_eq!(router.mode, RoutingMode::Additional);
1998 }
1999
2000 #[test]
2001 fn test_multiple_query_routers() {
2002 let sdl = r#"
2003 index documents {
2004 field uris: text [indexed, stored]
2005
2006 query_router {
2007 pattern: r"^doi:(10\.\d{4,}/[^\s]+)$"
2008 substitution: "doi://{1}"
2009 target_field: uris
2010 mode: exclusive
2011 }
2012
2013 query_router {
2014 pattern: r"^pmid:(\d+)$"
2015 substitution: "pubmed://{1}"
2016 target_field: uris
2017 mode: exclusive
2018 }
2019
2020 query_router {
2021 pattern: r"^arxiv:(\d+\.\d+)$"
2022 substitution: "arxiv://{1}"
2023 target_field: uris
2024 mode: additional
2025 }
2026 }
2027 "#;
2028
2029 let indexes = parse_sdl(sdl).unwrap();
2030 assert_eq!(indexes[0].query_routers.len(), 3);
2031 }
2032
2033 #[test]
2034 fn test_query_router_default_mode() {
2035 let sdl = r#"
2036 index documents {
2037 field uris: text [indexed, stored]
2038
2039 query_router {
2040 pattern: r"test"
2041 substitution: "{0}"
2042 target_field: uris
2043 }
2044 }
2045 "#;
2046
2047 let indexes = parse_sdl(sdl).unwrap();
2048 assert_eq!(indexes[0].query_routers[0].mode, RoutingMode::Additional);
2050 }
2051
2052 #[test]
2053 fn test_multi_attribute() {
2054 let sdl = r#"
2055 index documents {
2056 field uris: text [indexed, stored<multi>]
2057 field title: text [indexed, stored]
2058 }
2059 "#;
2060
2061 let indexes = parse_sdl(sdl).unwrap();
2062 assert_eq!(indexes.len(), 1);
2063
2064 let fields = &indexes[0].fields;
2065 assert_eq!(fields.len(), 2);
2066
2067 assert_eq!(fields[0].name, "uris");
2069 assert!(fields[0].multi, "uris field should have multi=true");
2070
2071 assert_eq!(fields[1].name, "title");
2073 assert!(!fields[1].multi, "title field should have multi=false");
2074
2075 let schema = indexes[0].to_schema();
2077 let uris_field = schema.get_field("uris").unwrap();
2078 let title_field = schema.get_field("title").unwrap();
2079
2080 assert!(schema.get_field_entry(uris_field).unwrap().multi);
2081 assert!(!schema.get_field_entry(title_field).unwrap().multi);
2082 }
2083
2084 #[test]
2085 fn test_sparse_vector_field() {
2086 let sdl = r#"
2087 index documents {
2088 field embedding: sparse_vector [indexed, stored]
2089 }
2090 "#;
2091
2092 let indexes = parse_sdl(sdl).unwrap();
2093 assert_eq!(indexes.len(), 1);
2094 assert_eq!(indexes[0].fields.len(), 1);
2095 assert_eq!(indexes[0].fields[0].name, "embedding");
2096 assert_eq!(indexes[0].fields[0].field_type, FieldType::SparseVector);
2097 assert!(indexes[0].fields[0].sparse_vector_config.is_none());
2098 }
2099
2100 #[test]
2101 fn test_sparse_vector_with_config() {
2102 let sdl = r#"
2103 index documents {
2104 field embedding: sparse_vector<u16> [indexed<quantization: uint8>, stored]
2105 field dense: sparse_vector<u32> [indexed<quantization: float32>]
2106 }
2107 "#;
2108
2109 let indexes = parse_sdl(sdl).unwrap();
2110 assert_eq!(indexes[0].fields.len(), 2);
2111
2112 let f1 = &indexes[0].fields[0];
2114 assert_eq!(f1.name, "embedding");
2115 let config1 = f1.sparse_vector_config.as_ref().unwrap();
2116 assert_eq!(config1.index_size, IndexSize::U16);
2117 assert_eq!(config1.weight_quantization, WeightQuantization::UInt8);
2118
2119 let f2 = &indexes[0].fields[1];
2121 assert_eq!(f2.name, "dense");
2122 let config2 = f2.sparse_vector_config.as_ref().unwrap();
2123 assert_eq!(config2.index_size, IndexSize::U32);
2124 assert_eq!(config2.weight_quantization, WeightQuantization::Float32);
2125 }
2126
2127 #[test]
2128 fn test_sparse_vector_bmp_block_size() {
2129 let sdl = r#"
2130 index documents {
2131 field emb: sparse_vector<u32> [indexed<format: bmp, dims: 105879, bmp_block_size: 256>]
2132 field emb2: sparse_vector<u32> [indexed<format: bmp, dims: 30522>]
2133 }
2134 "#;
2135
2136 let indexes = parse_sdl(sdl).unwrap();
2137 let config1 = indexes[0].fields[0].sparse_vector_config.as_ref().unwrap();
2138 assert_eq!(config1.format, SparseFormat::Bmp);
2139 assert_eq!(config1.bmp_block_size, 256);
2140
2141 let config2 = indexes[0].fields[1].sparse_vector_config.as_ref().unwrap();
2143 assert_eq!(
2144 config2.bmp_block_size,
2145 SparseVectorConfig::DEFAULT_BMP_BLOCK_SIZE
2146 );
2147 }
2148
2149 #[test]
2152 fn test_sparse_vector_bmp_grid_bits() {
2153 let sdl = r#"
2154 index documents {
2155 field emb: sparse_vector<u32> [indexed<format: bmp, dims: 105879, bmp_block_size: 256, bmp_grid_bits: 2>]
2156 field emb2: sparse_vector<u32> [indexed<format: bmp, dims: 30522>]
2157 field emb3: sparse_vector<u32> [indexed<format: bmp, dims: 30522, bmp_grid_bits: 3>]
2158 }
2159 "#;
2160
2161 let indexes = parse_sdl(sdl).unwrap();
2162 let config1 = indexes[0].fields[0].sparse_vector_config.as_ref().unwrap();
2163 assert_eq!(config1.bmp_grid_bits, 2);
2164 let config2 = indexes[0].fields[1].sparse_vector_config.as_ref().unwrap();
2166 assert_eq!(
2167 config2.bmp_grid_bits,
2168 SparseVectorConfig::DEFAULT_BMP_GRID_BITS
2169 );
2170 let config3 = indexes[0].fields[2].sparse_vector_config.as_ref().unwrap();
2172 assert_eq!(
2173 config3.bmp_grid_bits,
2174 SparseVectorConfig::DEFAULT_BMP_GRID_BITS
2175 );
2176 }
2177
2178 #[test]
2179 fn test_sparse_vector_with_weight_threshold() {
2180 let sdl = r#"
2181 index documents {
2182 field embedding: sparse_vector<u16> [indexed<quantization: uint8, weight_threshold: 0.1>, stored]
2183 field embedding2: sparse_vector<u32> [indexed<quantization: float16, weight_threshold: 0.05>]
2184 }
2185 "#;
2186
2187 let indexes = parse_sdl(sdl).unwrap();
2188 assert_eq!(indexes[0].fields.len(), 2);
2189
2190 let f1 = &indexes[0].fields[0];
2192 assert_eq!(f1.name, "embedding");
2193 let config1 = f1.sparse_vector_config.as_ref().unwrap();
2194 assert_eq!(config1.index_size, IndexSize::U16);
2195 assert_eq!(config1.weight_quantization, WeightQuantization::UInt8);
2196 assert!((config1.weight_threshold - 0.1).abs() < 0.001);
2197
2198 let f2 = &indexes[0].fields[1];
2200 assert_eq!(f2.name, "embedding2");
2201 let config2 = f2.sparse_vector_config.as_ref().unwrap();
2202 assert_eq!(config2.index_size, IndexSize::U32);
2203 assert_eq!(config2.weight_quantization, WeightQuantization::Float16);
2204 assert!((config2.weight_threshold - 0.05).abs() < 0.001);
2205 }
2206
2207 #[test]
2208 fn test_sparse_vector_with_pruning() {
2209 let sdl = r#"
2210 index documents {
2211 field embedding: sparse_vector [indexed<quantization: uint8, pruning: 0.1>, stored]
2212 }
2213 "#;
2214
2215 let indexes = parse_sdl(sdl).unwrap();
2216 let f = &indexes[0].fields[0];
2217 assert_eq!(f.name, "embedding");
2218 let config = f.sparse_vector_config.as_ref().unwrap();
2219 assert_eq!(config.weight_quantization, WeightQuantization::UInt8);
2220 assert_eq!(config.pruning, Some(0.1));
2221 }
2222
2223 #[test]
2224 fn test_sparse_vector_with_doc_mass() {
2225 let sdl = r#"
2226 index documents {
2227 field embedding: sparse_vector [indexed<quantization: uint8, doc_mass: 0.9>, stored]
2228 }
2229 "#;
2230
2231 let indexes = parse_sdl(sdl).unwrap();
2232 let config = indexes[0].fields[0].sparse_vector_config.as_ref().unwrap();
2233 assert_eq!(config.doc_mass, Some(0.9));
2234
2235 let sdl = r#"
2237 index documents {
2238 field embedding: sparse_vector [indexed<quantization: uint8>]
2239 }
2240 "#;
2241 let indexes = parse_sdl(sdl).unwrap();
2242 let config = indexes[0].fields[0].sparse_vector_config.as_ref().unwrap();
2243 assert_eq!(config.doc_mass, None);
2244 }
2245
2246 #[test]
2247 fn test_dense_vector_field() {
2248 let sdl = r#"
2249 index documents {
2250 field embedding: dense_vector<768> [indexed, stored]
2251 }
2252 "#;
2253
2254 let indexes = parse_sdl(sdl).unwrap();
2255 assert_eq!(indexes.len(), 1);
2256 assert_eq!(indexes[0].fields.len(), 1);
2257
2258 let f = &indexes[0].fields[0];
2259 assert_eq!(f.name, "embedding");
2260 assert_eq!(f.field_type, FieldType::DenseVector);
2261
2262 let config = f.dense_vector_config.as_ref().unwrap();
2263 assert_eq!(config.dim, 768);
2264 }
2265
2266 #[test]
2267 fn test_dense_vector_alias() {
2268 let sdl = r#"
2269 index documents {
2270 field embedding: vector<1536> [indexed]
2271 }
2272 "#;
2273
2274 let indexes = parse_sdl(sdl).unwrap();
2275 assert_eq!(indexes[0].fields[0].field_type, FieldType::DenseVector);
2276 assert_eq!(
2277 indexes[0].fields[0]
2278 .dense_vector_config
2279 .as_ref()
2280 .unwrap()
2281 .dim,
2282 1536
2283 );
2284 }
2285
2286 #[test]
2287 fn test_dense_vector_with_num_clusters() {
2288 let sdl = r#"
2289 index documents {
2290 field embedding: dense_vector<768> [indexed<ivf_tq, num_clusters: 256>, stored]
2291 }
2292 "#;
2293
2294 let indexes = parse_sdl(sdl).unwrap();
2295 assert_eq!(indexes.len(), 1);
2296
2297 let f = &indexes[0].fields[0];
2298 assert_eq!(f.name, "embedding");
2299 assert_eq!(f.field_type, FieldType::DenseVector);
2300
2301 let config = f.dense_vector_config.as_ref().unwrap();
2302 assert_eq!(config.dim, 768);
2303 assert_eq!(config.num_clusters, Some(256));
2304 assert_eq!(config.nprobe, 64); }
2306
2307 #[test]
2308 fn scann_float_and_binary_parse_billion_scale_settings() {
2309 let indexes = parse_sdl(
2310 r#"
2311 index billion_vectors {
2312 field embedding: dense_vector<1024, f16> [indexed<scann, num_clusters: 10000000, tree_levels: 2, nprobe: 1024>]
2313 field hash: binary_dense_vector<1024> [indexed<scann, num_clusters: 10000000, tree_levels: 3, nprobe: 2048>]
2314 }
2315 "#,
2316 )
2317 .unwrap();
2318
2319 let dense = indexes[0].fields[0].dense_vector_config.as_ref().unwrap();
2320 assert_eq!(
2321 dense.index_type,
2322 super::super::schema::VectorIndexType::Scann
2323 );
2324 assert_eq!(dense.num_clusters, Some(10_000_000));
2325 assert_eq!(dense.tree_levels, Some(2));
2326 assert_eq!(dense.nprobe, 1024);
2327
2328 let binary = indexes[0].fields[1]
2329 .binary_dense_vector_config
2330 .as_ref()
2331 .unwrap();
2332 assert_eq!(
2333 binary.index_type,
2334 super::super::schema::BinaryIndexType::Scann
2335 );
2336 assert_eq!(binary.num_clusters, Some(10_000_000));
2337 assert_eq!(binary.tree_levels, Some(3));
2338 assert_eq!(binary.nprobe, 2048);
2339 }
2340
2341 #[test]
2342 fn target_vectors_parses_for_float_and_binary_indexes() {
2343 let indexes = parse_sdl(
2344 r#"
2345 index streaming_vectors {
2346 field embedding: dense_vector<1024, f16> [indexed<scann, num_clusters: 1000000, target_vectors: 1000000000>]
2347 field hash: binary_dense_vector<2560> [indexed<ivf, target_vectors: 1000000000>]
2348 }
2349 "#,
2350 )
2351 .unwrap();
2352
2353 assert_eq!(
2354 indexes[0].fields[0]
2355 .dense_vector_config
2356 .as_ref()
2357 .unwrap()
2358 .target_vectors,
2359 Some(1_000_000_000)
2360 );
2361 assert_eq!(
2362 indexes[0].fields[0]
2363 .dense_vector_config
2364 .as_ref()
2365 .unwrap()
2366 .num_clusters,
2367 Some(1_000_000),
2368 "target_vectors may be persisted alongside an explicit, overriding topology"
2369 );
2370 assert_eq!(
2371 indexes[0].fields[1]
2372 .binary_dense_vector_config
2373 .as_ref()
2374 .unwrap()
2375 .target_vectors,
2376 Some(1_000_000_000)
2377 );
2378
2379 let error = parse_sdl(
2380 "index invalid { field hash: binary_dense_vector<256> [indexed<ivf, target_vectors: 0>] }",
2381 )
2382 .expect_err("zero target must fail");
2383 assert!(error.to_string().contains("greater than zero"), "{error}");
2384
2385 let error = parse_sdl(
2386 "index invalid { field embedding: dense_vector<256> [indexed<tq, target_vectors: 1000000>] }",
2387 )
2388 .expect_err("training-free topology hint must fail");
2389 assert!(error.to_string().contains("automatic topology"), "{error}");
2390
2391 for sdl in [
2392 "index invalid { field embedding: dense_vector<256> [indexed<flat, target_vectors: 1000000>] }",
2393 "index invalid { field hash: binary_dense_vector<256> [indexed<flat, target_vectors: 1000000>] }",
2394 ] {
2395 let error = parse_sdl(sdl).expect_err("flat topology hint must fail");
2396 assert!(error.to_string().contains("automatic topology"), "{error}");
2397 }
2398
2399 let error = parse_sdl(
2400 "index invalid { field hash: binary_dense_vector<256> [indexed<ivf, target_vectors: 18446744073709551616>] }",
2401 )
2402 .expect_err("u64 overflow must fail");
2403 assert!(error.to_string().contains("unsigned 64-bit"), "{error}");
2404 }
2405
2406 #[test]
2407 fn scann_rejects_invalid_geometry_and_algorithm_specific_options() {
2408 for (fragment, expected) in [
2409 ("scann, tree_levels: 0", "tree_levels"),
2410 ("scann, tree_levels: 4", "tree_levels"),
2411 ("scann, num_clusters: 30000001", "num_clusters"),
2412 ("scann, num_clusters: 1", "at least 2"),
2413 ("scann, routing: flat", "not configurable for ScaNN"),
2414 (
2415 "scann, num_clusters: 32, nprobe: 33",
2416 "cannot exceed explicit num_clusters",
2417 ),
2418 ("ivf_tq, tree_levels: 2", "only valid for a ScaNN"),
2419 ] {
2420 let sdl = format!(
2421 "index invalid {{ field embedding: dense_vector<128> [indexed<{fragment}>] }}"
2422 );
2423 let error = parse_sdl(&sdl).expect_err(fragment);
2424 assert!(error.to_string().contains(expected), "{fragment}: {error}");
2425 }
2426 }
2427
2428 #[test]
2429 fn binary_scann_accepts_selective_spilling_but_binary_ivf_rejects_it() {
2430 let indexes = parse_sdl(
2431 "index valid { field hash: binary_dense_vector<256> [indexed<scann, soar: selective>] }",
2432 )
2433 .unwrap();
2434 let soar = indexes[0].fields[0]
2435 .binary_dense_vector_config
2436 .as_ref()
2437 .unwrap()
2438 .soar
2439 .as_ref()
2440 .expect("binary ScaNN should retain explicit spilling");
2441 assert_eq!(soar.calibration_target(), Some(0.30));
2442
2443 let error = parse_sdl(
2444 "index invalid { field hash: binary_dense_vector<256> [indexed<ivf, soar: selective>] }",
2445 )
2446 .expect_err("binary IVF spilling must fail loudly");
2447 assert!(error.to_string().contains("requires the ScaNN"), "{error}");
2448 }
2449
2450 #[test]
2451 fn test_dense_vector_with_soar() {
2452 let sdl = r#"
2454 index documents {
2455 field embedding: dense_vector<768> [indexed<ivf_tq>]
2456 }
2457 "#;
2458 let indexes = parse_sdl(sdl).unwrap();
2459 let config = indexes[0].fields[0].dense_vector_config.as_ref().unwrap();
2460 let soar = config
2461 .soar
2462 .as_ref()
2463 .expect("omitted SOAR should enable selective spilling");
2464 assert_eq!(soar.num_secondary, 1);
2465 assert!(soar.selective);
2466 assert_eq!(soar.calibration_target(), Some(0.30));
2467
2468 let sdl = r#"
2470 index documents {
2471 field embedding: dense_vector<768> [indexed<ivf_tq, num_clusters: 256, soar: selective>, stored]
2472 }
2473 "#;
2474
2475 let indexes = parse_sdl(sdl).unwrap();
2476 let config = indexes[0].fields[0].dense_vector_config.as_ref().unwrap();
2477
2478 let soar = config.soar.as_ref().expect("soar should be enabled");
2479 assert_eq!(soar.num_secondary, 1);
2480 assert!(soar.selective);
2481
2482 let sdl = r#"
2484 index documents {
2485 field embedding: dense_vector<768> [indexed<ivf_tq, soar: aggressive>]
2486 }
2487 "#;
2488 let indexes = parse_sdl(sdl).unwrap();
2489 let config = indexes[0].fields[0].dense_vector_config.as_ref().unwrap();
2490 let soar = config.soar.as_ref().expect("soar should be enabled");
2491 assert_eq!(soar.num_secondary, 1);
2492 assert!(!soar.selective);
2493
2494 let sdl = r#"
2496 index documents {
2497 field embedding: dense_vector<768> [indexed<ivf_tq, soar: off>]
2498 }
2499 "#;
2500 let indexes = parse_sdl(sdl).unwrap();
2501 let config = indexes[0].fields[0].dense_vector_config.as_ref().unwrap();
2502 assert!(config.soar.is_none());
2503 }
2504
2505 #[test]
2506 fn omitted_soar_is_canonicalized_off_for_non_ivf_formats() {
2507 let sdl = r#"
2508 index documents {
2509 field tq: dense_vector<768> [indexed<tq>]
2510 field flat: dense_vector<768> [indexed<flat>]
2511 field scann: dense_vector<768> [indexed<scann>]
2512 }
2513 "#;
2514 let indexes = parse_sdl(sdl).unwrap();
2515 for field in &indexes[0].fields {
2516 assert!(
2517 field
2518 .dense_vector_config
2519 .as_ref()
2520 .expect("dense config")
2521 .soar
2522 .is_none(),
2523 "{} should not retain an ignored SOAR default",
2524 field.name,
2525 );
2526 }
2527 }
2528
2529 #[test]
2530 fn float_scann_rejects_explicit_soar_until_secondary_assignments_exist() {
2531 let error = parse_sdl(
2532 "index invalid { field embedding: dense_vector<256> [indexed<scann, soar: selective>] }",
2533 )
2534 .unwrap_err();
2535 assert!(error.to_string().contains("not implemented"));
2536 assert!(error.to_string().contains("Scann") || error.to_string().contains("ScaNN"));
2537 }
2538
2539 #[test]
2540 fn test_ivf_routing_modes_apply_to_float_and_binary_fields() {
2541 let indexes = parse_sdl(
2542 r#"
2543 index vectors {
2544 field embedding: dense_vector<768> [indexed<ivf_tq, routing: hnsw>]
2545 field hash: binary_dense_vector<512> [indexed<ivf, routing: two_level>]
2546 }
2547 "#,
2548 )
2549 .unwrap();
2550 let schema = indexes[0].to_schema();
2551 let embedding = schema.get_field("embedding").unwrap();
2552 let hash = schema.get_field("hash").unwrap();
2553 assert_eq!(
2554 schema
2555 .get_field_entry(embedding)
2556 .unwrap()
2557 .dense_vector_config
2558 .as_ref()
2559 .unwrap()
2560 .ivf_routing,
2561 super::super::schema::IvfRoutingMode::Hnsw
2562 );
2563 assert_eq!(
2564 schema
2565 .get_field_entry(hash)
2566 .unwrap()
2567 .binary_dense_vector_config
2568 .as_ref()
2569 .unwrap()
2570 .ivf_routing,
2571 super::super::schema::IvfRoutingMode::TwoLevel
2572 );
2573 }
2574
2575 #[test]
2576 fn test_binary_dense_vector_with_ivf() {
2577 let sdl = r#"
2578 index documents {
2579 field hash: binary_dense_vector<512> [indexed<ivf, num_clusters: 128, nprobe: 16>, stored]
2580 }
2581 "#;
2582
2583 let indexes = parse_sdl(sdl).unwrap();
2584 let config = indexes[0].fields[0]
2585 .binary_dense_vector_config
2586 .as_ref()
2587 .unwrap();
2588 assert_eq!(config.dim, 512);
2589 assert_eq!(
2590 config.index_type,
2591 super::super::schema::BinaryIndexType::Ivf
2592 );
2593 assert_eq!(config.num_clusters, Some(128));
2594 assert_eq!(config.nprobe, 16);
2595
2596 let sdl = r#"
2599 index documents {
2600 field hash: binary_dense_vector<512> [indexed]
2601 }
2602 "#;
2603 let indexes = parse_sdl(sdl).unwrap();
2604 let config = indexes[0].fields[0]
2605 .binary_dense_vector_config
2606 .as_ref()
2607 .unwrap();
2608 assert_eq!(
2609 config.index_type,
2610 super::super::schema::BinaryIndexType::Ivf
2611 );
2612 }
2613
2614 #[test]
2615 fn test_dense_vector_with_num_clusters_and_nprobe() {
2616 let sdl = r#"
2617 index documents {
2618 field embedding: dense_vector<1536> [indexed<ivf_tq, num_clusters: 512, nprobe: 64>]
2619 }
2620 "#;
2621
2622 let indexes = parse_sdl(sdl).unwrap();
2623 let config = indexes[0].fields[0].dense_vector_config.as_ref().unwrap();
2624
2625 assert_eq!(config.dim, 1536);
2626 assert_eq!(config.num_clusters, Some(512));
2627 assert_eq!(config.nprobe, 64);
2628 }
2629
2630 #[test]
2631 fn test_dense_vector_keyword_syntax() {
2632 let sdl = r#"
2633 index documents {
2634 field embedding: dense_vector<dims: 1536> [indexed, stored]
2635 }
2636 "#;
2637
2638 let indexes = parse_sdl(sdl).unwrap();
2639 let config = indexes[0].fields[0].dense_vector_config.as_ref().unwrap();
2640
2641 assert_eq!(config.dim, 1536);
2642 assert!(config.num_clusters.is_none());
2643 }
2644
2645 #[test]
2646 fn test_dense_vector_keyword_syntax_full() {
2647 let sdl = r#"
2648 index documents {
2649 field embedding: dense_vector<dims: 1536> [indexed<ivf_tq, num_clusters: 256, nprobe: 64>]
2650 }
2651 "#;
2652
2653 let indexes = parse_sdl(sdl).unwrap();
2654 let config = indexes[0].fields[0].dense_vector_config.as_ref().unwrap();
2655
2656 assert_eq!(config.dim, 1536);
2657 assert_eq!(config.num_clusters, Some(256));
2658 assert_eq!(config.nprobe, 64);
2659 }
2660
2661 #[test]
2662 fn test_dense_vector_keyword_syntax_partial() {
2663 let sdl = r#"
2664 index documents {
2665 field embedding: dense_vector<dims: 768> [indexed<ivf_tq, num_clusters: 128>]
2666 }
2667 "#;
2668
2669 let indexes = parse_sdl(sdl).unwrap();
2670 let config = indexes[0].fields[0].dense_vector_config.as_ref().unwrap();
2671
2672 assert_eq!(config.dim, 768);
2673 assert_eq!(config.num_clusters, Some(128));
2674 assert_eq!(config.nprobe, 64); }
2676
2677 #[test]
2678 fn test_dense_vector_ivf_tq_index_with_probe() {
2679 use crate::dsl::schema::VectorIndexType;
2680
2681 let sdl = r#"
2682 index documents {
2683 field embedding: dense_vector<dims: 768> [indexed<ivf_tq, num_clusters: 256, nprobe: 64>]
2684 }
2685 "#;
2686
2687 let indexes = parse_sdl(sdl).unwrap();
2688 let config = indexes[0].fields[0].dense_vector_config.as_ref().unwrap();
2689
2690 assert_eq!(config.dim, 768);
2691 assert_eq!(config.index_type, VectorIndexType::IvfTq);
2692 assert_eq!(config.num_clusters, Some(256));
2693 assert_eq!(config.nprobe, 64);
2694 }
2695
2696 #[test]
2697 fn test_dense_vector_ivf_tq_index_without_explicit_probe() {
2698 use crate::dsl::schema::VectorIndexType;
2699
2700 let sdl = r#"
2701 index documents {
2702 field embedding: dense_vector<dims: 1536> [indexed<ivf_tq, num_clusters: 512>]
2703 }
2704 "#;
2705
2706 let indexes = parse_sdl(sdl).unwrap();
2707 let config = indexes[0].fields[0].dense_vector_config.as_ref().unwrap();
2708
2709 assert_eq!(config.dim, 1536);
2710 assert_eq!(config.index_type, VectorIndexType::IvfTq);
2711 assert_eq!(config.num_clusters, Some(512));
2712 }
2713
2714 #[test]
2715 fn test_dense_vector_ivf_tq_no_clusters() {
2716 use crate::dsl::schema::VectorIndexType;
2717
2718 let sdl = r#"
2719 index documents {
2720 field embedding: dense_vector<dims: 768> [indexed<ivf_tq>]
2721 }
2722 "#;
2723
2724 let indexes = parse_sdl(sdl).unwrap();
2725 let config = indexes[0].fields[0].dense_vector_config.as_ref().unwrap();
2726
2727 assert_eq!(config.dim, 768);
2728 assert_eq!(config.index_type, VectorIndexType::IvfTq);
2729 assert!(config.num_clusters.is_none());
2730 }
2731
2732 #[test]
2733 fn removed_ivf_pq_still_parses_to_the_reserved_variant() {
2734 use crate::dsl::schema::VectorIndexType;
2735
2736 let sdl = r#"
2739 index test {
2740 field embedding: dense_vector<8> [indexed<ivf_pq>]
2741 }
2742 "#;
2743 let indexes = parse_sdl(sdl).unwrap();
2744 let config = indexes[0].fields[0].dense_vector_config.as_ref().unwrap();
2745 assert_eq!(config.index_type, VectorIndexType::IvfPq);
2746
2747 let mut builder = crate::dsl::SchemaBuilder::default();
2748 builder.add_dense_vector_field_with_config("embedding", true, true, config.clone());
2749 let schema = builder.build();
2750 let error = crate::dsl::schema::reject_removed_vector_index_types(&schema)
2751 .expect_err("removed index types must be rejected at the index gate");
2752 assert!(error.contains("ivf_tq"), "{error}");
2753 assert!(error.contains("removed"), "{error}");
2754 }
2755
2756 #[test]
2757 fn test_dense_vector_flat_index() {
2758 use crate::dsl::schema::VectorIndexType;
2759
2760 let sdl = r#"
2761 index documents {
2762 field embedding: dense_vector<dims: 768> [indexed<flat>]
2763 }
2764 "#;
2765
2766 let indexes = parse_sdl(sdl).unwrap();
2767 let config = indexes[0].fields[0].dense_vector_config.as_ref().unwrap();
2768
2769 assert_eq!(config.dim, 768);
2770 assert_eq!(config.index_type, VectorIndexType::Flat);
2771 }
2772
2773 #[test]
2774 fn test_dense_vector_default_index_type() {
2775 use crate::dsl::schema::VectorIndexType;
2776
2777 let sdl = r#"
2779 index documents {
2780 field embedding: dense_vector<dims: 768> [indexed]
2781 }
2782 "#;
2783
2784 let indexes = parse_sdl(sdl).unwrap();
2785 let config = indexes[0].fields[0].dense_vector_config.as_ref().unwrap();
2786
2787 assert_eq!(config.dim, 768);
2788 assert_eq!(config.index_type, VectorIndexType::IvfTq);
2789 }
2790
2791 #[test]
2792 fn test_dense_vector_f16_quantization() {
2793 use crate::dsl::schema::{DenseVectorQuantization, VectorIndexType};
2794
2795 let sdl = r#"
2796 index documents {
2797 field embedding: dense_vector<768, f16> [indexed]
2798 }
2799 "#;
2800
2801 let indexes = parse_sdl(sdl).unwrap();
2802 let config = indexes[0].fields[0].dense_vector_config.as_ref().unwrap();
2803
2804 assert_eq!(config.dim, 768);
2805 assert_eq!(config.quantization, DenseVectorQuantization::F16);
2806 assert_eq!(config.index_type, VectorIndexType::IvfTq);
2807 }
2808
2809 #[test]
2810 fn test_dense_vector_uint8_quantization() {
2811 use crate::dsl::schema::DenseVectorQuantization;
2812
2813 let sdl = r#"
2814 index documents {
2815 field embedding: dense_vector<1024, uint8> [indexed<ivf_tq>]
2816 }
2817 "#;
2818
2819 let indexes = parse_sdl(sdl).unwrap();
2820 let config = indexes[0].fields[0].dense_vector_config.as_ref().unwrap();
2821
2822 assert_eq!(config.dim, 1024);
2823 assert_eq!(config.quantization, DenseVectorQuantization::UInt8);
2824 }
2825
2826 #[test]
2827 fn test_dense_vector_u8_alias() {
2828 use crate::dsl::schema::DenseVectorQuantization;
2829
2830 let sdl = r#"
2831 index documents {
2832 field embedding: dense_vector<512, u8> [indexed]
2833 }
2834 "#;
2835
2836 let indexes = parse_sdl(sdl).unwrap();
2837 let config = indexes[0].fields[0].dense_vector_config.as_ref().unwrap();
2838
2839 assert_eq!(config.dim, 512);
2840 assert_eq!(config.quantization, DenseVectorQuantization::UInt8);
2841 }
2842
2843 #[test]
2844 fn test_dense_vector_default_f32_quantization() {
2845 use crate::dsl::schema::DenseVectorQuantization;
2846
2847 let sdl = r#"
2849 index documents {
2850 field embedding: dense_vector<768> [indexed]
2851 }
2852 "#;
2853
2854 let indexes = parse_sdl(sdl).unwrap();
2855 let config = indexes[0].fields[0].dense_vector_config.as_ref().unwrap();
2856
2857 assert_eq!(config.dim, 768);
2858 assert_eq!(config.quantization, DenseVectorQuantization::F32);
2859 }
2860
2861 #[test]
2862 fn test_dense_vector_keyword_with_quantization() {
2863 use crate::dsl::schema::DenseVectorQuantization;
2864
2865 let sdl = r#"
2866 index documents {
2867 field embedding: dense_vector<dims: 768, f16> [indexed]
2868 }
2869 "#;
2870
2871 let indexes = parse_sdl(sdl).unwrap();
2872 let config = indexes[0].fields[0].dense_vector_config.as_ref().unwrap();
2873
2874 assert_eq!(config.dim, 768);
2875 assert_eq!(config.quantization, DenseVectorQuantization::F16);
2876 }
2877
2878 #[test]
2879 fn test_json_field_type() {
2880 let sdl = r#"
2881 index documents {
2882 field title: text [indexed, stored]
2883 field metadata: json [stored]
2884 field extra: json
2885 }
2886 "#;
2887
2888 let indexes = parse_sdl(sdl).unwrap();
2889 let index = &indexes[0];
2890
2891 assert_eq!(index.fields.len(), 3);
2892
2893 assert_eq!(index.fields[1].name, "metadata");
2895 assert!(matches!(index.fields[1].field_type, FieldType::Json));
2896 assert!(index.fields[1].stored);
2897 assert_eq!(index.fields[2].name, "extra");
2901 assert!(matches!(index.fields[2].field_type, FieldType::Json));
2902
2903 let schema = index.to_schema();
2905 let metadata_field = schema.get_field("metadata").unwrap();
2906 let entry = schema.get_field_entry(metadata_field).unwrap();
2907 assert_eq!(entry.field_type, FieldType::Json);
2908 assert!(!entry.indexed); assert!(entry.stored);
2910 }
2911
2912 #[test]
2913 fn test_sparse_vector_query_config() {
2914 use crate::structures::QueryWeighting;
2915
2916 let sdl = r#"
2917 index documents {
2918 field embedding: sparse_vector<u16> [indexed<quantization: uint8, query<tokenizer: "Alibaba-NLP/gte-Qwen2-1.5B-instruct", weighting: idf>>]
2919 }
2920 "#;
2921
2922 let indexes = parse_sdl(sdl).unwrap();
2923 let index = &indexes[0];
2924
2925 assert_eq!(index.fields.len(), 1);
2926 assert_eq!(index.fields[0].name, "embedding");
2927 assert!(matches!(
2928 index.fields[0].field_type,
2929 FieldType::SparseVector
2930 ));
2931
2932 let config = index.fields[0].sparse_vector_config.as_ref().unwrap();
2933 assert_eq!(config.index_size, IndexSize::U16);
2934 assert_eq!(config.weight_quantization, WeightQuantization::UInt8);
2935
2936 let query_config = config.query_config.as_ref().unwrap();
2938 assert_eq!(
2939 query_config.tokenizer.as_deref(),
2940 Some("Alibaba-NLP/gte-Qwen2-1.5B-instruct")
2941 );
2942 assert_eq!(query_config.weighting, QueryWeighting::Idf);
2943
2944 let schema = index.to_schema();
2946 let embedding_field = schema.get_field("embedding").unwrap();
2947 let entry = schema.get_field_entry(embedding_field).unwrap();
2948 let sv_config = entry.sparse_vector_config.as_ref().unwrap();
2949 let qc = sv_config.query_config.as_ref().unwrap();
2950 assert_eq!(
2951 qc.tokenizer.as_deref(),
2952 Some("Alibaba-NLP/gte-Qwen2-1.5B-instruct")
2953 );
2954 assert_eq!(qc.weighting, QueryWeighting::Idf);
2955 }
2956
2957 #[test]
2958 fn test_sparse_vector_query_config_weighting_one() {
2959 use crate::structures::QueryWeighting;
2960
2961 let sdl = r#"
2962 index documents {
2963 field embedding: sparse_vector [indexed<query<weighting: one>>]
2964 }
2965 "#;
2966
2967 let indexes = parse_sdl(sdl).unwrap();
2968 let config = indexes[0].fields[0].sparse_vector_config.as_ref().unwrap();
2969
2970 let query_config = config.query_config.as_ref().unwrap();
2971 assert!(query_config.tokenizer.is_none());
2972 assert_eq!(query_config.weighting, QueryWeighting::One);
2973 }
2974
2975 #[test]
2976 fn test_sparse_vector_query_config_weighting_idf_file() {
2977 use crate::structures::QueryWeighting;
2978
2979 let sdl = r#"
2980 index documents {
2981 field embedding: sparse_vector<u16> [indexed<quantization: uint8, query<tokenizer: "opensearch-neural-sparse-encoding-v1", weighting: idf_file>>]
2982 }
2983 "#;
2984
2985 let indexes = parse_sdl(sdl).unwrap();
2986 let config = indexes[0].fields[0].sparse_vector_config.as_ref().unwrap();
2987
2988 let query_config = config.query_config.as_ref().unwrap();
2989 assert_eq!(
2990 query_config.tokenizer.as_deref(),
2991 Some("opensearch-neural-sparse-encoding-v1")
2992 );
2993 assert_eq!(query_config.weighting, QueryWeighting::IdfFile);
2994
2995 let schema = indexes[0].to_schema();
2997 let field = schema.get_field("embedding").unwrap();
2998 let entry = schema.get_field_entry(field).unwrap();
2999 let sc = entry.sparse_vector_config.as_ref().unwrap();
3000 let qc = sc.query_config.as_ref().unwrap();
3001 assert_eq!(qc.weighting, QueryWeighting::IdfFile);
3002 }
3003
3004 #[test]
3005 fn test_sparse_vector_query_config_pruning_params() {
3006 let sdl = r#"
3007 index documents {
3008 field embedding: sparse_vector<u16> [indexed<quantization: uint8, query<weighting: idf, weight_threshold: 0.03, max_dims: 25, pruning: 0.2, lsp_gamma: 500>>]
3009 }
3010 "#;
3011
3012 let indexes = parse_sdl(sdl).unwrap();
3013 let config = indexes[0].fields[0].sparse_vector_config.as_ref().unwrap();
3014
3015 let qc = config.query_config.as_ref().unwrap();
3016 assert_eq!(qc.weighting, QueryWeighting::Idf);
3017 assert!((qc.weight_threshold - 0.03).abs() < 0.001);
3018 assert_eq!(qc.max_query_dims, Some(25));
3019 assert!((qc.pruning.unwrap() - 0.2).abs() < 0.001);
3020 assert_eq!(qc.lsp_gamma, Some(500));
3021
3022 let schema = indexes[0].to_schema();
3024 let field = schema.get_field("embedding").unwrap();
3025 let entry = schema.get_field_entry(field).unwrap();
3026 let sc = entry.sparse_vector_config.as_ref().unwrap();
3027 let rqc = sc.query_config.as_ref().unwrap();
3028 assert!((rqc.weight_threshold - 0.03).abs() < 0.001);
3029 assert_eq!(rqc.max_query_dims, Some(25));
3030 assert!((rqc.pruning.unwrap() - 0.2).abs() < 0.001);
3031 assert_eq!(rqc.lsp_gamma, Some(500));
3032 }
3033
3034 #[test]
3035 fn test_sparse_vector_format_maxscore() {
3036 let sdl = r#"
3037 index documents {
3038 field embedding: sparse_vector<u16> [indexed<format: maxscore, quantization: uint8>]
3039 }
3040 "#;
3041
3042 let indexes = parse_sdl(sdl).unwrap();
3043 let config = indexes[0].fields[0].sparse_vector_config.as_ref().unwrap();
3044 assert_eq!(config.format, SparseFormat::MaxScore);
3045 assert_eq!(config.weight_quantization, WeightQuantization::UInt8);
3046
3047 let schema = indexes[0].to_schema();
3049 let field = schema.get_field("embedding").unwrap();
3050 let entry = schema.get_field_entry(field).unwrap();
3051 let sc = entry.sparse_vector_config.as_ref().unwrap();
3052 assert_eq!(sc.format, SparseFormat::MaxScore);
3053 }
3054
3055 #[test]
3056 fn test_sparse_vector_format_bmp() {
3057 let sdl = r#"
3058 index documents {
3059 field embedding: sparse_vector<u16> [indexed<format: bmp, quantization: uint8>]
3060 }
3061 "#;
3062
3063 let indexes = parse_sdl(sdl).unwrap();
3064 let config = indexes[0].fields[0].sparse_vector_config.as_ref().unwrap();
3065 assert_eq!(config.format, SparseFormat::Bmp);
3066 }
3067
3068 #[test]
3069 fn test_fast_attribute() {
3070 let sdl = r#"
3071 index products {
3072 field name: text [indexed, stored]
3073 field price: f64 [indexed, fast]
3074 field category: text [indexed, stored, fast]
3075 field count: u64 [fast]
3076 field score: i64 [indexed, stored, fast]
3077 }
3078 "#;
3079
3080 let indexes = parse_sdl(sdl).unwrap();
3081 assert_eq!(indexes.len(), 1);
3082 let index = &indexes[0];
3083 assert_eq!(index.fields.len(), 5);
3084
3085 assert!(!index.fields[0].fast);
3087 assert!(index.fields[1].fast);
3089 assert!(matches!(index.fields[1].field_type, FieldType::F64));
3090 assert!(index.fields[2].fast);
3092 assert!(matches!(index.fields[2].field_type, FieldType::Text));
3093 assert!(index.fields[3].fast);
3095 assert!(matches!(index.fields[3].field_type, FieldType::U64));
3096 assert!(index.fields[4].fast);
3098 assert!(matches!(index.fields[4].field_type, FieldType::I64));
3099
3100 let schema = index.to_schema();
3102 let price_field = schema.get_field("price").unwrap();
3103 assert!(schema.get_field_entry(price_field).unwrap().fast);
3104
3105 let category_field = schema.get_field("category").unwrap();
3106 assert!(schema.get_field_entry(category_field).unwrap().fast);
3107
3108 let name_field = schema.get_field("name").unwrap();
3109 assert!(!schema.get_field_entry(name_field).unwrap().fast);
3110 }
3111
3112 #[test]
3113 fn test_primary_attribute() {
3114 let sdl = r#"
3115 index documents {
3116 field id: text [primary, stored]
3117 field title: text [indexed, stored]
3118 }
3119 "#;
3120
3121 let indexes = parse_sdl(sdl).unwrap();
3122 assert_eq!(indexes.len(), 1);
3123 let index = &indexes[0];
3124 assert_eq!(index.fields.len(), 2);
3125
3126 let id_field = &index.fields[0];
3128 assert!(id_field.primary, "id should be primary");
3129 assert!(id_field.fast, "primary implies fast");
3130 assert!(id_field.indexed, "primary implies indexed");
3131
3132 assert!(!index.fields[1].primary);
3134
3135 let schema = index.to_schema();
3137 let id = schema.get_field("id").unwrap();
3138 let id_entry = schema.get_field_entry(id).unwrap();
3139 assert!(id_entry.primary_key);
3140 assert!(id_entry.fast);
3141 assert!(id_entry.indexed);
3142
3143 let title = schema.get_field("title").unwrap();
3144 assert!(!schema.get_field_entry(title).unwrap().primary_key);
3145
3146 assert_eq!(schema.primary_field(), Some(id));
3148 }
3149
3150 #[test]
3151 fn test_primary_with_other_attributes() {
3152 let sdl = r#"
3153 index documents {
3154 field id: text<simple> [primary, indexed, stored]
3155 field body: text [indexed]
3156 }
3157 "#;
3158
3159 let indexes = parse_sdl(sdl).unwrap();
3160 let id_field = &indexes[0].fields[0];
3161 assert!(id_field.primary);
3162 assert!(id_field.indexed);
3163 assert!(id_field.stored);
3164 assert!(id_field.fast);
3165 assert_eq!(id_field.tokenizer, Some("simple".to_string()));
3166 }
3167
3168 #[test]
3169 fn test_primary_only_one_allowed() {
3170 let sdl = r#"
3171 index documents {
3172 field id: text [primary]
3173 field alt_id: text [primary]
3174 }
3175 "#;
3176
3177 let result = parse_sdl(sdl);
3178 assert!(result.is_err());
3179 let err = result.unwrap_err().to_string();
3180 assert!(
3181 err.contains("primary key"),
3182 "Error should mention primary key: {}",
3183 err
3184 );
3185 }
3186
3187 #[test]
3188 fn test_primary_must_be_text() {
3189 let sdl = r#"
3190 index documents {
3191 field id: u64 [primary]
3192 }
3193 "#;
3194
3195 let result = parse_sdl(sdl);
3196 assert!(result.is_err());
3197 let err = result.unwrap_err().to_string();
3198 assert!(
3199 err.contains("text"),
3200 "Error should mention text type: {}",
3201 err
3202 );
3203 }
3204
3205 #[test]
3206 fn test_primary_cannot_be_multi() {
3207 let sdl = r#"
3208 index documents {
3209 field id: text [primary, stored<multi>]
3210 }
3211 "#;
3212
3213 let result = parse_sdl(sdl);
3214 assert!(result.is_err());
3215 let err = result.unwrap_err().to_string();
3216 assert!(err.contains("multi"), "Error should mention multi: {}", err);
3217 }
3218
3219 #[test]
3220 fn test_no_primary_field() {
3221 let sdl = r#"
3223 index documents {
3224 field title: text [indexed, stored]
3225 }
3226 "#;
3227
3228 let indexes = parse_sdl(sdl).unwrap();
3229 let schema = indexes[0].to_schema();
3230 assert!(schema.primary_field().is_none());
3231 }
3232
3233 #[test]
3234 fn bm25_parameters_parse_per_text_field() {
3235 let sdl = r#"
3236 index documents {
3237 field title: text<en_stem> [indexed<token_position, k1: 0.9, b: 0.4>]
3238 field body: text<en_stem> [indexed<chunked, token_position, b: 0.3>]
3239 field plain: text<en_stem> [indexed]
3240 }
3241 "#;
3242 let schema = parse_sdl(sdl).unwrap()[0].to_schema();
3243 let entry = |name: &str| {
3244 schema
3245 .get_field_entry(schema.get_field(name).unwrap())
3246 .unwrap()
3247 .clone()
3248 };
3249 assert_eq!(entry("title").bm25_k1, Some(0.9));
3250 assert_eq!(entry("title").bm25_b, Some(0.4));
3251 assert_eq!(entry("body").bm25_k1, None);
3252 assert_eq!(entry("body").bm25_b, Some(0.3));
3253 assert!(entry("body").chunked);
3254 assert_eq!(entry("plain").bm25_k1, None);
3255 assert_eq!(entry("plain").bm25_b, None);
3256 let params =
3257 crate::query::Bm25Params::for_field(&schema, schema.get_field("title").unwrap());
3258 assert_eq!((params.k1, params.b), (0.9, 0.4));
3259 let params =
3260 crate::query::Bm25Params::for_field(&schema, schema.get_field("plain").unwrap());
3261 assert_eq!((params.k1, params.b), (1.2, 0.75));
3262
3263 assert!(parse_sdl("index i { field t: text [indexed<b: 1.5>] }").is_err());
3265 assert!(parse_sdl("index i { field n: u64 [indexed<k1: 0.9>] }").is_err());
3266 }
3267
3268 #[test]
3269 fn test_reorder_attribute() {
3270 let sdl = r#"
3271 index documents {
3272 field embedding: sparse_vector<u16> [indexed<format: bmp, quantization: uint8>, reorder]
3273 field embedding2: sparse_vector [indexed<format: bmp>]
3274 }
3275 "#;
3276
3277 let indexes = parse_sdl(sdl).unwrap();
3278 assert_eq!(indexes[0].fields.len(), 2);
3279
3280 assert!(indexes[0].fields[0].reorder);
3282 assert!(!indexes[0].fields[1].reorder);
3284
3285 let schema = indexes[0].to_schema();
3287 let f1 = schema.get_field("embedding").unwrap();
3288 assert!(schema.get_field_entry(f1).unwrap().reorder);
3289
3290 let f2 = schema.get_field("embedding2").unwrap();
3291 assert!(!schema.get_field_entry(f2).unwrap().reorder);
3292
3293 assert!(!schema.reorder_on_merge());
3295 }
3296
3297 #[test]
3298 fn test_reorder_on_merge_index_option() {
3299 let sdl = r#"
3300 index documents {
3301 reorder_on_merge: true
3302 field embedding: sparse_vector<u16> [indexed<format: bmp>, reorder]
3303 }
3304 "#;
3305
3306 let indexes = parse_sdl(sdl).unwrap();
3307 assert!(indexes[0].reorder_on_merge);
3308 let schema = indexes[0].to_schema();
3309 assert!(schema.reorder_on_merge());
3310
3311 let sdl_off = r#"
3313 index documents {
3314 reorder_on_merge: false
3315 field embedding: sparse_vector<u16> [indexed<format: bmp>, reorder]
3316 }
3317 "#;
3318 let indexes = parse_sdl(sdl_off).unwrap();
3319 assert!(!indexes[0].reorder_on_merge);
3320 assert!(!indexes[0].to_schema().reorder_on_merge());
3321
3322 let schema_on = parse_sdl(sdl).unwrap()[0].to_schema();
3324 let json = serde_json::to_string(&schema_on).unwrap();
3325 let back: crate::dsl::Schema = serde_json::from_str(&json).unwrap();
3326 assert!(back.reorder_on_merge());
3327 }
3328}