44from typing import Any , Callable , Dict , List , Optional , Sequence , Tuple
55
66import numpy as np
7+ import pandas as pd
78from bidict import bidict
89from pydantic .typing import Literal
910from pymilvus import (
1718from pymilvus .client .types import IndexType
1819
1920from feast import Entity , FeatureView , RepoConfig
20- from feast .field import Field
2121from feast .infra .online_stores .online_store import OnlineStore
2222from feast .protos .feast .types .EntityKey_pb2 import EntityKey as EntityKeyProto
2323from feast .protos .feast .types .Value_pb2 import FloatList
@@ -102,14 +102,15 @@ def online_write_batch(
102102 progress : Optional [Callable [[int ], Any ]],
103103 ) -> None :
104104 with MilvusConnectionManager (config .online_store ):
105- try :
106- rows = self ._format_data_for_milvus (data )
107- collection_to_load_data = Collection (table .name )
108- collection_to_load_data .insert (rows )
109- # The flush call will seal any remaining segments and send them for indexing
110- collection_to_load_data .flush ()
111- except Exception as e :
112- logger .error (f"Batch writing data failed due to { e } " )
105+ collection_to_load_data = Collection (table .name )
106+ rows = self ._format_data_for_milvus (data , collection_to_load_data )
107+ collection_to_load_data .insert (rows )
108+ # The flush call will seal any remaining segments and send them for indexing
109+ collection_to_load_data .flush ()
110+ collection_to_load_data .load ()
111+ logger .info ("loading data into memory" )
112+ utility .wait_for_loading_complete (table .name )
113+ logger .info ("loading data into memory complete" )
113114
114115 def online_read (
115116 self ,
@@ -130,7 +131,8 @@ def online_read(
130131 query_result , collection , requested_features
131132 )
132133
133- return results
134+ # results do not have timestamps
135+ return [(None , row ) for row in results ]
134136
135137 @log_exceptions_and_usage (online_store = "milvus" )
136138 def update (
@@ -145,28 +147,33 @@ def update(
145147 with MilvusConnectionManager (config .online_store ):
146148 for table_to_keep in tables_to_keep :
147149 collection_available = utility .has_collection (table_to_keep .name )
148- try :
149- if collection_available :
150- logger .info (f"Collection { table_to_keep .name } already exists." )
151- else :
152- (
153- schema ,
154- indexes ,
155- ) = self ._convert_featureview_schema_to_milvus_readable (
156- table_to_keep .schema ,
150+
151+ if collection_available :
152+ logger .info (f"Collection { table_to_keep .name } already exists." )
153+ else :
154+ if not table_to_keep .schema :
155+ raise ValueError (
156+ f"a schema must be provided for feature_view: { table_to_keep } "
157157 )
158158
159- collection = Collection (name = table_to_keep .name , schema = schema )
159+ (
160+ schema ,
161+ indexes ,
162+ ) = self ._convert_featureview_schema_to_milvus_readable (
163+ table_to_keep ,
164+ )
165+
166+ logger .info (
167+ f"creating collection { table_to_keep .name } with schema: { schema } "
168+ )
169+ collection = Collection (name = table_to_keep .name , schema = schema )
160170
161- for field_name , index_params in indexes .items ():
162- collection .create_index (field_name , index_params )
171+ for field_name , index_params in indexes .items ():
172+ collection .create_index (field_name , index_params )
163173
164- logger .info (f"Collection name is { collection .name } " )
165- logger .info (
166- f"Collection { table_to_keep .name } has been created successfully."
167- )
168- except Exception as e :
169- logger .error (f"Collection update failed due to { e } " )
174+ logger .info (
175+ f"Collection { table_to_keep .name } has been created successfully."
176+ )
170177
171178 for table_to_delete in tables_to_delete :
172179 collection_available = utility .has_collection (table_to_delete .name )
@@ -198,35 +205,33 @@ def teardown(
198205 utility .drop_collection (collection_name )
199206
200207 def _convert_featureview_schema_to_milvus_readable (
201- self , feast_schema : List [ Field ]
208+ self , feature_view : FeatureView
202209 ) -> Tuple [CollectionSchema , Dict ]:
203210 """
204211 Converting a schema understood by Feast to a schema that is readable by Milvus so that it
205212 can be used when a collection is created in Milvus.
206213
207214 Parameters:
208- feast_schema (List[Field] ): Schema stored in FeatureView .
215+ feature_view (FeatureView ): the FeatureView that contains the schema .
209216
210217 Returns:
211218 (CollectionSchema): Schema readable by Milvus.
212219 (Dict): A dictionary of indexes to be created with the key as the vector field name and the value as the parameters
213220
214221 """
215- boolean_mapping_from_string = {"True" : True , "False" : False }
216222 field_list = []
217223 indexes = {}
218224
219- for field in feast_schema :
225+ for field in feature_view . schema :
220226
221227 field_name = field .name
222228 data_type = self ._get_milvus_type (field .dtype )
223229 dimensions = 0
230+ description = ""
231+ is_primary = True if field .name in feature_view .join_keys else False
224232
225233 if field .tags :
226- description = field .tags .get ("description" , " " )
227- is_primary = boolean_mapping_from_string .get (
228- field .tags .get ("is_primary" , "False" )
229- )
234+ description = field .tags .get ("description" , "" )
230235
231236 if self ._data_type_is_supported_vector (data_type ) and field .tags .get (
232237 "index_type"
@@ -281,27 +286,57 @@ def _data_type_is_supported_vector(self, data_type: DataType) -> bool:
281286
282287 return False
283288
284- def _format_data_for_milvus (self , feast_data ):
289+ def _format_data_for_milvus (self , feast_data , collection : Collection ):
285290 """
286291 Format Feast input for Milvus: Data stored into Milvus takes the grouped representation approach where each feature value is grouped together:
287292 [[1,2], [1,3]], [John, Lucy], [3,4]]
288293
289294 Parameters:
290295 feast_data: List[
291296 Tuple[EntityKeyProto, Dict[str, ValueProto], datetime, Optional[datetime]]: Data represented for batch write in Feast
297+ collection: target collection
292298
293299 Returns:
294- List[List] : transformed_data: Data that can be directly written into Milvus
300+ pd.DataFrame : transformed_data: Data that can be directly written into Milvus
295301 """
302+ # get the order of columns so that return data frame has the correct order. Milvus does need the correct order
303+ # and does not use the column names when a data frame is passed.
304+ schema = collection .schema
305+ field_names = [field .name for field in schema .fields ]
296306
297- milvus_data = []
307+ data = []
308+ feature_names = None
298309 for entity_key , values , timestamp , created_ts in feast_data :
299- feature = self ._process_values_for_milvus (values )
300- milvus_data .append (feature )
310+ feature_names = [entity_key .join_keys [0 ]]
311+ feature = [self ._get_value_from_value_proto (entity_key .entity_values [0 ])]
312+ for feature_name , val in values .items ():
313+ value = self ._get_value_from_value_proto (val )
314+ feature .append (value )
315+ feature_names .append (feature_name )
316+ data .append (feature )
317+
318+ df = pd .DataFrame (data , columns = feature_names )
319+ transformed_data = df .reindex (field_names , axis = 1 )
301320
302- transformed_data = [list (item ) for item in zip (* milvus_data )]
303321 return transformed_data
304322
323+ def _get_value_from_value_proto (self , proto : ValueProto ):
324+ """
325+ Get the raw value from a value proto.
326+
327+ Parameters:
328+ value (ValueProto): the value proto that contains the data.
329+
330+ Returns:
331+ value (Any): the extracted value.
332+ """
333+ val_type = proto .WhichOneof ("val" )
334+ value = getattr (proto , val_type ) # type: ignore
335+ if val_type == "float_list_val" :
336+ value = np .array (value .val )
337+
338+ return value
339+
305340 def _create_index_params (self , tags : Dict [str , str ], data_type : DataType ):
306341 """
307342 Parses the tags to generate the index_params needed to create the specified index
@@ -351,7 +386,7 @@ def _create_index_params(self, tags: Dict[str, str], data_type: DataType):
351386 }
352387
353388 def _convert_milvus_result_to_feast_type (
354- self , milvus_result , collection , features_to_request
389+ self , query_result , collection , requested_features
355390 ):
356391 """
357392 Convert Milvus result to Feast types.
@@ -365,27 +400,22 @@ def _convert_milvus_result_to_feast_type(
365400 List[Dict[str, ValueProto]]: Processed data with Feast types.
366401 """
367402
368- # Here we are constructing the feature list to request from Milvus with their relevant types
369-
403+ # constructing the feature list to request from Milvus with their respective types
370404 features_with_types = list (tuple ())
371405 for field in collection .schema .fields :
372- if field .name in features_to_request :
406+ if field .name in requested_features :
373407 features_with_types .append (
374408 (field .name , self ._get_feast_type (field .dtype ))
375409 )
376410
377- feast_type_result = []
411+ results = []
378412 prefix = "valuetype."
379-
380- for row in milvus_result :
413+ for row in query_result :
381414 result_row = {}
382415 for feature , feast_type in features_with_types :
383-
384416 value_proto = ValueProto ()
385417 feature_value = row [feature ]
386-
387418 if feature_value :
388- # Doing some pre-processing here to remove prefix
389419 value_type_method = f"{ feast_type .to_value_type ()} _val" .lower ()
390420 if value_type_method .startswith (prefix ):
391421 value_type_method = value_type_method [len (prefix ) :]
@@ -394,8 +424,9 @@ def _convert_milvus_result_to_feast_type(
394424 )
395425 result_row [feature ] = value_proto
396426 # Append result after conversion to Feast Type
397- feast_type_result .append (result_row )
398- return feast_type_result
427+ results .append (result_row )
428+
429+ return results
399430
400431 def _create_value_proto (self , val_proto , feature_val , value_type ) -> ValueProto :
401432 """
@@ -409,11 +440,11 @@ def _create_value_proto(self, val_proto, feature_val, value_type) -> ValueProto:
409440 Returns:
410441 val_proto (ValueProto): Constructed result that Feast can understand.
411442 """
412-
413443 if value_type == "float_list_val" :
414444 val_proto = ValueProto (float_list_val = FloatList (val = feature_val ))
415445 else :
416446 setattr (val_proto , value_type , feature_val )
447+
417448 return val_proto
418449
419450 def _construct_milvus_query (self , entities ) -> str :
@@ -435,47 +466,14 @@ def _construct_milvus_query(self, entities) -> str:
435466 for key in entity .join_keys :
436467 entity_join_key .append (key )
437468 for value in entity .entity_values :
438- value_to_search = self ._get_value_to_search_in_milvus (value )
469+ value_to_search = self ._get_value_from_value_proto (value )
439470 values_to_search .append (value_to_search )
440471
441472 # TODO: Enable multiple join key support. Currently only supporting a single primary key/ join key. This is a limitation in Feast.
442473 milvus_query_expr = f"{ entity_join_key [0 ]} in { values_to_search } "
443474
444475 return milvus_query_expr
445476
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-
479477 def _get_milvus_type (self , feast_type ) -> DataType :
480478 """
481479 Convert Feast type to Milvus type using the TYPE_MAPPING bidict.
0 commit comments