Skip to main content

luci/mapping/
quantization.rs

1use crate::core::{LuciError, Result};
2
3/// Quantization scheme for `dense_vector` fields.
4///
5/// Configurable via the `dense_vector` field mapping:
6/// ```json
7/// {"embedding": {"type": "dense_vector", "dims": 768, "quantization": "int8"}}
8/// ```
9///
10/// Recognized but unimplemented variants ([`Self::Int4`], [`Self::Bbq`])
11/// are rejected at mapping parse time with [`LuciError::InvalidQuery`].
12/// The mapping API will not accept a value the engine cannot honor —
13/// see [[code-must-not-lie]].
14#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
15pub enum QuantizationType {
16    /// No quantization — full float32 storage and scoring.
17    None,
18    /// Int8 scalar quantization (8 bits/dim). 4× memory reduction with
19    /// minimal recall loss in typical embedding workloads.
20    Int8,
21    /// Int4 scalar quantization (4 bits/dim).
22    ///
23    /// Recognized name; not yet implemented. Constructing a mapping
24    /// with this value returns [`LuciError::InvalidQuery`].
25    Int4,
26    /// Better Binary Quantization (1 bit/dim with a correction term and
27    /// oversampling-rerank).
28    ///
29    /// Recognized name; not yet implemented. Constructing a mapping
30    /// with this value returns [`LuciError::InvalidQuery`].
31    Bbq,
32}
33
34impl QuantizationType {
35    /// The default quantization for `dense_vector` fields when the user
36    /// does not specify one.
37    pub const DEFAULT: Self = Self::Int8;
38
39    /// Parse a quantization name from the mapping JSON `quantization` field.
40    ///
41    /// Returns [`LuciError::InvalidQuery`] for both unknown names and
42    /// recognized-but-unimplemented values (`int4`, `bbq`). The error
43    /// message names the rejected value and the reason — the system
44    /// will never silently substitute a different quantization for the
45    /// one the user asked for.
46    pub fn from_es_name(name: &str) -> Result<Self> {
47        match name {
48            "none" => Ok(Self::None),
49            "int8" => Ok(Self::Int8),
50            "int4" => Err(LuciError::InvalidQuery(
51                "quantization \"int4\" is recognized but not yet implemented; \
52                 supported values: \"none\", \"int8\""
53                    .into(),
54            )),
55            "bbq" => Err(LuciError::InvalidQuery(
56                "quantization \"bbq\" is recognized but not yet implemented; \
57                 supported values: \"none\", \"int8\""
58                    .into(),
59            )),
60            other => Err(LuciError::InvalidQuery(format!(
61                "unknown quantization type: \"{other}\" \
62                 (supported: \"none\", \"int8\")"
63            ))),
64        }
65    }
66
67    /// The ES-compatible name used in JSON mappings.
68    pub fn es_name(self) -> &'static str {
69        match self {
70            Self::None => "none",
71            Self::Int8 => "int8",
72            Self::Int4 => "int4",
73            Self::Bbq => "bbq",
74        }
75    }
76}
77
78#[cfg(test)]
79mod tests {
80    use super::*;
81
82    #[test]
83    fn supported_values_round_trip() {
84        for q in [QuantizationType::None, QuantizationType::Int8] {
85            let parsed = QuantizationType::from_es_name(q.es_name()).unwrap();
86            assert_eq!(parsed, q);
87        }
88    }
89
90    #[test]
91    fn int4_is_rejected_as_unimplemented() {
92        let err = QuantizationType::from_es_name("int4").unwrap_err();
93        let msg = format!("{err}");
94        assert!(msg.contains("int4"), "error must name the value: {msg}");
95        assert!(
96            msg.contains("not yet implemented"),
97            "error must explain why: {msg}"
98        );
99    }
100
101    #[test]
102    fn bbq_is_rejected_as_unimplemented() {
103        let err = QuantizationType::from_es_name("bbq").unwrap_err();
104        let msg = format!("{err}");
105        assert!(msg.contains("bbq"), "error must name the value: {msg}");
106        assert!(
107            msg.contains("not yet implemented"),
108            "error must explain why: {msg}"
109        );
110    }
111
112    #[test]
113    fn unknown_value_is_rejected() {
114        let err = QuantizationType::from_es_name("magic").unwrap_err();
115        let msg = format!("{err}");
116        assert!(msg.contains("magic"), "error must name the value: {msg}");
117        assert!(
118            msg.contains("supported"),
119            "error must list supported values: {msg}"
120        );
121    }
122
123    #[test]
124    fn empty_string_is_rejected() {
125        assert!(QuantizationType::from_es_name("").is_err());
126    }
127
128    #[test]
129    fn default_is_int8() {
130        assert_eq!(QuantizationType::DEFAULT, QuantizationType::Int8);
131    }
132}