88from enum import Enum
99from typing import Any , Dict , Literal , Optional , Tuple , Type , Union
1010
11- from pydantic import BaseModel , Field , field_validator
11+ from pydantic import BaseModel , Field , field_validator , model_validator
1212from redis .commands .search .field import Field as RedisField
1313from redis .commands .search .field import GeoField as RedisGeoField
1414from redis .commands .search .field import NumericField as RedisNumericField
@@ -51,6 +51,17 @@ class VectorDataType(str, Enum):
5151class VectorIndexAlgorithm (str , Enum ):
5252 FLAT = "FLAT"
5353 HNSW = "HNSW"
54+ SVS_VAMANA = "SVS-VAMANA"
55+
56+
57+ class CompressionType (str , Enum ):
58+ """Vector compression types for SVS-VAMANA algorithm"""
59+ LVQ4 = "LVQ4"
60+ LVQ4x4 = "LVQ4x4"
61+ LVQ4x8 = "LVQ4x8"
62+ LVQ8 = "LVQ8"
63+ LeanVec4x8 = "LeanVec4x8"
64+ LeanVec8x8 = "LeanVec8x8"
5465
5566
5667### Field Attributes ###
@@ -171,6 +182,61 @@ class HNSWVectorFieldAttributes(BaseVectorFieldAttributes):
171182 """Relative factor that sets the boundaries in which a range query may search for candidates"""
172183
173184
185+
186+ class SVSVectorFieldAttributes (BaseVectorFieldAttributes ):
187+ """SVS-VAMANA vector field attributes with optional compression support"""
188+ algorithm : Literal [VectorIndexAlgorithm .SVS_VAMANA ] = VectorIndexAlgorithm .SVS_VAMANA
189+ """The indexing algorithm for the vector field"""
190+
191+ # SVS-VAMANA graph parameters
192+ graph_max_degree : int = Field (default = 40 )
193+ """Maximum degree of the Vamana graph (number of edges per node)"""
194+ construction_window_size : int = Field (default = 250 )
195+ """Size of the candidate list during graph construction"""
196+ search_window_size : int = Field (default = 20 )
197+ """Size of the candidate list during search"""
198+ epsilon : float = Field (default = 0.01 )
199+ """Relative factor for range query boundaries"""
200+
201+ # SVS-VAMANA compression parameters (optional, to be implemented)
202+ compression : Optional [CompressionType ] = None
203+ """Vector compression type (LVQ or LeanVec)"""
204+ reduce : Optional [int ] = None
205+ """Reduced dimensionality for LeanVec compression (must be < dims)"""
206+ training_threshold : Optional [int ] = None
207+ """Minimum number of vectors required before compression training"""
208+
209+ @model_validator (mode = 'after' )
210+ def validate_svs_params (self ):
211+ """Validate SVS-VAMANA specific constraints"""
212+ # Datatype validation: SVS only supports FLOAT16 and FLOAT32
213+ if self .datatype not in (VectorDataType .FLOAT16 , VectorDataType .FLOAT32 ):
214+ raise ValueError (
215+ f"SVS-VAMANA only supports FLOAT16 and FLOAT32 datatypes. "
216+ f"Got: { self .datatype } . "
217+ f"Unsupported types: BFLOAT16, FLOAT64, INT8, UINT8."
218+ )
219+
220+ # Reduce validation: must be less than dims
221+ if self .reduce is not None :
222+ if self .reduce >= self .dims :
223+ raise ValueError (
224+ f"reduce ({ self .reduce } ) must be less than dims ({ self .dims } )"
225+ )
226+ # Phase C: Add warning for reduce without LeanVec
227+ # if not self.compression or not self.compression.value.startswith("LeanVec"):
228+ # logger.warning(
229+ # "reduce parameter is recommended with LeanVec compression"
230+ # )
231+
232+ # Phase C: Add warning for LeanVec without reduce
233+ # if self.compression and self.compression.value.startswith("LeanVec") and not self.reduce:
234+ # logger.warning(
235+ # f"LeanVec compression selected without 'reduce'. "
236+ # f"Consider setting reduce={self.dims//2} for better performance"
237+ # )
238+
239+ return self
174240### Field Classes ###
175241
176242
@@ -352,7 +418,7 @@ def as_redis_field(self) -> RedisField:
352418
353419
354420class FlatVectorField (BaseField ):
355- "Vector field with a FLAT index (brute force nearest neighbors search)"
421+ """ Vector field with a FLAT index (brute force nearest neighbors search)"" "
356422
357423 type : Literal [FieldTypes .VECTOR ] = FieldTypes .VECTOR
358424 attrs : FlatVectorFieldAttributes
@@ -387,6 +453,32 @@ def as_redis_field(self) -> RedisField:
387453 return RedisVectorField (name , self .attrs .algorithm , field_data , as_name = as_name )
388454
389455
456+ class SVSVectorField (BaseField ):
457+ """Vector field with an SVS-VAMANA index"""
458+ type : Literal [FieldTypes .VECTOR ] = FieldTypes .VECTOR
459+ attrs : SVSVectorFieldAttributes
460+
461+ def as_redis_field (self ) -> RedisField :
462+ name , as_name = self ._handle_names ()
463+ field_data = self .attrs .field_data
464+ field_data .update (
465+ {
466+ "GRAPH_MAX_DEGREE" : self .attrs .graph_max_degree ,
467+ "CONSTRUCTION_WINDOW_SIZE" : self .attrs .construction_window_size ,
468+ "SEARCH_WINDOW_SIZE" : self .attrs .search_window_size ,
469+ "EPSILON" : self .attrs .epsilon ,
470+ }
471+ )
472+ # Add compression parameters if specified
473+ if self .attrs .compression is not None :
474+ field_data ["COMPRESSION" ] = self .attrs .compression
475+ if self .attrs .reduce is not None :
476+ field_data ["REDUCE" ] = self .attrs .reduce
477+ if self .attrs .training_threshold is not None :
478+ field_data ["TRAINING_THRESHOLD" ] = self .attrs .training_threshold
479+ return RedisVectorField (name , self .attrs .algorithm , field_data , as_name = as_name )
480+
481+
390482FIELD_TYPE_MAP = {
391483 "tag" : TagField ,
392484 "text" : TextField ,
@@ -397,6 +489,7 @@ def as_redis_field(self) -> RedisField:
397489VECTOR_FIELD_TYPE_MAP = {
398490 "flat" : FlatVectorField ,
399491 "hnsw" : HNSWVectorField ,
492+ "svs-vamana" : SVSVectorField ,
400493}
401494
402495
0 commit comments