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