1use serde::{Deserialize, Serialize};
4
5#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
15pub enum SparseFormat {
16 MaxScore,
18 #[default]
20 Bmp,
21 Seismic,
23}
24
25fn legacy_sparse_format() -> SparseFormat {
28 SparseFormat::MaxScore
29}
30
31#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
33#[repr(u8)]
34pub enum IndexSize {
35 U16 = 0,
37 #[default]
39 U32 = 1,
40}
41
42impl IndexSize {
43 pub fn bytes(&self) -> usize {
45 match self {
46 IndexSize::U16 => 2,
47 IndexSize::U32 => 4,
48 }
49 }
50
51 pub fn max_value(&self) -> u32 {
53 match self {
54 IndexSize::U16 => u16::MAX as u32,
55 IndexSize::U32 => u32::MAX,
56 }
57 }
58
59 pub(crate) fn from_u8(v: u8) -> Option<Self> {
60 match v {
61 0 => Some(IndexSize::U16),
62 1 => Some(IndexSize::U32),
63 _ => None,
64 }
65 }
66}
67
68#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
75#[repr(u8)]
76pub enum WeightQuantization {
77 #[default]
79 Float32 = 0,
80 Float16 = 1,
82 UInt8 = 2,
84 UInt4 = 3,
86}
87
88impl WeightQuantization {
89 pub fn bytes_per_weight(&self) -> f32 {
91 match self {
92 WeightQuantization::Float32 => 4.0,
93 WeightQuantization::Float16 => 2.0,
94 WeightQuantization::UInt8 => 1.0,
95 WeightQuantization::UInt4 => 0.5,
96 }
97 }
98
99 pub(crate) fn from_u8(v: u8) -> Option<Self> {
100 match v {
101 0 => Some(WeightQuantization::Float32),
102 1 => Some(WeightQuantization::Float16),
103 2 => Some(WeightQuantization::UInt8),
104 3 => Some(WeightQuantization::UInt4),
105 _ => None,
106 }
107 }
108}
109
110#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
112#[serde(rename_all = "snake_case")]
113pub enum QueryWeighting {
114 #[default]
116 One,
117 Idf,
120 IdfFile,
123}
124
125#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
132pub struct SparseQueryConfig {
133 #[serde(default, skip_serializing_if = "Option::is_none")]
136 pub tokenizer: Option<String>,
137 #[serde(default)]
139 pub weighting: QueryWeighting,
140 #[serde(default = "default_heap_factor")]
146 pub heap_factor: f32,
147 #[serde(default)]
154 pub weight_threshold: f32,
155 #[serde(default, skip_serializing_if = "Option::is_none")]
161 pub max_query_dims: Option<usize>,
162 #[serde(default, skip_serializing_if = "Option::is_none")]
168 pub pruning: Option<f32>,
169 #[serde(default = "default_min_terms")]
174 pub min_query_dims: usize,
175 #[serde(default, skip_serializing_if = "Option::is_none")]
178 pub lsp_gamma: Option<usize>,
179 #[serde(default = "default_seismic_cut")]
181 pub seismic_cut: usize,
182 #[serde(default = "default_seismic_factor")]
184 pub seismic_factor: f32,
185 #[serde(default)]
187 pub exhaustive: bool,
188}
189
190fn default_seismic_cut() -> usize {
191 10
192}
193fn default_seismic_factor() -> f32 {
194 0.85
195}
196
197fn default_heap_factor() -> f32 {
198 1.0
199}
200
201impl Default for SparseQueryConfig {
202 fn default() -> Self {
203 Self {
204 tokenizer: None,
205 weighting: QueryWeighting::One,
206 heap_factor: 1.0,
207 weight_threshold: 0.0,
208 max_query_dims: None,
209 pruning: None,
210 min_query_dims: 4,
211 lsp_gamma: None,
212 seismic_cut: default_seismic_cut(),
213 seismic_factor: default_seismic_factor(),
214 exhaustive: false,
215 }
216 }
217}
218
219#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
221#[serde(default)]
222pub struct SeismicConfig {
223 pub postings: usize,
225 pub cluster_size: usize,
227 pub summary_energy: f32,
229 pub forward_compression: bool,
231}
232
233impl Default for SeismicConfig {
234 fn default() -> Self {
235 Self {
236 postings: 4096,
237 cluster_size: 64,
238 summary_energy: 0.4,
239 forward_compression: true,
240 }
241 }
242}
243
244impl SeismicConfig {
245 pub(crate) fn validate(&self) -> Result<(), String> {
246 if self.postings == 0 || self.postings > 65_536 {
247 return Err("seismic postings must be in 1..=65536".into());
248 }
249 if self.cluster_size == 0 || self.cluster_size > self.postings {
250 return Err("seismic cluster_size must be in 1..=postings".into());
251 }
252 if !self.summary_energy.is_finite()
253 || !(0.0..=1.0).contains(&self.summary_energy)
254 || self.summary_energy == 0.0
255 {
256 return Err("seismic summary_energy must be in (0, 1]".into());
257 }
258 Ok(())
259 }
260}
261
262#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
271pub struct SparseVectorConfig {
272 #[serde(default = "legacy_sparse_format")]
274 pub format: SparseFormat,
275 #[serde(default)]
276 pub seismic: SeismicConfig,
277 pub index_size: IndexSize,
279 pub weight_quantization: WeightQuantization,
281 #[serde(default)]
286 pub weight_threshold: f32,
287 #[serde(default, skip_serializing_if = "Option::is_none")]
298 pub doc_mass: Option<f32>,
299 #[serde(default = "default_block_size")]
303 pub block_size: usize,
304 #[serde(default = "default_bmp_block_size")]
312 pub bmp_block_size: u32,
313 #[serde(default = "default_bmp_grid_bits")]
321 pub bmp_grid_bits: u8,
322 #[serde(default = "default_bmp_forward_index", skip_serializing_if = "is_true")]
325 pub bmp_forward_index: bool,
326 #[serde(default, skip_serializing_if = "Option::is_none")]
338 pub pruning: Option<f32>,
339 #[serde(default, skip_serializing_if = "Option::is_none")]
341 pub query_config: Option<SparseQueryConfig>,
342 #[serde(default, skip_serializing_if = "Option::is_none")]
355 pub dims: Option<u32>,
356 #[serde(default, skip_serializing_if = "Option::is_none")]
364 pub max_weight: Option<f32>,
365 #[serde(default = "default_min_terms")]
371 pub min_terms: usize,
372}
373
374fn default_block_size() -> usize {
375 128
376}
377
378fn default_bmp_block_size() -> u32 {
379 SparseVectorConfig::DEFAULT_BMP_BLOCK_SIZE
380}
381
382fn default_bmp_grid_bits() -> u8 {
383 SparseVectorConfig::DEFAULT_BMP_GRID_BITS
384}
385
386fn default_bmp_forward_index() -> bool {
387 true
388}
389
390fn is_true(value: &bool) -> bool {
391 *value
392}
393
394fn default_min_terms() -> usize {
395 4
396}
397
398impl Default for SparseVectorConfig {
399 fn default() -> Self {
400 Self {
401 format: SparseFormat::Bmp,
402 seismic: SeismicConfig::default(),
403 index_size: IndexSize::U32,
404 weight_quantization: WeightQuantization::Float32,
405 weight_threshold: 0.0,
406 doc_mass: None,
407 block_size: 128,
408 bmp_block_size: default_bmp_block_size(),
409 bmp_grid_bits: default_bmp_grid_bits(),
410 bmp_forward_index: default_bmp_forward_index(),
411 pruning: None,
412 query_config: None,
413
414 dims: None,
415 max_weight: None,
416 min_terms: 4,
417 }
418 }
419}
420
421impl SparseVectorConfig {
422 pub const DEFAULT_BMP_BLOCK_SIZE: u32 = 32;
423 pub const DEFAULT_BMP_GRID_BITS: u8 = 4;
424
425 pub fn splade() -> Self {
434 Self {
435 format: SparseFormat::MaxScore,
436 seismic: SeismicConfig::default(),
437 index_size: IndexSize::U16,
438 weight_quantization: WeightQuantization::UInt8,
439 weight_threshold: 0.01, doc_mass: None,
441 block_size: 128,
442 bmp_block_size: default_bmp_block_size(),
443 bmp_grid_bits: default_bmp_grid_bits(),
444 bmp_forward_index: default_bmp_forward_index(),
445 pruning: None,
446 query_config: Some(SparseQueryConfig {
447 tokenizer: None,
448 weighting: QueryWeighting::One,
449 heap_factor: 1.0,
450 weight_threshold: 0.01,
451 max_query_dims: None,
452 pruning: None,
453 min_query_dims: 4,
454 lsp_gamma: None,
455 seismic_cut: default_seismic_cut(),
456 seismic_factor: default_seismic_factor(),
457 exhaustive: false,
458 }),
459
460 dims: None,
461 max_weight: None,
462 min_terms: 4,
463 }
464 }
465
466 pub fn splade_bmp() -> Self {
473 Self {
474 format: SparseFormat::Bmp,
475 seismic: SeismicConfig::default(),
476 index_size: IndexSize::U16,
477 weight_quantization: WeightQuantization::UInt8,
478 weight_threshold: 0.01,
479 doc_mass: None,
480 block_size: 128,
481 bmp_block_size: default_bmp_block_size(),
482 bmp_grid_bits: default_bmp_grid_bits(),
483 bmp_forward_index: default_bmp_forward_index(),
484 pruning: None,
485 query_config: Some(SparseQueryConfig {
486 tokenizer: None,
487 weighting: QueryWeighting::One,
488 heap_factor: 1.0,
489 weight_threshold: 0.01,
490 max_query_dims: None,
491 pruning: None,
492 min_query_dims: 4,
493 lsp_gamma: None,
494 seismic_cut: default_seismic_cut(),
495 seismic_factor: default_seismic_factor(),
496 exhaustive: false,
497 }),
498
499 dims: Some(105879),
500 max_weight: Some(5.0),
501 min_terms: 4,
502 }
503 }
504
505 pub fn compact() -> Self {
512 Self {
513 format: SparseFormat::MaxScore,
514 seismic: SeismicConfig::default(),
515 index_size: IndexSize::U16,
516 weight_quantization: WeightQuantization::UInt4,
517 weight_threshold: 0.02, doc_mass: None,
519 block_size: 128,
520 bmp_block_size: default_bmp_block_size(),
521 bmp_grid_bits: default_bmp_grid_bits(),
522 bmp_forward_index: default_bmp_forward_index(),
523 pruning: Some(0.15), query_config: Some(SparseQueryConfig {
525 tokenizer: None,
526 weighting: QueryWeighting::One,
527 heap_factor: 0.7, weight_threshold: 0.02, max_query_dims: Some(15), pruning: Some(0.15), min_query_dims: 4,
532 lsp_gamma: None,
533 seismic_cut: default_seismic_cut(),
534 seismic_factor: default_seismic_factor(),
535 exhaustive: false,
536 }),
537
538 dims: None,
539 max_weight: None,
540 min_terms: 4,
541 }
542 }
543
544 pub fn full_precision() -> Self {
548 Self {
549 format: SparseFormat::MaxScore,
550 seismic: SeismicConfig::default(),
551 index_size: IndexSize::U32,
552 weight_quantization: WeightQuantization::Float32,
553 weight_threshold: 0.0,
554 doc_mass: None,
555 block_size: 128,
556 bmp_block_size: default_bmp_block_size(),
557 bmp_grid_bits: default_bmp_grid_bits(),
558 bmp_forward_index: default_bmp_forward_index(),
559 pruning: None,
560 query_config: None,
561
562 dims: None,
563 max_weight: None,
564 min_terms: 4,
565 }
566 }
567
568 pub fn conservative() -> Self {
575 Self {
576 format: SparseFormat::MaxScore,
577 seismic: SeismicConfig::default(),
578 index_size: IndexSize::U32,
579 weight_quantization: WeightQuantization::Float16,
580 weight_threshold: 0.005, doc_mass: None,
582 block_size: 128,
583 bmp_block_size: default_bmp_block_size(),
584 bmp_grid_bits: default_bmp_grid_bits(),
585 bmp_forward_index: default_bmp_forward_index(),
586 pruning: None, query_config: Some(SparseQueryConfig {
588 tokenizer: None,
589 weighting: QueryWeighting::One,
590 heap_factor: 0.9, weight_threshold: 0.005, max_query_dims: Some(50), pruning: None, min_query_dims: 4,
595 lsp_gamma: None,
596 seismic_cut: default_seismic_cut(),
597 seismic_factor: default_seismic_factor(),
598 exhaustive: false,
599 }),
600
601 dims: None,
602 max_weight: None,
603 min_terms: 4,
604 }
605 }
606
607 pub fn with_weight_threshold(mut self, threshold: f32) -> Self {
609 self.weight_threshold = threshold;
610 self
611 }
612
613 pub fn with_doc_mass(mut self, fraction: f32) -> Self {
616 self.doc_mass = Some(fraction.clamp(0.0, 1.0));
617 self
618 }
619
620 pub fn with_pruning(mut self, fraction: f32) -> Self {
623 self.pruning = Some(fraction.clamp(0.0, 1.0));
624 self
625 }
626
627 pub fn bytes_per_entry(&self) -> f32 {
629 let dimension_bytes = if self.format == SparseFormat::Seismic {
630 4
631 } else {
632 self.index_size.bytes()
633 };
634 dimension_bytes as f32 + self.weight_quantization.bytes_per_weight()
635 }
636
637 pub fn to_byte(&self) -> u8 {
641 if self.format == SparseFormat::Seismic {
642 return 0x50 | self.weight_quantization as u8;
643 }
644 let format_bit = if self.format == SparseFormat::Bmp {
645 0x08
646 } else {
647 0
648 };
649 ((self.index_size as u8) << 4) | format_bit | (self.weight_quantization as u8)
650 }
651
652 pub fn from_byte(b: u8) -> Option<Self> {
657 if b & 0xfc == 0x50 {
658 return Some(Self {
659 format: SparseFormat::Seismic,
660 index_size: IndexSize::U32,
661 weight_quantization: WeightQuantization::from_u8(b & 3)?,
662 ..Default::default()
663 });
664 }
665 if b & 0xc0 != 0 {
666 return None;
667 }
668 let index_size = IndexSize::from_u8((b >> 4) & 0x03)?;
669 let format = if b & 0x08 != 0 {
670 SparseFormat::Bmp
671 } else {
672 SparseFormat::MaxScore
673 };
674 let weight_quantization = WeightQuantization::from_u8(b & 0x07)?;
675 Some(Self {
676 format,
677 seismic: SeismicConfig::default(),
678 index_size,
679 weight_quantization,
680 weight_threshold: 0.0,
681 doc_mass: None,
682 block_size: 128,
683 bmp_block_size: default_bmp_block_size(),
684 bmp_grid_bits: default_bmp_grid_bits(),
685 bmp_forward_index: default_bmp_forward_index(),
686 pruning: None,
687 query_config: None,
688
689 dims: None,
690 max_weight: None,
691 min_terms: 4,
692 })
693 }
694
695 pub fn with_block_size(mut self, size: usize) -> Self {
698 self.block_size = size.next_power_of_two();
699 self
700 }
701
702 pub fn with_query_config(mut self, config: SparseQueryConfig) -> Self {
704 self.query_config = Some(config);
705 self
706 }
707}
708
709#[derive(Debug, Clone, Copy, PartialEq)]
711pub struct SparseEntry {
712 pub dim_id: u32,
713 pub weight: f32,
714}
715
716#[derive(Debug, Clone, Default)]
718pub struct SparseVector {
719 pub(super) entries: Vec<SparseEntry>,
720}
721
722impl SparseVector {
723 pub fn new() -> Self {
725 Self {
726 entries: Vec::new(),
727 }
728 }
729
730 pub fn with_capacity(capacity: usize) -> Self {
732 Self {
733 entries: Vec::with_capacity(capacity),
734 }
735 }
736
737 pub fn from_entries(dim_ids: &[u32], weights: &[f32]) -> Self {
739 assert_eq!(dim_ids.len(), weights.len());
740 let mut entries: Vec<SparseEntry> = dim_ids
741 .iter()
742 .zip(weights.iter())
743 .map(|(&dim_id, &weight)| SparseEntry { dim_id, weight })
744 .collect();
745 entries.sort_by_key(|e| e.dim_id);
747 Self { entries }
748 }
749
750 pub fn push(&mut self, dim_id: u32, weight: f32) {
752 debug_assert!(
753 self.entries.is_empty() || self.entries.last().unwrap().dim_id < dim_id,
754 "Entries must be added in sorted order by dim_id"
755 );
756 self.entries.push(SparseEntry { dim_id, weight });
757 }
758
759 pub fn len(&self) -> usize {
761 self.entries.len()
762 }
763
764 pub fn is_empty(&self) -> bool {
766 self.entries.is_empty()
767 }
768
769 pub fn iter(&self) -> impl Iterator<Item = &SparseEntry> {
771 self.entries.iter()
772 }
773
774 pub fn sort_by_dim(&mut self) {
776 self.entries.sort_by_key(|e| e.dim_id);
777 }
778
779 pub fn sort_by_weight_desc(&mut self) {
781 self.entries.sort_by(|a, b| {
782 b.weight
783 .partial_cmp(&a.weight)
784 .unwrap_or(std::cmp::Ordering::Equal)
785 });
786 }
787
788 pub fn top_k(&self, k: usize) -> Vec<SparseEntry> {
790 let mut sorted = self.entries.clone();
791 sorted.sort_by(|a, b| {
792 b.weight
793 .partial_cmp(&a.weight)
794 .unwrap_or(std::cmp::Ordering::Equal)
795 });
796 sorted.truncate(k);
797 sorted
798 }
799
800 pub fn dot(&self, other: &SparseVector) -> f32 {
802 let mut result = 0.0f32;
803 let mut i = 0;
804 let mut j = 0;
805
806 while i < self.entries.len() && j < other.entries.len() {
807 let a = &self.entries[i];
808 let b = &other.entries[j];
809
810 match a.dim_id.cmp(&b.dim_id) {
811 std::cmp::Ordering::Less => i += 1,
812 std::cmp::Ordering::Greater => j += 1,
813 std::cmp::Ordering::Equal => {
814 result += a.weight * b.weight;
815 i += 1;
816 j += 1;
817 }
818 }
819 }
820
821 result
822 }
823
824 pub fn norm_squared(&self) -> f32 {
826 self.entries.iter().map(|e| e.weight * e.weight).sum()
827 }
828
829 pub fn norm(&self) -> f32 {
831 self.norm_squared().sqrt()
832 }
833
834 pub fn filter_by_weight(&self, min_weight: f32) -> Self {
836 let entries: Vec<SparseEntry> = self
837 .entries
838 .iter()
839 .filter(|e| e.weight.abs() >= min_weight)
840 .cloned()
841 .collect();
842 Self { entries }
843 }
844}
845
846impl From<Vec<(u32, f32)>> for SparseVector {
847 fn from(pairs: Vec<(u32, f32)>) -> Self {
848 Self {
849 entries: pairs
850 .into_iter()
851 .map(|(dim_id, weight)| SparseEntry { dim_id, weight })
852 .collect(),
853 }
854 }
855}
856
857impl From<SparseVector> for Vec<(u32, f32)> {
858 fn from(vec: SparseVector) -> Self {
859 vec.entries
860 .into_iter()
861 .map(|e| (e.dim_id, e.weight))
862 .collect()
863 }
864}
865
866#[cfg(test)]
867mod seismic_config_tests {
868 use super::*;
869
870 #[test]
871 fn sparse_default_uses_bmp_with_bounded_seismic_settings() {
872 let config = SparseVectorConfig::default();
873 assert_eq!(config.format, SparseFormat::Bmp);
874 config.seismic.validate().unwrap();
875 let query = SparseQueryConfig::default();
876 assert!(!query.exhaustive);
877 assert_eq!(query.seismic_cut, 10);
878 let restored: SparseVectorConfig =
879 serde_json::from_value(serde_json::to_value(config).unwrap()).unwrap();
880 assert_eq!(restored.format, SparseFormat::Bmp);
881 }
882
883 #[test]
884 fn seismic_forward_compression_defaults_on_and_preserves_explicit_opt_out() {
885 assert!(SeismicConfig::default().forward_compression);
886 let omitted: SeismicConfig = serde_json::from_str("{}").unwrap();
887 assert!(omitted.forward_compression);
888 let mut field = serde_json::to_value(SparseVectorConfig::default()).unwrap();
889 field.as_object_mut().unwrap().remove("seismic");
890 let omitted_field: SparseVectorConfig = serde_json::from_value(field).unwrap();
891 assert!(omitted_field.seismic.forward_compression);
892 let disabled: SeismicConfig =
893 serde_json::from_str(r#"{"forward_compression":false}"#).unwrap();
894 assert!(!disabled.forward_compression);
895 assert_eq!(
896 serde_json::from_value::<SeismicConfig>(serde_json::to_value(&disabled).unwrap())
897 .unwrap(),
898 disabled
899 );
900 }
901
902 #[test]
903 fn every_sparse_format_roundtrips_independently_of_the_default() {
904 for format in [
905 SparseFormat::Bmp,
906 SparseFormat::MaxScore,
907 SparseFormat::Seismic,
908 ] {
909 for weight_quantization in [
910 WeightQuantization::Float32,
911 WeightQuantization::Float16,
912 WeightQuantization::UInt8,
913 WeightQuantization::UInt4,
914 ] {
915 let config = SparseVectorConfig {
916 format,
917 weight_quantization,
918 ..Default::default()
919 };
920 let decoded = SparseVectorConfig::from_byte(config.to_byte()).unwrap();
921 assert_eq!(decoded.format, format);
922 assert_eq!(decoded.weight_quantization, weight_quantization);
923 let json = serde_json::to_vec(&config).unwrap();
924 assert_eq!(
925 serde_json::from_slice::<SparseVectorConfig>(&json).unwrap(),
926 config
927 );
928 }
929 }
930 assert!(SparseVectorConfig::from_byte(0x90).is_none());
931 }
932
933 #[test]
934 fn seismic_build_settings_reject_unbounded_or_nonfinite_work() {
935 for config in [
936 SeismicConfig {
937 postings: 0,
938 ..SeismicConfig::default()
939 },
940 SeismicConfig {
941 postings: 65_537,
942 ..SeismicConfig::default()
943 },
944 SeismicConfig {
945 cluster_size: 0,
946 ..SeismicConfig::default()
947 },
948 SeismicConfig {
949 cluster_size: 4097,
950 ..SeismicConfig::default()
951 },
952 SeismicConfig {
953 summary_energy: f32::NAN,
954 ..SeismicConfig::default()
955 },
956 SeismicConfig {
957 summary_energy: 0.0,
958 ..SeismicConfig::default()
959 },
960 ] {
961 assert!(config.validate().is_err());
962 }
963 }
964}