Skip to content

indexed_properties_schema is honoured on Qdrant, ignored on Milvus, and part of collection identity either way #1535

Description

@edwinyyyu

Summary

indexed_properties_schema is documented as a suggestion, so Milvus ignoring it is not strictly a contract violation. The problem is that it is acted on by Qdrant and ignored entirely by Milvus, a caller has no way to learn which, and the value is part of a collection's identity either way — so revising it forces a migration even on the backend where it does nothing.

common/vector_store/data_types.py:27-28:

indexed_properties_schema (dict[str, type[PropertyValue]]):
    Schema suggesting which properties should be indexed for filtering.

Qdrant acts on it

common/vector_store/qdrant_vector_store.py:767-775 iterates the schema and creates a payload index per property:

for prop_name, prop_type in config.indexed_properties_schema.items():
    index_type = QdrantVectorStore._PROPERTY_TYPE_TO_INDEX_TYPE.get(
        prop_type
    )
    if index_type is not None:
        await self._client.create_payload_index(
            collection_name=native_collection_name,
            field_name=prop_name,
            field_schema=index_type,

Milvus does not

common/vector_store/milvus_vector_store.py contains exactly two add_index( calls, at :557 and :692, and both are field_name=_VECTOR_FIELD. No index is created for any property.

The schema is read only to clear stale dynamic fields on upsert, at :192-194:

# Explicit nulls clear stale dynamic fields during native Milvus upserts.
for key in self._config.indexed_properties_schema:
    entity[_property_field(key)] = None

The collection is created with enable_dynamic_field=True (:662), and per Milvus's documentation "any undefined scalar fields are stored as key-value pairs in JSON format" in the reserved $meta field. Declared and undeclared properties are therefore stored identically, and every property filter is evaluated over that JSON. The schema has no effect on Milvus beyond the pre-null.

Why it still matters

The cost is invisible. Queries return correct results, just at scan cost. Two deployments configured identically perform differently for a reason a caller cannot see without reading backend source, and cross-backend benchmarks silently compare different amounts of work.

Revising it is a migration regardless. open_or_create_collection compares the whole config on reopen, milvus_vector_store.py:788-792:

existing_config = MilvusVectorStore._parse_entry(entry)
if existing_config != config:
    raise VectorStoreCollectionConfigMismatchError(
        namespace, name, existing_config, config
    )

So indexed_properties_schema is part of a collection's identity. A deployment that revises it must migrate to a new collection — paying the full cost of a schema change on Milvus for a value that buys nothing there.

The budget is finite where the suggestion is honoured. Qdrant's max_payload_index_count "caps the maximum number of payload index that can exist on a collection"; Qdrant Cloud sets it to 100 and notes that "Larger numbers of payload indexes lead to performance degradation (starting with Qdrant v1.16.0)". Deciding how to spend a budget that small requires knowing which backends spend it at all.

Options

1. Create the indexes. Milvus supports indexing a dynamic-field key: "Milvus supports creating an index on such an undefined scalar field, effectively by building a JSON path index", using json_path plus json_cast_type with INVERTED ("Currently, only INVERTED type is supported for JSON path indexing").

Two caveats worth weighing before choosing this:

2. Say so where a caller will see it. Note in VectorStoreCollectionConfig.indexed_properties_schema and in the Milvus backend docstring which backends act on the suggestion and which ignore it, so the difference is discoverable without reading source.

Either is fine. The problem is that a caller currently cannot find out, while still paying the migration cost of the schema being treated as identity.


Sources. Code read at 231ce171. External claims quoted from
Milvus 2.5 dynamic field documentation and
Qdrant Cloud cluster configuration.

Related: #1534.


🤖 Written by Claude Code (Opus 5) on behalf of @edwinyyyu.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions