VYPR
Critical severity9.8NVD Advisory· Published Mar 27, 2026· Updated May 10, 2026

CVE-2026-22738

CVE-2026-22738

Description

In Spring AI, a SpEL injection vulnerability exists in SimpleVectorStore when a user-supplied value is used as a filter expression key. A malicious actor could exploit this to execute arbitrary code. Only applications that use SimpleVectorStore and pass user-supplied input as a filter expression key are affected. This issue affects Spring AI: from 1.0.0 before 1.0.5, from 1.1.0 before 1.1.4.

Affected products

1
  • cpe:2.3:a:vmware:spring_ai:*:*:*:*:*:*:*:*
    Range: >=1.0.0,<1.0.5

Patches

1
ba9220b22383

Refactor internal filter evaluation in SimpleVectorStore

https://github.com/spring-projects/spring-aiChristian TzolovMar 17, 2026via ghsa
8 files changed · +528 423
  • spring-ai-docs/src/main/antora/modules/ROOT/pages/api/vectordbs.adoc+3 1 modified
    @@ -398,7 +398,9 @@ These are the available implementations of the `VectorStore` interface:
     * xref:api/vectordbs/typesense.adoc[Typesense Vector Store] - The https://typesense.org/docs/0.24.0/api/vector-search.html[Typesense] vector store.
     * xref:api/vectordbs/weaviate.adoc[Weaviate Vector Store] - The https://weaviate.io/[Weaviate] vector store.
     * xref:api/vectordbs/s3-vector-store.adoc[S3 Vector Store] - The https://aws.amazon.com/s3/features/vectors/[AWS S3] vector store.
    -* link:https://github.com/spring-projects/spring-ai/blob/main/spring-ai-vector-store/src/main/java/org/springframework/ai/vectorstore/SimpleVectorStore.java[SimpleVectorStore] - A simple implementation of persistent vector storage, good for educational purposes.
    +* link:https://github.com/spring-projects/spring-ai/blob/main/spring-ai-vector-store/src/main/java/org/springframework/ai/vectorstore/SimpleVectorStore.java[SimpleVectorStore] - A simple implementation of a vector storage, good only for testing purposes. 
    +
    +WARNING: The `SimpleVectorStore` implementation is not designed for production use and should only be used for testing or demonstration purposes.
     
     More implementations may be supported in future releases.
     
    
  • spring-ai-docs/src/main/antora/modules/ROOT/pages/api/vectordbs/understand-vectordbs.adoc+1 1 modified
    @@ -91,5 +91,5 @@ It expands the two-dimensional definitions of Magnitude and Dot Product given pr
     stem:[similarity(vec{A},vec{B}) = \cos(\theta) = \frac{ \sum_{i=1}^{n} {A_i  B_i} }{ \sqrt{\sum_{i=1}^{n}{A_i^2} \cdot \sum_{i=1}^{n}{B_i^2}}]
     ****
     
    -This is the key formula used in the simple implementation of a vector store and can be found in the `SimpleVectorStore` implementation.
    +This is the key formula used in the simple implementation of a vector store and can be found in the `SimpleVectorStore` implementation - applicable for testing and demonstration purposes only.
     
    
  • spring-ai-vector-store/src/main/java/org/springframework/ai/vectorstore/filter/converter/SimpleVectorStoreFilterExpressionConverter.java+0 171 removed
    @@ -1,171 +0,0 @@
    -/*
    - * Copyright 2023-present the original author or authors.
    - *
    - * Licensed under the Apache License, Version 2.0 (the "License");
    - * you may not use this file except in compliance with the License.
    - * You may obtain a copy of the License at
    - *
    - *      https://www.apache.org/licenses/LICENSE-2.0
    - *
    - * Unless required by applicable law or agreed to in writing, software
    - * distributed under the License is distributed on an "AS IS" BASIS,
    - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    - * See the License for the specific language governing permissions and
    - * limitations under the License.
    - */
    -
    -package org.springframework.ai.vectorstore.filter.converter;
    -
    -import java.time.ZoneOffset;
    -import java.time.format.DateTimeFormatter;
    -import java.util.Date;
    -import java.util.List;
    -
    -import org.springframework.ai.vectorstore.filter.Filter;
    -import org.springframework.ai.vectorstore.filter.Filter.Expression;
    -
    -/**
    - * Converts {@link Expression} into SpEL metadata filter expression format.
    - * (https://docs.spring.io/spring-framework/reference/core/expressions.html)
    - *
    - * @author Jemin Huh
    - */
    -public class SimpleVectorStoreFilterExpressionConverter extends AbstractFilterExpressionConverter {
    -
    -	private final DateTimeFormatter dateFormat;
    -
    -	public SimpleVectorStoreFilterExpressionConverter() {
    -		this.dateFormat = DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ss'Z'").withZone(ZoneOffset.UTC);
    -	}
    -
    -	@Override
    -	protected void doExpression(Filter.Expression expression, StringBuilder context) {
    -		this.convertOperand(expression.left(), context);
    -		context.append(getOperationSymbol(expression));
    -		if (expression.right() != null) {
    -			this.convertOperand(expression.right(), context);
    -		}
    -		else {
    -			context.append("null");
    -		}
    -	}
    -
    -	private String getOperationSymbol(Filter.Expression exp) {
    -		return switch (exp.type()) {
    -			case AND -> " and ";
    -			case OR -> " or ";
    -			case EQ -> " == ";
    -			case LT -> " < ";
    -			case LTE -> " <= ";
    -			case GT -> " > ";
    -			case GTE -> " >= ";
    -			case NE -> " != ";
    -			case IN -> " in ";
    -			case NIN -> " not in ";
    -			default -> throw new RuntimeException("Not supported expression type: " + exp.type());
    -		};
    -	}
    -
    -	@Override
    -	protected void doKey(Filter.Key key, StringBuilder context) {
    -		var identifier = hasOuterQuotes(key.key()) ? removeOuterQuotes(key.key()) : key.key();
    -		context.append("#metadata['").append(identifier).append("']");
    -	}
    -
    -	@Override
    -	protected void doValue(Filter.Value filterValue, StringBuilder context) {
    -		if (filterValue.value() instanceof List<?> list) {
    -			var formattedList = new StringBuilder("{");
    -			int c = 0;
    -			for (Object v : list) {
    -				this.doSingleValue(normalizeDateString(v), formattedList);
    -				if (c++ < list.size() - 1) {
    -					this.doAddValueRangeSpitter(filterValue, formattedList);
    -				}
    -			}
    -			formattedList.append("}");
    -
    -			if (context.lastIndexOf("in ") == -1) {
    -				context.append(formattedList);
    -			}
    -			else {
    -				appendSpELContains(formattedList, context);
    -			}
    -		}
    -		else {
    -			this.doSingleValue(normalizeDateString(filterValue.value()), context);
    -		}
    -	}
    -
    -	private void appendSpELContains(StringBuilder formattedList, StringBuilder context) {
    -		int metadataStart = context.lastIndexOf("#metadata");
    -		if (metadataStart == -1) {
    -			throw new RuntimeException("Wrong SpEL expression: " + context);
    -		}
    -		int metadataEnd = context.indexOf(" ", metadataStart);
    -		String metadata = context.substring(metadataStart, metadataEnd);
    -		context.setLength(context.lastIndexOf("in "));
    -		context.delete(metadataStart, metadataEnd + 1);
    -		context.append(formattedList).append(".contains(").append(metadata).append(")");
    -	}
    -
    -	/**
    -	 * Emit a SpEL-formatted string value with single quote wrapping and escaping by
    -	 * appending to the provided context.
    -	 * <p>
    -	 * Escapes single quotes (using backslash) and backslashes (double backslash)
    -	 * according to SpEL string literal rules.
    -	 * <p>
    -	 * This method prevents SpEL injection attacks by properly escaping special
    -	 * characters.
    -	 * @param text the string value to format
    -	 * @param context the context to append the SpEL string literal to
    -	 * @since 2.0.0
    -	 */
    -	protected static void emitSpelString(String text, StringBuilder context) {
    -		context.append("'"); // Opening quote
    -
    -		for (int i = 0; i < text.length(); i++) {
    -			char c = text.charAt(i);
    -
    -			switch (c) {
    -				case '\'':
    -					// SpEL: single quote → backslash escaped
    -					context.append("\\'");
    -					break;
    -				case '\\':
    -					// SpEL: backslash → double backslash
    -					context.append("\\\\");
    -					break;
    -				default:
    -					context.append(c);
    -					break;
    -			}
    -		}
    -
    -		context.append("'"); // Closing quote
    -	}
    -
    -	@Override
    -	protected void doSingleValue(Object value, StringBuilder context) {
    -		if (value instanceof Date date) {
    -			context.append("'");
    -			context.append(this.dateFormat.format(date.toInstant()));
    -			context.append("'");
    -		}
    -		else if (value instanceof String text) {
    -			emitSpelString(text, context);
    -		}
    -		else {
    -			context.append(value);
    -		}
    -	}
    -
    -	@Override
    -	protected void doGroup(Filter.Group group, StringBuilder context) {
    -		context.append("(");
    -		super.doGroup(group, context);
    -		context.append(")");
    -	}
    -
    -}
    
  • spring-ai-vector-store/src/main/java/org/springframework/ai/vectorstore/SimpleVectorStoreContent.java+5 6 modified
    @@ -38,7 +38,7 @@
      * embeddings. This class is thread-safe and all its fields are final and deeply
      * immutable. The embedding vector is required for all instances of this class.
      */
    -public final class SimpleVectorStoreContent implements Content {
    +final class SimpleVectorStoreContent implements Content {
     
     	private final String id;
     
    @@ -54,7 +54,7 @@ public final class SimpleVectorStoreContent implements Content {
     	 * @param text the content text, must not be null
     	 * @param embedding the embedding vector, must not be null
     	 */
    -	public SimpleVectorStoreContent(@JsonProperty("text") @JsonAlias("content") String text,
    +	SimpleVectorStoreContent(@JsonProperty("text") @JsonAlias("content") String text,
     			@JsonProperty("embedding") float[] embedding) {
     		this(text, new HashMap<>(), embedding);
     	}
    @@ -65,7 +65,7 @@ public SimpleVectorStoreContent(@JsonProperty("text") @JsonAlias("content") Stri
     	 * @param metadata the metadata map, must not be null
     	 * @param embedding the embedding vector, must not be null
     	 */
    -	public SimpleVectorStoreContent(String text, Map<String, Object> metadata, float[] embedding) {
    +	SimpleVectorStoreContent(String text, Map<String, Object> metadata, float[] embedding) {
     		this(text, metadata, new RandomIdGenerator(), embedding);
     	}
     
    @@ -77,8 +77,7 @@ public SimpleVectorStoreContent(String text, Map<String, Object> metadata, float
     	 * @param idGenerator the ID generator to use, must not be null
     	 * @param embedding the embedding vector, must not be null
     	 */
    -	public SimpleVectorStoreContent(String text, Map<String, Object> metadata, IdGenerator idGenerator,
    -			float[] embedding) {
    +	SimpleVectorStoreContent(String text, Map<String, Object> metadata, IdGenerator idGenerator, float[] embedding) {
     		this(idGenerator.generateId(text, metadata), text, metadata, embedding);
     	}
     
    @@ -91,7 +90,7 @@ public SimpleVectorStoreContent(String text, Map<String, Object> metadata, IdGen
     	 * @throws IllegalArgumentException if any parameter is null or if id is empty
     	 */
     	@JsonCreator(mode = JsonCreator.Mode.PROPERTIES)
    -	public SimpleVectorStoreContent(@JsonProperty("id") @Nullable String id,
    +	SimpleVectorStoreContent(@JsonProperty("id") @Nullable String id,
     			@JsonProperty("text") @JsonAlias("content") String text,
     			@JsonProperty("metadata") Map<String, Object> metadata, @JsonProperty("embedding") float[] embedding) {
     
    
  • spring-ai-vector-store/src/main/java/org/springframework/ai/vectorstore/SimpleVectorStoreFilterExpressionEvaluator.java+221 0 added
    @@ -0,0 +1,221 @@
    +/*
    + * Copyright 2023-present the original author or authors.
    + *
    + * Licensed under the Apache License, Version 2.0 (the "License");
    + * you may not use this file except in compliance with the License.
    + * You may obtain a copy of the License at
    + *
    + *      https://www.apache.org/licenses/LICENSE-2.0
    + *
    + * Unless required by applicable law or agreed to in writing, software
    + * distributed under the License is distributed on an "AS IS" BASIS,
    + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    + * See the License for the specific language governing permissions and
    + * limitations under the License.
    + */
    +
    +package org.springframework.ai.vectorstore;
    +
    +import java.time.ZoneOffset;
    +import java.time.format.DateTimeFormatter;
    +import java.util.Date;
    +import java.util.List;
    +import java.util.Map;
    +import java.util.Objects;
    +
    +import org.jspecify.annotations.Nullable;
    +
    +import org.springframework.ai.vectorstore.filter.Filter;
    +import org.springframework.ai.vectorstore.filter.FilterExpressionBuilder;
    +
    +/**
    + * Internal helper used by {@link SimpleVectorStore} to evaluate a
    + * {@link Filter.Expression} AST directly against a document metadata map, without
    + * converting to an intermediate string representation (e.g. SpEL or SQL).
    + *
    + * <p>
    + * Supports all {@link Filter.ExpressionType} operations:
    + * <ul>
    + * <li>Logical: {@code AND}, {@code OR}, {@code NOT}</li>
    + * <li>Comparison: {@code EQ}, {@code NE}, {@code GT}, {@code GTE}, {@code LT},
    + * {@code LTE}</li>
    + * <li>Collection: {@code IN}, {@code NIN}</li>
    + * <li>Null checks: {@code ISNULL}, {@code ISNOTNULL}</li>
    + * </ul>
    + *
    + * <p>
    + * <b>Type handling:</b> Numbers are promoted to {@code double} so that mixed
    + * {@code Integer}/{@code Long}/{@code Double} metadata values compare correctly.
    + * {@link Date} filter values are normalised to their ISO-8601 UTC string representation
    + * (matching the format used to store dates in metadata).
    + *
    + * <p>
    + * <b>Missing-key semantics:</b> A metadata key that is absent is treated as {@code null}.
    + * Null ordering follows SQL {@code NULLS FIRST} semantics — {@code null} is considered
    + * less than any non-null value. Consequently, ordered comparisons ({@code GT},
    + * {@code GTE}, {@code LT}, {@code LTE}) against a missing key evaluate as if that key
    + * holds the smallest possible value (e.g. {@code year > 2020} returns {@code false} when
    + * {@code year} is absent).
    + *
    + * @author Christian Tzolov
    + */
    +final class SimpleVectorStoreFilterExpressionEvaluator {
    +
    +	// 'Z' is intentionally a literal suffix, not the offset pattern 'X'. Combined with
    +	// withZone(UTC) this always produces the fixed form "yyyy-MM-dd'T'HH:mm:ss'Z'",
    +	// matching the format used to store Date values in document metadata.
    +	private static final DateTimeFormatter DATE_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ss'Z'")
    +		.withZone(ZoneOffset.UTC);
    +
    +	/**
    +	 * Evaluates the given filter expression against the provided metadata map.
    +	 * @param expression the filter expression to evaluate; must not be {@code null}
    +	 * @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);
    +			}
    +			case NIN -> {
    +				Object metaVal = metadataValue(left(expression), metadata);
    +				List<?> list = asList(filterValue(right(expression)), expression);
    +				yield list.stream().noneMatch(item -> compare(metaVal, item) == 0);
    +			}
    +			// Unary operators: only the left operand (the key) is used.
    +			// A non-null right operand is silently ignored here.
    +			case ISNULL -> metadataValue(left(expression), metadata) == null;
    +			case ISNOTNULL -> metadataValue(left(expression), metadata) != null;
    +		};
    +	}
    +
    +	private Filter.Operand left(Filter.Expression expression) {
    +		Filter.Operand left = expression.left();
    +		if (left == null) {
    +			throw new IllegalArgumentException(
    +					"Expression of type %s requires a left operand".formatted(expression.type()));
    +		}
    +		return left;
    +	}
    +
    +	private Filter.Operand right(Filter.Expression expression) {
    +		Filter.Operand right = expression.right();
    +		if (right == null) {
    +			throw new IllegalArgumentException(
    +					"Expression of type %s requires a right operand".formatted(expression.type()));
    +		}
    +		return right;
    +	}
    +
    +	/**
    +	 * Extracts the metadata value for the given {@link Filter.Key} operand. Outer quotes
    +	 * ({@code "..."} or {@code '...'}) are stripped from the key name to match the format
    +	 * used by {@link FilterExpressionBuilder} and the text parser.
    +	 */
    +	private @Nullable Object metadataValue(Filter.Operand operand, Map<String, Object> metadata) {
    +		if (operand instanceof Filter.Key key) {
    +			String k = key.key();
    +			if (k.length() >= 2
    +					&& ((k.startsWith("\"") && k.endsWith("\"")) || (k.startsWith("'") && k.endsWith("'")))) {
    +				k = k.substring(1, k.length() - 1);
    +			}
    +			return metadata.get(k);
    +		}
    +		throw new IllegalArgumentException("Expected a Key operand but got: " + operand.getClass().getName());
    +	}
    +
    +	/**
    +	 * Extracts the constant value from a {@link Filter.Value} operand. {@link Date}
    +	 * instances are formatted to their ISO-8601 UTC string so they can be compared
    +	 * directly with metadata strings stored in the same format.
    +	 */
    +	private Object filterValue(Filter.Operand operand) {
    +		if (operand instanceof Filter.Value filterValue) {
    +			Object value = filterValue.value();
    +			return (value instanceof Date date) ? DATE_FORMATTER.format(date.toInstant()) : value;
    +		}
    +		throw new IllegalArgumentException("Expected a Value operand but got: " + operand.getClass().getName());
    +	}
    +
    +	/**
    +	 * Compares two values. Numbers are promoted to {@code double} to allow cross-type
    +	 * numeric comparison (e.g. {@code Integer} vs {@code Double}). All other
    +	 * {@link Comparable} types are compared directly.
    +	 *
    +	 * <p>
    +	 * Null ordering follows SQL {@code NULLS FIRST} semantics: {@code null} is considered
    +	 * less than any non-null value. As a result, a missing metadata key causes ordered
    +	 * comparisons ({@code GT}, {@code GTE}, {@code LT}, {@code LTE}) to behave as if the
    +	 * key holds the smallest possible value — e.g. {@code year > 2020} returns
    +	 * {@code false} when {@code year} is absent.
    +	 */
    +	@SuppressWarnings("unchecked")
    +	private int compare(@Nullable Object metaVal, @Nullable Object filterVal) {
    +		if (metaVal == null && filterVal == null) {
    +			return 0;
    +		}
    +		if (metaVal == null) {
    +			return -1;
    +		}
    +		if (filterVal == null) {
    +			return 1;
    +		}
    +		if (metaVal instanceof Number n1 && filterVal instanceof Number n2) {
    +			return Double.compare(n1.doubleValue(), n2.doubleValue());
    +		}
    +		if (Objects.equals(metaVal, filterVal)) {
    +			return 0;
    +		}
    +		if (metaVal instanceof Comparable comparable && filterVal instanceof Comparable) {
    +			try {
    +				return comparable.compareTo(filterVal);
    +			}
    +			catch (ClassCastException ex) {
    +				throw new IllegalArgumentException("Cannot compare values of incompatible types %s and %s"
    +					.formatted(metaVal.getClass().getName(), filterVal.getClass().getName()), ex);
    +			}
    +		}
    +		throw new IllegalArgumentException("Cannot compare values of types %s and %s"
    +			.formatted(metaVal.getClass().getName(), filterVal.getClass().getName()));
    +	}
    +
    +	private List<?> asList(Object value, Filter.Expression expression) {
    +		if (value instanceof List<?> list) {
    +			return list;
    +		}
    +		throw new IllegalArgumentException(
    +				"Expected a List value for %s expression but got: %s".formatted(expression.type(), value));
    +	}
    +
    +}
    
  • spring-ai-vector-store/src/main/java/org/springframework/ai/vectorstore/SimpleVectorStore.java+25 24 modified
    @@ -47,24 +47,34 @@
     import org.springframework.ai.observation.conventions.VectorStoreSimilarityMetric;
     import org.springframework.ai.util.JacksonUtils;
     import org.springframework.ai.vectorstore.filter.Filter;
    -import org.springframework.ai.vectorstore.filter.FilterExpressionConverter;
    -import org.springframework.ai.vectorstore.filter.converter.SimpleVectorStoreFilterExpressionConverter;
     import org.springframework.ai.vectorstore.observation.AbstractObservationVectorStore;
     import org.springframework.ai.vectorstore.observation.VectorStoreObservationContext;
     import org.springframework.core.io.Resource;
    -import org.springframework.expression.ExpressionParser;
    -import org.springframework.expression.spel.standard.SpelExpressionParser;
    -import org.springframework.expression.spel.support.StandardEvaluationContext;
     
     /**
    - * SimpleVectorStore is a simple implementation of the VectorStore interface.
    + * A simple, in-memory implementation of the <a href=
    + * "https://docs.spring.io/spring-ai/reference/api/vectordbs.html#_understanding_vectors">VectorStore</a>
    + * interface.
      *
    - * It also provides methods to save the current state of the vectors to a file, and to
    - * load vectors from a file.
    + * <p>
    + * Uses a {@link java.util.concurrent.ConcurrentHashMap} to store vectors and their
    + * associated metadata. Map keys are document IDs; values are
    + * {@link SimpleVectorStoreContent} instances that encapsulate each document's text,
    + * metadata, and embedding vector.
      *
    - * For a deeper understanding of the mathematical concepts and computations involved in
    - * calculating similarity scores among vectors, refer to this
    - * [resource](https://docs.spring.io/spring-ai/reference/api/vectordbs.html#_understanding_vectors).
    + * <p>
    + * Similarity search is performed using cosine similarity over all stored vectors. Filter
    + * expressions on document metadata are evaluated via
    + * {@link SimpleVectorStoreFilterExpressionEvaluator}.
    + *
    + * <p>
    + * The store can be persisted to and restored from a JSON file via the
    + * {@link #save(java.io.File)} and {@link #load(java.io.File)} /
    + * {@link #load(org.springframework.core.io.Resource)} methods.
    + *
    + * <p>
    + * <b>NOTE</b>: This implementation is not designed for production use and should only be
    + * used for testing or demonstration purposes.
      *
      * @author Raphael Yu
      * @author Dingmeng Xue
    @@ -75,24 +85,22 @@
      * @author Thomas Vitale
      * @author Jemin Huh
      * @author David Yu
    + * @since 1.0.0
      */
     public class SimpleVectorStore extends AbstractObservationVectorStore {
     
     	private static final Logger logger = LoggerFactory.getLogger(SimpleVectorStore.class);
     
     	private final JsonMapper jsonMapper;
     
    -	private final ExpressionParser expressionParser;
    -
    -	private final FilterExpressionConverter filterExpressionConverter;
    +	private final SimpleVectorStoreFilterExpressionEvaluator filterExpressionEvaluator;
     
     	protected Map<String, SimpleVectorStoreContent> store = new ConcurrentHashMap<>();
     
     	protected SimpleVectorStore(SimpleVectorStoreBuilder builder) {
     		super(builder);
     		this.jsonMapper = JsonMapper.builder().addModules(JacksonUtils.instantiateAvailableModules()).build();
    -		this.expressionParser = new SpelExpressionParser();
    -		this.filterExpressionConverter = new SimpleVectorStoreFilterExpressionConverter();
    +		this.filterExpressionEvaluator = new SimpleVectorStoreFilterExpressionEvaluator();
     	}
     
     	/**
    @@ -154,14 +162,7 @@ private Predicate<SimpleVectorStoreContent> doFilterPredicate(Filter.@Nullable E
     		if (filterExpression == null) {
     			return document -> true;
     		}
    -
    -		return document -> {
    -			StandardEvaluationContext context = new StandardEvaluationContext();
    -			context.setVariable("metadata", document.getMetadata());
    -			return Boolean.TRUE.equals(this.expressionParser
    -				.parseExpression(this.filterExpressionConverter.convertExpression(filterExpression))
    -				.getValue(context, Boolean.class));
    -		};
    +		return document -> this.filterExpressionEvaluator.evaluate(filterExpression, document.getMetadata());
     	}
     
     	/**
    
  • spring-ai-vector-store/src/test/java/org/springframework/ai/vectorstore/filter/converter/SimpleVectorStoreFilterExpressionConverterTests.java+0 220 removed
    @@ -1,220 +0,0 @@
    -/*
    - * Copyright 2023-present the original author or authors.
    - *
    - * Licensed under the Apache License, Version 2.0 (the "License");
    - * you may not use this file except in compliance with the License.
    - * You may obtain a copy of the License at
    - *
    - *      https://www.apache.org/licenses/LICENSE-2.0
    - *
    - * Unless required by applicable law or agreed to in writing, software
    - * distributed under the License is distributed on an "AS IS" BASIS,
    - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    - * See the License for the specific language governing permissions and
    - * limitations under the License.
    - */
    -
    -package org.springframework.ai.vectorstore.filter.converter;
    -
    -import java.util.Date;
    -import java.util.List;
    -import java.util.Map;
    -import java.util.stream.IntStream;
    -
    -import org.junit.jupiter.api.Assertions;
    -import org.junit.jupiter.api.Test;
    -
    -import org.springframework.ai.vectorstore.filter.Filter;
    -import org.springframework.ai.vectorstore.filter.FilterExpressionConverter;
    -import org.springframework.expression.ExpressionParser;
    -import org.springframework.expression.spel.standard.SpelExpressionParser;
    -import org.springframework.expression.spel.support.StandardEvaluationContext;
    -
    -import static org.assertj.core.api.Assertions.assertThat;
    -import static org.springframework.ai.vectorstore.filter.Filter.ExpressionType.AND;
    -import static org.springframework.ai.vectorstore.filter.Filter.ExpressionType.EQ;
    -import static org.springframework.ai.vectorstore.filter.Filter.ExpressionType.GTE;
    -import static org.springframework.ai.vectorstore.filter.Filter.ExpressionType.IN;
    -import static org.springframework.ai.vectorstore.filter.Filter.ExpressionType.LTE;
    -import static org.springframework.ai.vectorstore.filter.Filter.ExpressionType.NE;
    -import static org.springframework.ai.vectorstore.filter.Filter.ExpressionType.NIN;
    -import static org.springframework.ai.vectorstore.filter.Filter.ExpressionType.OR;
    -
    -/**
    - * @author Jemin Huh
    - */
    -public class SimpleVectorStoreFilterExpressionConverterTests {
    -
    -	final FilterExpressionConverter converter = new SimpleVectorStoreFilterExpressionConverter();
    -
    -	@Test
    -	public void testDate() {
    -		String vectorExpr = this.converter.convertExpression(new Filter.Expression(EQ, new Filter.Key("activationDate"),
    -				new Filter.Value(new Date(1704637752148L))));
    -		assertThat(vectorExpr).isEqualTo("#metadata['activationDate'] == '2024-01-07T14:29:12Z'");
    -
    -		StandardEvaluationContext context = new StandardEvaluationContext();
    -		ExpressionParser parser = new SpelExpressionParser();
    -		context.setVariable("metadata",
    -				Map.of("activationDate", "2024-01-07T14:29:12Z", "year", 2020, "country", "BG"));
    -		Assertions.assertEquals(Boolean.TRUE, parser.parseExpression(vectorExpr).getValue(context, Boolean.class));
    -
    -		vectorExpr = this.converter.convertExpression(
    -				new Filter.Expression(EQ, new Filter.Key("activationDate"), new Filter.Value("1970-01-01T00:00:02Z")));
    -		assertThat(vectorExpr).isEqualTo("#metadata['activationDate'] == '1970-01-01T00:00:02Z'");
    -
    -		context.setVariable("metadata",
    -				Map.of("activationDate", "1970-01-01T00:00:02Z", "year", 2020, "country", "BG"));
    -		Assertions.assertEquals(Boolean.TRUE, parser.parseExpression(vectorExpr).getValue(context, Boolean.class));
    -
    -	}
    -
    -	@Test
    -	public void testDatesConcurrently() {
    -		IntStream.range(0, 10).parallel().forEach(i -> {
    -			String vectorExpr = this.converter.convertExpression(new Filter.Expression(EQ,
    -					new Filter.Key("activationDate"), new Filter.Value(new Date(1704637752148L))));
    -			String vectorExpr2 = this.converter.convertExpression(new Filter.Expression(EQ,
    -					new Filter.Key("activationDate"), new Filter.Value(new Date(1704637753150L))));
    -			assertThat(vectorExpr).isEqualTo("#metadata['activationDate'] == '2024-01-07T14:29:12Z'");
    -			assertThat(vectorExpr2).isEqualTo("#metadata['activationDate'] == '2024-01-07T14:29:13Z'");
    -		});
    -	}
    -
    -	@Test
    -	public void testEQ() {
    -		String vectorExpr = this.converter
    -			.convertExpression(new Filter.Expression(EQ, new Filter.Key("country"), new Filter.Value("BG")));
    -		assertThat(vectorExpr).isEqualTo("#metadata['country'] == 'BG'");
    -
    -		StandardEvaluationContext context = new StandardEvaluationContext();
    -		ExpressionParser parser = new SpelExpressionParser();
    -		context.setVariable("metadata", Map.of("city", "Seoul", "year", 2020, "country", "BG"));
    -		Assertions.assertEquals(Boolean.TRUE, parser.parseExpression(vectorExpr).getValue(context, Boolean.class));
    -
    -	}
    -
    -	@Test
    -	public void tesEqAndGte() {
    -		String vectorExpr = this.converter.convertExpression(new Filter.Expression(AND,
    -				new Filter.Expression(EQ, new Filter.Key("genre"), new Filter.Value("drama")),
    -				new Filter.Expression(GTE, new Filter.Key("year"), new Filter.Value(2020))));
    -		assertThat(vectorExpr).isEqualTo("#metadata['genre'] == 'drama' and #metadata['year'] >= 2020");
    -
    -		StandardEvaluationContext context = new StandardEvaluationContext();
    -		ExpressionParser parser = new SpelExpressionParser();
    -		context.setVariable("metadata", Map.of("genre", "drama", "year", 2020, "country", "BG"));
    -		Assertions.assertEquals(Boolean.TRUE, parser.parseExpression(vectorExpr).getValue(context, Boolean.class));
    -
    -	}
    -
    -	@Test
    -	public void tesIn() {
    -		String vectorExpr = this.converter.convertExpression(new Filter.Expression(IN, new Filter.Key("genre"),
    -				new Filter.Value(List.of("comedy", "documentary", "drama"))));
    -		assertThat(vectorExpr).isEqualTo("{'comedy','documentary','drama'}.contains(#metadata['genre'])");
    -
    -		StandardEvaluationContext context = new StandardEvaluationContext();
    -		ExpressionParser parser = new SpelExpressionParser();
    -		context.setVariable("metadata", Map.of("genre", "drama", "year", 2020, "country", "BG"));
    -		Assertions.assertEquals(Boolean.TRUE, parser.parseExpression(vectorExpr).getValue(context, Boolean.class));
    -
    -	}
    -
    -	@Test
    -	public void testNe() {
    -		String vectorExpr = this.converter.convertExpression(
    -				new Filter.Expression(OR, new Filter.Expression(GTE, new Filter.Key("year"), new Filter.Value(2020)),
    -						new Filter.Expression(AND,
    -								new Filter.Expression(EQ, new Filter.Key("country"), new Filter.Value("BG")),
    -								new Filter.Expression(NE, new Filter.Key("city"), new Filter.Value("Sofia")))));
    -		assertThat(vectorExpr)
    -			.isEqualTo("#metadata['year'] >= 2020 or #metadata['country'] == 'BG' and #metadata['city'] != 'Sofia'");
    -
    -		StandardEvaluationContext context = new StandardEvaluationContext();
    -		ExpressionParser parser = new SpelExpressionParser();
    -		context.setVariable("metadata", Map.of("city", "Seoul", "year", 2020, "country", "BG"));
    -		Assertions.assertEquals(Boolean.TRUE, parser.parseExpression(vectorExpr).getValue(context, Boolean.class));
    -
    -	}
    -
    -	@Test
    -	public void testGroup() {
    -		String vectorExpr = this.converter.convertExpression(new Filter.Expression(AND,
    -				new Filter.Group(new Filter.Expression(OR,
    -						new Filter.Expression(GTE, new Filter.Key("year"), new Filter.Value(2020)),
    -						new Filter.Expression(EQ, new Filter.Key("country"), new Filter.Value("BG")))),
    -				new Filter.Expression(NIN, new Filter.Key("city"), new Filter.Value(List.of("Sofia", "Plovdiv")))));
    -		assertThat(vectorExpr).isEqualTo(
    -				"(#metadata['year'] >= 2020 or #metadata['country'] == 'BG') and not {'Sofia','Plovdiv'}.contains(#metadata['city'])");
    -
    -		StandardEvaluationContext context = new StandardEvaluationContext();
    -		ExpressionParser parser = new SpelExpressionParser();
    -		context.setVariable("metadata", Map.of("city", "Seoul", "year", 2020, "country", "BG"));
    -		Assertions.assertEquals(Boolean.TRUE, parser.parseExpression(vectorExpr).getValue(context, Boolean.class));
    -
    -	}
    -
    -	@Test
    -	public void tesBoolean() {
    -		String vectorExpr = this.converter.convertExpression(new Filter.Expression(AND,
    -				new Filter.Expression(AND, new Filter.Expression(EQ, new Filter.Key("isOpen"), new Filter.Value(true)),
    -						new Filter.Expression(GTE, new Filter.Key("year"), new Filter.Value(2020))),
    -				new Filter.Expression(IN, new Filter.Key("country"), new Filter.Value(List.of("BG", "NL", "US")))));
    -
    -		assertThat(vectorExpr).isEqualTo(
    -				"#metadata['isOpen'] == true and #metadata['year'] >= 2020 and {'BG','NL','US'}.contains(#metadata['country'])");
    -
    -		StandardEvaluationContext context = new StandardEvaluationContext();
    -		ExpressionParser parser = new SpelExpressionParser();
    -		context.setVariable("metadata", Map.of("isOpen", true, "year", 2020, "country", "NL"));
    -		Assertions.assertEquals(Boolean.TRUE, parser.parseExpression(vectorExpr).getValue(context, Boolean.class));
    -
    -		vectorExpr = this.converter.convertExpression(new Filter.Expression(AND,
    -				new Filter.Expression(AND, new Filter.Expression(EQ, new Filter.Key("isOpen"), new Filter.Value(true)),
    -						new Filter.Expression(GTE, new Filter.Key("year"), new Filter.Value(2020))),
    -				new Filter.Expression(NIN, new Filter.Key("country"), new Filter.Value(List.of("BG", "NL", "US")))));
    -
    -		assertThat(vectorExpr).isEqualTo(
    -				"#metadata['isOpen'] == true and #metadata['year'] >= 2020 and not {'BG','NL','US'}.contains(#metadata['country'])");
    -
    -		context.setVariable("metadata", Map.of("isOpen", true, "year", 2020, "country", "KR"));
    -		Assertions.assertEquals(Boolean.TRUE, parser.parseExpression(vectorExpr).getValue(context, Boolean.class));
    -	}
    -
    -	@Test
    -	public void testDecimal() {
    -		String vectorExpr = this.converter.convertExpression(new Filter.Expression(AND,
    -				new Filter.Expression(GTE, new Filter.Key("temperature"), new Filter.Value(-15.6)),
    -				new Filter.Expression(LTE, new Filter.Key("temperature"), new Filter.Value(20.13))));
    -
    -		assertThat(vectorExpr).isEqualTo("#metadata['temperature'] >= -15.6 and #metadata['temperature'] <= 20.13");
    -
    -		StandardEvaluationContext context = new StandardEvaluationContext();
    -		ExpressionParser parser = new SpelExpressionParser();
    -		context.setVariable("metadata", Map.of("temperature", -15.6));
    -		Assertions.assertEquals(Boolean.TRUE, parser.parseExpression(vectorExpr).getValue(context, Boolean.class));
    -		context.setVariable("metadata", Map.of("temperature", 20.13));
    -		Assertions.assertEquals(Boolean.TRUE, parser.parseExpression(vectorExpr).getValue(context, Boolean.class));
    -		context.setVariable("metadata", Map.of("temperature", -1.6));
    -		Assertions.assertEquals(Boolean.TRUE, parser.parseExpression(vectorExpr).getValue(context, Boolean.class));
    -
    -	}
    -
    -	@Test
    -	public void testComplexIdentifiers() {
    -		String vectorExpr = this.converter
    -			.convertExpression(new Filter.Expression(EQ, new Filter.Key("\"country 1 2 3\""), new Filter.Value("BG")));
    -		assertThat(vectorExpr).isEqualTo("#metadata['country 1 2 3'] == 'BG'");
    -
    -		vectorExpr = this.converter
    -			.convertExpression(new Filter.Expression(EQ, new Filter.Key("'country 1 2 3'"), new Filter.Value("BG")));
    -		assertThat(vectorExpr).isEqualTo("#metadata['country 1 2 3'] == 'BG'");
    -
    -		StandardEvaluationContext context = new StandardEvaluationContext();
    -		ExpressionParser parser = new SpelExpressionParser();
    -		context.setVariable("metadata", Map.of("country 1 2 3", "BG"));
    -		Assertions.assertEquals(Boolean.TRUE, parser.parseExpression(vectorExpr).getValue(context, Boolean.class));
    -	}
    -
    -}
    
  • spring-ai-vector-store/src/test/java/org/springframework/ai/vectorstore/SimpleVectorStoreFilterExpressionEvaluatorTests.java+273 0 added
    @@ -0,0 +1,273 @@
    +/*
    + * Copyright 2023-present the original author or authors.
    + *
    + * Licensed under the Apache License, Version 2.0 (the "License");
    + * you may not use this file except in compliance with the License.
    + * You may obtain a copy of the License at
    + *
    + *      https://www.apache.org/licenses/LICENSE-2.0
    + *
    + * Unless required by applicable law or agreed to in writing, software
    + * distributed under the License is distributed on an "AS IS" BASIS,
    + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    + * See the License for the specific language governing permissions and
    + * limitations under the License.
    + */
    +
    +package org.springframework.ai.vectorstore;
    +
    +import java.util.Date;
    +import java.util.List;
    +import java.util.Map;
    +
    +import org.junit.jupiter.api.Test;
    +
    +import org.springframework.ai.vectorstore.filter.Filter;
    +
    +import static org.assertj.core.api.Assertions.assertThat;
    +import static org.springframework.ai.vectorstore.filter.Filter.ExpressionType.AND;
    +import static org.springframework.ai.vectorstore.filter.Filter.ExpressionType.EQ;
    +import static org.springframework.ai.vectorstore.filter.Filter.ExpressionType.GT;
    +import static org.springframework.ai.vectorstore.filter.Filter.ExpressionType.GTE;
    +import static org.springframework.ai.vectorstore.filter.Filter.ExpressionType.IN;
    +import static org.springframework.ai.vectorstore.filter.Filter.ExpressionType.ISNOTNULL;
    +import static org.springframework.ai.vectorstore.filter.Filter.ExpressionType.ISNULL;
    +import static org.springframework.ai.vectorstore.filter.Filter.ExpressionType.LT;
    +import static org.springframework.ai.vectorstore.filter.Filter.ExpressionType.LTE;
    +import static org.springframework.ai.vectorstore.filter.Filter.ExpressionType.NE;
    +import static org.springframework.ai.vectorstore.filter.Filter.ExpressionType.NIN;
    +import static org.springframework.ai.vectorstore.filter.Filter.ExpressionType.NOT;
    +import static org.springframework.ai.vectorstore.filter.Filter.ExpressionType.OR;
    +
    +/**
    + * Tests for {@link SimpleVectorStoreFilterExpressionEvaluator}.
    + *
    + * @author Christian Tzolov
    + */
    +class SimpleVectorStoreFilterExpressionEvaluatorTests {
    +
    +	private final SimpleVectorStoreFilterExpressionEvaluator evaluator = new SimpleVectorStoreFilterExpressionEvaluator();
    +
    +	// -------------------------------------------------------------------------
    +	// Comparison operators
    +	// -------------------------------------------------------------------------
    +
    +	@Test
    +	void testEq() {
    +		var expr = new Filter.Expression(EQ, new Filter.Key("country"), new Filter.Value("BG"));
    +		assertThat(this.evaluator.evaluate(expr, Map.of("country", "BG"))).isTrue();
    +		assertThat(this.evaluator.evaluate(expr, Map.of("country", "NL"))).isFalse();
    +	}
    +
    +	@Test
    +	void testNe() {
    +		var expr = new Filter.Expression(NE, new Filter.Key("country"), new Filter.Value("BG"));
    +		assertThat(this.evaluator.evaluate(expr, Map.of("country", "NL"))).isTrue();
    +		assertThat(this.evaluator.evaluate(expr, Map.of("country", "BG"))).isFalse();
    +	}
    +
    +	@Test
    +	void testGt() {
    +		var expr = new Filter.Expression(GT, new Filter.Key("year"), new Filter.Value(2020));
    +		assertThat(this.evaluator.evaluate(expr, Map.of("year", 2021))).isTrue();
    +		assertThat(this.evaluator.evaluate(expr, Map.of("year", 2020))).isFalse();
    +		assertThat(this.evaluator.evaluate(expr, Map.of("year", 2019))).isFalse();
    +	}
    +
    +	@Test
    +	void testGte() {
    +		var expr = new Filter.Expression(GTE, new Filter.Key("year"), new Filter.Value(2020));
    +		assertThat(this.evaluator.evaluate(expr, Map.of("year", 2020))).isTrue();
    +		assertThat(this.evaluator.evaluate(expr, Map.of("year", 2021))).isTrue();
    +		assertThat(this.evaluator.evaluate(expr, Map.of("year", 2019))).isFalse();
    +	}
    +
    +	@Test
    +	void testLt() {
    +		var expr = new Filter.Expression(LT, new Filter.Key("year"), new Filter.Value(2020));
    +		assertThat(this.evaluator.evaluate(expr, Map.of("year", 2019))).isTrue();
    +		assertThat(this.evaluator.evaluate(expr, Map.of("year", 2020))).isFalse();
    +	}
    +
    +	@Test
    +	void testLte() {
    +		var expr = new Filter.Expression(LTE, new Filter.Key("year"), new Filter.Value(2020));
    +		assertThat(this.evaluator.evaluate(expr, Map.of("year", 2020))).isTrue();
    +		assertThat(this.evaluator.evaluate(expr, Map.of("year", 2019))).isTrue();
    +		assertThat(this.evaluator.evaluate(expr, Map.of("year", 2021))).isFalse();
    +	}
    +
    +	// -------------------------------------------------------------------------
    +	// Logical operators
    +	// -------------------------------------------------------------------------
    +
    +	@Test
    +	void testAnd() {
    +		var expr = new Filter.Expression(AND,
    +				new Filter.Expression(EQ, new Filter.Key("genre"), new Filter.Value("drama")),
    +				new Filter.Expression(GTE, new Filter.Key("year"), new Filter.Value(2020)));
    +
    +		assertThat(this.evaluator.evaluate(expr, Map.of("genre", "drama", "year", 2020))).isTrue();
    +		assertThat(this.evaluator.evaluate(expr, Map.of("genre", "comedy", "year", 2020))).isFalse();
    +		assertThat(this.evaluator.evaluate(expr, Map.of("genre", "drama", "year", 2019))).isFalse();
    +	}
    +
    +	@Test
    +	void testOr() {
    +		var expr = new Filter.Expression(OR, new Filter.Expression(GTE, new Filter.Key("year"), new Filter.Value(2020)),
    +				new Filter.Expression(AND, new Filter.Expression(EQ, new Filter.Key("country"), new Filter.Value("BG")),
    +						new Filter.Expression(NE, new Filter.Key("city"), new Filter.Value("Sofia"))));
    +
    +		assertThat(this.evaluator.evaluate(expr, Map.of("city", "Seoul", "year", 2020, "country", "BG"))).isTrue();
    +		assertThat(this.evaluator.evaluate(expr, Map.of("city", "Seoul", "year", 2019, "country", "BG"))).isTrue();
    +		assertThat(this.evaluator.evaluate(expr, Map.of("city", "Sofia", "year", 2019, "country", "BG"))).isFalse();
    +	}
    +
    +	@Test
    +	void testNot() {
    +		var expr = new Filter.Expression(NOT,
    +				new Filter.Expression(EQ, new Filter.Key("country"), new Filter.Value("BG")));
    +		assertThat(this.evaluator.evaluate(expr, Map.of("country", "NL"))).isTrue();
    +		assertThat(this.evaluator.evaluate(expr, Map.of("country", "BG"))).isFalse();
    +	}
    +
    +	// -------------------------------------------------------------------------
    +	// Collection operators
    +	// -------------------------------------------------------------------------
    +
    +	@Test
    +	void testIn() {
    +		var expr = new Filter.Expression(IN, new Filter.Key("genre"),
    +				new Filter.Value(List.of("comedy", "documentary", "drama")));
    +		assertThat(this.evaluator.evaluate(expr, Map.of("genre", "drama"))).isTrue();
    +		assertThat(this.evaluator.evaluate(expr, Map.of("genre", "comedy"))).isTrue();
    +		assertThat(this.evaluator.evaluate(expr, Map.of("genre", "action"))).isFalse();
    +	}
    +
    +	@Test
    +	void testNin() {
    +		var expr = new Filter.Expression(NIN, new Filter.Key("city"), new Filter.Value(List.of("Sofia", "Plovdiv")));
    +		assertThat(this.evaluator.evaluate(expr, Map.of("city", "Seoul"))).isTrue();
    +		assertThat(this.evaluator.evaluate(expr, Map.of("city", "Sofia"))).isFalse();
    +	}
    +
    +	// -------------------------------------------------------------------------
    +	// Null checks
    +	// -------------------------------------------------------------------------
    +
    +	@Test
    +	void testIsNull() {
    +		var expr = new Filter.Expression(ISNULL, new Filter.Key("country"));
    +		Map<String, Object> withNull = new java.util.HashMap<>();
    +		withNull.put("country", null);
    +		assertThat(this.evaluator.evaluate(expr, withNull)).isTrue();
    +		// missing key → null
    +		assertThat(this.evaluator.evaluate(expr, Map.of("year", 2020))).isTrue();
    +		assertThat(this.evaluator.evaluate(expr, Map.of("country", "BG"))).isFalse();
    +	}
    +
    +	@Test
    +	void testIsNotNull() {
    +		var expr = new Filter.Expression(ISNOTNULL, new Filter.Key("country"));
    +		assertThat(this.evaluator.evaluate(expr, Map.of("country", "BG"))).isTrue();
    +		// missing key → null
    +		assertThat(this.evaluator.evaluate(expr, Map.of("year", 2020))).isFalse();
    +	}
    +
    +	// -------------------------------------------------------------------------
    +	// Group (precedence)
    +	// -------------------------------------------------------------------------
    +
    +	@Test
    +	void testGroup() {
    +		// (year >= 2020 OR country == "BG") AND city NIN ["Sofia", "Plovdiv"]
    +		var expr = new Filter.Expression(AND,
    +				new Filter.Group(new Filter.Expression(OR,
    +						new Filter.Expression(GTE, new Filter.Key("year"), new Filter.Value(2020)),
    +						new Filter.Expression(EQ, new Filter.Key("country"), new Filter.Value("BG")))),
    +				new Filter.Expression(NIN, new Filter.Key("city"), new Filter.Value(List.of("Sofia", "Plovdiv"))));
    +
    +		assertThat(this.evaluator.evaluate(expr, Map.of("city", "Seoul", "year", 2020, "country", "BG"))).isTrue();
    +		assertThat(this.evaluator.evaluate(expr, Map.of("city", "Sofia", "year", 2020, "country", "BG"))).isFalse();
    +		assertThat(this.evaluator.evaluate(expr, Map.of("city", "Seoul", "year", 2019, "country", "NL"))).isFalse();
    +	}
    +
    +	// -------------------------------------------------------------------------
    +	// Type handling
    +	// -------------------------------------------------------------------------
    +
    +	@Test
    +	void testBoolean() {
    +		// isOpen == true AND year >= 2020 AND country IN ["BG", "NL", "US"]
    +		var expr = new Filter.Expression(AND,
    +				new Filter.Expression(AND, new Filter.Expression(EQ, new Filter.Key("isOpen"), new Filter.Value(true)),
    +						new Filter.Expression(GTE, new Filter.Key("year"), new Filter.Value(2020))),
    +				new Filter.Expression(IN, new Filter.Key("country"), new Filter.Value(List.of("BG", "NL", "US"))));
    +
    +		assertThat(this.evaluator.evaluate(expr, Map.of("isOpen", true, "year", 2020, "country", "NL"))).isTrue();
    +		assertThat(this.evaluator.evaluate(expr, Map.of("isOpen", false, "year", 2020, "country", "NL"))).isFalse();
    +		assertThat(this.evaluator.evaluate(expr, Map.of("isOpen", true, "year", 2019, "country", "NL"))).isFalse();
    +		assertThat(this.evaluator.evaluate(expr, Map.of("isOpen", true, "year", 2020, "country", "KR"))).isFalse();
    +	}
    +
    +	@Test
    +	void testDecimal() {
    +		var expr = new Filter.Expression(AND,
    +				new Filter.Expression(GTE, new Filter.Key("temperature"), new Filter.Value(-15.6)),
    +				new Filter.Expression(LTE, new Filter.Key("temperature"), new Filter.Value(20.13)));
    +
    +		assertThat(this.evaluator.evaluate(expr, Map.of("temperature", -15.6))).isTrue();
    +		assertThat(this.evaluator.evaluate(expr, Map.of("temperature", 20.13))).isTrue();
    +		assertThat(this.evaluator.evaluate(expr, Map.of("temperature", -1.6))).isTrue();
    +		assertThat(this.evaluator.evaluate(expr, Map.of("temperature", -16.0))).isFalse();
    +		assertThat(this.evaluator.evaluate(expr, Map.of("temperature", 21.0))).isFalse();
    +	}
    +
    +	@Test
    +	void testNumericCrossTypeComparison() {
    +		// metadata Integer vs filter Integer — should work via double promotion
    +		var expr = new Filter.Expression(GTE, new Filter.Key("year"), new Filter.Value(2020));
    +		assertThat(this.evaluator.evaluate(expr, Map.of("year", 2020))).isTrue();
    +		// metadata Integer vs filter Double
    +		assertThat(this.evaluator.evaluate(expr, Map.of("year", Integer.valueOf(2020)))).isTrue();
    +	}
    +
    +	@Test
    +	void testInNumericCrossType() {
    +		// metadata Integer, filter list contains Long — must match via double promotion
    +		var expr = new Filter.Expression(IN, new Filter.Key("year"), new Filter.Value(List.of(2019L, 2020L, 2021L)));
    +		assertThat(this.evaluator.evaluate(expr, Map.of("year", Integer.valueOf(2020)))).isTrue();
    +		assertThat(this.evaluator.evaluate(expr, Map.of("year", Integer.valueOf(2022)))).isFalse();
    +
    +		// metadata Double, filter list contains Integer
    +		var expr2 = new Filter.Expression(IN, new Filter.Key("score"), new Filter.Value(List.of(1, 2, 3)));
    +		assertThat(this.evaluator.evaluate(expr2, Map.of("score", 2.0))).isTrue();
    +		assertThat(this.evaluator.evaluate(expr2, Map.of("score", 4.0))).isFalse();
    +	}
    +
    +	@Test
    +	void testNinNumericCrossType() {
    +		// metadata Long, filter list contains Integer
    +		var expr = new Filter.Expression(NIN, new Filter.Key("year"), new Filter.Value(List.of(2020, 2021)));
    +		assertThat(this.evaluator.evaluate(expr, Map.of("year", Long.valueOf(2022)))).isTrue();
    +		assertThat(this.evaluator.evaluate(expr, Map.of("year", Long.valueOf(2020)))).isFalse();
    +	}
    +
    +	@Test
    +	void testDate() {
    +		var expr = new Filter.Expression(EQ, new Filter.Key("activationDate"),
    +				new Filter.Value(new Date(1704637752148L)));
    +		assertThat(this.evaluator.evaluate(expr, Map.of("activationDate", "2024-01-07T14:29:12Z"))).isTrue();
    +		assertThat(this.evaluator.evaluate(expr, Map.of("activationDate", "2024-01-07T00:00:00Z"))).isFalse();
    +	}
    +
    +	@Test
    +	void testQuotedKey() {
    +		var exprDoubleQuote = new Filter.Expression(EQ, new Filter.Key("\"country 1 2 3\""), new Filter.Value("BG"));
    +		assertThat(this.evaluator.evaluate(exprDoubleQuote, Map.of("country 1 2 3", "BG"))).isTrue();
    +
    +		var exprSingleQuote = new Filter.Expression(EQ, new Filter.Key("'country 1 2 3'"), new Filter.Value("BG"));
    +		assertThat(this.evaluator.evaluate(exprSingleQuote, Map.of("country 1 2 3", "BG"))).isTrue();
    +	}
    +
    +}
    

Vulnerability mechanics

Generated by null/stub on May 9, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.

References

6

News mentions

0

No linked articles in our index yet.