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