Visitar URL original
feat: Support for Map value data type (#5768) by nquinn408 · Pull Request #5772 · feast-dev/feast · GitHub
Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Next Next commit
feat: Support for Map value data type #5768
Signed-off-by: Nick Quinn <nicholas_quinn@apple.com>
  • Loading branch information
nickquinn408 committed Dec 13, 2025
commit f14f40b930be14e6d6ec820a9c9095ee3b6d8fa3
15 changes: 14 additions & 1 deletion protos/feast/types/Value.proto
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@

/*
* Copyright 2018 The Feast Authors
*
Expand Down Expand Up @@ -42,6 +43,8 @@ message ValueType {
BOOL_LIST = 17;
UNIX_TIMESTAMP_LIST = 18;
NULL = 19;
MAP = 20;
MAP_LIST = 21;
}
}

Expand All @@ -67,6 +70,8 @@ message Value {
BoolList bool_list_val = 17;
Int64List unix_timestamp_list_val = 18;
Null null_val = 19;
Map map_val = 20;
MapList map_list_val = 21;
}
}

Expand Down Expand Up @@ -102,8 +107,16 @@ message BoolList {
repeated bool val = 1;
}

message Map {
map<string, Value> val = 1;
}

message MapList {
repeated Map val = 1;
}

// This is to avoid an issue of being unable to specify `repeated value` in oneofs or maps
// In JSON "val" field can be omitted
message RepeatedValue {
repeated Value val = 1;
}
}
100 changes: 97 additions & 3 deletions sdk/python/feast/type_map.py
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,8 @@
Int32List,
Int64List,
StringList,
Map,
MapList,
)
from feast.protos.feast.types.Value_pb2 import Value as ProtoValue
from feast.value_type import ListType, ValueType
Expand Down Expand Up @@ -73,7 +75,13 @@ def feast_value_type_to_python_type(field_value_proto: ProtoValue) -> Any:
if val_attr is None:
return None
val = getattr(field_value_proto, val_attr)


# Handle Map and MapList types FIRST (before generic list processing)
if val_attr == "map_val":
return _handle_map_value(val)
elif val_attr == "map_list_val":
return _handle_map_list_value(val)

# If it's a _LIST type extract the list.
if hasattr(val, "val"):
val = list(val.val)
Expand All @@ -97,6 +105,27 @@ def feast_value_type_to_python_type(field_value_proto: ProtoValue) -> Any:

return val

def _handle_map_value(map_message) -> Dict[str, Any]:
"""Handle Map proto message containing map<string, Value> val."""
result = {}

for key, value in map_message.val.items():
# Recursively handle the Value message
result[key] = feast_value_type_to_python_type(value)

return result


def _handle_map_list_value(map_list_message) -> List[Dict[str, Any]]:
"""Handle MapList proto message containing repeated Map val."""
result = []

for map_item in map_list_message.val:
# Handle each Map in the list
processed_map = _handle_map_value(map_item)
result.append(processed_map)

return result

def feast_value_type_to_pandas_type(value_type: ValueType) -> Any:
value_type_to_pandas_type: Dict[ValueType, str] = {
Expand All @@ -109,7 +138,7 @@ def feast_value_type_to_pandas_type(value_type: ValueType) -> Any:
ValueType.BOOL: "bool",
ValueType.UNIX_TIMESTAMP: "datetime64[ns]",
}
if value_type.name.endswith("_LIST"):
if value_type.name == "MAP" or value_type.name.endswith("_LIST"):
return "object"
if value_type in value_type_to_pandas_type:
return value_type_to_pandas_type[value_type]
Expand Down Expand Up @@ -178,6 +207,9 @@ def python_type_to_feast_value_type(
actual_type = type(value).__name__.lower()
if actual_type == "str":
return ValueType.STRING
# Check if it's a dictionary (could be a Map)
elif actual_type == "dict":
return ValueType.MAP
# If it's a different type wrapped in object, try to infer from the value
elif actual_type in type_map:
return type_map[actual_type]
Expand All @@ -187,6 +219,9 @@ def python_type_to_feast_value_type(
return ValueType[item_type.name + "_LIST"]

if isinstance(value, (list, np.ndarray)):
# Check if it's a list of maps
if value and isinstance(value[0], dict):
return ValueType.MAP_LIST
# if the value's type is "ndarray" and we couldn't infer from "value.dtype"
# this is most probably array of "object",
# so we need to iterate over objects and try to infer type of each item
Expand Down Expand Up @@ -222,6 +257,10 @@ def python_type_to_feast_value_type(
return ValueType.UNKNOWN
return ValueType[common_item_value_type.name + "_LIST"]

# Check if it's a dictionary (Map type)
if isinstance(value, dict):
return ValueType.MAP

raise ValueError(
f"Value with native type {type_name} cannot be converted into Feast value type"
)
Expand Down Expand Up @@ -372,9 +411,22 @@ def _python_value_to_proto_value(
Returns:
List of Feast Value Proto
"""
# Handle Map and MapList types first
if feast_value_type == ValueType.MAP:
return [
ProtoValue(map_val=_python_dict_to_map_proto(value)) if value is not None else ProtoValue()
for value in values
]

if feast_value_type == ValueType.MAP_LIST:
return [
ProtoValue(map_list_val=_python_list_to_map_list_proto(value)) if value is not None else ProtoValue()
for value in values
]

# ToDo: make a better sample for type checks (more than one element)
sample = next(filter(_non_empty_value, values), None) # first not empty value

# Detect list type and handle separately
if "list" in feast_value_type.name.lower():
# Feature can be list but None is still valid
Expand Down Expand Up @@ -512,6 +564,45 @@ def _python_value_to_proto_value(
raise Exception(f"Unsupported data type: ${str(type(values[0]))}")


def _python_dict_to_map_proto(python_dict: Dict[str, Any]) -> Map:
"""Convert a Python dictionary to a Map proto message."""
map_proto = Map()
for key, value in python_dict.items():
# Handle None values explicitly
if value is None:
map_proto.val[key].CopyFrom(ProtoValue()) # Empty ProtoValue represents None
continue

if isinstance(value, dict):
# Nested map
nested_map_proto = _python_dict_to_map_proto(value)
map_proto.val[key].CopyFrom(ProtoValue(map_val=nested_map_proto))
elif isinstance(value, list) and value and isinstance(value[0], dict):
# List of maps (MapList)
map_list_proto = _python_list_to_map_list_proto(value)
map_proto.val[key].CopyFrom(ProtoValue(map_list_val=map_list_proto))
else:
# Handle scalar values and regular lists
# Let python_values_to_proto_values infer the type
proto_values = python_values_to_proto_values([value], ValueType.UNKNOWN)
map_proto.val[key].CopyFrom(proto_values[0])
return map_proto


def _python_list_to_map_list_proto(python_list: List[Dict[str, Any]]) -> MapList:
"""Convert a Python list of dictionaries to a MapList proto message."""
map_list_proto = MapList()

for item in python_list:
if isinstance(item, dict):
map_proto = _python_dict_to_map_proto(item)
map_list_proto.val.append(map_proto)
else:
raise ValueError(f"MapList can only contain dictionaries, got {type(item)}")

return map_list_proto


def python_values_to_proto_values(
values: List[Any], feature_type: ValueType = ValueType.UNKNOWN
) -> List[ProtoValue]:
Expand Down Expand Up @@ -555,6 +646,8 @@ def python_values_to_proto_values(
"string_list_val": ValueType.STRING_LIST,
"bytes_list_val": ValueType.BYTES_LIST,
"bool_list_val": ValueType.BOOL_LIST,
"map_val": ValueType.MAP,
"map_list_val": ValueType.MAP_LIST,
}

VALUE_TYPE_TO_PROTO_VALUE_MAP: Dict[ValueType, str] = {
Expand Down Expand Up @@ -1177,3 +1270,4 @@ def convert_array_item(item) -> Union[np.ndarray, Any]:
return item

return series.apply(convert_array_item)

9 changes: 9 additions & 0 deletions sdk/python/feast/types.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@
"FLOAT32": "FLOAT",
"BOOL": "BOOL",
"UNIX_TIMESTAMP": "UNIX_TIMESTAMP",
"MAP": "MAP",
}


Expand Down Expand Up @@ -83,6 +84,7 @@ class PrimitiveFeastType(Enum):
UNIX_TIMESTAMP = 8
PDF_BYTES = 9
IMAGE_BYTES = 10
MAP = 11

def to_value_type(self) -> ValueType:
"""
Expand Down Expand Up @@ -115,6 +117,7 @@ def __hash__(self):
Float32 = PrimitiveFeastType.FLOAT32
Float64 = PrimitiveFeastType.FLOAT64
UnixTimestamp = PrimitiveFeastType.UNIX_TIMESTAMP
Map = PrimitiveFeastType.MAP

SUPPORTED_BASE_TYPES = [
Invalid,
Expand All @@ -128,6 +131,7 @@ def __hash__(self):
Float32,
Float64,
UnixTimestamp,
Map,
]

PRIMITIVE_FEAST_TYPES_TO_STRING = {
Expand All @@ -142,6 +146,7 @@ def __hash__(self):
"FLOAT32": "Float32",
"FLOAT64": "Float64",
"UNIX_TIMESTAMP": "UnixTimestamp",
"MAP": "Map",
}


Expand Down Expand Up @@ -195,6 +200,8 @@ def __str__(self):
ValueType.FLOAT_LIST: Array(Float32),
ValueType.BOOL_LIST: Array(Bool),
ValueType.UNIX_TIMESTAMP_LIST: Array(UnixTimestamp),
ValueType.MAP: Map,
ValueType.MAP_LIST: Array(Map),
}

FEAST_TYPES_TO_PYARROW_TYPES = {
Expand All @@ -214,6 +221,7 @@ def __str__(self):
ValueType.INT64_LIST,
ValueType.FLOAT_LIST,
ValueType.BOOL_LIST,
ValueType.MAP_LIST,
]
for k in VALUE_TYPES_TO_FEAST_TYPES:
if k in FEAST_VECTOR_TYPES:
Expand Down Expand Up @@ -282,3 +290,4 @@ def from_feast_type(
]

raise ValueError(f"Could not convert feast type {feast_type} to ValueType.")

8 changes: 5 additions & 3 deletions sdk/python/feast/value_type.py
Original file line number Diff line number Diff line change
Expand Up @@ -48,9 +48,10 @@ class ValueType(enum.Enum):
BOOL_LIST = 17
UNIX_TIMESTAMP_LIST = 18
NULL = 19
PDF_BYTES = 20
IMAGE_BYTES = 21

MAP = 20
MAP_LIST = 21
PDF_BYTES = 22
IMAGE_BYTES = 23

ListType = Union[
Type[BoolList],
Expand All @@ -61,3 +62,4 @@ class ValueType(enum.Enum):
Type[Int64List],
Type[StringList],
]

Loading