MemPalace/mempalace · error · UnsupportedFilterError

operator {key!r} not supported by qdrant

Error message

operator {key!r} not supported by qdrant

What it means

The qdrant backend supports only a fixed set of filter operators (_SUPPORTED_OPERATORS). _validate_where walks the whole filter tree, and any $-prefixed key outside that set raises UnsupportedFilterError before any request hits Qdrant — so unsupported ChromaDB-style filters fail fast with a precise message instead of behaving inconsistently.

Source

Thrown at mempalace/backends/qdrant.py:132

        for term, freq in tf.items():
            num = freq * (k1 + 1)
            den = freq + k1 * (1 - b + b * dl / avgdl)
            score += float(idf[term]) * num / den
        scores.append(score)
    return scores


def _validate_where(where: Optional[dict]) -> None:
    if not where:
        return
    stack = [where]
    while stack:
        node = stack.pop()
        if not isinstance(node, dict):
            continue
        for key, value in node.items():
            if key.startswith("$") and key not in _SUPPORTED_OPERATORS:
                raise UnsupportedFilterError(f"operator {key!r} not supported by qdrant")
            if isinstance(value, dict):
                stack.append(value)
            elif isinstance(value, list):
                stack.extend(item for item in value if isinstance(item, dict))


def _coerce_comparable(value: Any):
    if isinstance(value, bool):
        return int(value)
    return value


def _compare(actual: Any, op: str, expected: Any) -> bool:
    actual = _coerce_comparable(actual)
    expected = _coerce_comparable(expected)
    if op == "$eq":
        return actual == expected
    if op == "$ne":

View on GitHub (pinned to 06cb6987f0)

Solutions

  1. Rewrite the filter using only the operators listed in qdrant.py's _SUPPORTED_OPERATORS (equality, $and/$or, common comparators).
  2. Fetch with a broader pushdown filter and post-filter rows locally via _matches_where-compatible logic.
  3. Centralize filter construction per backend rather than sharing one Mongo-dialect builder.

Example fix

# before
col.get(where={"status": {"$ne": "archived"}})

# after
col.get(where={"$or": [{"status": "active"}, {"status": "pending"}]})
Defensive patterns

Strategy: validation

Validate before calling

SUPPORTED = {"$and", "$or", "$eq", "$ne", "$gt", "$gte", "$lt", "$lte", "$in", "$contains"}
def validate_filter(where):
    stack = [where]
    while stack:
        node = stack.pop()
        if not isinstance(node, dict):
            continue
        for k in node:
            if k.startswith("$") and k not in SUPPORTED:
                raise ValueError(f"filter uses unsupported operator {k}")
            v = node[k]
            if isinstance(v, dict): stack.append(v)
            elif isinstance(v, list): stack.extend(x for x in v if isinstance(x, dict))
    return where

Type guard

def is_supported_filter(where: dict, supported: set) -> bool:
    stack = [where]
    while stack:
        node = stack.pop()
        if not isinstance(node, dict): continue
        for k, v in node.items():
            if k.startswith("$") and k not in supported: return False
            if isinstance(v, dict): stack.append(v)
            elif isinstance(v, list): stack.extend(x for x in v if isinstance(x, dict))
    return True

Try / catch

try:
    col.get(where=filters)
except UnsupportedFilterError as e:
    logger.warning("filter rejected by qdrant, simplifying: %s", e)
    col.get(where=simplify(filters))

Prevention

When it happens

Trigger: Passing where={"meta": {"$nin": [1,2]}} or operators like $ne/$not (whichever are absent from _SUPPORTED_OPERATORS) to get/query/count on a qdrant collection.

Common situations: Copying filters from ChromaDB code; a shared filter-builder utility emitting the full Mongo-style operator set against multiple backends.

Related errors


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