What happens
Measured on main at ad8ff24 against SQLiteVectorStore and SQLiteVecVectorStore (identical results, "SQLite" below), QdrantVectorStore on Qdrant 1.17.0 (REST and gRPC identical), and MilvusVectorStore on Milvus 2.6.24 and Milvus Lite 3.2.1 (identical unless noted). d_* properties are declared in indexed_properties_schema; undeclared properties give the same results unless noted.
Datetimes and strings (DT is 2024-06-15T12:00:00Z):
| filter |
records |
SQLite |
Qdrant |
Milvus |
d_dt = '2024-06-15T12:00:00Z' (a string) |
datetime DT |
none |
DT |
none |
d_dt = '2024-06-15T12:00:00+00:00' (a string) |
datetime DT, and DT at +05:00 |
none |
none |
both |
d_str = DT (a datetime) |
string '2024-06-15T12:00:00+00:00' |
none |
match |
match |
d_str <= DT |
strings '2', '', and the one above |
none |
the ISO string |
all three, by text order |
Bools against numbers, and ordering on bools and strings:
| filter |
records |
SQLite |
Qdrant |
Milvus |
d_int < True |
ints -1, 0, 1, 2, and 3 |
ArgumentError |
-1 and 0 |
2.6.24: none; Lite: -1 and 0 |
d_bool < True |
True, False |
ArgumentError |
none |
False |
d_str < 'a' |
strings |
byte order |
TypeError from the store |
byte order |
Ints against floats:
| filter |
records |
SQLite |
Qdrant |
Milvus |
d_float = 2 (an int) |
float 2.0 |
none |
none |
match |
d_int = 2.0 (a float) |
int 2 |
none |
match |
match |
d_float < 2 (an int) |
floats 1.5, 0.0, and -0.0 |
none |
all three |
all three |
d_int < 1.5 |
ints -1, 0, and 1 |
none |
-1 and 0 |
all three |
d_int = 1.5 |
int 1 |
none |
match |
none |
On Qdrant the last two hold only for a declared int property; an undeclared one compares by value.
In, whose values are declared list[int] | list[str]:
| filter |
SQLite |
Qdrant |
Milvus |
d_int in [2.0, 1.5] |
none |
ValidationError |
int 2 |
d_bool in [True] |
the True records |
ValidationError |
the True records |
d_str in [DT] |
none |
ValidationError |
the ISO string |
d_int in [1, '2'] |
int 1 |
ValidationError |
2.6.24: int 1; Lite: refused, "list elements must be of compatible types" |
d_int in ['2', 1] |
none |
ValidationError |
same as above |
Why
- SQLite: properties are stored type-tagged (
packages/server/src/memmachine_server/common/properties_json.py:33-63) and every leaf is gated on the filter value's type tag (packages/server/src/memmachine_server/common/filter/sql_filter_util.py:181-187), so an int never matches a float. An ordering on a bool builds column < True, which SQLAlchemy refuses. In takes its type gate and column cast from its first member (:163-178), so the answer depends on member order.
- Qdrant: payload values are stored as given (
packages/server/src/memmachine_server/common/vector_store/qdrant_vector_store.py:253-269). Int, bool, and string equality use MatchValue, float equality a Range(gte=v, lte=v), and datetime equality a DatetimeRange (:112-184); Qdrant applies the DatetimeRange to string payloads that parse as datetimes, and a string MatchValue matches a datetime payload's stored text. A bool passes the store's OrderedValue check (:132) and orders as a number, a string ordering raises in the store (:132-137), and In becomes MatchAny (:186-194), whose model accepts only an all-int or all-string list. On the integer payload index created for a declared int property (:535-543, :750-754), a fractional bound behaves as truncated.
- Milvus: every property is an untyped dynamic JSON field, datetimes as UTC ISO text (
packages/server/src/memmachine_server/common/vector_store/milvus_vector_store.py:92-96, :177-198), and filter literals are rendered the same way (:76-84), so Milvus's own JSON comparison decides: datetimes and strings compare as text, ints and floats by value, and bools as False < True. Milvus 2.6.24 and Milvus Lite disagree on bools against numbers and on mixed In lists.
Expected
Every filter returns the same records on every store: a value is compared only with properties of its own type, ordering is defined only on numbers and datetimes, and an In list holds values of one type.
Fix
#1628 makes a store filter only on the properties it declares, a value of another type than its key declares matching nothing. #1616 closes the filter union and allows ordering only on numbers and datetimes. #1670 and #1702 take user properties out of the vector store. A filter type check is being added on top of #1616.
🤖 Written by Claude Code (Claude Opus 5.5) on behalf of @edwinyyyu.
What happens
Measured on
mainat ad8ff24 againstSQLiteVectorStoreandSQLiteVecVectorStore(identical results, "SQLite" below),QdrantVectorStoreon Qdrant 1.17.0 (REST and gRPC identical), andMilvusVectorStoreon Milvus 2.6.24 and Milvus Lite 3.2.1 (identical unless noted).d_*properties are declared inindexed_properties_schema; undeclared properties give the same results unless noted.Datetimes and strings (
DTis 2024-06-15T12:00:00Z):d_dt = '2024-06-15T12:00:00Z'(a string)DTDTd_dt = '2024-06-15T12:00:00+00:00'(a string)DT, andDTat +05:00d_str = DT(a datetime)'2024-06-15T12:00:00+00:00'd_str <= DT'2','', and the one aboveBools against numbers, and ordering on bools and strings:
d_int < TrueArgumentErrord_bool < TrueTrue,FalseArgumentErrorFalsed_str < 'a'TypeErrorfrom the storeInts against floats:
d_float = 2(an int)d_int = 2.0(a float)d_float < 2(an int)d_int < 1.5d_int = 1.5On Qdrant the last two hold only for a declared
intproperty; an undeclared one compares by value.In, whose values are declaredlist[int] | list[str]:d_int in [2.0, 1.5]ValidationErrord_bool in [True]TruerecordsValidationErrorTruerecordsd_str in [DT]ValidationErrord_int in [1, '2']ValidationErrord_int in ['2', 1]ValidationErrorWhy
packages/server/src/memmachine_server/common/properties_json.py:33-63) and every leaf is gated on the filter value's type tag (packages/server/src/memmachine_server/common/filter/sql_filter_util.py:181-187), so an int never matches a float. An ordering on a bool buildscolumn < True, which SQLAlchemy refuses.Intakes its type gate and column cast from its first member (:163-178), so the answer depends on member order.packages/server/src/memmachine_server/common/vector_store/qdrant_vector_store.py:253-269). Int, bool, and string equality useMatchValue, float equality aRange(gte=v, lte=v), and datetime equality aDatetimeRange(:112-184); Qdrant applies theDatetimeRangeto string payloads that parse as datetimes, and a stringMatchValuematches a datetime payload's stored text. A bool passes the store'sOrderedValuecheck (:132) and orders as a number, a string ordering raises in the store (:132-137), andInbecomesMatchAny(:186-194), whose model accepts only an all-int or all-string list. On the integer payload index created for a declaredintproperty (:535-543,:750-754), a fractional bound behaves as truncated.packages/server/src/memmachine_server/common/vector_store/milvus_vector_store.py:92-96,:177-198), and filter literals are rendered the same way (:76-84), so Milvus's own JSON comparison decides: datetimes and strings compare as text, ints and floats by value, and bools asFalse < True. Milvus 2.6.24 and Milvus Lite disagree on bools against numbers and on mixedInlists.Expected
Every filter returns the same records on every store: a value is compared only with properties of its own type, ordering is defined only on numbers and datetimes, and an
Inlist holds values of one type.Fix
#1628 makes a store filter only on the properties it declares, a value of another type than its key declares matching nothing. #1616 closes the filter union and allows ordering only on numbers and datetimes. #1670 and #1702 take user properties out of the vector store. A filter type check is being added on top of #1616.
🤖 Written by Claude Code (Claude Opus 5.5) on behalf of @edwinyyyu.