Skip to content

feat: Add feature view versioning to Qdrant online store - #6753

Open
arose26 wants to merge 1 commit into
feast-dev:masterfrom
arose26:feat/qdrant-versioned-collections
Open

feat: Add feature view versioning to Qdrant online store#6753
arose26 wants to merge 1 commit into
feast-dev:masterfrom
arose26:feat/qdrant-versioned-collections

Conversation

@arose26

@arose26 arose26 commented Aug 18, 2026

Copy link
Copy Markdown

Closes #6179. Part of #2728.

What this does

Routes every Qdrant collection reference through one helper, so that with
registry.enable_online_feature_view_versioning: true each feature view version gets
its own collection (driver_stats_v2) instead of all versions sharing one. Applied to
the write, create, update, teardown and document-retrieval paths.

One deliberate deviation from the FAISS/Milvus precedent

Those stores use compute_table_id(project, table, versioning), which yields
{project}_{name}[_v{N}]. This PR uses compute_versioned_name instead, which yields
{name}[_v{N}] with no project prefix.

The reason is that Qdrant collections have always been named by the bare table.name
unlike Milvus, which already had the {project}_ prefix before versioning was added to it,
so gaining the flag there was non-breaking. Switching Qdrant to compute_table_id would
rename every collection in every existing deployment and orphan their data. With versioning
disabled the collection name here is byte-identical to today's, which
test_unversioned_store_still_round_trips pins.

Happy to switch to the project-prefixed form if you'd rather have consistency across
stores and want to handle the migration — just say so.

Why online_read is not added to the versioned-read allowlist

I did not add QdrantOnlineStore to OnlineStore._is_versioned_read_supported(), because
QdrantOnlineStore.online_read cannot currently serve any read, versioned or not. It looks
like it has never been exercised — there is no unit test for it, and it fails on two
independent counts before reaching Qdrant:

  1. it passes raw EntityKeyProto objects into models.MatchAny(any=entity_keys), which
    pydantic rejects (Input should be a valid string), and
  2. it reads collection_name=config.online_store.collection_name, but neither
    QdrantOnlineStoreConfig nor VectorStoreConfig defines collection_name, so that
    line raises AttributeError.

It also returns one entry per stored point holding a base64 str, where the
OnlineStore.online_read contract (see sqlite.py) is one entry per requested entity key,
in order, holding ValueProto values, with (None, None) for a miss.

Reproduction, stock main, no external service — an in-process Qdrant is enough:

from datetime import datetime, timedelta
from feast import Entity, FeatureView, Field, FileSource, RepoConfig
from feast.types import Int64, String
from feast.value_type import ValueType
from feast.protos.feast.types.EntityKey_pb2 import EntityKey as EntityKeyProto
from feast.protos.feast.types.Value_pb2 import Value as ValueProto
from feast.infra.online_stores.qdrant_online_store.qdrant import (
    QdrantOnlineStore, QdrantOnlineStoreConfig,
)

cfg = RepoConfig(
    project="proj",
    online_store=QdrantOnlineStoreConfig(
        type="qdrant", location=":memory:", vector_enabled=True, similarity="cosine"
    ),
    registry="dummy",
    entity_key_serialization_version=3,
)
fv = FeatureView(
    name="stats",
    entities=[Entity(name="user_id", value_type=ValueType.INT64)],
    ttl=timedelta(days=1),
    schema=[Field(name="user_id", dtype=Int64), Field(name="f1", dtype=String)],
    source=FileSource(path="t.parquet", timestamp_field="event_timestamp"),
)
store = QdrantOnlineStore()
store.update(cfg, [], [fv], [], [], partial=False)

ek = EntityKeyProto(join_keys=["user_id"], entity_values=[ValueProto(int64_val=1)])
store.online_write_batch(
    cfg, fv, [(ek, {"f1": ValueProto(string_val="v1")}, datetime(2024, 1, 1), None)], None
)          # -> succeeds

store.online_read(cfg, fv, [ek], ["f1"])
# pydantic_core._pydantic_core.ValidationError: 2 validation errors for MatchAny
#   any.list[str].0  Input should be a valid string ... input_type=EntityKey

Fixing that properly means deciding how entity_key should be stored — it is currently
written into the payload as raw bytes from serialize_entity_key, which MatchAny cannot
filter on, and which retrieve_online_documents then reads back through
str(payload.get("entity_key")), producing a bytes repr rather than the value
_build_retrieve_online_document_record expects. That is a storage-format call I did not
want to make unilaterally inside a versioning PR, so I have kept it out of scope. Happy to
open a separate issue, or to take it in a follow-up once you've said which encoding you want.

So this PR delivers the versioned collection namespace; versioned scalar reads stay
correctly gated behind VersionedOnlineReadNotSupported until online_read works.

Tests

New sdk/python/tests/unit/infra/online_store/test_qdrant_online_store.py, 6 tests, running
against an in-process Qdrant (location=":memory:") — no service required, and skipped via
importorskip when qdrant-client is absent.

  • naming: unversioned unchanged, version tag ignored when the flag is off, _v2 when on
  • test_versions_write_to_separate_collections — v1 and v2 collections both created, one
    point each, no cross-contamination
  • test_teardown_removes_only_the_targeted_version
  • test_unversioned_store_still_round_trips — the control

Verified red-before/green-after: with the change reverted, the two versioning behaviour
tests fail while the unversioned control still passes, so they are testing the change rather
than the setup.

Regression check across the 56 unit test files touching online stores: the set of failing
and erroring tests is identical with and without this change (diff of the sorted
FAILED/ERROR lines is empty). Those pre-existing failures are missing optional
dependencies in my environment. ruff check, ruff format and mypy are clean on both
files.


🤖 Written with Claude Code (Claude Opus 5), reviewed by @arose26.

Route every Qdrant collection reference through a single helper so that,
when registry.enable_online_feature_view_versioning is set, each feature
view version gets its own collection (driver_stats_v2) instead of all
versions sharing one.

The helper is built on compute_versioned_name rather than
compute_table_id, which the other stores use. Qdrant collections have
always been named by the bare feature view name, with no {project}_
prefix, so adopting compute_table_id would rename every collection in
every existing deployment. With versioning disabled the collection name
is byte-identical to today's.

Covered by write, update, teardown and document-retrieval paths.
online_read is intentionally left out of the versioned-read allowlist:
it has a separate pre-existing defect and cannot serve a versioned read
correctly yet. See the PR description for a runnable reproduction.

Part of feast-dev#2728. Closes feast-dev#6179

Signed-off-by: arose26 <145766958+arose26@users.noreply.github.com>
@arose26
arose26 requested a review from a team as a code owner August 18, 2026 11:13
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Add feature view versioning support to Qdrant online store

1 participant