use crate::executor::expression::functions::signature::{FunctionSignature, ValueType};
use crate::executor::expression::{ExpressionError, ExpressionErrorType};
use linkrs_core::Value;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum VectorFunction {
CosineSimilarity,
DotProduct,
EuclideanDistance,
ManhattanDistance,
Dimension,
L2Norm,
Nnz,
Normalize,
ArrayValue,
ArrayCosineSimilarity,
ArrayDistance,
ArraySquaredDistance,
ArrayInnerProduct,
ArrayDotProduct,
}
impl VectorFunction {
pub fn name(&self) -> &'static str {
match self {
VectorFunction::CosineSimilarity => "cosine_similarity",
VectorFunction::DotProduct => "dot_product",
VectorFunction::EuclideanDistance => "euclidean_distance",
VectorFunction::ManhattanDistance => "manhattan_distance",
VectorFunction::Dimension => "dimension",
VectorFunction::L2Norm => "l2_norm",
VectorFunction::Nnz => "nnz",
VectorFunction::Normalize => "normalize",
VectorFunction::ArrayValue => "array_value",
VectorFunction::ArrayCosineSimilarity => "array_cosine_similarity",
VectorFunction::ArrayDistance => "array_distance",
VectorFunction::ArraySquaredDistance => "array_squared_distance",
VectorFunction::ArrayInnerProduct => "array_inner_product",
VectorFunction::ArrayDotProduct => "array_dot_product",
}
}
pub fn signature(&self) -> FunctionSignature {
match self {
VectorFunction::CosineSimilarity => FunctionSignature::new(
"cosine_similarity",
vec![ValueType::Any, ValueType::Any],
Some(ValueType::Float),
false,
),
VectorFunction::DotProduct => FunctionSignature::new(
"dot_product",
vec![ValueType::Any, ValueType::Any],
Some(ValueType::Float),
false,
),
VectorFunction::EuclideanDistance => FunctionSignature::new(
"euclidean_distance",
vec![ValueType::Any, ValueType::Any],
Some(ValueType::Float),
false,
),
VectorFunction::ManhattanDistance => FunctionSignature::new(
"manhattan_distance",
vec![ValueType::Any, ValueType::Any],
Some(ValueType::Float),
false,
),
VectorFunction::Dimension => FunctionSignature::new(
"dimension",
vec![ValueType::Any],
Some(ValueType::Int),
false,
),
VectorFunction::L2Norm => FunctionSignature::new(
"l2_norm",
vec![ValueType::Any],
Some(ValueType::Float),
false,
),
VectorFunction::Nnz => {
FunctionSignature::new("nnz", vec![ValueType::Any], Some(ValueType::Int), false)
}
VectorFunction::Normalize => FunctionSignature::new(
"normalize",
vec![ValueType::Any],
Some(ValueType::Any),
false,
),
VectorFunction::ArrayValue => {
FunctionSignature::new("array_value", vec![], Some(ValueType::Any), true)
}
VectorFunction::ArrayCosineSimilarity => FunctionSignature::new(
"array_cosine_similarity",
vec![ValueType::Any, ValueType::Any],
Some(ValueType::Float),
false,
),
VectorFunction::ArrayDistance => FunctionSignature::new(
"array_distance",
vec![ValueType::Any, ValueType::Any],
Some(ValueType::Float),
false,
),
VectorFunction::ArraySquaredDistance => FunctionSignature::new(
"array_squared_distance",
vec![ValueType::Any, ValueType::Any],
Some(ValueType::Float),
false,
),
VectorFunction::ArrayInnerProduct => FunctionSignature::new(
"array_inner_product",
vec![ValueType::Any, ValueType::Any],
Some(ValueType::Float),
false,
),
VectorFunction::ArrayDotProduct => FunctionSignature::new(
"array_dot_product",
vec![ValueType::Any, ValueType::Any],
Some(ValueType::Float),
false,
),
}
}
pub fn arity(&self) -> usize {
match self {
VectorFunction::CosineSimilarity => 2,
VectorFunction::DotProduct => 2,
VectorFunction::EuclideanDistance => 2,
VectorFunction::ManhattanDistance => 2,
VectorFunction::Dimension => 1,
VectorFunction::L2Norm => 1,
VectorFunction::Nnz => 1,
VectorFunction::Normalize => 1,
VectorFunction::ArrayValue => 0,
VectorFunction::ArrayCosineSimilarity => 2,
VectorFunction::ArrayDistance => 2,
VectorFunction::ArraySquaredDistance => 2,
VectorFunction::ArrayInnerProduct => 2,
VectorFunction::ArrayDotProduct => 2,
}
}
pub fn is_variadic(&self) -> bool {
matches!(self, VectorFunction::ArrayValue)
}
pub fn description(&self) -> &'static str {
match self {
VectorFunction::CosineSimilarity => "Compute cosine similarity between two vectors",
VectorFunction::DotProduct => "Compute dot product of two vectors",
VectorFunction::EuclideanDistance => "Compute Euclidean distance between two vectors",
VectorFunction::ManhattanDistance => "Compute Manhattan distance between two vectors",
VectorFunction::Dimension => "Get the dimension of a vector",
VectorFunction::L2Norm => "Compute L2 norm of a vector",
VectorFunction::Nnz => "Get number of non-zero elements in a vector",
VectorFunction::Normalize => "Normalize a vector to unit length",
VectorFunction::ArrayValue => "Create an array from values",
VectorFunction::ArrayCosineSimilarity => "Compute cosine similarity between two arrays",
VectorFunction::ArrayDistance => "Compute Euclidean distance between two arrays",
VectorFunction::ArraySquaredDistance => {
"Compute squared Euclidean distance between two arrays"
}
VectorFunction::ArrayInnerProduct => "Compute inner product of two arrays",
VectorFunction::ArrayDotProduct => "Compute dot product of two arrays",
}
}
pub fn execute(&self, args: &[Value]) -> Result<Value, ExpressionError> {
if !self.is_variadic() && args.len() != self.arity() {
return Err(ExpressionError::invalid_arity(
self.name(),
self.arity(),
args.len(),
));
}
match self {
VectorFunction::CosineSimilarity => self.execute_cosine_similarity(args),
VectorFunction::DotProduct => self.execute_dot_product(args),
VectorFunction::EuclideanDistance => self.execute_euclidean_distance(args),
VectorFunction::ManhattanDistance => self.execute_manhattan_distance(args),
VectorFunction::Dimension => self.execute_dimension(args),
VectorFunction::L2Norm => self.execute_l2_norm(args),
VectorFunction::Nnz => self.execute_nnz(args),
VectorFunction::Normalize => self.execute_normalize(args),
VectorFunction::ArrayValue => self.execute_array_value(args),
VectorFunction::ArrayCosineSimilarity => self.execute_cosine_similarity(args),
VectorFunction::ArrayDistance => self.execute_euclidean_distance(args),
VectorFunction::ArraySquaredDistance => self.execute_array_squared_distance(args),
VectorFunction::ArrayInnerProduct => self.execute_dot_product(args),
VectorFunction::ArrayDotProduct => self.execute_dot_product(args),
}
}
fn execute_cosine_similarity(&self, args: &[Value]) -> Result<Value, ExpressionError> {
let vec1 = extract_vector(&args[0])?;
let vec2 = extract_vector(&args[1])?;
let similarity = compute_cosine_similarity(&vec1, &vec2)?;
Ok(Value::Double(similarity as f64))
}
fn execute_dot_product(&self, args: &[Value]) -> Result<Value, ExpressionError> {
let vec1 = extract_vector(&args[0])?;
let vec2 = extract_vector(&args[1])?;
let dot = compute_dot_product(&vec1, &vec2)?;
Ok(Value::Double(dot as f64))
}
fn execute_euclidean_distance(&self, args: &[Value]) -> Result<Value, ExpressionError> {
let vec1 = extract_vector(&args[0])?;
let vec2 = extract_vector(&args[1])?;
let distance = compute_euclidean_distance(&vec1, &vec2)?;
Ok(Value::Double(distance as f64))
}
fn execute_manhattan_distance(&self, args: &[Value]) -> Result<Value, ExpressionError> {
let vec1 = extract_vector(&args[0])?;
let vec2 = extract_vector(&args[1])?;
let distance = compute_manhattan_distance(&vec1, &vec2)?;
Ok(Value::Double(distance as f64))
}
fn execute_dimension(&self, args: &[Value]) -> Result<Value, ExpressionError> {
let vec = extract_vector(&args[0])?;
Ok(Value::BigInt(vec.len() as i64))
}
fn execute_l2_norm(&self, args: &[Value]) -> Result<Value, ExpressionError> {
let vec = extract_vector(&args[0])?;
let norm = compute_l2_norm(&vec);
Ok(Value::Double(norm as f64))
}
fn execute_nnz(&self, args: &[Value]) -> Result<Value, ExpressionError> {
let vec = extract_vector(&args[0])?;
let nnz = vec.iter().filter(|&&x| x != 0.0).count();
Ok(Value::BigInt(nnz as i64))
}
fn execute_normalize(&self, args: &[Value]) -> Result<Value, ExpressionError> {
let vec = extract_vector(&args[0])?;
let norm = compute_l2_norm(&vec);
if norm == 0.0 {
return Ok(Value::vector(vec)); }
let normalized: Vec<f32> = vec.iter().map(|&x| x / norm).collect();
Ok(Value::vector(normalized))
}
fn execute_array_value(&self, args: &[Value]) -> Result<Value, ExpressionError> {
let values: Vec<Value> = args.to_vec();
Ok(Value::list(linkrs_core::value::list::List { values }))
}
fn execute_array_squared_distance(&self, args: &[Value]) -> Result<Value, ExpressionError> {
let vec1 = extract_vector(&args[0])?;
let vec2 = extract_vector(&args[1])?;
if vec1.len() != vec2.len() {
return Err(ExpressionError::new(
ExpressionErrorType::TypeError,
format!(
"Array dimensions must match: {} vs {}",
vec1.len(),
vec2.len()
),
));
}
let sum: f32 = vec1
.iter()
.zip(vec2.iter())
.map(|(&a, &b)| (a - b).powi(2))
.sum();
Ok(Value::Double(sum as f64))
}
}
fn extract_vector(value: &Value) -> Result<Vec<f32>, ExpressionError> {
match value {
Value::Vector(vec) => Ok(vec.to_dense()),
Value::List(list) => {
let mut vec = Vec::with_capacity(list.values.len());
for item in &list.values {
match item {
Value::Float(f) => vec.push(*f),
Value::Double(f) => vec.push(*f as f32),
Value::SmallInt(i) => vec.push(*i as f32),
Value::Int(i) => vec.push(*i as f32),
Value::BigInt(i) => vec.push(*i as f32),
_ => {
return Err(ExpressionError::new(
ExpressionErrorType::TypeError,
format!("Vector elements must be numeric, got {:?}", item.get_type()),
))
}
}
}
Ok(vec)
}
_ => Err(ExpressionError::new(
ExpressionErrorType::TypeError,
format!(
"Expected vector or list of numbers, got {:?}",
value.get_type()
),
)),
}
}
fn compute_cosine_similarity(vec1: &[f32], vec2: &[f32]) -> Result<f32, ExpressionError> {
if vec1.len() != vec2.len() {
return Err(ExpressionError::new(
ExpressionErrorType::TypeError,
format!(
"Vector dimensions must match: {} vs {}",
vec1.len(),
vec2.len()
),
));
}
let dot_product: f32 = vec1.iter().zip(vec2.iter()).map(|(&a, &b)| a * b).sum();
let norm1: f32 = vec1.iter().map(|&x| x * x).sum::<f32>().sqrt();
let norm2: f32 = vec2.iter().map(|&x| x * x).sum::<f32>().sqrt();
if norm1 == 0.0 || norm2 == 0.0 {
return Ok(0.0);
}
Ok(dot_product / (norm1 * norm2))
}
fn compute_dot_product(vec1: &[f32], vec2: &[f32]) -> Result<f32, ExpressionError> {
if vec1.len() != vec2.len() {
return Err(ExpressionError::new(
ExpressionErrorType::TypeError,
format!(
"Vector dimensions must match: {} vs {}",
vec1.len(),
vec2.len()
),
));
}
Ok(vec1.iter().zip(vec2.iter()).map(|(&a, &b)| a * b).sum())
}
fn compute_euclidean_distance(vec1: &[f32], vec2: &[f32]) -> Result<f32, ExpressionError> {
if vec1.len() != vec2.len() {
return Err(ExpressionError::new(
ExpressionErrorType::TypeError,
format!(
"Vector dimensions must match: {} vs {}",
vec1.len(),
vec2.len()
),
));
}
let sum: f32 = vec1
.iter()
.zip(vec2.iter())
.map(|(&a, &b)| (a - b).powi(2))
.sum();
Ok(sum.sqrt())
}
fn compute_manhattan_distance(vec1: &[f32], vec2: &[f32]) -> Result<f32, ExpressionError> {
if vec1.len() != vec2.len() {
return Err(ExpressionError::new(
ExpressionErrorType::TypeError,
format!(
"Vector dimensions must match: {} vs {}",
vec1.len(),
vec2.len()
),
));
}
Ok(vec1
.iter()
.zip(vec2.iter())
.map(|(&a, &b)| (a - b).abs())
.sum())
}
fn compute_l2_norm(vec: &[f32]) -> f32 {
vec.iter().map(|&x| x * x).sum::<f32>().sqrt()
}
pub fn register_vector_functions(
registry: &mut crate::executor::expression::functions::FunctionRegistry,
) {
registry.register_builtin(
crate::executor::expression::functions::BuiltinFunction::Vector(
VectorFunction::CosineSimilarity,
),
);
registry.register_builtin(
crate::executor::expression::functions::BuiltinFunction::Vector(VectorFunction::DotProduct),
);
registry.register_builtin(
crate::executor::expression::functions::BuiltinFunction::Vector(
VectorFunction::EuclideanDistance,
),
);
registry.register_builtin(
crate::executor::expression::functions::BuiltinFunction::Vector(
VectorFunction::ManhattanDistance,
),
);
registry.register_builtin(
crate::executor::expression::functions::BuiltinFunction::Vector(VectorFunction::Dimension),
);
registry.register_builtin(
crate::executor::expression::functions::BuiltinFunction::Vector(VectorFunction::L2Norm),
);
registry.register_builtin(
crate::executor::expression::functions::BuiltinFunction::Vector(VectorFunction::Nnz),
);
registry.register_builtin(
crate::executor::expression::functions::BuiltinFunction::Vector(VectorFunction::Normalize),
);
registry.register_builtin(
crate::executor::expression::functions::BuiltinFunction::Vector(VectorFunction::ArrayValue),
);
registry.register_builtin(
crate::executor::expression::functions::BuiltinFunction::Vector(
VectorFunction::ArrayCosineSimilarity,
),
);
registry.register_builtin(
crate::executor::expression::functions::BuiltinFunction::Vector(
VectorFunction::ArrayDistance,
),
);
registry.register_builtin(
crate::executor::expression::functions::BuiltinFunction::Vector(
VectorFunction::ArraySquaredDistance,
),
);
registry.register_builtin(
crate::executor::expression::functions::BuiltinFunction::Vector(
VectorFunction::ArrayInnerProduct,
),
);
registry.register_builtin(
crate::executor::expression::functions::BuiltinFunction::Vector(
VectorFunction::ArrayDotProduct,
),
);
}
#[cfg(test)]
mod tests {
use super::*;
use linkrs_core::value::list::List;
#[test]
fn test_cosine_similarity() {
let vec1 = Value::vector(vec![1.0, 0.0, 0.0]);
let vec2 = Value::vector(vec![0.0, 1.0, 0.0]);
let result = VectorFunction::CosineSimilarity
.execute(&[vec1, vec2])
.unwrap();
assert!(matches!(result, Value::Double(_)));
if let Value::Double(score) = result {
assert!((score - 0.0).abs() < 1e-6);
}
}
#[test]
fn test_dot_product() {
let vec1 = Value::vector(vec![1.0, 2.0, 3.0]);
let vec2 = Value::vector(vec![4.0, 5.0, 6.0]);
let result = VectorFunction::DotProduct.execute(&[vec1, vec2]).unwrap();
assert!(matches!(result, Value::Double(_)));
if let Value::Double(dot) = result {
assert!((dot - 32.0).abs() < 1e-6);
}
}
#[test]
fn test_euclidean_distance() {
let vec1 = Value::vector(vec![0.0, 0.0]);
let vec2 = Value::vector(vec![3.0, 4.0]);
let result = VectorFunction::EuclideanDistance
.execute(&[vec1, vec2])
.unwrap();
assert!(matches!(result, Value::Double(_)));
if let Value::Double(dist) = result {
assert!((dist - 5.0).abs() < 1e-6);
}
}
#[test]
fn test_manhattan_distance() {
let vec1 = Value::vector(vec![0.0, 0.0]);
let vec2 = Value::vector(vec![3.0, 4.0]);
let result = VectorFunction::ManhattanDistance
.execute(&[vec1, vec2])
.unwrap();
assert!(matches!(result, Value::Double(_)));
if let Value::Double(dist) = result {
assert!((dist - 7.0).abs() < 1e-6);
}
}
#[test]
fn test_dimension() {
let vec = Value::vector(vec![0.1; 1536]);
let result = VectorFunction::Dimension.execute(&[vec]).unwrap();
assert!(matches!(result, Value::BigInt(_)));
if let Value::BigInt(dim) = result {
assert_eq!(dim, 1536);
}
}
#[test]
fn test_l2_norm() {
let vec = Value::vector(vec![3.0, 4.0]);
let result = VectorFunction::L2Norm.execute(&[vec]).unwrap();
assert!(matches!(result, Value::Double(_)));
if let Value::Double(norm) = result {
assert!((norm - 5.0).abs() < 1e-6);
}
}
#[test]
fn test_nnz() {
let vec = Value::vector(vec![1.0, 0.0, 2.0, 0.0, 3.0]);
let result = VectorFunction::Nnz.execute(&[vec]).unwrap();
assert!(matches!(result, Value::BigInt(_)));
if let Value::BigInt(n) = result {
assert_eq!(n, 3);
}
}
#[test]
fn test_normalize() {
let vec = Value::vector(vec![3.0, 4.0]);
let result = VectorFunction::Normalize.execute(&[vec]).unwrap();
assert!(matches!(result, Value::Vector(_)));
if let Value::Vector(normalized) = result {
let data = normalized.to_dense();
assert!((data[0] - 0.6).abs() < 1e-6); assert!((data[1] - 0.8).abs() < 1e-6); }
}
#[test]
fn test_dimension_mismatch() {
let vec1 = Value::vector(vec![1.0, 2.0]);
let vec2 = Value::vector(vec![1.0, 2.0, 3.0]);
let result = VectorFunction::CosineSimilarity.execute(&[vec1, vec2]);
assert!(result.is_err());
}
#[test]
fn test_array_value() {
let result = VectorFunction::ArrayValue
.execute(&[Value::Int(1), Value::Int(2), Value::Int(3)])
.unwrap();
assert!(matches!(result, Value::List(_)));
}
#[test]
fn test_array_cosine_similarity() {
let arr1 = Value::list(List {
values: vec![Value::Float(1.0), Value::Float(0.0), Value::Float(0.0)],
});
let arr2 = Value::list(List {
values: vec![Value::Float(0.0), Value::Float(1.0), Value::Float(0.0)],
});
let result = VectorFunction::ArrayCosineSimilarity
.execute(&[arr1, arr2])
.unwrap();
assert!(matches!(result, Value::Double(_)));
if let Value::Double(score) = result {
assert!((score - 0.0).abs() < 1e-6);
}
}
#[test]
fn test_array_distance() {
let arr1 = Value::list(List {
values: vec![Value::Float(0.0), Value::Float(0.0)],
});
let arr2 = Value::list(List {
values: vec![Value::Float(3.0), Value::Float(4.0)],
});
let result = VectorFunction::ArrayDistance
.execute(&[arr1, arr2])
.unwrap();
assert!(matches!(result, Value::Double(_)));
if let Value::Double(dist) = result {
assert!((dist - 5.0).abs() < 1e-6);
}
}
#[test]
fn test_array_squared_distance() {
let arr1 = Value::list(List {
values: vec![Value::Float(0.0), Value::Float(0.0)],
});
let arr2 = Value::list(List {
values: vec![Value::Float(3.0), Value::Float(4.0)],
});
let result = VectorFunction::ArraySquaredDistance
.execute(&[arr1, arr2])
.unwrap();
assert!(matches!(result, Value::Double(_)));
if let Value::Double(dist) = result {
assert!((dist - 25.0).abs() < 1e-6);
}
}
#[test]
fn test_array_inner_product() {
let arr1 = Value::list(List {
values: vec![Value::Float(1.0), Value::Float(2.0), Value::Float(3.0)],
});
let arr2 = Value::list(List {
values: vec![Value::Float(4.0), Value::Float(5.0), Value::Float(6.0)],
});
let result = VectorFunction::ArrayInnerProduct
.execute(&[arr1, arr2])
.unwrap();
assert!(matches!(result, Value::Double(_)));
if let Value::Double(dot) = result {
assert!((dot - 32.0).abs() < 1e-6);
}
}
#[test]
fn test_array_dot_product() {
let arr1 = Value::list(List {
values: vec![Value::Float(1.0), Value::Float(2.0), Value::Float(3.0)],
});
let arr2 = Value::list(List {
values: vec![Value::Float(4.0), Value::Float(5.0), Value::Float(6.0)],
});
let result = VectorFunction::ArrayDotProduct
.execute(&[arr1, arr2])
.unwrap();
assert!(matches!(result, Value::Double(_)));
if let Value::Double(dot) = result {
assert!((dot - 32.0).abs() < 1e-6);
}
}
}