Skip to content

Commit 004ca49

Browse files
Manisha4Manisha Sudhirmbackes
authored
Msudhir/add milvus read (#22)
* initial online read implementation * Adding online read functionality * linting error fix * removed utils out and added all the functionality to milvus_online_store * changeing method types to internal * fixing linting errors * Added bidict and removed existing feast/milvus conversion functionality * Removed the ValueProto redundancy in the code * added bidict to requirements files * Added changes to further PR comments * removing primary_key from feast_schema in tests * keeping formatting consistent --------- Co-authored-by: Manisha Sudhir <msudhir@expediagroup.com> Co-authored-by: mbackes <mbackes@hotwire.com>
1 parent ffc8c83 commit 004ca49

9 files changed

Lines changed: 314 additions & 77 deletions

File tree

sdk/python/feast/expediagroup/vectordb/milvus_online_store.py

Lines changed: 178 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
from typing import Any, Callable, Dict, List, Optional, Sequence, Tuple
55

66
import numpy as np
7+
from bidict import bidict
78
from pydantic.typing import Literal
89
from pymilvus import (
910
Collection,
@@ -19,23 +20,27 @@
1920
from feast.field import Field
2021
from feast.infra.online_stores.online_store import OnlineStore
2122
from feast.protos.feast.types.EntityKey_pb2 import EntityKey as EntityKeyProto
23+
from feast.protos.feast.types.Value_pb2 import FloatList
2224
from feast.protos.feast.types.Value_pb2 import Value as ValueProto
2325
from 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
3527
from feast.usage import log_exceptions_and_usage
3628

3729
logger = 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

4045
class 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)

sdk/python/requirements/py3.10-ci-requirements.txt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,8 @@ backcall==0.2.0
8282
# via ipython
8383
beautifulsoup4==4.12.2
8484
# via nbconvert
85+
bidict==0.22.1
86+
# via feast (setup.py)
8587
black==22.12.0
8688
# via eg-feast (setup.py)
8789
bleach==6.0.0

sdk/python/requirements/py3.10-requirements.txt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,8 @@ attrs==23.1.0
1515
# via
1616
# bowler
1717
# jsonschema
18+
bidict==0.22.1
19+
# via feast (setup.py)
1820
bowler==0.9.0
1921
# via feast (setup.py)
2022
certifi==2023.5.7

sdk/python/requirements/py3.8-ci-requirements.txt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,8 @@ backports-zoneinfo==0.2.1;python_version<"3.9"
8484
# tzlocal
8585
beautifulsoup4==4.12.2
8686
# via nbconvert
87+
bidict==0.22.1
88+
# via feast (setup.py)
8789
black==22.12.0
8890
# via feast (setup.py)
8991
bleach==6.0.0

sdk/python/requirements/py3.8-requirements.txt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,8 @@ attrs==23.1.0
1515
# via
1616
# bowler
1717
# jsonschema
18+
bidict==0.22.1
19+
# via feast (setup.py)
1820
bowler==0.9.0
1921
# via feast (setup.py)
2022
certifi==2023.5.7

sdk/python/requirements/py3.9-ci-requirements.txt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,8 @@ backcall==0.2.0
8282
# via ipython
8383
beautifulsoup4==4.12.2
8484
# via nbconvert
85+
bidict==0.22.1
86+
# via feast (setup.py)
8587
black==22.12.0
8688
# via eg-feast (setup.py)
8789
bleach==6.0.0

sdk/python/requirements/py3.9-requirements.txt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,8 @@ attrs==23.1.0
1515
# via
1616
# bowler
1717
# jsonschema
18+
bidict==0.22.1
19+
# via feast (setup.py)
1820
bowler==0.9.0
1921
# via feast (setup.py)
2022
certifi==2023.5.7

0 commit comments

Comments
 (0)