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.
Summary
indexed_properties_schemais 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:Qdrant acts on it
common/vector_store/qdrant_vector_store.py:767-775iterates the schema and creates a payload index per property:Milvus does not
common/vector_store/milvus_vector_store.pycontains exactly twoadd_index(calls, at:557and:692, and both arefield_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: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$metafield. 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_collectioncompares the whole config on reopen,milvus_vector_store.py:788-792:So
indexed_properties_schemais 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_pathplusjson_cast_typewithINVERTED("Currently, onlyINVERTEDtype is supported for JSON path indexing").Two caveats worth weighing before choosing this:
json_cast_typeaccepts onlybool,doubleandvarchar, so theintanddatetimeproperty types inPropertyValueneed a deliberate cast choice.VectorStoreCollection(see VectorStoreCollection's undeclared-property mandate: refusable by Qdrant strict mode, unimplementable where attribute types are fixed, unbounded in key cardinality #1534), a key holding more than one type would be silently and partially indexed.2. Say so where a caller will see it. Note in
VectorStoreCollectionConfig.indexed_properties_schemaand 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 fromMilvus 2.5 dynamic field documentation and
Qdrant Cloud cluster configuration.
Related: #1534.
🤖 Written by Claude Code (Opus 5) on behalf of @edwinyyyu.