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.

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.

Appendix: 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)