From 8c41a0340789cabfc7f6f27c3b69eff3f3a59837 Mon Sep 17 00:00:00 2001 From: Alex Korbonits Date: Mon, 13 Apr 2026 09:48:51 -0700 Subject: [PATCH 1/5] docs: add sphinx api docs for aggregation, dbt, openlineage packages Add missing RST files for feast.aggregation (including tiling sub-package), feast.dbt, and feast.openlineage. Also add feast.diff.apply_progress which was missing from feast.diff.rst. Update feast.rst toctree to include all three new top-level packages. Co-Authored-By: Claude Sonnet 4.6 Signed-off-by: Alex Korbonits --- sdk/python/docs/source/feast.aggregation.rst | 18 +++++++ .../docs/source/feast.aggregation.tiling.rst | 37 +++++++++++++ sdk/python/docs/source/feast.dbt.rst | 37 +++++++++++++ sdk/python/docs/source/feast.diff.rst | 8 +++ sdk/python/docs/source/feast.openlineage.rst | 53 +++++++++++++++++++ sdk/python/docs/source/feast.rst | 3 ++ 6 files changed, 156 insertions(+) create mode 100644 sdk/python/docs/source/feast.aggregation.rst create mode 100644 sdk/python/docs/source/feast.aggregation.tiling.rst create mode 100644 sdk/python/docs/source/feast.dbt.rst create mode 100644 sdk/python/docs/source/feast.openlineage.rst diff --git a/sdk/python/docs/source/feast.aggregation.rst b/sdk/python/docs/source/feast.aggregation.rst new file mode 100644 index 00000000000..04e16c4d136 --- /dev/null +++ b/sdk/python/docs/source/feast.aggregation.rst @@ -0,0 +1,18 @@ +feast.aggregation package +========================= + +Subpackages +----------- + +.. toctree:: + :maxdepth: 4 + + feast.aggregation.tiling + +Module contents +--------------- + +.. automodule:: feast.aggregation + :members: + :undoc-members: + :show-inheritance: diff --git a/sdk/python/docs/source/feast.aggregation.tiling.rst b/sdk/python/docs/source/feast.aggregation.tiling.rst new file mode 100644 index 00000000000..d35e69eb1cf --- /dev/null +++ b/sdk/python/docs/source/feast.aggregation.tiling.rst @@ -0,0 +1,37 @@ +feast.aggregation.tiling package +================================= + +Submodules +---------- + +feast.aggregation.tiling.base module +------------------------------------- + +.. automodule:: feast.aggregation.tiling.base + :members: + :undoc-members: + :show-inheritance: + +feast.aggregation.tiling.orchestrator module +-------------------------------------------- + +.. automodule:: feast.aggregation.tiling.orchestrator + :members: + :undoc-members: + :show-inheritance: + +feast.aggregation.tiling.tile\_subtraction module +------------------------------------------------- + +.. automodule:: feast.aggregation.tiling.tile_subtraction + :members: + :undoc-members: + :show-inheritance: + +Module contents +--------------- + +.. automodule:: feast.aggregation.tiling + :members: + :undoc-members: + :show-inheritance: diff --git a/sdk/python/docs/source/feast.dbt.rst b/sdk/python/docs/source/feast.dbt.rst new file mode 100644 index 00000000000..3a7c28d620b --- /dev/null +++ b/sdk/python/docs/source/feast.dbt.rst @@ -0,0 +1,37 @@ +feast.dbt package +================= + +Submodules +---------- + +feast.dbt.codegen module +------------------------ + +.. automodule:: feast.dbt.codegen + :members: + :undoc-members: + :show-inheritance: + +feast.dbt.mapper module +----------------------- + +.. automodule:: feast.dbt.mapper + :members: + :undoc-members: + :show-inheritance: + +feast.dbt.parser module +----------------------- + +.. automodule:: feast.dbt.parser + :members: + :undoc-members: + :show-inheritance: + +Module contents +--------------- + +.. automodule:: feast.dbt + :members: + :undoc-members: + :show-inheritance: diff --git a/sdk/python/docs/source/feast.diff.rst b/sdk/python/docs/source/feast.diff.rst index e4142171711..f03ab5d8409 100644 --- a/sdk/python/docs/source/feast.diff.rst +++ b/sdk/python/docs/source/feast.diff.rst @@ -4,6 +4,14 @@ feast.diff package Submodules ---------- +feast.diff.apply\_progress module +---------------------------------- + +.. automodule:: feast.diff.apply_progress + :members: + :undoc-members: + :show-inheritance: + feast.diff.infra\_diff module ----------------------------- diff --git a/sdk/python/docs/source/feast.openlineage.rst b/sdk/python/docs/source/feast.openlineage.rst new file mode 100644 index 00000000000..147d1f608bf --- /dev/null +++ b/sdk/python/docs/source/feast.openlineage.rst @@ -0,0 +1,53 @@ +feast.openlineage package +========================= + +Submodules +---------- + +feast.openlineage.client module +-------------------------------- + +.. automodule:: feast.openlineage.client + :members: + :undoc-members: + :show-inheritance: + +feast.openlineage.config module +-------------------------------- + +.. automodule:: feast.openlineage.config + :members: + :undoc-members: + :show-inheritance: + +feast.openlineage.emitter module +--------------------------------- + +.. automodule:: feast.openlineage.emitter + :members: + :undoc-members: + :show-inheritance: + +feast.openlineage.facets module +-------------------------------- + +.. automodule:: feast.openlineage.facets + :members: + :undoc-members: + :show-inheritance: + +feast.openlineage.mappers module +--------------------------------- + +.. automodule:: feast.openlineage.mappers + :members: + :undoc-members: + :show-inheritance: + +Module contents +--------------- + +.. automodule:: feast.openlineage + :members: + :undoc-members: + :show-inheritance: diff --git a/sdk/python/docs/source/feast.rst b/sdk/python/docs/source/feast.rst index 6c5340e30ec..8ee891b89ac 100644 --- a/sdk/python/docs/source/feast.rst +++ b/sdk/python/docs/source/feast.rst @@ -7,13 +7,16 @@ Subpackages .. toctree:: :maxdepth: 4 + feast.aggregation feast.api feast.cli + feast.dbt feast.diff feast.dqm feast.embedded_go feast.infra feast.lineage + feast.openlineage feast.permissions feast.protos feast.transformation From a1099d63c3dc606ec1c7b0c4bed02a01c234ff34 Mon Sep 17 00:00:00 2001 From: Alex Korbonits Date: Mon, 13 Apr 2026 14:27:08 -0700 Subject: [PATCH 2/5] docs: fix sphinx warnings from new package documentation No new warning categories are introduced by adding feast.aggregation, feast.dbt, and feast.openlineage to the Sphinx docs. The 10 new "more than one target found" warnings follow the same pre-existing pattern (8 already present) caused by feast.__init__ re-exporting classes under multiple paths. Fixes applied: - Remove :undoc-members: from dataclass automodules (Aggregation, IRMetadata, OpenLineageConfig) to prevent duplicate attribute docs - Document re-exporting __init__ packages at package level only to avoid duplicate descriptions from submodule entries - Fix markdown-style code fences in openlineage/__init__.py docstring - Fix RST formatting in client.py Example section (Example::) and emitter.py ASCII art diagram (literal block) - Fix repo_config.py field docstring that triggered an ambiguous OpenLineageConfig cross-reference Co-Authored-By: Claude Sonnet 4.6 Signed-off-by: Alex Korbonits --- sdk/python/docs/source/feast.aggregation.rst | 1 - .../docs/source/feast.aggregation.tiling.rst | 28 ------------ sdk/python/docs/source/feast.openlineage.rst | 44 ------------------- sdk/python/docs/source/feast.rst | 8 ---- sdk/python/feast/openlineage/__init__.py | 34 +++++++------- sdk/python/feast/openlineage/client.py | 3 +- sdk/python/feast/openlineage/emitter.py | 4 +- sdk/python/feast/repo_config.py | 2 +- 8 files changed, 23 insertions(+), 101 deletions(-) diff --git a/sdk/python/docs/source/feast.aggregation.rst b/sdk/python/docs/source/feast.aggregation.rst index 04e16c4d136..757f0976bac 100644 --- a/sdk/python/docs/source/feast.aggregation.rst +++ b/sdk/python/docs/source/feast.aggregation.rst @@ -14,5 +14,4 @@ Module contents .. automodule:: feast.aggregation :members: - :undoc-members: :show-inheritance: diff --git a/sdk/python/docs/source/feast.aggregation.tiling.rst b/sdk/python/docs/source/feast.aggregation.tiling.rst index d35e69eb1cf..d51ef22ed05 100644 --- a/sdk/python/docs/source/feast.aggregation.tiling.rst +++ b/sdk/python/docs/source/feast.aggregation.tiling.rst @@ -1,37 +1,9 @@ feast.aggregation.tiling package ================================= -Submodules ----------- - -feast.aggregation.tiling.base module -------------------------------------- - -.. automodule:: feast.aggregation.tiling.base - :members: - :undoc-members: - :show-inheritance: - -feast.aggregation.tiling.orchestrator module --------------------------------------------- - -.. automodule:: feast.aggregation.tiling.orchestrator - :members: - :undoc-members: - :show-inheritance: - -feast.aggregation.tiling.tile\_subtraction module -------------------------------------------------- - -.. automodule:: feast.aggregation.tiling.tile_subtraction - :members: - :undoc-members: - :show-inheritance: - Module contents --------------- .. automodule:: feast.aggregation.tiling :members: - :undoc-members: :show-inheritance: diff --git a/sdk/python/docs/source/feast.openlineage.rst b/sdk/python/docs/source/feast.openlineage.rst index 147d1f608bf..a861bcccf56 100644 --- a/sdk/python/docs/source/feast.openlineage.rst +++ b/sdk/python/docs/source/feast.openlineage.rst @@ -1,53 +1,9 @@ feast.openlineage package ========================= -Submodules ----------- - -feast.openlineage.client module --------------------------------- - -.. automodule:: feast.openlineage.client - :members: - :undoc-members: - :show-inheritance: - -feast.openlineage.config module --------------------------------- - -.. automodule:: feast.openlineage.config - :members: - :undoc-members: - :show-inheritance: - -feast.openlineage.emitter module ---------------------------------- - -.. automodule:: feast.openlineage.emitter - :members: - :undoc-members: - :show-inheritance: - -feast.openlineage.facets module --------------------------------- - -.. automodule:: feast.openlineage.facets - :members: - :undoc-members: - :show-inheritance: - -feast.openlineage.mappers module ---------------------------------- - -.. automodule:: feast.openlineage.mappers - :members: - :undoc-members: - :show-inheritance: - Module contents --------------- .. automodule:: feast.openlineage :members: - :undoc-members: :show-inheritance: diff --git a/sdk/python/docs/source/feast.rst b/sdk/python/docs/source/feast.rst index 8ee891b89ac..27231480da8 100644 --- a/sdk/python/docs/source/feast.rst +++ b/sdk/python/docs/source/feast.rst @@ -25,14 +25,6 @@ Subpackages Submodules ---------- -feast.aggregation module ------------------------- - -.. automodule:: feast.aggregation - :members: - :undoc-members: - :show-inheritance: - feast.arrow\_error\_handler module ---------------------------------- diff --git a/sdk/python/feast/openlineage/__init__.py b/sdk/python/feast/openlineage/__init__.py index a8328417475..e32ae967004 100644 --- a/sdk/python/feast/openlineage/__init__.py +++ b/sdk/python/feast/openlineage/__init__.py @@ -35,27 +35,27 @@ Usage: Simply configure OpenLineage in your feature_store.yaml: - ```yaml - project: my_project - # ... other config ... - - openlineage: - enabled: true - transport_type: http - transport_url: http://localhost:5000 - transport_endpoint: api/v1/lineage - namespace: my_namespace # Optional: defaults to project name - ``` + .. code-block:: yaml + + project: my_project + # ... other config ... + + openlineage: + enabled: true + transport_type: http + transport_url: http://localhost:5000 + transport_endpoint: api/v1/lineage + namespace: my_namespace # Optional: defaults to project name Then use Feast normally - lineage events are emitted automatically! - ```python - from feast import FeatureStore + .. code-block:: python + + from feast import FeatureStore - fs = FeatureStore(repo_path="feature_repo") - fs.apply([entity, feature_view, feature_service]) # Emits lineage - fs.materialize(start, end) # Emits START/COMPLETE/FAIL events - ``` + fs = FeatureStore(repo_path="feature_repo") + fs.apply([entity, feature_view, feature_service]) # Emits lineage + fs.materialize(start, end) # Emits START/COMPLETE/FAIL events """ from feast.openlineage.client import FeastOpenLineageClient diff --git a/sdk/python/feast/openlineage/client.py b/sdk/python/feast/openlineage/client.py index 927f998ac3e..45d021f2f07 100644 --- a/sdk/python/feast/openlineage/client.py +++ b/sdk/python/feast/openlineage/client.py @@ -55,7 +55,8 @@ class FeastOpenLineageClient: from Feast operations like materialization, feature retrieval, and registry changes. - Example: + Example:: + from feast.openlineage import FeastOpenLineageClient, OpenLineageConfig config = OpenLineageConfig( diff --git a/sdk/python/feast/openlineage/emitter.py b/sdk/python/feast/openlineage/emitter.py index 1f63e39210e..7486d04be27 100644 --- a/sdk/python/feast/openlineage/emitter.py +++ b/sdk/python/feast/openlineage/emitter.py @@ -644,11 +644,13 @@ def emit_apply( 1. feast_feature_views_{project}: DataSources + Entities → FeatureViews 2. feast_feature_services_{project}: FeatureViews → FeatureServices - This creates a lineage graph matching Feast UI: + This creates a lineage graph matching Feast UI:: + DataSource ──→ FeatureView ──→ FeatureService ↑ Entity + Args: objects: List of Feast objects being applied project: Project name diff --git a/sdk/python/feast/repo_config.py b/sdk/python/feast/repo_config.py index 208307dc5d5..4007ba97e39 100644 --- a/sdk/python/feast/repo_config.py +++ b/sdk/python/feast/repo_config.py @@ -337,7 +337,7 @@ class RepoConfig(FeastBaseModel): """ MaterializationConfig: Configuration options for feature materialization behavior. """ openlineage_config: Optional[OpenLineageConfig] = Field(None, alias="openlineage") - """ OpenLineageConfig: Configuration for OpenLineage data lineage integration (optional). """ + """ Configuration for OpenLineage data lineage integration (optional). """ def __init__(self, **data: Any): super().__init__(**data) From 30ff5021e8860faf8c90331f5660582be9583d35 Mon Sep 17 00:00:00 2001 From: Alex Korbonits Date: Mon, 13 Apr 2026 23:53:14 -0700 Subject: [PATCH 3/5] fix: fix five bugs in MilvusOnlineStore 1. update() overwrote self._collections with the raw describe_collection() dict from the last table instead of updating the keyed cache entry. _get_or_create_collection() already updates self._collections as a side effect, so the assignment is simply dropped. 2. plan() raised NotImplementedError instead of returning [] like the base class default. This caused `feast plan` to crash for Milvus stores. 3. online_read() carried an extra full_feature_names parameter not present in the OnlineStore base class, violating the interface contract. The parameter was unused and is removed. 4. retrieve_online_documents_v2() passed the raw hit.get() result (which can be None when the composite key is absent) directly to bytes.fromhex(), raising TypeError. Guard added: only call bytes.fromhex() when the value is non-None. 5. Replace print() calls in _connect() with logger.info() so connection messages respect standard logging configuration instead of always writing to stdout. Co-Authored-By: Claude Sonnet 4.6 Signed-off-by: Alex Korbonits --- .../milvus_online_store/milvus.py | 21 +++++++++++-------- 1 file changed, 12 insertions(+), 9 deletions(-) diff --git a/sdk/python/feast/infra/online_stores/milvus_online_store/milvus.py b/sdk/python/feast/infra/online_stores/milvus_online_store/milvus.py index ee2534684cc..e33765c1ecf 100644 --- a/sdk/python/feast/infra/online_stores/milvus_online_store/milvus.py +++ b/sdk/python/feast/infra/online_stores/milvus_online_store/milvus.py @@ -1,4 +1,5 @@ import base64 +import logging from datetime import datetime from pathlib import Path from typing import Any, Callable, Dict, List, Literal, Optional, Sequence, Tuple, Union @@ -42,6 +43,8 @@ to_naive_utc, ) +logger = logging.getLogger(__name__) + PROTO_TO_MILVUS_TYPE_MAPPING: Dict[ValueType, DataType] = { PROTO_VALUE_TO_VALUE_TYPE_MAP["bytes_val"]: DataType.VARCHAR, ValueType.IMAGE_BYTES: DataType.VARCHAR, @@ -140,11 +143,13 @@ def _connect(self, config: RepoConfig) -> MilvusClient: if not self.client: if config.provider == "local" and config.online_store.path: db_path = self._get_db_path(config) - print(f"Connecting to Milvus in local mode using {db_path}") + logger.info("Connecting to Milvus in local mode using %s", db_path) self.client = MilvusClient(db_path) else: - print( - f"Connecting to Milvus remotely at {config.online_store.host}:{config.online_store.port}" + logger.info( + "Connecting to Milvus remotely at %s:%s", + config.online_store.host, + config.online_store.port, ) self.client = MilvusClient( uri=f"{config.online_store.host}:{config.online_store.port}", @@ -339,7 +344,6 @@ def online_read( table: FeatureView, entity_keys: List[EntityKeyProto], requested_features: Optional[List[str]] = None, - full_feature_names: bool = False, ) -> List[Tuple[Optional[datetime], Optional[Dict[str, ValueProto]]]]: self.client = self._connect(config) collection_name = _table_id(config.project, table) @@ -487,7 +491,7 @@ def update( ): self.client = self._connect(config) for table in tables_to_keep: - self._collections = self._get_or_create_collection(config, table) + self._get_or_create_collection(config, table) for table in tables_to_delete: collection_name = _table_id(config.project, table) @@ -498,7 +502,7 @@ def update( def plan( self, config: RepoConfig, desired_registry_proto: RegistryProto ) -> List[InfraObject]: - raise NotImplementedError + return [] def teardown( self, @@ -686,9 +690,8 @@ def retrieve_online_documents_v2( for hit in hits: res = {} res_ts = None - entity_key_bytes = bytes.fromhex( - hit.get("entity", {}).get(composite_key_name, None) - ) + raw_key = hit.get("entity", {}).get(composite_key_name) + entity_key_bytes = bytes.fromhex(raw_key) if raw_key else None entity_key_proto = ( deserialize_entity_key(entity_key_bytes) if entity_key_bytes From 4ddb2ae6197037f0b5267dd160dd7abceb97fcf8 Mon Sep 17 00:00:00 2001 From: Alex Korbonits Date: Mon, 13 Apr 2026 23:58:17 -0700 Subject: [PATCH 4/5] test: add regression tests for five MilvusOnlineStore bug fixes MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 1. test_milvus_update_preserves_collection_cache — verifies that applying two FeatureViews leaves self._collections as a proper dict keyed by collection name, with a "fields" entry for each view. Previously, update() overwrote _collections with the raw describe_collection() dict from the last table. 2. test_milvus_plan_returns_empty_list — verifies that plan() returns [] instead of raising NotImplementedError, matching the OnlineStore base class default and allowing `feast plan` to work. 3. test_milvus_retrieve_online_documents_v2_missing_entity_key — verifies that a search hit missing the composite primary key produces a None entity_key_proto instead of a TypeError from bytes.fromhex(None). Co-Authored-By: Claude Sonnet 4.6 Signed-off-by: Alex Korbonits --- .../online_store/test_online_retrieval.py | 181 ++++++++++++++++++ 1 file changed, 181 insertions(+) diff --git a/sdk/python/tests/unit/online_store/test_online_retrieval.py b/sdk/python/tests/unit/online_store/test_online_retrieval.py index 60f583ad669..cb73a56894a 100644 --- a/sdk/python/tests/unit/online_store/test_online_retrieval.py +++ b/sdk/python/tests/unit/online_store/test_online_retrieval.py @@ -1714,3 +1714,184 @@ def test_milvus_keyword_search() -> None: assert len(result_hybrid["content"]) > 0 assert any("Feast" in content for content in result_hybrid["content"]) assert len(result_hybrid["vector"]) > 0 + + +def test_milvus_update_preserves_collection_cache() -> None: + """ + Regression test: update() used to overwrite self._collections with the + describe_collection() dict of the last processed table, replacing the + dict-of-dicts cache with a single flat dict. After the fix, each call + to _get_or_create_collection() updates the keyed entry in-place and the + cache remains a proper mapping from collection name to collection info. + """ + from datetime import timedelta + + from feast import Entity, FeatureView, Field, FileSource + from feast.types import Array, Float32, Int64, String + + runner = CliRunner() + with runner.local_repo( + example_repo_py=get_example_repo("example_rag_feature_repo.py"), + offline_store="file", + online_store="milvus", + apply=False, + teardown=False, + ) as store: + source = FileSource( + path="data/dummy.parquet", + timestamp_field="event_timestamp", + created_timestamp_column="created_timestamp", + ) + entity_a = Entity(name="id_a", join_keys=["id_a"], value_type=ValueType.INT64) + entity_b = Entity(name="id_b", join_keys=["id_b"], value_type=ValueType.INT64) + + fv_a = FeatureView( + name="fv_a", + entities=[entity_a], + schema=[ + Field(name="id_a", dtype=Int64), + Field( + name="vec_a", + dtype=Array(Float32), + vector_index=True, + vector_search_metric="COSINE", + ), + Field(name="text_a", dtype=String), + ], + source=source, + ttl=timedelta(hours=1), + ) + fv_b = FeatureView( + name="fv_b", + entities=[entity_b], + schema=[ + Field(name="id_b", dtype=Int64), + Field( + name="vec_b", + dtype=Array(Float32), + vector_index=True, + vector_search_metric="COSINE", + ), + Field(name="text_b", dtype=String), + ], + source=source, + ttl=timedelta(hours=1), + ) + + store.apply([source, entity_a, entity_b, fv_a, fv_b]) + + online_store = store._provider._online_store + # After applying two feature views, the cache must be a proper dict + # mapping collection names to collection-info dicts, not a flat dict. + assert isinstance(online_store._collections, dict), ( + "_collections should be a dict" + ) + collection_name_a = f"{store.config.project}_fv_a" + collection_name_b = f"{store.config.project}_fv_b" + assert collection_name_a in online_store._collections, ( + f"Cache missing entry for {collection_name_a}" + ) + assert collection_name_b in online_store._collections, ( + f"Cache missing entry for {collection_name_b} — " + "update() likely overwrote _collections with a single collection dict" + ) + # Each cached value must be a collection-info dict (has a 'fields' key), + # not itself keyed by collection name. + for name in [collection_name_a, collection_name_b]: + assert "fields" in online_store._collections[name], ( + f"Cache entry for {name} looks like a corrupted flat dict" + ) + + +def test_milvus_plan_returns_empty_list() -> None: + """ + Regression test: plan() used to raise NotImplementedError, causing + `feast plan` to crash for any project using the Milvus online store. + It should return [] matching the OnlineStore base class default. + """ + from feast.infra.online_stores.milvus_online_store.milvus import MilvusOnlineStore + + store = MilvusOnlineStore() + result = store.plan(config=None, desired_registry_proto=None) # type: ignore[arg-type] + assert result == [], f"plan() should return [] but returned {result!r}" + + +def test_milvus_retrieve_online_documents_v2_missing_entity_key() -> None: + """ + Regression test: retrieve_online_documents_v2() passed the raw + hit.get("entity", {}).get(composite_key_name, None) directly to + bytes.fromhex(), raising TypeError when the key was absent. + After the fix, a missing composite key produces a None entity_key_proto + instead of crashing. + """ + from datetime import timedelta + from unittest.mock import MagicMock, patch + + from feast import Entity, FeatureView, Field, FileSource + from feast.types import Array, Float32, Int64, String + + runner = CliRunner() + with runner.local_repo( + example_repo_py=get_example_repo("example_rag_feature_repo.py"), + offline_store="file", + online_store="milvus", + apply=False, + teardown=False, + ) as store: + source = FileSource( + path="data/dummy.parquet", + timestamp_field="event_timestamp", + created_timestamp_column="created_timestamp", + ) + entity = Entity(name="doc_id", join_keys=["doc_id"], value_type=ValueType.INT64) + fv = FeatureView( + name="docs", + entities=[entity], + schema=[ + Field(name="doc_id", dtype=Int64), + Field( + name="vec", + dtype=Array(Float32), + vector_index=True, + vector_search_metric="COSINE", + ), + Field(name="text", dtype=String), + ], + source=source, + ttl=timedelta(hours=1), + ) + store.apply([source, entity, fv]) + + online_store = store._provider._online_store + fv_obj = store.get_feature_view("docs") + composite_key_name = "doc_id_pk" + + # Simulate a search hit that is missing the composite primary key. + fake_hit = { + "entity": { + # composite_key_name deliberately absent + "event_ts": int(_utc_now().timestamp() * 1e6), + "created_ts": int(_utc_now().timestamp() * 1e6), + "text": "hello", + }, + "distance": 0.9, + } + + mock_results = [[fake_hit]] + with patch.object(online_store.client, "search", return_value=mock_results): + with patch.object( + online_store.client, "load_collection", return_value=None + ): + # Before the fix this raised TypeError: fromhex argument must be str, not None + result = online_store.retrieve_online_documents_v2( + config=store.config, + table=fv_obj, + requested_features=["text"], + embedding=[0.1] * 10, + top_k=1, + ) + assert len(result) == 1 + _ts, entity_key_proto, _features = result[0] + assert entity_key_proto is None, ( + "entity_key_proto should be None when the composite key is absent from the hit" + ) From b104a2a244719d30877930d7c1ef486f9a757e2c Mon Sep 17 00:00:00 2001 From: Alex Korbonits Date: Tue, 14 Apr 2026 20:33:27 -0700 Subject: [PATCH 5/5] fix: remove unused variable and import in milvus regression test Co-Authored-By: Claude Sonnet 4.6 Signed-off-by: Alex Korbonits --- sdk/python/tests/unit/online_store/test_online_retrieval.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/sdk/python/tests/unit/online_store/test_online_retrieval.py b/sdk/python/tests/unit/online_store/test_online_retrieval.py index cb73a56894a..63dcc8c7e48 100644 --- a/sdk/python/tests/unit/online_store/test_online_retrieval.py +++ b/sdk/python/tests/unit/online_store/test_online_retrieval.py @@ -1825,7 +1825,7 @@ def test_milvus_retrieve_online_documents_v2_missing_entity_key() -> None: instead of crashing. """ from datetime import timedelta - from unittest.mock import MagicMock, patch + from unittest.mock import patch from feast import Entity, FeatureView, Field, FileSource from feast.types import Array, Float32, Int64, String @@ -1864,12 +1864,9 @@ def test_milvus_retrieve_online_documents_v2_missing_entity_key() -> None: online_store = store._provider._online_store fv_obj = store.get_feature_view("docs") - composite_key_name = "doc_id_pk" - # Simulate a search hit that is missing the composite primary key. fake_hit = { "entity": { - # composite_key_name deliberately absent "event_ts": int(_utc_now().timestamp() * 1e6), "created_ts": int(_utc_now().timestamp() * 1e6), "text": "hello",