Bug description FilterExpressionTextParser does not support 'null' values, so I can't write

SearchRequest searchRequest = SearchRequest.builder()
                        .query("Spring AI")
                        .similarityThresholdAll()
                        .topK(3)
                        .filterExpression("meta1 ==  null")
                        .build();

At the same time, FilterExpressionBuilder do support it.

var b = new FilterExpressionBuilder();
SearchRequest searchRequest = SearchRequest.builder()
                        .query("Spring AI")
                        .similarityThresholdAll()
                        .topK(3)
                        .filterExpression(b.eq("meta1", null).build())
                        .build();

Environment Spring AI v1.0.0, Java 17, Vector Store - ClickHouse (but it doesn't depend on vector store type)

Steps to reproduce

SearchRequest searchRequest = SearchRequest.builder()
                        .query("Spring AI")
                        .similarityThresholdAll()
                        .topK(3)
                        .filterExpression("meta1 == null")
                        .build();

Expected behavior I expect that Filter.Expression::right will return null when it called from filter expression converter (FilterExpressionConverter::convertExpression)

Minimal Complete Reproducible example https://github.com/linarkou/spring-ai-clickhouse-store/blob/main/spring-ai-clickhouse-store/src/test/java/org/springframework/ai/vectorstore/clickhouse/ClickhouseVectorStoreIT.java

    private static List<Document> documents() {
        return List.of(
                new Document("1", getText("classpath:/test/data/spring.ai.txt"), Map.of("meta1", "meta1","intMeta", 456)),
                new Document("2", getText("classpath:/test/data/time.shelter.txt"), Map.of()),
                new Document("3", getText("classpath:/test/data/great.depression.txt"),
                        Map.of("meta2", "meta2", "intMeta", 123)));
    }

    private static String getText(String uri) {
        var resource = new DefaultResourceLoader().getResource(uri);
        try {
            return resource.getContentAsString(StandardCharsets.UTF_8);
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
    }

    public static Stream<Arguments> filterExpressions() {
        var b = new FilterExpressionBuilder();
        return Stream.of(
                Arguments.of(
                        b.ne("meta1", null).build(),
                        "meta1 != null",
                        List.of("1")),
                Arguments.of(
                        b.eq("meta1", null).build(),
                        "meta1 == null",
                        List.of("2", "3"))
        );
    }

    @MethodSource("filterExpressions")
    @ParameterizedTest
    void testSimilaritySearchWithFilters(Filter.Expression filterExpression, String nativeFilterExpression,
                                         List<String> expectedIds) {
        this.contextRunner.run(context -> {
            VectorStore vectorStore = context.getBean(AbstractObservationVectorStore.class);

            List<Document> originalDocuments = documents();
            vectorStore.doAdd(originalDocuments);

            if (filterExpression != null) {
                SearchRequest searchRequest = SearchRequest.builder()
                        .query("Spring AI")
                        .similarityThresholdAll()
                        .topK(3)
                        .filterExpression(filterExpression)
                        .build();
                List<Document> documents = vectorStore.doSimilaritySearch(searchRequest);
                assertEquals(expectedIds, documents.stream().map(Document::getId).collect(Collectors.toList()));
            }

            if (nativeFilterExpression != null) {
                SearchRequest searchRequest = SearchRequest.builder()
                        .query("Spring AI")
                        .similarityThresholdAll()
                        .topK(3)
                        .filterExpression(nativeFilterExpression)
                        .build();
                List<Document> documents = vectorStore.doSimilaritySearch(searchRequest);
                assertEquals(expectedIds, documents.stream().map(Document::getId).collect(Collectors.toList()));
            }

            vectorStore.doDelete(originalDocuments.stream().map(Document::getId).toList());
            vectorStore.close();
        });
    }

Comment From: dev-jonghoonpark

@linarkou It seems likely that the code provided in Bug Description contains a mistake, as both pieces of code appear identical. Based on the provided reproducible example, I understand what was intended. However, revising the code in Bug Description would make the distinction clearer.

Comment From: linarkou

@dev-jonghoonpark thank you! Modified original post.

Comment From: dev-jonghoonpark

Error Occurrence Point

FilterExpressionTextParser.java:line147

Filter.Operand operand = filterExpressionVisitor.visit(parser.where());

Error Message

Error: no viable alternative at input 'null'

Relevant Library

antlr

Comment From: dev-jonghoonpark

@linarkou

I have submitted a PR to address this issue. Please review it.

https://github.com/spring-projects/spring-ai/pull/3706