MemPalace/mempalace · error · UnsupportedFilterError
Milvus filters do not support null comparisons
Error message
Milvus filters do not support null comparisons
What it means
Raised by _quote_value() in the Milvus backend when a metadata filter value is None. The backend translates the portable Mongo-style where DSL into Milvus filter expression strings, and Milvus filter syntax has no literal for SQL NULL, so any $eq/$ne/$in/etc. operand of None cannot be rendered and is rejected before the query reaches Milvus. It surfaces as UnsupportedFilterError (a BackendError subclass) from any query() call that passes a null filter value.
Source
Thrown at mempalace/backends/milvus.py:93
def _utcnow() -> str:
return datetime.now(timezone.utc).isoformat()
def milvus_uri_is_server(uri: Optional[str]) -> bool:
"""Return whether ``uri`` targets service-managed Milvus storage."""
if not uri:
return False
normalized = uri.strip().lower()
return normalized.startswith(("http://", "https://", "tcp://", "grpc://"))
def _quote_value(value: Any) -> str:
if isinstance(value, bool):
return "true" if value else "false"
if isinstance(value, (int, float)):
return repr(value)
if value is None:
raise UnsupportedFilterError("Milvus filters do not support null comparisons")
text = str(value).replace("\\", "\\\\").replace('"', '\\"')
return f'"{text}"'
def _like_value(value: Any) -> str:
text = str(value).replace("\\", "\\\\").replace('"', '\\"')
return f'"%{text}%"'
def _field_name(name: str) -> str:
if not isinstance(name, str) or not _FIELD_RE.match(name):
raise UnsupportedFilterError(f"Milvus filter field {name!r} is not a safe identifier")
return name
def _translate_field(field: str, expected: Any) -> str:
field = _field_name(field)
if isinstance(expected, dict):View on GitHub (pinned to 06cb6987f0)
Solutions
- Remove None operands from the where clause before calling query (skip keys whose value is None)
- If you need 'field is absent/null' semantics, filter on a sentinel string like "__unset__" stored at ingest time
- Catch UnsupportedFilterError and re-issue the query without the null predicate if null-checking is optional
Example fix
// before
results = collection.query(query_texts=[q], where={"wing": None}, n_results=5)
// after
where = {k: v for k, v in filters.items() if v is not None}
results = collection.query(query_texts=[q], where=where or None, n_results=5) Defensive patterns
Strategy: validation
Validate before calling
def strip_null_filters(where):
if not where:
return None
out = {}
for k, v in where.items():
if isinstance(v, dict):
cleaned = {op: val for op, val in v.items() if val is not None}
if not cleaned:
continue
if "$in" in cleaned or "$nin" in cleaned:
for op in ("$in", "$nin"):
if op in cleaned:
cleaned[op] = [x for x in cleaned[op] if x is not None]
if any(not cleaned[op] for op in ("$in", "$nin") if op in cleaned):
continue
out[k] = cleaned
elif v is not None:
out[k] = v
return out or None Type guard
def has_no_null_values(where: dict) -> bool:
def check(v):
if v is None:
return False
if isinstance(v, dict):
return all(check(x) for x in v.values())
if isinstance(v, list):
return all(check(x) for x in v)
return True
return all(check(v) for v in where.values()) Try / catch
from mempalace.backends.base import UnsupportedFilterError
try:
results = collection.query(query_texts=[q], where=where, n_results=k)
except UnsupportedFilterError as e:
if "null" in str(e):
where = {k: v for k, v in where.items() if v is not None}
results = collection.query(query_texts=[q], where=where, n_results=k)
else:
raise Prevention
- Never put None into where clauses; use sentinel strings for 'unset'
- Centralize filter construction in one builder that drops nulls
- Write a unit test asserting queries with None values are pre-filtered
When it happens
Trigger: Calling query/count/delete with where={"wing": None}, where={"room": {"$ne": None}}, or an $in list containing a None element, e.g. where={"tag": {"$in": ["a", None]}}. Any operator path that routes the operand through _quote_value() with value None triggers it.
Common situations: Code ported from ChromaDB backend (which tolerates None values), dynamically built filters from optional dict fields that default to None, or JSON metadata where a key was explicitly set to null during ingest.
Related errors
- Milvus filter field {name!r} is not a safe identifier
- $in requires a non-empty list for {field!r}
- $nin requires a non-empty list for {field!r}
- operator {op!r} not supported by milvus
- where clause must be a dict, got {type(clause).__name__}
AI-assisted analysis of MemPalace/mempalace@06cb6987f0 (2026-08-15).
Data as JSON: /api/errors/ed8faa6e62341c30.
Report an issue: GitHub.