use super::{VectorAlgorithm, VectorField, VectorFieldCommon, VectorType};
use crate::{RedisWrite, ToRedisArgs};
#[derive(Debug, Copy, Clone)]
#[non_exhaustive]
#[allow(missing_docs)]
pub enum VamanaVectorType {
Float16,
Float32,
}
impl From<VamanaVectorType> for VectorType {
fn from(vt: VamanaVectorType) -> Self {
match vt {
VamanaVectorType::Float16 => Self::Float16,
VamanaVectorType::Float32 => Self::Float32,
}
}
}
#[derive(Debug, Copy, Clone)]
#[non_exhaustive]
#[allow(missing_docs)]
pub enum CompressionType {
LVQ8,
LVQ4,
LVQ4x4,
LVQ4x8,
LeanVec4x8,
LeanVec8x8,
}
impl ToRedisArgs for CompressionType {
fn write_redis_args<W>(&self, out: &mut W)
where
W: ?Sized + RedisWrite,
{
out.write_arg(match self {
Self::LVQ8 => b"LVQ8",
Self::LVQ4 => b"LVQ4",
Self::LVQ4x4 => b"LVQ4x4",
Self::LVQ4x8 => b"LVQ4x8",
Self::LeanVec4x8 => b"LeanVec4x8",
Self::LeanVec8x8 => b"LeanVec8x8",
});
}
}
#[derive(Debug, Clone)]
pub(crate) struct VamanaVectorOptions {
compression: Option<CompressionType>,
construction_window_size: Option<u32>,
graph_max_degree: Option<u32>,
search_window_size: Option<u32>,
epsilon: Option<f64>,
training_threshold: Option<u32>,
reduce: Option<u32>,
}
impl ToRedisArgs for VamanaVectorOptions {
fn write_redis_args<W>(&self, out: &mut W)
where
W: ?Sized + RedisWrite,
{
if let Some(compression) = &self.compression {
out.write_arg(b"COMPRESSION");
compression.write_redis_args(out);
}
if let Some(construction_window_size) = self.construction_window_size {
out.write_arg(b"CONSTRUCTION_WINDOW_SIZE");
construction_window_size.write_redis_args(out);
}
if let Some(graph_max_degree) = self.graph_max_degree {
out.write_arg(b"GRAPH_MAX_DEGREE");
graph_max_degree.write_redis_args(out);
}
if let Some(search_window_size) = self.search_window_size {
out.write_arg(b"SEARCH_WINDOW_SIZE");
search_window_size.write_redis_args(out);
}
if let Some(epsilon) = self.epsilon {
out.write_arg(b"EPSILON");
epsilon.write_redis_args(out);
}
if let Some(training_threshold) = self.training_threshold {
out.write_arg(b"TRAINING_THRESHOLD");
training_threshold.write_redis_args(out);
}
if let Some(reduce) = self.reduce {
out.write_arg(b"REDUCE");
reduce.write_redis_args(out);
}
}
fn num_of_args(&self) -> usize {
let mut count = 0;
if self.compression.is_some() {
count += 2;
}
if self.construction_window_size.is_some() {
count += 2;
}
if self.graph_max_degree.is_some() {
count += 2;
}
if self.search_window_size.is_some() {
count += 2;
}
if self.epsilon.is_some() {
count += 2;
}
if self.training_threshold.is_some() {
count += 2;
}
if self.reduce.is_some() {
count += 2;
}
count
}
}
#[must_use = "The builder has no effect until .build() is called"]
#[derive(Debug, Clone)]
#[non_exhaustive]
pub struct VamanaVectorFieldBuilder {
common: VectorFieldCommon,
compression: Option<CompressionType>,
construction_window_size: Option<u32>,
graph_max_degree: Option<u32>,
search_window_size: Option<u32>,
epsilon: Option<f64>,
training_threshold: Option<u32>,
reduce: Option<u32>,
}
impl VamanaVectorFieldBuilder {
pub(super) fn new(common: VectorFieldCommon) -> Self {
Self {
common,
compression: None,
construction_window_size: None,
graph_max_degree: None,
search_window_size: None,
epsilon: None,
training_threshold: None,
reduce: None,
}
}
pub fn compression(mut self, compression: CompressionType) -> Self {
self.compression = Some(compression);
self
}
pub fn construction_window_size(mut self, construction_window_size: u32) -> Self {
self.construction_window_size = Some(construction_window_size);
self
}
pub fn graph_max_degree(mut self, graph_max_degree: u32) -> Self {
self.graph_max_degree = Some(graph_max_degree);
self
}
pub fn search_window_size(mut self, search_window_size: u32) -> Self {
self.search_window_size = Some(search_window_size);
self
}
pub fn epsilon(mut self, epsilon: f64) -> Self {
self.epsilon = Some(epsilon);
self
}
pub fn training_threshold(mut self, training_threshold: u32) -> Self {
self.training_threshold = Some(training_threshold);
self
}
pub fn reduce(mut self, reduce: u32) -> Self {
self.reduce = Some(reduce);
self
}
pub fn alias(mut self, alias: impl Into<String>) -> Self {
self.common = self.common.alias(alias);
self
}
pub fn index_missing(mut self, index_missing: bool) -> Self {
self.common = self.common.index_missing(index_missing);
self
}
pub fn build(self) -> VectorField {
VectorField {
common: self.common,
algorithm: VectorAlgorithm::Vamana(VamanaVectorOptions {
compression: self.compression,
construction_window_size: self.construction_window_size,
graph_max_degree: self.graph_max_degree,
search_window_size: self.search_window_size,
epsilon: self.epsilon,
training_threshold: self.training_threshold,
reduce: self.reduce,
}),
}
}
}
#[cfg(test)]
mod tests {
use super::super::DistanceMetric;
use super::*;
use crate::schema;
use crate::search::FtCreateCommand;
static INDEX_NAME: &str = "index";
static VECTOR_FIELD_NAME: &str = "embedding";
#[test]
fn test_vector_field_vamana_algorithm() {
let ft_create = FtCreateCommand::new(
INDEX_NAME,
schema! {
VECTOR_FIELD_NAME => VectorField::vamana(VamanaVectorType::Float32, 1024, DistanceMetric::Cosine)
.compression(CompressionType::LeanVec4x8)
.construction_window_size(300)
.graph_max_degree(128)
.search_window_size(20)
.epsilon(0.02)
.training_threshold(2048)
.reduce(512)
.build(),
},
);
assert_eq!(
ft_create.into_args(),
"FT.CREATE index SCHEMA embedding VECTOR SVS-VAMANA 20 TYPE FLOAT32 DIM 1024 DISTANCE_METRIC COSINE COMPRESSION LeanVec4x8 CONSTRUCTION_WINDOW_SIZE 300 GRAPH_MAX_DEGREE 128 SEARCH_WINDOW_SIZE 20 EPSILON 0.02 TRAINING_THRESHOLD 2048 REDUCE 512"
);
}
#[test]
fn test_conditional_parameters_are_sent_as_given() {
let ft_create = FtCreateCommand::new(
INDEX_NAME,
schema! {
VECTOR_FIELD_NAME => VectorField::vamana(VamanaVectorType::Float32, 1024, DistanceMetric::Cosine)
.training_threshold(200_000)
.reduce(0)
.build(),
},
);
assert_eq!(
ft_create.into_args(),
"FT.CREATE index SCHEMA embedding VECTOR SVS-VAMANA 10 TYPE FLOAT32 DIM 1024 DISTANCE_METRIC COSINE TRAINING_THRESHOLD 200000 REDUCE 0"
);
}
#[test]
fn test_conditional_parameters_do_not_depend_on_call_order() {
let compression_first =
VectorField::vamana(VamanaVectorType::Float32, 1024, DistanceMetric::Cosine)
.compression(CompressionType::LeanVec8x8)
.training_threshold(2048)
.reduce(256)
.build();
let compression_last =
VectorField::vamana(VamanaVectorType::Float32, 1024, DistanceMetric::Cosine)
.training_threshold(2048)
.reduce(256)
.compression(CompressionType::LeanVec8x8)
.build();
let rendered = |field| {
FtCreateCommand::new(INDEX_NAME, schema! { VECTOR_FIELD_NAME => field }).into_args()
};
let expected = "FT.CREATE index SCHEMA embedding VECTOR SVS-VAMANA 12 TYPE FLOAT32 DIM 1024 DISTANCE_METRIC COSINE COMPRESSION LeanVec8x8 TRAINING_THRESHOLD 2048 REDUCE 256";
assert_eq!(rendered(compression_first), expected);
assert_eq!(rendered(compression_last), expected);
}
#[test]
fn test_compression_types() {
for (compression, expected) in [
(CompressionType::LVQ8, "LVQ8"),
(CompressionType::LVQ4, "LVQ4"),
(CompressionType::LVQ4x4, "LVQ4x4"),
(CompressionType::LVQ4x8, "LVQ4x8"),
(CompressionType::LeanVec4x8, "LeanVec4x8"),
(CompressionType::LeanVec8x8, "LeanVec8x8"),
] {
let ft_create = FtCreateCommand::new(
INDEX_NAME,
schema! {
VECTOR_FIELD_NAME => VectorField::vamana(VamanaVectorType::Float32, 8, DistanceMetric::L2)
.compression(compression)
.build(),
},
);
assert_eq!(
ft_create.into_args(),
format!(
"FT.CREATE index SCHEMA embedding VECTOR SVS-VAMANA 8 TYPE FLOAT32 DIM 8 DISTANCE_METRIC L2 COMPRESSION {expected}"
)
);
}
}
#[test]
fn test_vamana_vector_types() {
for (vector_type, expected) in [
(VamanaVectorType::Float16, "FLOAT16"),
(VamanaVectorType::Float32, "FLOAT32"),
] {
let ft_create = FtCreateCommand::new(
INDEX_NAME,
schema! {
VECTOR_FIELD_NAME => VectorField::vamana(vector_type, 8, DistanceMetric::L2).build(),
},
);
assert_eq!(
ft_create.into_args(),
format!(
"FT.CREATE index SCHEMA embedding VECTOR SVS-VAMANA 6 TYPE {expected} DIM 8 DISTANCE_METRIC L2"
)
);
}
}
}