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