1use std::{error::Error, fmt};
7
8pub const PRODUCT_NAME: &str = "hyphae";
10
11pub const API_VERSION: &str = "v1";
13
14pub const DISK_FORMAT_VERSION: u16 = 2;
16
17pub const MIN_DISK_FORMAT_VERSION: u16 = 1;
19
20pub const DURABLE_RETRIEVAL_DISK_FORMAT_VERSION: u16 = 2;
22
23pub const MAX_VECTOR_SPACE_NAME_BYTES: usize = 128;
25
26pub const MAX_VECTOR_DIMENSIONS: usize = 4_096;
28
29pub const SCORE_NANOS_SCALE: i64 = 1_000_000_000;
31
32#[derive(Clone, Copy, Debug, Eq, PartialEq)]
34pub enum VectorValueError {
35 EmptySpaceName,
37 SpaceNameTooLong,
39 InvalidSpaceName,
41 EmptyVector,
43 DimensionTooLarge,
45 InvalidQ15Element,
47 ZeroVector,
49 DimensionMismatch,
51}
52
53impl fmt::Display for VectorValueError {
54 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
55 let message = match self {
56 Self::EmptySpaceName => "vector-space name must be nonempty",
57 Self::SpaceNameTooLong => "vector-space name exceeds 128 bytes",
58 Self::InvalidSpaceName => {
59 "vector-space name does not match the canonical ASCII grammar"
60 }
61 Self::EmptyVector => "vector must be nonempty",
62 Self::DimensionTooLarge => "vector dimension exceeds 4096",
63 Self::InvalidQ15Element => "Q15 vector elements cannot equal -32768",
64 Self::ZeroVector => "vector must have nonzero magnitude",
65 Self::DimensionMismatch => "vector dimension does not match the named space",
66 };
67 formatter.write_str(message)
68 }
69}
70
71impl Error for VectorValueError {}
72
73#[derive(Clone, Debug, Eq, Ord, PartialEq, PartialOrd)]
75pub struct VectorSpaceName(String);
76
77impl VectorSpaceName {
78 pub fn new(value: impl Into<String>) -> Result<Self, VectorValueError> {
85 let value = value.into();
86 if value.is_empty() {
87 return Err(VectorValueError::EmptySpaceName);
88 }
89 if value.len() > MAX_VECTOR_SPACE_NAME_BYTES {
90 return Err(VectorValueError::SpaceNameTooLong);
91 }
92 let mut bytes = value.bytes();
93 let first = bytes.next().ok_or(VectorValueError::EmptySpaceName)?;
94 if !first.is_ascii_alphabetic()
95 || !bytes.all(|byte| byte.is_ascii_alphanumeric() || b"._-".contains(&byte))
96 {
97 return Err(VectorValueError::InvalidSpaceName);
98 }
99 Ok(Self(value))
100 }
101
102 pub fn as_str(&self) -> &str {
104 &self.0
105 }
106
107 pub fn into_string(self) -> String {
109 self.0
110 }
111}
112
113impl fmt::Display for VectorSpaceName {
114 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
115 formatter.write_str(self.as_str())
116 }
117}
118
119#[derive(Clone, Debug, Eq, PartialEq)]
121pub struct Q15Vector(Vec<i16>);
122
123impl Q15Vector {
124 pub fn new(values: impl Into<Vec<i16>>) -> Result<Self, VectorValueError> {
131 let values = values.into();
132 if values.is_empty() {
133 return Err(VectorValueError::EmptyVector);
134 }
135 if values.len() > MAX_VECTOR_DIMENSIONS {
136 return Err(VectorValueError::DimensionTooLarge);
137 }
138 if values.contains(&i16::MIN) {
139 return Err(VectorValueError::InvalidQ15Element);
140 }
141 if values.iter().all(|value| *value == 0) {
142 return Err(VectorValueError::ZeroVector);
143 }
144 Ok(Self(values))
145 }
146
147 pub fn as_slice(&self) -> &[i16] {
149 &self.0
150 }
151
152 pub fn dimension(&self) -> u16 {
154 u16::try_from(self.0.len()).unwrap_or(u16::MAX)
155 }
156
157 pub fn into_vec(self) -> Vec<i16> {
159 self.0
160 }
161}
162
163#[derive(Clone, Copy, Debug, Eq, PartialEq)]
165#[repr(u8)]
166pub enum VectorMetric {
167 Cosine = 1,
169}
170
171#[derive(Clone, Debug, Eq, PartialEq)]
173pub struct VectorSpaceDefinition {
174 pub name: VectorSpaceName,
176 pub dimension: u16,
178 pub metric: VectorMetric,
180}
181
182impl VectorSpaceDefinition {
183 pub fn cosine(name: VectorSpaceName, dimension: u16) -> Result<Self, VectorValueError> {
189 if dimension == 0 {
190 return Err(VectorValueError::EmptyVector);
191 }
192 if usize::from(dimension) > MAX_VECTOR_DIMENSIONS {
193 return Err(VectorValueError::DimensionTooLarge);
194 }
195 Ok(Self {
196 name,
197 dimension,
198 metric: VectorMetric::Cosine,
199 })
200 }
201
202 pub fn validate_vector(&self, vector: &Q15Vector) -> Result<(), VectorValueError> {
208 if vector.dimension() == self.dimension {
209 Ok(())
210 } else {
211 Err(VectorValueError::DimensionMismatch)
212 }
213 }
214}
215
216#[derive(Clone, Copy, Debug, Eq, PartialEq)]
218pub struct VersionInfo {
219 pub product: &'static str,
221 pub engine: &'static str,
223 pub api: &'static str,
225 pub disk_format: u16,
227}
228
229pub const fn current_version() -> VersionInfo {
231 VersionInfo {
232 product: PRODUCT_NAME,
233 engine: env!("CARGO_PKG_VERSION"),
234 api: API_VERSION,
235 disk_format: DISK_FORMAT_VERSION,
236 }
237}
238
239#[cfg(test)]
240mod tests {
241 use super::{
242 API_VERSION, DISK_FORMAT_VERSION, PRODUCT_NAME, Q15Vector, VectorSpaceDefinition,
243 VectorSpaceName, VectorValueError, current_version,
244 };
245
246 #[test]
247 fn current_version_matches_public_constants() {
248 let version = current_version();
249 assert_eq!(version.product, PRODUCT_NAME);
250 assert_eq!(version.api, API_VERSION);
251 assert_eq!(version.disk_format, DISK_FORMAT_VERSION);
252 assert!(!version.engine.is_empty());
253 }
254
255 #[test]
256 fn vector_space_names_follow_the_canonical_ascii_grammar() -> Result<(), VectorValueError> {
257 let name = VectorSpaceName::new("semantic.v1")?;
258 assert_eq!(name.as_str(), "semantic.v1");
259 assert_eq!(
260 VectorSpaceName::new("1semantic"),
261 Err(VectorValueError::InvalidSpaceName)
262 );
263 assert_eq!(
264 VectorSpaceName::new("semántica"),
265 Err(VectorValueError::InvalidSpaceName)
266 );
267 Ok(())
268 }
269
270 #[test]
271 fn q15_vectors_are_nonzero_bounded_and_dimension_checked() -> Result<(), VectorValueError> {
272 let vector = Q15Vector::new(vec![32_767, 0])?;
273 let space = VectorSpaceDefinition::cosine(VectorSpaceName::new("semantic")?, 2)?;
274 assert_eq!(space.validate_vector(&vector), Ok(()));
275 assert_eq!(
276 Q15Vector::new(vec![i16::MIN]),
277 Err(VectorValueError::InvalidQ15Element)
278 );
279 assert_eq!(
280 Q15Vector::new(vec![0, 0]),
281 Err(VectorValueError::ZeroVector)
282 );
283 Ok(())
284 }
285}