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

  1. Remove None operands from the where clause before calling query (skip keys whose value is None)
  2. If you need 'field is absent/null' semantics, filter on a sentinel string like "__unset__" stored at ingest time
  3. 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

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


AI-assisted analysis of MemPalace/mempalace@06cb6987f0 (2026-08-15). Data as JSON: /api/errors/ed8faa6e62341c30. Report an issue: GitHub.