Skip to content

Commit 01867f4

Browse files
Merge pull request #23 from ExpediaGroup/feature/integration_test_improvements
Feature/integration test improvements
2 parents 004ca49 + b5067c2 commit 01867f4

6 files changed

Lines changed: 116 additions & 112 deletions

File tree

docs/reference/online-stores/milvus.md

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -54,22 +54,19 @@ An example feature view:
5454
Field(
5555
name="book_id",
5656
dtype=Int64,
57-
tags={
58-
"is_primary": "True",
59-
},
6057
),
6158
Field(
6259
name="book_embedding",
6360
dtype=Array(Float32),
6461
tags={
65-
"is_primary": "False",
6662
"description": "book embedding of the content",
6763
"dimensions": "2200",
6864
"index_type": IndexType.ivf_flat.value,
6965
"index_params": {
7066
"nlist": 1024,
7167
},
7268
"metric_type": "L2",
69+
}
7370
),
7471
],
7572
source=SOURCE,
@@ -110,7 +107,7 @@ Below is a matrix indicating which functionality is supported by the Milvus onli
110107
| readable by Python SDK | yes |
111108
| readable by Java | no |
112109
| readable by Go | no |
113-
| support for entityless feature views | yes |
110+
| support for entityless feature views | no |
114111
| support for concurrent writing to the same key | yes |
115112
| support for ttl (time to live) at retrieval | no |
116113
| support for deleting expired data | no |

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

Lines changed: 87 additions & 89 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+
import pandas as pd
78
from bidict import bidict
89
from pydantic.typing import Literal
910
from pymilvus import (
@@ -17,7 +18,6 @@
1718
from pymilvus.client.types import IndexType
1819

1920
from feast import Entity, FeatureView, RepoConfig
20-
from feast.field import Field
2121
from feast.infra.online_stores.online_store import OnlineStore
2222
from feast.protos.feast.types.EntityKey_pb2 import EntityKey as EntityKeyProto
2323
from 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.

sdk/python/tests/expediagroup/milvus_online_store_creator.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ def create_online_store(self) -> Dict[str, str]:
2121
"Milvus Proxy successfully initialized and ready to serve!"
2222
)
2323
wait_for_logs(
24-
container=self.container, predicate=log_string_to_wait_for, timeout=30
24+
container=self.container, predicate=log_string_to_wait_for, timeout=60
2525
)
2626
exposed_port = self.container.get_exposed_port("19530")
2727

0 commit comments

Comments
 (0)