44from typing import Any , Callable , Dict , List , Optional , Sequence , Tuple
55
66import numpy as np
7+ from bidict import bidict
78from pydantic .typing import Literal
89from pymilvus import (
910 Collection ,
1920from feast .field import Field
2021from feast .infra .online_stores .online_store import OnlineStore
2122from feast .protos .feast .types .EntityKey_pb2 import EntityKey as EntityKeyProto
23+ from feast .protos .feast .types .Value_pb2 import FloatList
2224from feast .protos .feast .types .Value_pb2 import Value as ValueProto
2325from feast .repo_config import FeastConfigBaseModel
24- from feast .types import (
25- Array ,
26- Bytes ,
27- FeastType ,
28- Float32 ,
29- Float64 ,
30- Int32 ,
31- Int64 ,
32- Invalid ,
33- String ,
34- )
26+ from feast .types import Array , Bytes , Float32 , Float64 , Int32 , Int64 , Invalid , String
3527from feast .usage import log_exceptions_and_usage
3628
3729logger = logging .getLogger (__name__ )
3830
31+ TYPE_MAPPING = bidict (
32+ {
33+ DataType .INT32 : Int32 ,
34+ DataType .INT64 : Int64 ,
35+ DataType .FLOAT : Float32 ,
36+ DataType .DOUBLE : Float64 ,
37+ DataType .STRING : String ,
38+ DataType .UNKNOWN : Invalid ,
39+ DataType .FLOAT_VECTOR : Array (Float32 ),
40+ DataType .BINARY_VECTOR : Array (Bytes ),
41+ }
42+ )
43+
3944
4045class MilvusOnlineStoreConfig (FeastConfigBaseModel ):
4146 """Online store config for the Milvus online store"""
@@ -98,7 +103,7 @@ def online_write_batch(
98103 ) -> None :
99104 with MilvusConnectionManager (config .online_store ):
100105 try :
101- rows = self .format_data_for_milvus (data )
106+ rows = self ._format_data_for_milvus (data )
102107 collection_to_load_data = Collection (table .name )
103108 collection_to_load_data .insert (rows )
104109 # The flush call will seal any remaining segments and send them for indexing
@@ -113,9 +118,19 @@ def online_read(
113118 entity_keys : List [EntityKeyProto ],
114119 requested_features : Optional [List [str ]] = None ,
115120 ) -> List [Tuple [Optional [datetime ], Optional [Dict [str , ValueProto ]]]]:
116- raise NotImplementedError (
117- "to be implemented in https://jira.expedia.biz/browse/EAPC-7972"
118- )
121+
122+ with MilvusConnectionManager (config .online_store ):
123+
124+ quer_expr = self ._construct_milvus_query (entity_keys )
125+ collection = Collection (table .name )
126+ query_result = collection .query (
127+ expr = quer_expr , output_fields = requested_features
128+ )
129+ results = self ._convert_milvus_result_to_feast_type (
130+ query_result , collection , requested_features
131+ )
132+
133+ return results
119134
120135 @log_exceptions_and_usage (online_store = "milvus" )
121136 def update (
@@ -204,7 +219,7 @@ def _convert_featureview_schema_to_milvus_readable(
204219 for field in feast_schema :
205220
206221 field_name = field .name
207- data_type = self ._feast_to_milvus_data_type (field .dtype )
222+ data_type = self ._get_milvus_type (field .dtype )
208223 dimensions = 0
209224
210225 if field .tags :
@@ -253,10 +268,10 @@ def _data_type_is_supported_vector(self, data_type: DataType) -> bool:
253268 whether the Milvus data type is a supported vector in this implementation
254269
255270 Parameters:
256- data_type (DataType): data type of field in schema
271+ data_type (DataType): data type of field in schema
257272
258273 Returns:
259- bool: True is supported, False if not
274+ bool: True is supported, False if not
260275 """
261276 if data_type in [
262277 DataType .BINARY_VECTOR ,
@@ -266,31 +281,9 @@ def _data_type_is_supported_vector(self, data_type: DataType) -> bool:
266281
267282 return False
268283
269- def _feast_to_milvus_data_type (self , feast_type : FeastType ) -> DataType :
270- """
271- Mapping for converting Feast data type to a data type compatible wih Milvus.
272-
273- Parameters:
274- feast_type (FeastType): This is a type associated with a Feature that is stored in a FeatureView, readable with Feast.
275-
276- Returns:
277- DataType : DataType associated with what Milvus can understand and associate its Feature types to
278- """
279-
280- return {
281- Int32 : DataType .INT32 ,
282- Int64 : DataType .INT64 ,
283- Float32 : DataType .FLOAT ,
284- Float64 : DataType .DOUBLE ,
285- String : DataType .STRING ,
286- Invalid : DataType .UNKNOWN ,
287- Array (Float32 ): DataType .FLOAT_VECTOR ,
288- Array (Bytes ): DataType .BINARY_VECTOR ,
289- }.get (feast_type , None )
290-
291- def format_data_for_milvus (self , feast_data ):
284+ def _format_data_for_milvus (self , feast_data ):
292285 """
293- Data stored into Milvus takes the grouped representation approach where each feature value is grouped together:
286+ Format Feast input for Milvus: Data stored into Milvus takes the grouped representation approach where each feature value is grouped together:
294287 [[1,2], [1,3]], [John, Lucy], [3,4]]
295288
296289 Parameters:
@@ -303,14 +296,7 @@ def format_data_for_milvus(self, feast_data):
303296
304297 milvus_data = []
305298 for entity_key , values , timestamp , created_ts in feast_data :
306- feature = []
307- for feature_name , val in values .items ():
308- val_type = val .WhichOneof ("val" )
309- value = getattr (val , val_type )
310- if val_type == "float_list_val" :
311- value = np .array (value .val )
312- # TODO: Check binary vector conversion
313- feature .append (value )
299+ feature = self ._process_values_for_milvus (values )
314300 milvus_data .append (feature )
315301
316302 transformed_data = [list (item ) for item in zip (* milvus_data )]
@@ -321,12 +307,12 @@ def _create_index_params(self, tags: Dict[str, str], data_type: DataType):
321307 Parses the tags to generate the index_params needed to create the specified index
322308
323309 Parameters:
324- index_type (MilvusIndexType): the index type to be created
325- tags (Dict): the tags associated with the field
326- data_type (DateType): the data type of the field
310+ index_type (MilvusIndexType): the index type to be created
311+ tags (Dict): the tags associated with the field
312+ data_type (DateType): the data type of the field
327313
328314 Returns:
329- (Dict): a dictionary formatted for the create_index params argument
315+ (Dict): a dictionary formatted for the create_index params argument
330316 """
331317 valid_indexes = IndexType ._member_map_
332318 index_type_tag = tags .get ("index_type" , "" ).upper ().strip ("BIN_" )
@@ -363,3 +349,141 @@ def _create_index_params(self, tags: Dict[str, str], data_type: DataType):
363349 "index_type" : index_type_name ,
364350 "params" : params ,
365351 }
352+
353+ def _convert_milvus_result_to_feast_type (
354+ self , milvus_result , collection , features_to_request
355+ ):
356+ """
357+ Convert Milvus result to Feast types.
358+
359+ Parameters:
360+ milvus_result (List[Dict[str, any]]): Milvus query result.
361+ collection (Collection): Milvus collection schema.
362+ features_to_request (List[str]): Features to request from Milvus.
363+
364+ Returns:
365+ List[Dict[str, ValueProto]]: Processed data with Feast types.
366+ """
367+
368+ # Here we are constructing the feature list to request from Milvus with their relevant types
369+
370+ features_with_types = list (tuple ())
371+ for field in collection .schema .fields :
372+ if field .name in features_to_request :
373+ features_with_types .append (
374+ (field .name , self ._get_feast_type (field .dtype ))
375+ )
376+
377+ feast_type_result = []
378+ prefix = "valuetype."
379+
380+ for row in milvus_result :
381+ result_row = {}
382+ for feature , feast_type in features_with_types :
383+
384+ value_proto = ValueProto ()
385+ feature_value = row [feature ]
386+
387+ if feature_value :
388+ # Doing some pre-processing here to remove prefix
389+ value_type_method = f"{ feast_type .to_value_type ()} _val" .lower ()
390+ if value_type_method .startswith (prefix ):
391+ value_type_method = value_type_method [len (prefix ) :]
392+ value_proto = self ._create_value_proto (
393+ value_proto , feature_value , value_type_method
394+ )
395+ result_row [feature ] = value_proto
396+ # Append result after conversion to Feast Type
397+ feast_type_result .append (result_row )
398+ return feast_type_result
399+
400+ def _create_value_proto (self , val_proto , feature_val , value_type ) -> ValueProto :
401+ """
402+ Construct Value Proto so that Feast can interpret Milvus results
403+
404+ Parameters:
405+ val_proto (ValueProto): Initialised Value Proto
406+ feature_val (Union[list, int, str, double, float, bool, bytes]): A row/ an item in the result that Milvus returns.
407+ value_type (Str): Feast Value type; example: int64_val, float_val, etc.
408+
409+ Returns:
410+ val_proto (ValueProto): Constructed result that Feast can understand.
411+ """
412+
413+ if value_type == "float_list_val" :
414+ val_proto = ValueProto (float_list_val = FloatList (val = feature_val ))
415+ else :
416+ setattr (val_proto , value_type , feature_val )
417+ return val_proto
418+
419+ def _construct_milvus_query (self , entities ) -> str :
420+ """
421+ Construct a Milvus query expression based on entity_keys provided.
422+
423+ Parameters:
424+ entities (List[Entity]): List of entities with join keys and values.
425+
426+ Returns:
427+ str: Constructed Milvus query expression.
428+ """
429+
430+ milvus_query_expr = ""
431+ entity_join_key = []
432+ values_to_search = []
433+
434+ for entity in entities :
435+ for key in entity .join_keys :
436+ entity_join_key .append (key )
437+ for value in entity .entity_values :
438+ value_to_search = self ._get_value_to_search_in_milvus (value )
439+ values_to_search .append (value_to_search )
440+
441+ # TODO: Enable multiple join key support. Currently only supporting a single primary key/ join key. This is a limitation in Feast.
442+ milvus_query_expr = f"{ entity_join_key [0 ]} in { values_to_search } "
443+
444+ return milvus_query_expr
445+
446+ def _process_values_for_milvus (self , values ) -> List :
447+ """
448+ Process values to prepare them for using in Milvus.
449+
450+ Parameters:
451+ values: (Dict[str, ValueProto]): Dictionary of values from Feast data.
452+
453+ Returns:
454+ (List): Processed feature values ready for storing in Milvus.
455+ """
456+ feature = []
457+ for feature_name , val in values .items ():
458+ value = self ._get_value_to_search_in_milvus (val )
459+ feature .append (value )
460+ return feature
461+
462+ def _get_value_to_search_in_milvus (self , value ) -> Any :
463+ """
464+ Process a value to prepare it for searching in Milvus.
465+
466+ Parameters:
467+ value (ValueProto): A value from Feast data.
468+
469+ Returns:
470+ value (Any): Processed value ready for Milvus searching.
471+ """
472+ val_type = value .WhichOneof ("val" )
473+ if val_type == "float_list_val" :
474+ value = np .array (value .float_list_val .val )
475+ else :
476+ value = getattr (value , val_type )
477+ return value
478+
479+ def _get_milvus_type (self , feast_type ) -> DataType :
480+ """
481+ Convert Feast type to Milvus type using the TYPE_MAPPING bidict.
482+ """
483+ return TYPE_MAPPING .inverse .get (feast_type , None )
484+
485+ def _get_feast_type (self , milvus_type ) -> object :
486+ """
487+ Convert Milvus type to Feast type using the TYPE_MAPPING bidict.
488+ """
489+ return TYPE_MAPPING .get (milvus_type , None )
0 commit comments