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 nprobe: Option<usize>,
272 ivf_routing: Option<super::schema::IvfRoutingMode>,
273 soar: SoarDirective,
274 binary_index_type: Option<super::schema::BinaryIndexType>,
275 sparse_format: Option<SparseFormat>,
277 quantization: Option<WeightQuantization>,
278 weight_threshold: Option<f32>,
279 block_size: Option<usize>,
280 bmp_block_size: Option<u32>,
281 bmp_grid_bits: Option<u8>,
282 pruning: Option<f32>,
283 min_terms: Option<usize>,
284 doc_mass: Option<f32>,
285 query_tokenizer: Option<String>,
287 query_weighting: Option<QueryWeighting>,
288 query_weight_threshold: Option<f32>,
289 query_max_dims: Option<usize>,
290 query_pruning: Option<f32>,
291 query_min_query_dims: Option<usize>,
292 query_lsp_gamma: Option<usize>,
293 dims: Option<u32>,
295 max_weight: Option<f32>,
296 positions: Option<super::schema::PositionMode>,
298}
299
300struct ParsedAttributes {
302 indexed: bool,
303 stored: bool,
304 multi: bool,
305 fast: bool,
306 primary: bool,
307 reorder: bool,
308 index_config: Option<IndexConfig>,
309}
310
311fn parse_attributes(pair: pest::iterators::Pair<Rule>) -> ParsedAttributes {
313 let mut attrs = ParsedAttributes {
314 indexed: false,
315 stored: false,
316 multi: false,
317 fast: false,
318 primary: false,
319 reorder: false,
320 index_config: None,
321 };
322
323 for attr in pair.into_inner() {
324 if attr.as_rule() == Rule::attribute {
325 let mut found_config = false;
326 for inner in attr.clone().into_inner() {
327 match inner.as_rule() {
328 Rule::indexed_with_config => {
329 attrs.indexed = true;
330 attrs.index_config = Some(parse_index_config(inner));
331 found_config = true;
332 break;
333 }
334 Rule::stored_with_config => {
335 attrs.stored = true;
336 attrs.multi = true; found_config = true;
338 break;
339 }
340 _ => {}
341 }
342 }
343 if !found_config {
344 match attr.as_str() {
345 "indexed" => attrs.indexed = true,
346 "stored" => attrs.stored = true,
347 "fast" => attrs.fast = true,
348 "primary" => attrs.primary = true,
349 "reorder" => attrs.reorder = true,
350 _ => {}
351 }
352 }
353 }
354 }
355
356 attrs
357}
358
359fn parse_index_config(pair: pest::iterators::Pair<Rule>) -> IndexConfig {
361 let mut config = IndexConfig::default();
362
363 for inner in pair.into_inner() {
368 if inner.as_rule() == Rule::index_config_params {
369 for param in inner.into_inner() {
370 if param.as_rule() == Rule::index_config_param {
371 for p in param.into_inner() {
372 parse_single_index_config_param(&mut config, p);
373 }
374 }
375 }
376 }
377 }
378
379 config
380}
381
382fn parse_single_index_config_param(config: &mut IndexConfig, p: pest::iterators::Pair<Rule>) {
384 use super::schema::VectorIndexType;
385
386 match p.as_rule() {
387 Rule::index_type_spec => match p.as_str() {
388 "flat" => {
389 config.index_type = Some(VectorIndexType::Flat);
390 config.binary_index_type = Some(super::schema::BinaryIndexType::Flat);
391 }
392 "ivf" => config.binary_index_type = Some(super::schema::BinaryIndexType::Ivf),
393 "ivf_pq" => config.index_type = Some(VectorIndexType::IvfPq),
394 "ivf_tq" => config.index_type = Some(VectorIndexType::IvfTq),
395 "tq" => config.index_type = Some(VectorIndexType::Tq),
396 _ => {}
397 },
398 Rule::index_type_kwarg => {
399 if let Some(t) = p.into_inner().next() {
401 match t.as_str() {
402 "flat" => {
403 config.index_type = Some(VectorIndexType::Flat);
404 config.binary_index_type = Some(super::schema::BinaryIndexType::Flat);
405 }
406 "ivf" => config.binary_index_type = Some(super::schema::BinaryIndexType::Ivf),
407 "ivf_pq" => config.index_type = Some(VectorIndexType::IvfPq),
408 "ivf_tq" => config.index_type = Some(VectorIndexType::IvfTq),
409 "tq" => config.index_type = Some(VectorIndexType::Tq),
410 _ => {}
411 }
412 }
413 }
414 Rule::num_clusters_kwarg => {
415 if let Some(n) = p.into_inner().next() {
417 config.num_clusters = Some(n.as_str().parse().unwrap_or_else(|_| {
418 log::warn!(
419 "Invalid num_clusters value '{}', using default 256",
420 n.as_str()
421 );
422 256
423 }));
424 }
425 }
426 Rule::nprobe_kwarg => {
427 if let Some(n) = p.into_inner().next() {
429 config.nprobe = Some(n.as_str().parse().unwrap_or_else(|_| {
430 log::warn!("Invalid nprobe value '{}', using default 64", n.as_str());
431 64
432 }));
433 }
434 }
435 Rule::routing_kwarg => {
436 if let Some(value) = p.into_inner().next() {
437 config.ivf_routing = Some(match value.as_str() {
438 "flat" => super::schema::IvfRoutingMode::Flat,
439 "two_level" => super::schema::IvfRoutingMode::TwoLevel,
440 "hnsw" => super::schema::IvfRoutingMode::Hnsw,
441 _ => super::schema::IvfRoutingMode::Auto,
442 });
443 }
444 }
445 Rule::soar_kwarg => {
446 if let Some(s) = p.into_inner().next() {
448 use crate::structures::SoarConfig;
449 config.soar = match s.as_str() {
450 "selective" => SoarDirective::Enabled(SoarConfig::new()),
451 "full" => SoarDirective::Enabled(SoarConfig::full()),
452 "aggressive" => SoarDirective::Enabled(SoarConfig::aggressive()),
453 _ => SoarDirective::Disabled, };
455 }
456 }
457 Rule::quantization_kwarg => {
458 if let Some(q) = p.into_inner().next() {
460 config.quantization = Some(match q.as_str() {
461 "float32" | "f32" => WeightQuantization::Float32,
462 "float16" | "f16" => WeightQuantization::Float16,
463 "uint8" | "u8" => WeightQuantization::UInt8,
464 "uint4" | "u4" => WeightQuantization::UInt4,
465 _ => WeightQuantization::default(),
466 });
467 }
468 }
469 Rule::weight_threshold_kwarg => {
470 if let Some(t) = p.into_inner().next() {
472 config.weight_threshold = Some(t.as_str().parse().unwrap_or_else(|_| {
473 log::warn!(
474 "Invalid weight_threshold value '{}', using default 0.0",
475 t.as_str()
476 );
477 0.0
478 }));
479 }
480 }
481 Rule::block_size_kwarg => {
482 if let Some(n) = p.into_inner().next() {
484 config.block_size = Some(n.as_str().parse().unwrap_or_else(|_| {
485 log::warn!(
486 "Invalid block_size value '{}', using default 128",
487 n.as_str()
488 );
489 128
490 }));
491 }
492 }
493 Rule::bmp_grid_bits_kwarg => {
494 if let Some(n) = p.into_inner().next() {
496 config.bmp_grid_bits = Some(n.as_str().parse().unwrap_or_else(|_| {
497 log::warn!(
498 "Invalid bmp_grid_bits value '{}', using default {}",
499 n.as_str(),
500 SparseVectorConfig::DEFAULT_BMP_GRID_BITS,
501 );
502 SparseVectorConfig::DEFAULT_BMP_GRID_BITS
503 }));
504 }
505 }
506 Rule::bmp_block_size_kwarg => {
507 if let Some(n) = p.into_inner().next() {
509 config.bmp_block_size = Some(n.as_str().parse().unwrap_or_else(|_| {
510 log::warn!(
511 "Invalid bmp_block_size value '{}', using default {}",
512 n.as_str(),
513 SparseVectorConfig::DEFAULT_BMP_BLOCK_SIZE,
514 );
515 SparseVectorConfig::DEFAULT_BMP_BLOCK_SIZE
516 }));
517 }
518 }
519 Rule::pruning_kwarg => {
520 if let Some(f) = p.into_inner().next() {
522 config.pruning = Some(f.as_str().parse().unwrap_or_else(|_| {
523 log::warn!("Invalid pruning value '{}', using default 1.0", f.as_str());
524 1.0
525 }));
526 }
527 }
528 Rule::doc_mass_kwarg => {
529 if let Some(f) = p.into_inner().next() {
531 config.doc_mass = Some(f.as_str().parse().unwrap_or_else(|_| {
532 log::warn!("Invalid doc_mass value '{}', using 1.0 (off)", f.as_str());
533 1.0
534 }));
535 }
536 }
537 Rule::min_terms_kwarg => {
538 if let Some(n) = p.into_inner().next() {
539 config.min_terms = Some(n.as_str().parse().unwrap_or_else(|_| {
540 log::warn!("Invalid min_terms value '{}', using default 4", n.as_str());
541 4
542 }));
543 }
544 }
545 Rule::sparse_format_kwarg => {
546 if let Some(f) = p.into_inner().next() {
548 config.sparse_format = Some(match f.as_str() {
549 "bmp" => SparseFormat::Bmp,
550 "maxscore" => SparseFormat::MaxScore,
551 _ => SparseFormat::default(),
552 });
553 }
554 }
555 Rule::sparse_dims_kwarg => {
556 if let Some(n) = p.into_inner().next() {
557 config.dims = Some(n.as_str().parse().unwrap_or_else(|_| {
558 log::warn!("Invalid dims value '{}', using default 105879", n.as_str());
559 105879
560 }));
561 }
562 }
563 Rule::sparse_max_weight_kwarg => {
564 if let Some(f) = p.into_inner().next() {
565 config.max_weight = Some(f.as_str().parse().unwrap_or_else(|_| {
566 log::warn!(
567 "Invalid max_weight value '{}', using default 5.0",
568 f.as_str()
569 );
570 5.0
571 }));
572 }
573 }
574 Rule::query_config_block => {
575 parse_query_config_block(config, p);
577 }
578 Rule::positions_kwarg => {
579 use super::schema::PositionMode;
581 config.positions = Some(match p.as_str() {
582 "ordinal" => PositionMode::Ordinal,
583 "token_position" => PositionMode::TokenPosition,
584 _ => PositionMode::Full, });
586 }
587 _ => {}
588 }
589}
590
591fn parse_query_config_block(config: &mut IndexConfig, pair: pest::iterators::Pair<Rule>) {
593 for inner in pair.into_inner() {
594 if inner.as_rule() == Rule::query_config_params {
595 for param in inner.into_inner() {
596 if param.as_rule() == Rule::query_config_param {
597 for p in param.into_inner() {
598 match p.as_rule() {
599 Rule::query_tokenizer_kwarg => {
600 if let Some(path) = p.into_inner().next()
602 && let Some(inner_path) = path.into_inner().next()
603 {
604 config.query_tokenizer = Some(inner_path.as_str().to_string());
605 }
606 }
607 Rule::query_weighting_kwarg => {
608 if let Some(w) = p.into_inner().next() {
610 config.query_weighting = Some(match w.as_str() {
611 "one" => QueryWeighting::One,
612 "idf" => QueryWeighting::Idf,
613 "idf_file" => QueryWeighting::IdfFile,
614 _ => QueryWeighting::One,
615 });
616 }
617 }
618 Rule::query_weight_threshold_kwarg => {
619 if let Some(t) = p.into_inner().next() {
620 config.query_weight_threshold =
621 Some(t.as_str().parse().unwrap_or_else(|_| {
622 log::warn!(
623 "Invalid query weight_threshold '{}', using 0.0",
624 t.as_str()
625 );
626 0.0
627 }));
628 }
629 }
630 Rule::query_max_dims_kwarg => {
631 if let Some(t) = p.into_inner().next() {
632 config.query_max_dims =
633 Some(t.as_str().parse().unwrap_or_else(|_| {
634 log::warn!(
635 "Invalid query max_dims '{}', using 0",
636 t.as_str()
637 );
638 0
639 }));
640 }
641 }
642 Rule::query_pruning_kwarg => {
643 if let Some(t) = p.into_inner().next() {
644 config.query_pruning =
645 Some(t.as_str().parse().unwrap_or_else(|_| {
646 log::warn!(
647 "Invalid query pruning '{}', using 1.0",
648 t.as_str()
649 );
650 1.0
651 }));
652 }
653 }
654 Rule::query_min_query_dims_kwarg => {
655 if let Some(t) = p.into_inner().next() {
656 config.query_min_query_dims =
657 Some(t.as_str().parse().unwrap_or_else(|_| {
658 log::warn!(
659 "Invalid query min_query_dims '{}', using 4",
660 t.as_str()
661 );
662 4
663 }));
664 }
665 }
666 Rule::query_lsp_gamma_kwarg => {
667 if let Some(value) = p.into_inner().next() {
668 config.query_lsp_gamma =
669 Some(value.as_str().parse().unwrap_or_else(|_| {
670 log::warn!(
671 "Invalid query lsp_gamma '{}', using 0",
672 value.as_str()
673 );
674 0
675 }));
676 }
677 }
678 _ => {}
679 }
680 }
681 }
682 }
683 }
684 }
685}
686
687fn parse_field_def(pair: pest::iterators::Pair<Rule>) -> Result<FieldDef> {
689 let mut inner = pair.into_inner();
690
691 let name = inner
692 .next()
693 .ok_or_else(|| Error::Schema("Missing field name".to_string()))?
694 .as_str()
695 .to_string();
696
697 let field_type_str = inner
698 .next()
699 .ok_or_else(|| Error::Schema("Missing field type".to_string()))?
700 .as_str();
701
702 let field_type = parse_field_type(field_type_str)?;
703
704 let mut tokenizer = None;
706 let mut sparse_vector_config = None;
707 let mut dense_vector_config = None;
708 let mut binary_dense_vector_config = None;
709 let mut indexed = true;
710 let mut stored = true;
711 let mut multi = false;
712 let mut fast = false;
713 let mut primary = false;
714 let mut reorder = false;
715 let mut index_config: Option<IndexConfig> = None;
716
717 for item in inner {
718 match item.as_rule() {
719 Rule::tokenizer_spec => {
720 if let Some(tok_name) = item.into_inner().next() {
722 tokenizer = Some(tok_name.as_str().to_string());
723 }
724 }
725 Rule::sparse_vector_config => {
726 sparse_vector_config = Some(parse_sparse_vector_config(item));
728 }
729 Rule::dense_vector_config => {
730 dense_vector_config = Some(parse_dense_vector_config(item));
732 }
733 Rule::binary_dense_vector_config => {
734 let dim: usize = item
736 .into_inner()
737 .next()
738 .map(|d| d.as_str().parse().unwrap_or(0))
739 .unwrap_or(0);
740 if dim == 0 || !dim.is_multiple_of(8) {
741 return Err(Error::Schema(format!(
742 "BinaryDenseVector dimension must be a positive multiple of 8, got {dim}"
743 )));
744 }
745 binary_dense_vector_config = Some(BinaryDenseVectorConfig::new(dim));
746 }
747 Rule::attributes => {
748 let attrs = parse_attributes(item);
749 indexed = attrs.indexed;
750 stored = attrs.stored;
751 multi = attrs.multi;
752 fast = attrs.fast;
753 primary = attrs.primary;
754 reorder = attrs.reorder;
755 index_config = attrs.index_config;
756 }
757 _ => {}
758 }
759 }
760
761 if field_type == FieldType::BinaryDenseVector
765 && binary_dense_vector_config.is_none()
766 && let Some(ref dv_config) = dense_vector_config
767 {
768 let dim = dv_config.dim;
769 if dim == 0 || !dim.is_multiple_of(8) {
770 return Err(Error::Schema(format!(
771 "BinaryDenseVector dimension must be a positive multiple of 8, got {dim}"
772 )));
773 }
774 binary_dense_vector_config = Some(BinaryDenseVectorConfig::new(dim));
775 dense_vector_config = None;
776 }
777
778 if primary {
780 fast = true;
781 indexed = true;
782 }
783
784 let mut positions = None;
786 if let Some(idx_cfg) = index_config {
787 positions = idx_cfg.positions;
788 if let Some(ref mut bv_config) = binary_dense_vector_config {
789 apply_index_config_to_binary_dense_vector(bv_config, idx_cfg);
790 } else if let Some(ref mut dv_config) = dense_vector_config {
791 apply_index_config_to_dense_vector(dv_config, idx_cfg);
792 } else if field_type == FieldType::SparseVector {
793 let sv_config = sparse_vector_config.get_or_insert(SparseVectorConfig::default());
795 apply_index_config_to_sparse_vector(sv_config, idx_cfg);
796 }
797 }
798
799 Ok(FieldDef {
800 name,
801 field_type,
802 indexed,
803 stored,
804 tokenizer,
805 multi,
806 positions,
807 sparse_vector_config,
808 dense_vector_config,
809 binary_dense_vector_config,
810 fast,
811 primary,
812 reorder,
813 })
814}
815
816fn apply_index_config_to_binary_dense_vector(
818 config: &mut BinaryDenseVectorConfig,
819 idx_cfg: IndexConfig,
820) {
821 if let Some(index_type) = idx_cfg.binary_index_type {
822 config.index_type = index_type;
823 }
824 if idx_cfg.num_clusters.is_some() {
825 config.num_clusters = idx_cfg.num_clusters;
826 }
827 if let Some(nprobe) = idx_cfg.nprobe {
828 config.nprobe = nprobe;
829 }
830 if let Some(routing) = idx_cfg.ivf_routing {
831 config.ivf_routing = routing;
832 }
833}
834
835fn apply_index_config_to_dense_vector(config: &mut DenseVectorConfig, idx_cfg: IndexConfig) {
837 if let Some(index_type) = idx_cfg.index_type {
839 config.index_type = index_type;
840 }
841
842 if config.index_type == super::schema::VectorIndexType::Tq {
845 for (option, present) in [
846 ("num_clusters", idx_cfg.num_clusters.is_some()),
847 ("nprobe", idx_cfg.nprobe.is_some()),
848 ("routing", idx_cfg.ivf_routing.is_some()),
849 ] {
850 if present {
851 log::warn!(
852 "'{option}' has no effect on the 'tq' index (training-free full \
853 scan); ignoring"
854 );
855 }
856 }
857 config.num_clusters = None;
860 config.nprobe = 0;
861 config.ivf_routing = super::schema::IvfRoutingMode::Flat;
862 return apply_soar_to_dense_vector(config, idx_cfg);
863 }
864
865 if idx_cfg.num_clusters.is_some() {
867 config.num_clusters = idx_cfg.num_clusters;
868 }
869
870 if let Some(nprobe) = idx_cfg.nprobe {
872 config.nprobe = nprobe;
873 }
874 if let Some(routing) = idx_cfg.ivf_routing {
875 config.ivf_routing = routing;
876 }
877
878 apply_soar_to_dense_vector(config, idx_cfg);
879}
880
881fn apply_soar_to_dense_vector(config: &mut DenseVectorConfig, idx_cfg: IndexConfig) {
883 match idx_cfg.soar {
884 SoarDirective::Unspecified => {
885 config.soar = config
886 .uses_ivf()
887 .then(crate::structures::SoarConfig::default);
888 }
889 SoarDirective::Disabled => {
890 config.soar = None;
891 }
892 SoarDirective::Enabled(soar) => {
893 if config.uses_ivf() {
894 config.soar = Some(soar);
895 } else {
896 config.soar = None;
897 log::warn!(
898 "'soar' requires the IVF-TQ index; \
899 ignoring for index type {:?}",
900 config.index_type
901 );
902 }
903 }
904 }
905}
906
907fn parse_sparse_vector_config(pair: pest::iterators::Pair<Rule>) -> SparseVectorConfig {
910 let mut index_size = IndexSize::default();
911
912 for inner in pair.into_inner() {
914 if inner.as_rule() == Rule::index_size_spec {
915 index_size = match inner.as_str() {
916 "u16" => IndexSize::U16,
917 "u32" => IndexSize::U32,
918 _ => IndexSize::default(),
919 };
920 }
921 }
922
923 SparseVectorConfig {
924 index_size,
925 ..SparseVectorConfig::default()
926 }
927}
928
929fn apply_index_config_to_sparse_vector(config: &mut SparseVectorConfig, idx_cfg: IndexConfig) {
931 if let Some(f) = idx_cfg.sparse_format {
932 config.format = f;
933 }
934 if let Some(q) = idx_cfg.quantization {
935 config.weight_quantization = q;
936 }
937 if let Some(t) = idx_cfg.weight_threshold {
938 config.weight_threshold = t;
939 }
940 if let Some(bs) = idx_cfg.block_size {
941 let adjusted = bs.next_power_of_two();
942 if adjusted != bs {
943 log::warn!(
944 "block_size {} adjusted to next power of two: {}",
945 bs,
946 adjusted
947 );
948 }
949 config.block_size = adjusted;
950 }
951 if let Some(bs) = idx_cfg.bmp_block_size {
952 let adjusted = bs.next_power_of_two().clamp(1, 256);
953 if adjusted != bs {
954 log::warn!(
955 "bmp_block_size {} adjusted to power of two in 1..=256: {}",
956 bs,
957 adjusted
958 );
959 }
960 config.bmp_block_size = adjusted;
961 }
962 if let Some(bits) = idx_cfg.bmp_grid_bits {
963 if bits == 2 || bits == 4 {
964 config.bmp_grid_bits = bits;
965 } else {
966 log::warn!(
967 "bmp_grid_bits {} unsupported (must be 2 or 4), using {}",
968 bits,
969 SparseVectorConfig::DEFAULT_BMP_GRID_BITS,
970 );
971 config.bmp_grid_bits = SparseVectorConfig::DEFAULT_BMP_GRID_BITS;
972 }
973 }
974 if let Some(p) = idx_cfg.pruning {
975 let clamped = p.clamp(0.0, 1.0);
976 if (clamped - p).abs() > f32::EPSILON {
977 log::warn!(
978 "pruning {} clamped to valid range [0.0, 1.0]: {}",
979 p,
980 clamped
981 );
982 }
983 config.pruning = Some(clamped);
984 }
985 if let Some(mt) = idx_cfg.min_terms {
986 config.min_terms = mt;
987 }
988 if let Some(dm) = idx_cfg.doc_mass {
989 let clamped = dm.clamp(0.0, 1.0);
990 if (clamped - dm).abs() > f32::EPSILON {
991 log::warn!(
992 "doc_mass {} clamped to valid range [0.0, 1.0]: {}",
993 dm,
994 clamped
995 );
996 }
997 config.doc_mass = Some(clamped);
998 }
999 if let Some(d) = idx_cfg.dims {
1000 config.dims = Some(d);
1001 }
1002 if let Some(mw) = idx_cfg.max_weight {
1003 config.max_weight = Some(mw);
1004 }
1005 if idx_cfg.query_tokenizer.is_some()
1007 || idx_cfg.query_weighting.is_some()
1008 || idx_cfg.query_weight_threshold.is_some()
1009 || idx_cfg.query_max_dims.is_some()
1010 || idx_cfg.query_pruning.is_some()
1011 || idx_cfg.query_min_query_dims.is_some()
1012 || idx_cfg.query_lsp_gamma.is_some()
1013 {
1014 let query_config = config
1015 .query_config
1016 .get_or_insert(SparseQueryConfig::default());
1017 if let Some(tokenizer) = idx_cfg.query_tokenizer {
1018 query_config.tokenizer = Some(tokenizer);
1019 }
1020 if let Some(weighting) = idx_cfg.query_weighting {
1021 query_config.weighting = weighting;
1022 }
1023 if let Some(t) = idx_cfg.query_weight_threshold {
1024 query_config.weight_threshold = t;
1025 }
1026 if let Some(d) = idx_cfg.query_max_dims {
1027 query_config.max_query_dims = Some(d);
1028 }
1029 if let Some(p) = idx_cfg.query_pruning {
1030 query_config.pruning = Some(p);
1031 }
1032 if let Some(m) = idx_cfg.query_min_query_dims {
1033 query_config.min_query_dims = m;
1034 }
1035 if let Some(gamma) = idx_cfg.query_lsp_gamma {
1036 query_config.lsp_gamma = Some(gamma);
1037 }
1038 }
1039}
1040
1041fn parse_dense_vector_config(pair: pest::iterators::Pair<Rule>) -> DenseVectorConfig {
1044 let mut dim: usize = 0;
1045 let mut quantization = DenseVectorQuantization::F32;
1046
1047 for params in pair.into_inner() {
1049 if params.as_rule() == Rule::dense_vector_params {
1050 for inner in params.into_inner() {
1051 match inner.as_rule() {
1052 Rule::dense_vector_keyword_params => {
1053 for kwarg in inner.into_inner() {
1054 match kwarg.as_rule() {
1055 Rule::dims_kwarg => {
1056 if let Some(d) = kwarg.into_inner().next() {
1057 dim = d.as_str().parse().unwrap_or(0);
1058 }
1059 }
1060 Rule::quant_type_spec => {
1061 quantization = parse_quant_type(kwarg.as_str());
1062 }
1063 _ => {}
1064 }
1065 }
1066 }
1067 Rule::dense_vector_positional_params => {
1068 for item in inner.into_inner() {
1069 match item.as_rule() {
1070 Rule::dimension_spec => {
1071 dim = item.as_str().parse().unwrap_or(0);
1072 }
1073 Rule::quant_type_spec => {
1074 quantization = parse_quant_type(item.as_str());
1075 }
1076 _ => {}
1077 }
1078 }
1079 }
1080 _ => {}
1081 }
1082 }
1083 }
1084 }
1085
1086 DenseVectorConfig::new(dim).with_quantization(quantization)
1087}
1088
1089fn parse_quant_type(s: &str) -> DenseVectorQuantization {
1090 match s.trim() {
1091 "f16" => DenseVectorQuantization::F16,
1092 "uint8" | "u8" => DenseVectorQuantization::UInt8,
1093 _ => DenseVectorQuantization::F32,
1094 }
1095}
1096
1097fn parse_default_fields_def(pair: pest::iterators::Pair<Rule>) -> Vec<String> {
1099 pair.into_inner().map(|p| p.as_str().to_string()).collect()
1100}
1101
1102fn parse_query_router_def(pair: pest::iterators::Pair<Rule>) -> Result<QueryRouterRule> {
1104 let mut pattern = String::new();
1105 let mut substitution = String::new();
1106 let mut target_field = String::new();
1107 let mut mode = RoutingMode::Additional;
1108
1109 for prop in pair.into_inner() {
1110 if prop.as_rule() != Rule::query_router_prop {
1111 continue;
1112 }
1113
1114 for inner in prop.into_inner() {
1115 match inner.as_rule() {
1116 Rule::query_router_pattern => {
1117 if let Some(regex_str) = inner.into_inner().next() {
1118 pattern = parse_string_value(regex_str);
1119 }
1120 }
1121 Rule::query_router_substitution => {
1122 if let Some(quoted) = inner.into_inner().next() {
1123 substitution = parse_string_value(quoted);
1124 }
1125 }
1126 Rule::query_router_target => {
1127 if let Some(ident) = inner.into_inner().next() {
1128 target_field = ident.as_str().to_string();
1129 }
1130 }
1131 Rule::query_router_mode => {
1132 if let Some(mode_val) = inner.into_inner().next() {
1133 mode = match mode_val.as_str() {
1134 "exclusive" => RoutingMode::Exclusive,
1135 "additional" => RoutingMode::Additional,
1136 _ => RoutingMode::Additional,
1137 };
1138 }
1139 }
1140 _ => {}
1141 }
1142 }
1143 }
1144
1145 if pattern.is_empty() {
1146 return Err(Error::Schema("query_router missing 'pattern'".to_string()));
1147 }
1148 if substitution.is_empty() {
1149 return Err(Error::Schema(
1150 "query_router missing 'substitution'".to_string(),
1151 ));
1152 }
1153 if target_field.is_empty() {
1154 return Err(Error::Schema(
1155 "query_router missing 'target_field'".to_string(),
1156 ));
1157 }
1158
1159 Ok(QueryRouterRule {
1160 pattern,
1161 substitution,
1162 target_field,
1163 mode,
1164 })
1165}
1166
1167fn parse_string_value(pair: pest::iterators::Pair<Rule>) -> String {
1169 let s = pair.as_str();
1170 match pair.as_rule() {
1171 Rule::regex_string => {
1172 if let Some(inner) = pair.into_inner().next() {
1174 parse_string_value(inner)
1175 } else {
1176 s.to_string()
1177 }
1178 }
1179 Rule::raw_string => {
1180 s[2..s.len() - 1].to_string()
1182 }
1183 Rule::quoted_string => {
1184 let inner = &s[1..s.len() - 1];
1186 inner
1188 .replace("\\n", "\n")
1189 .replace("\\t", "\t")
1190 .replace("\\\"", "\"")
1191 .replace("\\\\", "\\")
1192 }
1193 _ => s.to_string(),
1194 }
1195}
1196
1197fn parse_index_def(pair: pest::iterators::Pair<Rule>) -> Result<IndexDef> {
1199 let mut inner = pair.into_inner();
1200
1201 let name = inner
1202 .next()
1203 .ok_or_else(|| Error::Schema("Missing index name".to_string()))?
1204 .as_str()
1205 .to_string();
1206
1207 let mut fields = Vec::new();
1208 let mut default_fields = Vec::new();
1209 let mut query_routers = Vec::new();
1210 let mut reorder_on_merge = false;
1211
1212 for item in inner {
1213 match item.as_rule() {
1214 Rule::field_def => {
1215 fields.push(parse_field_def(item)?);
1216 }
1217 Rule::default_fields_def => {
1218 default_fields = parse_default_fields_def(item);
1219 }
1220 Rule::query_router_def => {
1221 query_routers.push(parse_query_router_def(item)?);
1222 }
1223 Rule::reorder_on_merge_def => {
1224 let value = item
1225 .into_inner()
1226 .next()
1227 .map(|b| b.as_str() == "true")
1228 .unwrap_or(false);
1229 reorder_on_merge = value;
1230 }
1231 _ => {}
1232 }
1233 }
1234
1235 let primary_fields: Vec<&FieldDef> = fields.iter().filter(|f| f.primary).collect();
1237 if primary_fields.len() > 1 {
1238 return Err(Error::Schema(format!(
1239 "Index '{}' has {} primary key fields, but at most one is allowed",
1240 name,
1241 primary_fields.len()
1242 )));
1243 }
1244 if let Some(pk) = primary_fields.first() {
1245 if pk.field_type != FieldType::Text {
1246 return Err(Error::Schema(format!(
1247 "Primary key field '{}' must be of type text, got {:?}",
1248 pk.name, pk.field_type
1249 )));
1250 }
1251 if pk.multi {
1252 return Err(Error::Schema(format!(
1253 "Primary key field '{}' cannot be multi-valued",
1254 pk.name
1255 )));
1256 }
1257 }
1258
1259 Ok(IndexDef {
1260 name,
1261 fields,
1262 default_fields,
1263 query_routers,
1264 reorder_on_merge,
1265 })
1266}
1267
1268pub fn parse_sdl(input: &str) -> Result<Vec<IndexDef>> {
1270 let pairs = SdlParser::parse(Rule::file, input)
1271 .map_err(|e| Error::Schema(format!("Parse error: {}", e)))?;
1272
1273 let mut indexes = Vec::new();
1274
1275 for pair in pairs {
1276 if pair.as_rule() == Rule::file {
1277 for inner in pair.into_inner() {
1278 if inner.as_rule() == Rule::index_def {
1279 indexes.push(parse_index_def(inner)?);
1280 }
1281 }
1282 }
1283 }
1284
1285 Ok(indexes)
1286}
1287
1288pub fn parse_single_index(input: &str) -> Result<IndexDef> {
1290 let indexes = parse_sdl(input)?;
1291
1292 if indexes.is_empty() {
1293 return Err(Error::Schema("No index definition found".to_string()));
1294 }
1295
1296 if indexes.len() > 1 {
1297 return Err(Error::Schema(
1298 "Multiple index definitions found, expected one".to_string(),
1299 ));
1300 }
1301
1302 Ok(indexes.into_iter().next().unwrap())
1303}
1304
1305#[cfg(test)]
1306mod tests {
1307 use super::*;
1308
1309 #[test]
1310 fn test_parse_simple_schema() {
1311 let sdl = r#"
1312 index articles {
1313 field title: text [indexed, stored]
1314 field body: text [indexed]
1315 }
1316 "#;
1317
1318 let indexes = parse_sdl(sdl).unwrap();
1319 assert_eq!(indexes.len(), 1);
1320
1321 let index = &indexes[0];
1322 assert_eq!(index.name, "articles");
1323 assert_eq!(index.fields.len(), 2);
1324
1325 assert_eq!(index.fields[0].name, "title");
1326 assert!(matches!(index.fields[0].field_type, FieldType::Text));
1327 assert!(index.fields[0].indexed);
1328 assert!(index.fields[0].stored);
1329
1330 assert_eq!(index.fields[1].name, "body");
1331 assert!(matches!(index.fields[1].field_type, FieldType::Text));
1332 assert!(index.fields[1].indexed);
1333 assert!(!index.fields[1].stored);
1334 }
1335
1336 #[test]
1337 fn test_parse_all_field_types() {
1338 let sdl = r#"
1339 index test {
1340 field text_field: text [indexed, stored]
1341 field u64_field: u64 [indexed, stored]
1342 field i64_field: i64 [indexed, stored]
1343 field f64_field: f64 [indexed, stored]
1344 field bytes_field: bytes [stored]
1345 }
1346 "#;
1347
1348 let indexes = parse_sdl(sdl).unwrap();
1349 let index = &indexes[0];
1350
1351 assert!(matches!(index.fields[0].field_type, FieldType::Text));
1352 assert!(matches!(index.fields[1].field_type, FieldType::U64));
1353 assert!(matches!(index.fields[2].field_type, FieldType::I64));
1354 assert!(matches!(index.fields[3].field_type, FieldType::F64));
1355 assert!(matches!(index.fields[4].field_type, FieldType::Bytes));
1356 }
1357
1358 #[test]
1359 fn test_parse_with_comments() {
1360 let sdl = r#"
1361 # This is a comment
1362 index articles {
1363 # Title field
1364 field title: text [indexed, stored]
1365 field body: text [indexed] # inline comment not supported yet
1366 }
1367 "#;
1368
1369 let indexes = parse_sdl(sdl).unwrap();
1370 assert_eq!(indexes[0].fields.len(), 2);
1371 }
1372
1373 #[test]
1374 fn test_parse_type_aliases() {
1375 let sdl = r#"
1376 index test {
1377 field a: string [indexed]
1378 field b: int [indexed]
1379 field c: uint [indexed]
1380 field d: float [indexed]
1381 field e: binary [stored]
1382 }
1383 "#;
1384
1385 let indexes = parse_sdl(sdl).unwrap();
1386 let index = &indexes[0];
1387
1388 assert!(matches!(index.fields[0].field_type, FieldType::Text));
1389 assert!(matches!(index.fields[1].field_type, FieldType::I64));
1390 assert!(matches!(index.fields[2].field_type, FieldType::U64));
1391 assert!(matches!(index.fields[3].field_type, FieldType::F64));
1392 assert!(matches!(index.fields[4].field_type, FieldType::Bytes));
1393 }
1394
1395 #[test]
1396 fn test_to_schema() {
1397 let sdl = r#"
1398 index articles {
1399 field title: text [indexed, stored]
1400 field views: u64 [indexed, stored]
1401 }
1402 "#;
1403
1404 let indexes = parse_sdl(sdl).unwrap();
1405 let schema = indexes[0].to_schema();
1406
1407 assert!(schema.get_field("title").is_some());
1408 assert!(schema.get_field("views").is_some());
1409 assert!(schema.get_field("nonexistent").is_none());
1410 }
1411
1412 #[test]
1413 fn test_default_attributes() {
1414 let sdl = r#"
1415 index test {
1416 field title: text
1417 }
1418 "#;
1419
1420 let indexes = parse_sdl(sdl).unwrap();
1421 let field = &indexes[0].fields[0];
1422
1423 assert!(field.indexed);
1425 assert!(field.stored);
1426 }
1427
1428 #[test]
1429 fn test_multiple_indexes() {
1430 let sdl = r#"
1431 index articles {
1432 field title: text [indexed, stored]
1433 }
1434
1435 index users {
1436 field name: text [indexed, stored]
1437 field email: text [indexed, stored]
1438 }
1439 "#;
1440
1441 let indexes = parse_sdl(sdl).unwrap();
1442 assert_eq!(indexes.len(), 2);
1443 assert_eq!(indexes[0].name, "articles");
1444 assert_eq!(indexes[1].name, "users");
1445 }
1446
1447 #[test]
1448 fn test_tokenizer_spec() {
1449 let sdl = r#"
1450 index articles {
1451 field title: text<en_stem> [indexed, stored]
1452 field body: text<simple> [indexed]
1453 field author: text [indexed, stored]
1454 }
1455 "#;
1456
1457 let indexes = parse_sdl(sdl).unwrap();
1458 let index = &indexes[0];
1459
1460 assert_eq!(index.fields[0].name, "title");
1461 assert_eq!(index.fields[0].tokenizer, Some("en_stem".to_string()));
1462
1463 assert_eq!(index.fields[1].name, "body");
1464 assert_eq!(index.fields[1].tokenizer, Some("simple".to_string()));
1465
1466 assert_eq!(index.fields[2].name, "author");
1467 assert_eq!(index.fields[2].tokenizer, None); }
1469
1470 #[test]
1471 fn test_tokenizer_in_schema() {
1472 let sdl = r#"
1473 index articles {
1474 field title: text<german> [indexed, stored]
1475 field body: text<en_stem> [indexed]
1476 }
1477 "#;
1478
1479 let indexes = parse_sdl(sdl).unwrap();
1480 let schema = indexes[0].to_schema();
1481
1482 let title_field = schema.get_field("title").unwrap();
1483 let title_entry = schema.get_field_entry(title_field).unwrap();
1484 assert_eq!(title_entry.tokenizer, Some("german".to_string()));
1485
1486 let body_field = schema.get_field("body").unwrap();
1487 let body_entry = schema.get_field_entry(body_field).unwrap();
1488 assert_eq!(body_entry.tokenizer, Some("en_stem".to_string()));
1489 }
1490
1491 #[test]
1492 fn test_query_router_basic() {
1493 let sdl = r#"
1494 index documents {
1495 field title: text [indexed, stored]
1496 field uri: text [indexed, stored]
1497
1498 query_router {
1499 pattern: "10\\.\\d{4,}/[^\\s]+"
1500 substitution: "doi://{0}"
1501 target_field: uris
1502 mode: exclusive
1503 }
1504 }
1505 "#;
1506
1507 let indexes = parse_sdl(sdl).unwrap();
1508 let index = &indexes[0];
1509
1510 assert_eq!(index.query_routers.len(), 1);
1511 let router = &index.query_routers[0];
1512 assert_eq!(router.pattern, r"10\.\d{4,}/[^\s]+");
1513 assert_eq!(router.substitution, "doi://{0}");
1514 assert_eq!(router.target_field, "uris");
1515 assert_eq!(router.mode, RoutingMode::Exclusive);
1516 }
1517
1518 #[test]
1519 fn test_query_router_raw_string() {
1520 let sdl = r#"
1521 index documents {
1522 field uris: text [indexed, stored]
1523
1524 query_router {
1525 pattern: r"^pmid:(\d+)$"
1526 substitution: "pubmed://{1}"
1527 target_field: uris
1528 mode: additional
1529 }
1530 }
1531 "#;
1532
1533 let indexes = parse_sdl(sdl).unwrap();
1534 let router = &indexes[0].query_routers[0];
1535
1536 assert_eq!(router.pattern, r"^pmid:(\d+)$");
1537 assert_eq!(router.substitution, "pubmed://{1}");
1538 assert_eq!(router.mode, RoutingMode::Additional);
1539 }
1540
1541 #[test]
1542 fn test_multiple_query_routers() {
1543 let sdl = r#"
1544 index documents {
1545 field uris: text [indexed, stored]
1546
1547 query_router {
1548 pattern: r"^doi:(10\.\d{4,}/[^\s]+)$"
1549 substitution: "doi://{1}"
1550 target_field: uris
1551 mode: exclusive
1552 }
1553
1554 query_router {
1555 pattern: r"^pmid:(\d+)$"
1556 substitution: "pubmed://{1}"
1557 target_field: uris
1558 mode: exclusive
1559 }
1560
1561 query_router {
1562 pattern: r"^arxiv:(\d+\.\d+)$"
1563 substitution: "arxiv://{1}"
1564 target_field: uris
1565 mode: additional
1566 }
1567 }
1568 "#;
1569
1570 let indexes = parse_sdl(sdl).unwrap();
1571 assert_eq!(indexes[0].query_routers.len(), 3);
1572 }
1573
1574 #[test]
1575 fn test_query_router_default_mode() {
1576 let sdl = r#"
1577 index documents {
1578 field uris: text [indexed, stored]
1579
1580 query_router {
1581 pattern: r"test"
1582 substitution: "{0}"
1583 target_field: uris
1584 }
1585 }
1586 "#;
1587
1588 let indexes = parse_sdl(sdl).unwrap();
1589 assert_eq!(indexes[0].query_routers[0].mode, RoutingMode::Additional);
1591 }
1592
1593 #[test]
1594 fn test_multi_attribute() {
1595 let sdl = r#"
1596 index documents {
1597 field uris: text [indexed, stored<multi>]
1598 field title: text [indexed, stored]
1599 }
1600 "#;
1601
1602 let indexes = parse_sdl(sdl).unwrap();
1603 assert_eq!(indexes.len(), 1);
1604
1605 let fields = &indexes[0].fields;
1606 assert_eq!(fields.len(), 2);
1607
1608 assert_eq!(fields[0].name, "uris");
1610 assert!(fields[0].multi, "uris field should have multi=true");
1611
1612 assert_eq!(fields[1].name, "title");
1614 assert!(!fields[1].multi, "title field should have multi=false");
1615
1616 let schema = indexes[0].to_schema();
1618 let uris_field = schema.get_field("uris").unwrap();
1619 let title_field = schema.get_field("title").unwrap();
1620
1621 assert!(schema.get_field_entry(uris_field).unwrap().multi);
1622 assert!(!schema.get_field_entry(title_field).unwrap().multi);
1623 }
1624
1625 #[test]
1626 fn test_sparse_vector_field() {
1627 let sdl = r#"
1628 index documents {
1629 field embedding: sparse_vector [indexed, stored]
1630 }
1631 "#;
1632
1633 let indexes = parse_sdl(sdl).unwrap();
1634 assert_eq!(indexes.len(), 1);
1635 assert_eq!(indexes[0].fields.len(), 1);
1636 assert_eq!(indexes[0].fields[0].name, "embedding");
1637 assert_eq!(indexes[0].fields[0].field_type, FieldType::SparseVector);
1638 assert!(indexes[0].fields[0].sparse_vector_config.is_none());
1639 }
1640
1641 #[test]
1642 fn test_sparse_vector_with_config() {
1643 let sdl = r#"
1644 index documents {
1645 field embedding: sparse_vector<u16> [indexed<quantization: uint8>, stored]
1646 field dense: sparse_vector<u32> [indexed<quantization: float32>]
1647 }
1648 "#;
1649
1650 let indexes = parse_sdl(sdl).unwrap();
1651 assert_eq!(indexes[0].fields.len(), 2);
1652
1653 let f1 = &indexes[0].fields[0];
1655 assert_eq!(f1.name, "embedding");
1656 let config1 = f1.sparse_vector_config.as_ref().unwrap();
1657 assert_eq!(config1.index_size, IndexSize::U16);
1658 assert_eq!(config1.weight_quantization, WeightQuantization::UInt8);
1659
1660 let f2 = &indexes[0].fields[1];
1662 assert_eq!(f2.name, "dense");
1663 let config2 = f2.sparse_vector_config.as_ref().unwrap();
1664 assert_eq!(config2.index_size, IndexSize::U32);
1665 assert_eq!(config2.weight_quantization, WeightQuantization::Float32);
1666 }
1667
1668 #[test]
1669 fn test_sparse_vector_bmp_block_size() {
1670 let sdl = r#"
1671 index documents {
1672 field emb: sparse_vector<u32> [indexed<format: bmp, dims: 105879, bmp_block_size: 256>]
1673 field emb2: sparse_vector<u32> [indexed<format: bmp, dims: 30522>]
1674 }
1675 "#;
1676
1677 let indexes = parse_sdl(sdl).unwrap();
1678 let config1 = indexes[0].fields[0].sparse_vector_config.as_ref().unwrap();
1679 assert_eq!(config1.format, SparseFormat::Bmp);
1680 assert_eq!(config1.bmp_block_size, 256);
1681
1682 let config2 = indexes[0].fields[1].sparse_vector_config.as_ref().unwrap();
1684 assert_eq!(
1685 config2.bmp_block_size,
1686 SparseVectorConfig::DEFAULT_BMP_BLOCK_SIZE
1687 );
1688 }
1689
1690 #[test]
1693 fn test_sparse_vector_bmp_grid_bits() {
1694 let sdl = r#"
1695 index documents {
1696 field emb: sparse_vector<u32> [indexed<format: bmp, dims: 105879, bmp_block_size: 256, bmp_grid_bits: 2>]
1697 field emb2: sparse_vector<u32> [indexed<format: bmp, dims: 30522>]
1698 field emb3: sparse_vector<u32> [indexed<format: bmp, dims: 30522, bmp_grid_bits: 3>]
1699 }
1700 "#;
1701
1702 let indexes = parse_sdl(sdl).unwrap();
1703 let config1 = indexes[0].fields[0].sparse_vector_config.as_ref().unwrap();
1704 assert_eq!(config1.bmp_grid_bits, 2);
1705 let config2 = indexes[0].fields[1].sparse_vector_config.as_ref().unwrap();
1707 assert_eq!(
1708 config2.bmp_grid_bits,
1709 SparseVectorConfig::DEFAULT_BMP_GRID_BITS
1710 );
1711 let config3 = indexes[0].fields[2].sparse_vector_config.as_ref().unwrap();
1713 assert_eq!(
1714 config3.bmp_grid_bits,
1715 SparseVectorConfig::DEFAULT_BMP_GRID_BITS
1716 );
1717 }
1718
1719 #[test]
1720 fn test_sparse_vector_with_weight_threshold() {
1721 let sdl = r#"
1722 index documents {
1723 field embedding: sparse_vector<u16> [indexed<quantization: uint8, weight_threshold: 0.1>, stored]
1724 field embedding2: sparse_vector<u32> [indexed<quantization: float16, weight_threshold: 0.05>]
1725 }
1726 "#;
1727
1728 let indexes = parse_sdl(sdl).unwrap();
1729 assert_eq!(indexes[0].fields.len(), 2);
1730
1731 let f1 = &indexes[0].fields[0];
1733 assert_eq!(f1.name, "embedding");
1734 let config1 = f1.sparse_vector_config.as_ref().unwrap();
1735 assert_eq!(config1.index_size, IndexSize::U16);
1736 assert_eq!(config1.weight_quantization, WeightQuantization::UInt8);
1737 assert!((config1.weight_threshold - 0.1).abs() < 0.001);
1738
1739 let f2 = &indexes[0].fields[1];
1741 assert_eq!(f2.name, "embedding2");
1742 let config2 = f2.sparse_vector_config.as_ref().unwrap();
1743 assert_eq!(config2.index_size, IndexSize::U32);
1744 assert_eq!(config2.weight_quantization, WeightQuantization::Float16);
1745 assert!((config2.weight_threshold - 0.05).abs() < 0.001);
1746 }
1747
1748 #[test]
1749 fn test_sparse_vector_with_pruning() {
1750 let sdl = r#"
1751 index documents {
1752 field embedding: sparse_vector [indexed<quantization: uint8, pruning: 0.1>, stored]
1753 }
1754 "#;
1755
1756 let indexes = parse_sdl(sdl).unwrap();
1757 let f = &indexes[0].fields[0];
1758 assert_eq!(f.name, "embedding");
1759 let config = f.sparse_vector_config.as_ref().unwrap();
1760 assert_eq!(config.weight_quantization, WeightQuantization::UInt8);
1761 assert_eq!(config.pruning, Some(0.1));
1762 }
1763
1764 #[test]
1765 fn test_sparse_vector_with_doc_mass() {
1766 let sdl = r#"
1767 index documents {
1768 field embedding: sparse_vector [indexed<quantization: uint8, doc_mass: 0.9>, stored]
1769 }
1770 "#;
1771
1772 let indexes = parse_sdl(sdl).unwrap();
1773 let config = indexes[0].fields[0].sparse_vector_config.as_ref().unwrap();
1774 assert_eq!(config.doc_mass, Some(0.9));
1775
1776 let sdl = r#"
1778 index documents {
1779 field embedding: sparse_vector [indexed<quantization: uint8>]
1780 }
1781 "#;
1782 let indexes = parse_sdl(sdl).unwrap();
1783 let config = indexes[0].fields[0].sparse_vector_config.as_ref().unwrap();
1784 assert_eq!(config.doc_mass, None);
1785 }
1786
1787 #[test]
1788 fn test_dense_vector_field() {
1789 let sdl = r#"
1790 index documents {
1791 field embedding: dense_vector<768> [indexed, stored]
1792 }
1793 "#;
1794
1795 let indexes = parse_sdl(sdl).unwrap();
1796 assert_eq!(indexes.len(), 1);
1797 assert_eq!(indexes[0].fields.len(), 1);
1798
1799 let f = &indexes[0].fields[0];
1800 assert_eq!(f.name, "embedding");
1801 assert_eq!(f.field_type, FieldType::DenseVector);
1802
1803 let config = f.dense_vector_config.as_ref().unwrap();
1804 assert_eq!(config.dim, 768);
1805 }
1806
1807 #[test]
1808 fn test_dense_vector_alias() {
1809 let sdl = r#"
1810 index documents {
1811 field embedding: vector<1536> [indexed]
1812 }
1813 "#;
1814
1815 let indexes = parse_sdl(sdl).unwrap();
1816 assert_eq!(indexes[0].fields[0].field_type, FieldType::DenseVector);
1817 assert_eq!(
1818 indexes[0].fields[0]
1819 .dense_vector_config
1820 .as_ref()
1821 .unwrap()
1822 .dim,
1823 1536
1824 );
1825 }
1826
1827 #[test]
1828 fn test_dense_vector_with_num_clusters() {
1829 let sdl = r#"
1830 index documents {
1831 field embedding: dense_vector<768> [indexed<ivf_tq, num_clusters: 256>, stored]
1832 }
1833 "#;
1834
1835 let indexes = parse_sdl(sdl).unwrap();
1836 assert_eq!(indexes.len(), 1);
1837
1838 let f = &indexes[0].fields[0];
1839 assert_eq!(f.name, "embedding");
1840 assert_eq!(f.field_type, FieldType::DenseVector);
1841
1842 let config = f.dense_vector_config.as_ref().unwrap();
1843 assert_eq!(config.dim, 768);
1844 assert_eq!(config.num_clusters, Some(256));
1845 assert_eq!(config.nprobe, 64); }
1847
1848 #[test]
1849 fn test_dense_vector_with_soar() {
1850 let sdl = r#"
1852 index documents {
1853 field embedding: dense_vector<768> [indexed<ivf_tq>]
1854 }
1855 "#;
1856 let indexes = parse_sdl(sdl).unwrap();
1857 let config = indexes[0].fields[0].dense_vector_config.as_ref().unwrap();
1858 let soar = config
1859 .soar
1860 .as_ref()
1861 .expect("omitted SOAR should enable selective spilling");
1862 assert_eq!(soar.num_secondary, 1);
1863 assert!(soar.selective);
1864 assert_eq!(soar.calibration_target(), Some(0.30));
1865
1866 let sdl = r#"
1868 index documents {
1869 field embedding: dense_vector<768> [indexed<ivf_tq, num_clusters: 256, soar: selective>, stored]
1870 }
1871 "#;
1872
1873 let indexes = parse_sdl(sdl).unwrap();
1874 let config = indexes[0].fields[0].dense_vector_config.as_ref().unwrap();
1875
1876 let soar = config.soar.as_ref().expect("soar should be enabled");
1877 assert_eq!(soar.num_secondary, 1);
1878 assert!(soar.selective);
1879
1880 let sdl = r#"
1882 index documents {
1883 field embedding: dense_vector<768> [indexed<ivf_tq, soar: aggressive>]
1884 }
1885 "#;
1886 let indexes = parse_sdl(sdl).unwrap();
1887 let config = indexes[0].fields[0].dense_vector_config.as_ref().unwrap();
1888 let soar = config.soar.as_ref().expect("soar should be enabled");
1889 assert_eq!(soar.num_secondary, 1);
1890 assert!(!soar.selective);
1891
1892 let sdl = r#"
1894 index documents {
1895 field embedding: dense_vector<768> [indexed<ivf_tq, soar: off>]
1896 }
1897 "#;
1898 let indexes = parse_sdl(sdl).unwrap();
1899 let config = indexes[0].fields[0].dense_vector_config.as_ref().unwrap();
1900 assert!(config.soar.is_none());
1901 }
1902
1903 #[test]
1904 fn omitted_soar_is_canonicalized_off_for_non_ivf_formats() {
1905 let sdl = r#"
1906 index documents {
1907 field tq: dense_vector<768> [indexed<tq>]
1908 field flat: dense_vector<768> [indexed<flat>]
1909 }
1910 "#;
1911 let indexes = parse_sdl(sdl).unwrap();
1912 for field in &indexes[0].fields {
1913 assert!(
1914 field
1915 .dense_vector_config
1916 .as_ref()
1917 .expect("dense config")
1918 .soar
1919 .is_none(),
1920 "{} should not retain an ignored SOAR default",
1921 field.name,
1922 );
1923 }
1924 }
1925
1926 #[test]
1927 fn test_ivf_routing_modes_apply_to_float_and_binary_fields() {
1928 let indexes = parse_sdl(
1929 r#"
1930 index vectors {
1931 field embedding: dense_vector<768> [indexed<ivf_tq, routing: hnsw>]
1932 field hash: binary_dense_vector<512> [indexed<ivf, routing: two_level>]
1933 }
1934 "#,
1935 )
1936 .unwrap();
1937 let schema = indexes[0].to_schema();
1938 let embedding = schema.get_field("embedding").unwrap();
1939 let hash = schema.get_field("hash").unwrap();
1940 assert_eq!(
1941 schema
1942 .get_field_entry(embedding)
1943 .unwrap()
1944 .dense_vector_config
1945 .as_ref()
1946 .unwrap()
1947 .ivf_routing,
1948 super::super::schema::IvfRoutingMode::Hnsw
1949 );
1950 assert_eq!(
1951 schema
1952 .get_field_entry(hash)
1953 .unwrap()
1954 .binary_dense_vector_config
1955 .as_ref()
1956 .unwrap()
1957 .ivf_routing,
1958 super::super::schema::IvfRoutingMode::TwoLevel
1959 );
1960 }
1961
1962 #[test]
1963 fn test_binary_dense_vector_with_ivf() {
1964 let sdl = r#"
1965 index documents {
1966 field hash: binary_dense_vector<512> [indexed<ivf, num_clusters: 128, nprobe: 16>, stored]
1967 }
1968 "#;
1969
1970 let indexes = parse_sdl(sdl).unwrap();
1971 let config = indexes[0].fields[0]
1972 .binary_dense_vector_config
1973 .as_ref()
1974 .unwrap();
1975 assert_eq!(config.dim, 512);
1976 assert_eq!(
1977 config.index_type,
1978 super::super::schema::BinaryIndexType::Ivf
1979 );
1980 assert_eq!(config.num_clusters, Some(128));
1981 assert_eq!(config.nprobe, 16);
1982
1983 let sdl = r#"
1986 index documents {
1987 field hash: binary_dense_vector<512> [indexed]
1988 }
1989 "#;
1990 let indexes = parse_sdl(sdl).unwrap();
1991 let config = indexes[0].fields[0]
1992 .binary_dense_vector_config
1993 .as_ref()
1994 .unwrap();
1995 assert_eq!(
1996 config.index_type,
1997 super::super::schema::BinaryIndexType::Ivf
1998 );
1999 }
2000
2001 #[test]
2002 fn test_dense_vector_with_num_clusters_and_nprobe() {
2003 let sdl = r#"
2004 index documents {
2005 field embedding: dense_vector<1536> [indexed<ivf_tq, num_clusters: 512, nprobe: 64>]
2006 }
2007 "#;
2008
2009 let indexes = parse_sdl(sdl).unwrap();
2010 let config = indexes[0].fields[0].dense_vector_config.as_ref().unwrap();
2011
2012 assert_eq!(config.dim, 1536);
2013 assert_eq!(config.num_clusters, Some(512));
2014 assert_eq!(config.nprobe, 64);
2015 }
2016
2017 #[test]
2018 fn test_dense_vector_keyword_syntax() {
2019 let sdl = r#"
2020 index documents {
2021 field embedding: dense_vector<dims: 1536> [indexed, stored]
2022 }
2023 "#;
2024
2025 let indexes = parse_sdl(sdl).unwrap();
2026 let config = indexes[0].fields[0].dense_vector_config.as_ref().unwrap();
2027
2028 assert_eq!(config.dim, 1536);
2029 assert!(config.num_clusters.is_none());
2030 }
2031
2032 #[test]
2033 fn test_dense_vector_keyword_syntax_full() {
2034 let sdl = r#"
2035 index documents {
2036 field embedding: dense_vector<dims: 1536> [indexed<ivf_tq, num_clusters: 256, nprobe: 64>]
2037 }
2038 "#;
2039
2040 let indexes = parse_sdl(sdl).unwrap();
2041 let config = indexes[0].fields[0].dense_vector_config.as_ref().unwrap();
2042
2043 assert_eq!(config.dim, 1536);
2044 assert_eq!(config.num_clusters, Some(256));
2045 assert_eq!(config.nprobe, 64);
2046 }
2047
2048 #[test]
2049 fn test_dense_vector_keyword_syntax_partial() {
2050 let sdl = r#"
2051 index documents {
2052 field embedding: dense_vector<dims: 768> [indexed<ivf_tq, num_clusters: 128>]
2053 }
2054 "#;
2055
2056 let indexes = parse_sdl(sdl).unwrap();
2057 let config = indexes[0].fields[0].dense_vector_config.as_ref().unwrap();
2058
2059 assert_eq!(config.dim, 768);
2060 assert_eq!(config.num_clusters, Some(128));
2061 assert_eq!(config.nprobe, 64); }
2063
2064 #[test]
2065 fn test_dense_vector_ivf_tq_index_with_probe() {
2066 use crate::dsl::schema::VectorIndexType;
2067
2068 let sdl = r#"
2069 index documents {
2070 field embedding: dense_vector<dims: 768> [indexed<ivf_tq, num_clusters: 256, nprobe: 64>]
2071 }
2072 "#;
2073
2074 let indexes = parse_sdl(sdl).unwrap();
2075 let config = indexes[0].fields[0].dense_vector_config.as_ref().unwrap();
2076
2077 assert_eq!(config.dim, 768);
2078 assert_eq!(config.index_type, VectorIndexType::IvfTq);
2079 assert_eq!(config.num_clusters, Some(256));
2080 assert_eq!(config.nprobe, 64);
2081 }
2082
2083 #[test]
2084 fn test_dense_vector_ivf_tq_index_without_explicit_probe() {
2085 use crate::dsl::schema::VectorIndexType;
2086
2087 let sdl = r#"
2088 index documents {
2089 field embedding: dense_vector<dims: 1536> [indexed<ivf_tq, num_clusters: 512>]
2090 }
2091 "#;
2092
2093 let indexes = parse_sdl(sdl).unwrap();
2094 let config = indexes[0].fields[0].dense_vector_config.as_ref().unwrap();
2095
2096 assert_eq!(config.dim, 1536);
2097 assert_eq!(config.index_type, VectorIndexType::IvfTq);
2098 assert_eq!(config.num_clusters, Some(512));
2099 }
2100
2101 #[test]
2102 fn test_dense_vector_ivf_tq_no_clusters() {
2103 use crate::dsl::schema::VectorIndexType;
2104
2105 let sdl = r#"
2106 index documents {
2107 field embedding: dense_vector<dims: 768> [indexed<ivf_tq>]
2108 }
2109 "#;
2110
2111 let indexes = parse_sdl(sdl).unwrap();
2112 let config = indexes[0].fields[0].dense_vector_config.as_ref().unwrap();
2113
2114 assert_eq!(config.dim, 768);
2115 assert_eq!(config.index_type, VectorIndexType::IvfTq);
2116 assert!(config.num_clusters.is_none());
2117 }
2118
2119 #[test]
2120 fn removed_ivf_pq_still_parses_to_the_reserved_variant() {
2121 use crate::dsl::schema::VectorIndexType;
2122
2123 let sdl = r#"
2126 index test {
2127 field embedding: dense_vector<8> [indexed<ivf_pq>]
2128 }
2129 "#;
2130 let indexes = parse_sdl(sdl).unwrap();
2131 let config = indexes[0].fields[0].dense_vector_config.as_ref().unwrap();
2132 assert_eq!(config.index_type, VectorIndexType::IvfPq);
2133
2134 let mut builder = crate::dsl::SchemaBuilder::default();
2135 builder.add_dense_vector_field_with_config("embedding", true, true, config.clone());
2136 let schema = builder.build();
2137 let error = crate::dsl::schema::reject_removed_vector_index_types(&schema)
2138 .expect_err("removed index types must be rejected at the index gate");
2139 assert!(error.contains("ivf_tq"), "{error}");
2140 assert!(error.contains("removed"), "{error}");
2141 }
2142
2143 #[test]
2144 fn test_dense_vector_flat_index() {
2145 use crate::dsl::schema::VectorIndexType;
2146
2147 let sdl = r#"
2148 index documents {
2149 field embedding: dense_vector<dims: 768> [indexed<flat>]
2150 }
2151 "#;
2152
2153 let indexes = parse_sdl(sdl).unwrap();
2154 let config = indexes[0].fields[0].dense_vector_config.as_ref().unwrap();
2155
2156 assert_eq!(config.dim, 768);
2157 assert_eq!(config.index_type, VectorIndexType::Flat);
2158 }
2159
2160 #[test]
2161 fn test_dense_vector_default_index_type() {
2162 use crate::dsl::schema::VectorIndexType;
2163
2164 let sdl = r#"
2166 index documents {
2167 field embedding: dense_vector<dims: 768> [indexed]
2168 }
2169 "#;
2170
2171 let indexes = parse_sdl(sdl).unwrap();
2172 let config = indexes[0].fields[0].dense_vector_config.as_ref().unwrap();
2173
2174 assert_eq!(config.dim, 768);
2175 assert_eq!(config.index_type, VectorIndexType::IvfTq);
2176 }
2177
2178 #[test]
2179 fn test_dense_vector_f16_quantization() {
2180 use crate::dsl::schema::{DenseVectorQuantization, VectorIndexType};
2181
2182 let sdl = r#"
2183 index documents {
2184 field embedding: dense_vector<768, f16> [indexed]
2185 }
2186 "#;
2187
2188 let indexes = parse_sdl(sdl).unwrap();
2189 let config = indexes[0].fields[0].dense_vector_config.as_ref().unwrap();
2190
2191 assert_eq!(config.dim, 768);
2192 assert_eq!(config.quantization, DenseVectorQuantization::F16);
2193 assert_eq!(config.index_type, VectorIndexType::IvfTq);
2194 }
2195
2196 #[test]
2197 fn test_dense_vector_uint8_quantization() {
2198 use crate::dsl::schema::DenseVectorQuantization;
2199
2200 let sdl = r#"
2201 index documents {
2202 field embedding: dense_vector<1024, uint8> [indexed<ivf_tq>]
2203 }
2204 "#;
2205
2206 let indexes = parse_sdl(sdl).unwrap();
2207 let config = indexes[0].fields[0].dense_vector_config.as_ref().unwrap();
2208
2209 assert_eq!(config.dim, 1024);
2210 assert_eq!(config.quantization, DenseVectorQuantization::UInt8);
2211 }
2212
2213 #[test]
2214 fn test_dense_vector_u8_alias() {
2215 use crate::dsl::schema::DenseVectorQuantization;
2216
2217 let sdl = r#"
2218 index documents {
2219 field embedding: dense_vector<512, u8> [indexed]
2220 }
2221 "#;
2222
2223 let indexes = parse_sdl(sdl).unwrap();
2224 let config = indexes[0].fields[0].dense_vector_config.as_ref().unwrap();
2225
2226 assert_eq!(config.dim, 512);
2227 assert_eq!(config.quantization, DenseVectorQuantization::UInt8);
2228 }
2229
2230 #[test]
2231 fn test_dense_vector_default_f32_quantization() {
2232 use crate::dsl::schema::DenseVectorQuantization;
2233
2234 let sdl = r#"
2236 index documents {
2237 field embedding: dense_vector<768> [indexed]
2238 }
2239 "#;
2240
2241 let indexes = parse_sdl(sdl).unwrap();
2242 let config = indexes[0].fields[0].dense_vector_config.as_ref().unwrap();
2243
2244 assert_eq!(config.dim, 768);
2245 assert_eq!(config.quantization, DenseVectorQuantization::F32);
2246 }
2247
2248 #[test]
2249 fn test_dense_vector_keyword_with_quantization() {
2250 use crate::dsl::schema::DenseVectorQuantization;
2251
2252 let sdl = r#"
2253 index documents {
2254 field embedding: dense_vector<dims: 768, f16> [indexed]
2255 }
2256 "#;
2257
2258 let indexes = parse_sdl(sdl).unwrap();
2259 let config = indexes[0].fields[0].dense_vector_config.as_ref().unwrap();
2260
2261 assert_eq!(config.dim, 768);
2262 assert_eq!(config.quantization, DenseVectorQuantization::F16);
2263 }
2264
2265 #[test]
2266 fn test_json_field_type() {
2267 let sdl = r#"
2268 index documents {
2269 field title: text [indexed, stored]
2270 field metadata: json [stored]
2271 field extra: json
2272 }
2273 "#;
2274
2275 let indexes = parse_sdl(sdl).unwrap();
2276 let index = &indexes[0];
2277
2278 assert_eq!(index.fields.len(), 3);
2279
2280 assert_eq!(index.fields[1].name, "metadata");
2282 assert!(matches!(index.fields[1].field_type, FieldType::Json));
2283 assert!(index.fields[1].stored);
2284 assert_eq!(index.fields[2].name, "extra");
2288 assert!(matches!(index.fields[2].field_type, FieldType::Json));
2289
2290 let schema = index.to_schema();
2292 let metadata_field = schema.get_field("metadata").unwrap();
2293 let entry = schema.get_field_entry(metadata_field).unwrap();
2294 assert_eq!(entry.field_type, FieldType::Json);
2295 assert!(!entry.indexed); assert!(entry.stored);
2297 }
2298
2299 #[test]
2300 fn test_sparse_vector_query_config() {
2301 use crate::structures::QueryWeighting;
2302
2303 let sdl = r#"
2304 index documents {
2305 field embedding: sparse_vector<u16> [indexed<quantization: uint8, query<tokenizer: "Alibaba-NLP/gte-Qwen2-1.5B-instruct", weighting: idf>>]
2306 }
2307 "#;
2308
2309 let indexes = parse_sdl(sdl).unwrap();
2310 let index = &indexes[0];
2311
2312 assert_eq!(index.fields.len(), 1);
2313 assert_eq!(index.fields[0].name, "embedding");
2314 assert!(matches!(
2315 index.fields[0].field_type,
2316 FieldType::SparseVector
2317 ));
2318
2319 let config = index.fields[0].sparse_vector_config.as_ref().unwrap();
2320 assert_eq!(config.index_size, IndexSize::U16);
2321 assert_eq!(config.weight_quantization, WeightQuantization::UInt8);
2322
2323 let query_config = config.query_config.as_ref().unwrap();
2325 assert_eq!(
2326 query_config.tokenizer.as_deref(),
2327 Some("Alibaba-NLP/gte-Qwen2-1.5B-instruct")
2328 );
2329 assert_eq!(query_config.weighting, QueryWeighting::Idf);
2330
2331 let schema = index.to_schema();
2333 let embedding_field = schema.get_field("embedding").unwrap();
2334 let entry = schema.get_field_entry(embedding_field).unwrap();
2335 let sv_config = entry.sparse_vector_config.as_ref().unwrap();
2336 let qc = sv_config.query_config.as_ref().unwrap();
2337 assert_eq!(
2338 qc.tokenizer.as_deref(),
2339 Some("Alibaba-NLP/gte-Qwen2-1.5B-instruct")
2340 );
2341 assert_eq!(qc.weighting, QueryWeighting::Idf);
2342 }
2343
2344 #[test]
2345 fn test_sparse_vector_query_config_weighting_one() {
2346 use crate::structures::QueryWeighting;
2347
2348 let sdl = r#"
2349 index documents {
2350 field embedding: sparse_vector [indexed<query<weighting: one>>]
2351 }
2352 "#;
2353
2354 let indexes = parse_sdl(sdl).unwrap();
2355 let config = indexes[0].fields[0].sparse_vector_config.as_ref().unwrap();
2356
2357 let query_config = config.query_config.as_ref().unwrap();
2358 assert!(query_config.tokenizer.is_none());
2359 assert_eq!(query_config.weighting, QueryWeighting::One);
2360 }
2361
2362 #[test]
2363 fn test_sparse_vector_query_config_weighting_idf_file() {
2364 use crate::structures::QueryWeighting;
2365
2366 let sdl = r#"
2367 index documents {
2368 field embedding: sparse_vector<u16> [indexed<quantization: uint8, query<tokenizer: "opensearch-neural-sparse-encoding-v1", weighting: idf_file>>]
2369 }
2370 "#;
2371
2372 let indexes = parse_sdl(sdl).unwrap();
2373 let config = indexes[0].fields[0].sparse_vector_config.as_ref().unwrap();
2374
2375 let query_config = config.query_config.as_ref().unwrap();
2376 assert_eq!(
2377 query_config.tokenizer.as_deref(),
2378 Some("opensearch-neural-sparse-encoding-v1")
2379 );
2380 assert_eq!(query_config.weighting, QueryWeighting::IdfFile);
2381
2382 let schema = indexes[0].to_schema();
2384 let field = schema.get_field("embedding").unwrap();
2385 let entry = schema.get_field_entry(field).unwrap();
2386 let sc = entry.sparse_vector_config.as_ref().unwrap();
2387 let qc = sc.query_config.as_ref().unwrap();
2388 assert_eq!(qc.weighting, QueryWeighting::IdfFile);
2389 }
2390
2391 #[test]
2392 fn test_sparse_vector_query_config_pruning_params() {
2393 let sdl = r#"
2394 index documents {
2395 field embedding: sparse_vector<u16> [indexed<quantization: uint8, query<weighting: idf, weight_threshold: 0.03, max_dims: 25, pruning: 0.2, lsp_gamma: 500>>]
2396 }
2397 "#;
2398
2399 let indexes = parse_sdl(sdl).unwrap();
2400 let config = indexes[0].fields[0].sparse_vector_config.as_ref().unwrap();
2401
2402 let qc = config.query_config.as_ref().unwrap();
2403 assert_eq!(qc.weighting, QueryWeighting::Idf);
2404 assert!((qc.weight_threshold - 0.03).abs() < 0.001);
2405 assert_eq!(qc.max_query_dims, Some(25));
2406 assert!((qc.pruning.unwrap() - 0.2).abs() < 0.001);
2407 assert_eq!(qc.lsp_gamma, Some(500));
2408
2409 let schema = indexes[0].to_schema();
2411 let field = schema.get_field("embedding").unwrap();
2412 let entry = schema.get_field_entry(field).unwrap();
2413 let sc = entry.sparse_vector_config.as_ref().unwrap();
2414 let rqc = sc.query_config.as_ref().unwrap();
2415 assert!((rqc.weight_threshold - 0.03).abs() < 0.001);
2416 assert_eq!(rqc.max_query_dims, Some(25));
2417 assert!((rqc.pruning.unwrap() - 0.2).abs() < 0.001);
2418 assert_eq!(rqc.lsp_gamma, Some(500));
2419 }
2420
2421 #[test]
2422 fn test_sparse_vector_format_maxscore() {
2423 let sdl = r#"
2424 index documents {
2425 field embedding: sparse_vector<u16> [indexed<format: maxscore, quantization: uint8>]
2426 }
2427 "#;
2428
2429 let indexes = parse_sdl(sdl).unwrap();
2430 let config = indexes[0].fields[0].sparse_vector_config.as_ref().unwrap();
2431 assert_eq!(config.format, SparseFormat::MaxScore);
2432 assert_eq!(config.weight_quantization, WeightQuantization::UInt8);
2433
2434 let schema = indexes[0].to_schema();
2436 let field = schema.get_field("embedding").unwrap();
2437 let entry = schema.get_field_entry(field).unwrap();
2438 let sc = entry.sparse_vector_config.as_ref().unwrap();
2439 assert_eq!(sc.format, SparseFormat::MaxScore);
2440 }
2441
2442 #[test]
2443 fn test_sparse_vector_format_bmp() {
2444 let sdl = r#"
2445 index documents {
2446 field embedding: sparse_vector<u16> [indexed<format: bmp, quantization: uint8>]
2447 }
2448 "#;
2449
2450 let indexes = parse_sdl(sdl).unwrap();
2451 let config = indexes[0].fields[0].sparse_vector_config.as_ref().unwrap();
2452 assert_eq!(config.format, SparseFormat::Bmp);
2453 }
2454
2455 #[test]
2456 fn test_fast_attribute() {
2457 let sdl = r#"
2458 index products {
2459 field name: text [indexed, stored]
2460 field price: f64 [indexed, fast]
2461 field category: text [indexed, stored, fast]
2462 field count: u64 [fast]
2463 field score: i64 [indexed, stored, fast]
2464 }
2465 "#;
2466
2467 let indexes = parse_sdl(sdl).unwrap();
2468 assert_eq!(indexes.len(), 1);
2469 let index = &indexes[0];
2470 assert_eq!(index.fields.len(), 5);
2471
2472 assert!(!index.fields[0].fast);
2474 assert!(index.fields[1].fast);
2476 assert!(matches!(index.fields[1].field_type, FieldType::F64));
2477 assert!(index.fields[2].fast);
2479 assert!(matches!(index.fields[2].field_type, FieldType::Text));
2480 assert!(index.fields[3].fast);
2482 assert!(matches!(index.fields[3].field_type, FieldType::U64));
2483 assert!(index.fields[4].fast);
2485 assert!(matches!(index.fields[4].field_type, FieldType::I64));
2486
2487 let schema = index.to_schema();
2489 let price_field = schema.get_field("price").unwrap();
2490 assert!(schema.get_field_entry(price_field).unwrap().fast);
2491
2492 let category_field = schema.get_field("category").unwrap();
2493 assert!(schema.get_field_entry(category_field).unwrap().fast);
2494
2495 let name_field = schema.get_field("name").unwrap();
2496 assert!(!schema.get_field_entry(name_field).unwrap().fast);
2497 }
2498
2499 #[test]
2500 fn test_primary_attribute() {
2501 let sdl = r#"
2502 index documents {
2503 field id: text [primary, stored]
2504 field title: text [indexed, stored]
2505 }
2506 "#;
2507
2508 let indexes = parse_sdl(sdl).unwrap();
2509 assert_eq!(indexes.len(), 1);
2510 let index = &indexes[0];
2511 assert_eq!(index.fields.len(), 2);
2512
2513 let id_field = &index.fields[0];
2515 assert!(id_field.primary, "id should be primary");
2516 assert!(id_field.fast, "primary implies fast");
2517 assert!(id_field.indexed, "primary implies indexed");
2518
2519 assert!(!index.fields[1].primary);
2521
2522 let schema = index.to_schema();
2524 let id = schema.get_field("id").unwrap();
2525 let id_entry = schema.get_field_entry(id).unwrap();
2526 assert!(id_entry.primary_key);
2527 assert!(id_entry.fast);
2528 assert!(id_entry.indexed);
2529
2530 let title = schema.get_field("title").unwrap();
2531 assert!(!schema.get_field_entry(title).unwrap().primary_key);
2532
2533 assert_eq!(schema.primary_field(), Some(id));
2535 }
2536
2537 #[test]
2538 fn test_primary_with_other_attributes() {
2539 let sdl = r#"
2540 index documents {
2541 field id: text<simple> [primary, indexed, stored]
2542 field body: text [indexed]
2543 }
2544 "#;
2545
2546 let indexes = parse_sdl(sdl).unwrap();
2547 let id_field = &indexes[0].fields[0];
2548 assert!(id_field.primary);
2549 assert!(id_field.indexed);
2550 assert!(id_field.stored);
2551 assert!(id_field.fast);
2552 assert_eq!(id_field.tokenizer, Some("simple".to_string()));
2553 }
2554
2555 #[test]
2556 fn test_primary_only_one_allowed() {
2557 let sdl = r#"
2558 index documents {
2559 field id: text [primary]
2560 field alt_id: text [primary]
2561 }
2562 "#;
2563
2564 let result = parse_sdl(sdl);
2565 assert!(result.is_err());
2566 let err = result.unwrap_err().to_string();
2567 assert!(
2568 err.contains("primary key"),
2569 "Error should mention primary key: {}",
2570 err
2571 );
2572 }
2573
2574 #[test]
2575 fn test_primary_must_be_text() {
2576 let sdl = r#"
2577 index documents {
2578 field id: u64 [primary]
2579 }
2580 "#;
2581
2582 let result = parse_sdl(sdl);
2583 assert!(result.is_err());
2584 let err = result.unwrap_err().to_string();
2585 assert!(
2586 err.contains("text"),
2587 "Error should mention text type: {}",
2588 err
2589 );
2590 }
2591
2592 #[test]
2593 fn test_primary_cannot_be_multi() {
2594 let sdl = r#"
2595 index documents {
2596 field id: text [primary, stored<multi>]
2597 }
2598 "#;
2599
2600 let result = parse_sdl(sdl);
2601 assert!(result.is_err());
2602 let err = result.unwrap_err().to_string();
2603 assert!(err.contains("multi"), "Error should mention multi: {}", err);
2604 }
2605
2606 #[test]
2607 fn test_no_primary_field() {
2608 let sdl = r#"
2610 index documents {
2611 field title: text [indexed, stored]
2612 }
2613 "#;
2614
2615 let indexes = parse_sdl(sdl).unwrap();
2616 let schema = indexes[0].to_schema();
2617 assert!(schema.primary_field().is_none());
2618 }
2619
2620 #[test]
2621 fn test_reorder_attribute() {
2622 let sdl = r#"
2623 index documents {
2624 field embedding: sparse_vector<u16> [indexed<format: bmp, quantization: uint8>, reorder]
2625 field embedding2: sparse_vector [indexed<format: bmp>]
2626 }
2627 "#;
2628
2629 let indexes = parse_sdl(sdl).unwrap();
2630 assert_eq!(indexes[0].fields.len(), 2);
2631
2632 assert!(indexes[0].fields[0].reorder);
2634 assert!(!indexes[0].fields[1].reorder);
2636
2637 let schema = indexes[0].to_schema();
2639 let f1 = schema.get_field("embedding").unwrap();
2640 assert!(schema.get_field_entry(f1).unwrap().reorder);
2641
2642 let f2 = schema.get_field("embedding2").unwrap();
2643 assert!(!schema.get_field_entry(f2).unwrap().reorder);
2644
2645 assert!(!schema.reorder_on_merge());
2647 }
2648
2649 #[test]
2650 fn test_reorder_on_merge_index_option() {
2651 let sdl = r#"
2652 index documents {
2653 reorder_on_merge: true
2654 field embedding: sparse_vector<u16> [indexed<format: bmp>, reorder]
2655 }
2656 "#;
2657
2658 let indexes = parse_sdl(sdl).unwrap();
2659 assert!(indexes[0].reorder_on_merge);
2660 let schema = indexes[0].to_schema();
2661 assert!(schema.reorder_on_merge());
2662
2663 let sdl_off = r#"
2665 index documents {
2666 reorder_on_merge: false
2667 field embedding: sparse_vector<u16> [indexed<format: bmp>, reorder]
2668 }
2669 "#;
2670 let indexes = parse_sdl(sdl_off).unwrap();
2671 assert!(!indexes[0].reorder_on_merge);
2672 assert!(!indexes[0].to_schema().reorder_on_merge());
2673
2674 let schema_on = parse_sdl(sdl).unwrap()[0].to_schema();
2676 let json = serde_json::to_string(&schema_on).unwrap();
2677 let back: crate::dsl::Schema = serde_json::from_str(&json).unwrap();
2678 assert!(back.reorder_on_merge());
2679 }
2680}