Skip to main content

hawdb_core/vector_index/
codec.rs

1// Copyright 2026 Nowledge
2// Licensed under the Apache License, Version 2.0.
3
4use super::*;
5
6const HEADER: &[u8; 8] = b"HDVIX001";
7
8impl VectorIndexDescriptor {
9    /// Encode after admitting `MAX_VECTOR_INDEX_DESCRIPTOR_BYTES` of scratch.
10    /// The enclosing immutable artifact supplies integrity and source identity.
11    pub fn encode(&self) -> Vec<u8> {
12        let mut output = Vec::with_capacity(MAX_VECTOR_INDEX_DESCRIPTOR_BYTES);
13        output.extend_from_slice(HEADER);
14        write_name(&mut output, &self.name);
15        match &self.target {
16            VectorIndexTarget::Relational {
17                table,
18                column,
19                column_ordinal,
20            } => {
21                output.push(0);
22                write_name(&mut output, table);
23                write_name(&mut output, column);
24                output.extend_from_slice(&column_ordinal.to_le_bytes());
25            }
26            VectorIndexTarget::NodeProperty {
27                table,
28                property,
29                table_id,
30                property_id,
31            } => {
32                output.push(1);
33                write_name(&mut output, table);
34                write_name(&mut output, property);
35                output.extend_from_slice(&table_id.0.to_le_bytes());
36                output.extend_from_slice(&property_id.0.to_le_bytes());
37            }
38        }
39        output.extend_from_slice(&self.dimension.get().to_le_bytes());
40        output.push(match self.distance {
41            VectorDistance::Cosine => 0,
42            VectorDistance::Euclidean => 1,
43        });
44        match self.backend {
45            VectorIndexBackend::RaBitQ {
46                bit_width,
47                segment_rows,
48                transform_seed,
49            } => {
50                output.push(0);
51                output.push(bit_width);
52                output.extend_from_slice(&segment_rows.to_le_bytes());
53                output.extend_from_slice(&transform_seed.to_le_bytes());
54            }
55        }
56        output.extend_from_slice(&self.schema_digest);
57        output
58    }
59
60    /// Decode after admitting the descriptor bound. Unknown formats and trailing
61    /// bytes are refused; partially understood definitions cannot serve queries.
62    pub fn decode(bytes: &[u8]) -> Result<Self> {
63        if bytes.len() > MAX_VECTOR_INDEX_DESCRIPTOR_BYTES {
64            return Err(invalid("descriptor exceeds its encoded size bound"));
65        }
66        let mut input = Input(bytes);
67        if &input.read::<8>()? != HEADER {
68            return Err(invalid("unsupported descriptor format"));
69        }
70        let name = input.name(128)?;
71        let kind = input.read::<1>()?[0];
72        let table = input.name(1024)?;
73        let field = input.name(1024)?;
74        let target = match kind {
75            0 => VectorIndexTarget::Relational {
76                table,
77                column: field,
78                column_ordinal: u32::from_le_bytes(input.read()?),
79            },
80            1 => VectorIndexTarget::NodeProperty {
81                table,
82                property: field,
83                table_id: TableId(u32::from_le_bytes(input.read()?)),
84                property_id: PropertyId(u32::from_le_bytes(input.read()?)),
85            },
86            _ => return Err(invalid("unsupported target kind")),
87        };
88        let dimension = VectorDimension::new(u32::from_le_bytes(input.read()?))
89            .ok_or_else(|| invalid("dimension must be positive"))?;
90        let distance = match input.read::<1>()?[0] {
91            0 => VectorDistance::Cosine,
92            _ => return Err(invalid("unsupported index metric")),
93        };
94        let backend = match input.read::<1>()?[0] {
95            0 => VectorIndexBackend::RaBitQ {
96                bit_width: input.read::<1>()?[0],
97                segment_rows: u32::from_le_bytes(input.read()?),
98                transform_seed: u64::from_le_bytes(input.read()?),
99            },
100            _ => return Err(invalid("unsupported index backend")),
101        };
102        let digest = input.read()?;
103        if !input.0.is_empty() {
104            return Err(invalid("trailing descriptor bytes"));
105        }
106        Self::new(name, target, dimension, distance, backend, digest)
107    }
108}
109
110fn write_name(output: &mut Vec<u8>, name: &str) {
111    let length = u16::try_from(name.len()).expect("validated descriptor name length");
112    output.extend_from_slice(&length.to_le_bytes());
113    output.extend_from_slice(name.as_bytes());
114}
115
116struct Input<'a>(&'a [u8]);
117
118impl Input<'_> {
119    fn read<const N: usize>(&mut self) -> Result<[u8; N]> {
120        let (value, rest) = self
121            .0
122            .split_at_checked(N)
123            .ok_or_else(|| invalid("truncated descriptor"))?;
124        self.0 = rest;
125        Ok(value.try_into().expect("fixed-size descriptor field"))
126    }
127
128    fn name(&mut self, maximum: usize) -> Result<String> {
129        let length = usize::from(u16::from_le_bytes(self.read()?));
130        if length == 0 || length > maximum {
131            return Err(invalid("descriptor name exceeds its size bound"));
132        }
133        let (value, rest) = self
134            .0
135            .split_at_checked(length)
136            .ok_or_else(|| invalid("truncated descriptor name"))?;
137        let value =
138            std::str::from_utf8(value).map_err(|_| invalid("descriptor name is not UTF-8"))?;
139        self.0 = rest;
140        Ok(value.to_owned())
141    }
142}
143
144#[cfg(test)]
145mod tests {
146    use super::*;
147
148    fn descriptor(target: VectorIndexTarget) -> VectorIndexDescriptor {
149        VectorIndexDescriptor::new(
150            "memory.embedding".into(),
151            target,
152            VectorDimension::new(3).unwrap(),
153            VectorDistance::Cosine,
154            VectorIndexBackend::RaBitQ {
155                bit_width: 4,
156                segment_rows: 1024,
157                transform_seed: 42,
158            },
159            [0x5a; 32],
160        )
161        .unwrap()
162    }
163
164    #[test]
165    fn vector_index_binding_round_trip_and_truncation() {
166        for target in [
167            VectorIndexTarget::Relational {
168                table: "memories".into(),
169                column: "embedding".into(),
170                column_ordinal: 2,
171            },
172            VectorIndexTarget::NodeProperty {
173                table: "Memory".into(),
174                property: "embedding".into(),
175                table_id: TableId(7),
176                property_id: PropertyId(19),
177            },
178        ] {
179            let binding = descriptor(target);
180            let encoded = binding.encode();
181            assert!(encoded.len() <= MAX_VECTOR_INDEX_DESCRIPTOR_BYTES);
182            assert_eq!(VectorIndexDescriptor::decode(&encoded).unwrap(), binding);
183            for length in 0..encoded.len() {
184                assert!(VectorIndexDescriptor::decode(&encoded[..length]).is_err());
185            }
186            let mut trailing = encoded.clone();
187            trailing.push(0);
188            assert!(VectorIndexDescriptor::decode(&trailing).is_err());
189            let mut unknown = encoded;
190            unknown[7] = b'2';
191            assert!(VectorIndexDescriptor::decode(&unknown).is_err());
192        }
193    }
194
195    #[test]
196    fn vector_index_binding_rejects_unsupported_metric_and_build_parameters() {
197        let target = VectorIndexTarget::Relational {
198            table: "memories".into(),
199            column: "embedding".into(),
200            column_ordinal: 1,
201        };
202        let valid = descriptor(target.clone());
203        for (metric, backend) in [
204            (VectorDistance::Euclidean, valid.backend()),
205            (
206                VectorDistance::Cosine,
207                VectorIndexBackend::RaBitQ {
208                    bit_width: 2,
209                    segment_rows: 1024,
210                    transform_seed: 0,
211                },
212            ),
213            (
214                VectorDistance::Cosine,
215                VectorIndexBackend::RaBitQ {
216                    bit_width: 4,
217                    segment_rows: 0,
218                    transform_seed: 0,
219                },
220            ),
221        ] {
222            assert!(VectorIndexDescriptor::new(
223                "index".into(),
224                target.clone(),
225                valid.dimension(),
226                metric,
227                backend,
228                [0; 32]
229            )
230            .is_err());
231        }
232        let mut oversized_name = valid.encode();
233        oversized_name[8..10].copy_from_slice(&u16::MAX.to_le_bytes());
234        assert!(VectorIndexDescriptor::decode(&oversized_name).is_err());
235    }
236}