spring-projects/spring-ai · error · IllegalArgumentException

Unsupported operand type:

Error message

Unsupported operand type: 

What it means

SimpleVectorStoreFilterExpressionEvaluator.evaluateOperand throws IllegalArgumentException 'Unsupported operand type: <class name>' when a Filter.Operand passed to it is not a Filter.Expression, Filter.Key, or Filter.Value. Per the source, Key/Value leaves are consumed inside evaluateExpression, so reaching this throw means the expression tree contains an operand type the evaluator does not recognize.

Solutions

  1. Inspect the logged class name and build filters only via supported DSL operators (and/or/not/eq/gt/lt/in etc.).
  2. Do not pass Filter.Key/Filter.Value to evaluateOperand directly; wrap them in a comparison Expression.
  3. Upgrade spring-ai so the evaluator recognizes any newly introduced operand types.

Example fix

// before
evaluator.evaluateOperand(new Filter.Key("genre"), metadata); // unsupported here

// after
Filter.Expression expr = new Filter.Expression(ExpressionType.EQ,
    new Filter.Key("genre"), new Filter.Value("sci-fi"));
evaluator.evaluate(expr, metadata);
Defensive patterns

Strategy: validation

Validate before calling

// build filters only with supported DSL operators
SearchRequest request = SearchRequest.query(q)
    .withFilterExpression("genre == 'sci-fi' and year > 2020");

Type guard

static boolean isSupportedOperand(Filter.Operand o) {
    return o instanceof Filter.Expression; // Key/Value must be wrapped in an Expression
}

Try / catch

try {
    return evaluator.evaluate(expression, metadata);
} catch (IllegalArgumentException e) {
    if (e.getMessage().startsWith("Unsupported operand type")) {
        logger.warn("Unsupported filter operand: {}", e.getMessage());
        return false;
    }
    throw e;
}

Prevention

When it happens

Trigger: Building a Filter expression tree programmatically with a custom or unanticipated Operand subclass; calling evaluateOperand directly with a Filter.Key or Filter.Value; a newer Vector Search DSL introducing operand types the evaluator does not handle.

Common situations: Custom filter construction instead of using Filter expression builders (eq, gt, and, or...); version drift between the search DSL and the store's evaluator.

Understand the failure class

Background: UnsupportedOperationException and "is not supported" errors: when a library deliberately refuses a call — this error's family across 30 libraries.

Related errors


AI-assisted analysis of spring-projects/spring-ai@98a7beda4f (2026-09-11). Data as JSON: /api/errors/cd0b405ccd71f917. Report an issue: GitHub.

Appendix: source

Thrown at spring-ai-vector-store/src/main/java/org/springframework/ai/vectorstore/SimpleVectorStoreFilterExpressionEvaluator.java:91

	 * @param metadata the document metadata to match against; must not be {@code null}
	 * @return {@code true} if the metadata satisfies the expression
	 */
	public boolean evaluate(Filter.Expression expression, Map<String, Object> metadata) {
		return evaluateExpression(expression, metadata);
	}

	private boolean evaluateOperand(Filter.Operand operand, Map<String, Object> metadata) {
		if (operand instanceof Filter.Group group) {
			return evaluateOperand(group.content(), metadata);
		}
		if (operand instanceof Filter.Expression expression) {
			return evaluateExpression(expression, metadata);
		}
		// Filter.Key and Filter.Value are leaf operands consumed directly by
		// metadataValue() and filterValue() inside evaluateExpression(). They are never
		// passed here as top-level boolean operands, so this branch is unreachable under
		// normal usage.
		throw new IllegalArgumentException("Unsupported operand type: " + operand.getClass().getName());
	}

	private boolean evaluateExpression(Filter.Expression expression, Map<String, Object> metadata) {
		return switch (expression.type()) {
			case AND -> evaluateOperand(left(expression), metadata) && evaluateOperand(right(expression), metadata);
			case OR -> evaluateOperand(left(expression), metadata) || evaluateOperand(right(expression), metadata);
			// Unary operator: only the left operand is used. Ignore right operand
			case NOT -> !evaluateOperand(left(expression), metadata);
			case EQ -> compare(metadataValue(left(expression), metadata), filterValue(right(expression))) == 0;
			case NE -> compare(metadataValue(left(expression), metadata), filterValue(right(expression))) != 0;
			case GT -> compare(metadataValue(left(expression), metadata), filterValue(right(expression))) > 0;
			case GTE -> compare(metadataValue(left(expression), metadata), filterValue(right(expression))) >= 0;
			case LT -> compare(metadataValue(left(expression), metadata), filterValue(right(expression))) < 0;
			case LTE -> compare(metadataValue(left(expression), metadata), filterValue(right(expression))) <= 0;
			case IN -> {
				Object metaVal = metadataValue(left(expression), metadata);
				List<?> list = asList(filterValue(right(expression)), expression);
				yield list.stream().anyMatch(item -> compare(metaVal, item) == 0);

View on GitHub (pinned to 98a7beda4f)