use std::{error::Error, fmt};
pub const PRODUCT_NAME: &str = "hyphae";
pub const API_VERSION: &str = "v1";
pub const DISK_FORMAT_VERSION: u16 = 2;
pub const MIN_DISK_FORMAT_VERSION: u16 = 1;
pub const DURABLE_RETRIEVAL_DISK_FORMAT_VERSION: u16 = 2;
pub const MAX_VECTOR_SPACE_NAME_BYTES: usize = 128;
pub const MAX_VECTOR_DIMENSIONS: usize = 4_096;
pub const SCORE_NANOS_SCALE: i64 = 1_000_000_000;
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum VectorValueError {
EmptySpaceName,
SpaceNameTooLong,
InvalidSpaceName,
EmptyVector,
DimensionTooLarge,
InvalidQ15Element,
ZeroVector,
DimensionMismatch,
}
impl fmt::Display for VectorValueError {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
let message = match self {
Self::EmptySpaceName => "vector-space name must be nonempty",
Self::SpaceNameTooLong => "vector-space name exceeds 128 bytes",
Self::InvalidSpaceName => {
"vector-space name does not match the canonical ASCII grammar"
}
Self::EmptyVector => "vector must be nonempty",
Self::DimensionTooLarge => "vector dimension exceeds 4096",
Self::InvalidQ15Element => "Q15 vector elements cannot equal -32768",
Self::ZeroVector => "vector must have nonzero magnitude",
Self::DimensionMismatch => "vector dimension does not match the named space",
};
formatter.write_str(message)
}
}
impl Error for VectorValueError {}
#[derive(Clone, Debug, Eq, Ord, PartialEq, PartialOrd)]
pub struct VectorSpaceName(String);
impl VectorSpaceName {
pub fn new(value: impl Into<String>) -> Result<Self, VectorValueError> {
let value = value.into();
if value.is_empty() {
return Err(VectorValueError::EmptySpaceName);
}
if value.len() > MAX_VECTOR_SPACE_NAME_BYTES {
return Err(VectorValueError::SpaceNameTooLong);
}
let mut bytes = value.bytes();
let first = bytes.next().ok_or(VectorValueError::EmptySpaceName)?;
if !first.is_ascii_alphabetic()
|| !bytes.all(|byte| byte.is_ascii_alphanumeric() || b"._-".contains(&byte))
{
return Err(VectorValueError::InvalidSpaceName);
}
Ok(Self(value))
}
pub fn as_str(&self) -> &str {
&self.0
}
pub fn into_string(self) -> String {
self.0
}
}
impl fmt::Display for VectorSpaceName {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter.write_str(self.as_str())
}
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct Q15Vector(Vec<i16>);
impl Q15Vector {
pub fn new(values: impl Into<Vec<i16>>) -> Result<Self, VectorValueError> {
let values = values.into();
if values.is_empty() {
return Err(VectorValueError::EmptyVector);
}
if values.len() > MAX_VECTOR_DIMENSIONS {
return Err(VectorValueError::DimensionTooLarge);
}
if values.contains(&i16::MIN) {
return Err(VectorValueError::InvalidQ15Element);
}
if values.iter().all(|value| *value == 0) {
return Err(VectorValueError::ZeroVector);
}
Ok(Self(values))
}
pub fn as_slice(&self) -> &[i16] {
&self.0
}
pub fn dimension(&self) -> u16 {
u16::try_from(self.0.len()).unwrap_or(u16::MAX)
}
pub fn into_vec(self) -> Vec<i16> {
self.0
}
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
#[repr(u8)]
pub enum VectorMetric {
Cosine = 1,
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct VectorSpaceDefinition {
pub name: VectorSpaceName,
pub dimension: u16,
pub metric: VectorMetric,
}
impl VectorSpaceDefinition {
pub fn cosine(name: VectorSpaceName, dimension: u16) -> Result<Self, VectorValueError> {
if dimension == 0 {
return Err(VectorValueError::EmptyVector);
}
if usize::from(dimension) > MAX_VECTOR_DIMENSIONS {
return Err(VectorValueError::DimensionTooLarge);
}
Ok(Self {
name,
dimension,
metric: VectorMetric::Cosine,
})
}
pub fn validate_vector(&self, vector: &Q15Vector) -> Result<(), VectorValueError> {
if vector.dimension() == self.dimension {
Ok(())
} else {
Err(VectorValueError::DimensionMismatch)
}
}
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub struct VersionInfo {
pub product: &'static str,
pub engine: &'static str,
pub api: &'static str,
pub disk_format: u16,
}
pub const fn current_version() -> VersionInfo {
VersionInfo {
product: PRODUCT_NAME,
engine: env!("CARGO_PKG_VERSION"),
api: API_VERSION,
disk_format: DISK_FORMAT_VERSION,
}
}
#[cfg(test)]
mod tests {
use super::{
API_VERSION, DISK_FORMAT_VERSION, PRODUCT_NAME, Q15Vector, VectorSpaceDefinition,
VectorSpaceName, VectorValueError, current_version,
};
#[test]
fn current_version_matches_public_constants() {
let version = current_version();
assert_eq!(version.product, PRODUCT_NAME);
assert_eq!(version.api, API_VERSION);
assert_eq!(version.disk_format, DISK_FORMAT_VERSION);
assert!(!version.engine.is_empty());
}
#[test]
fn vector_space_names_follow_the_canonical_ascii_grammar() -> Result<(), VectorValueError> {
let name = VectorSpaceName::new("semantic.v1")?;
assert_eq!(name.as_str(), "semantic.v1");
assert_eq!(
VectorSpaceName::new("1semantic"),
Err(VectorValueError::InvalidSpaceName)
);
assert_eq!(
VectorSpaceName::new("semántica"),
Err(VectorValueError::InvalidSpaceName)
);
Ok(())
}
#[test]
fn q15_vectors_are_nonzero_bounded_and_dimension_checked() -> Result<(), VectorValueError> {
let vector = Q15Vector::new(vec![32_767, 0])?;
let space = VectorSpaceDefinition::cosine(VectorSpaceName::new("semantic")?, 2)?;
assert_eq!(space.validate_vector(&vector), Ok(()));
assert_eq!(
Q15Vector::new(vec![i16::MIN]),
Err(VectorValueError::InvalidQ15Element)
);
assert_eq!(
Q15Vector::new(vec![0, 0]),
Err(VectorValueError::ZeroVector)
);
Ok(())
}
}