diff --git a/vector-stores/spring-ai-cassandra-store/src/main/java/org/springframework/ai/vectorstore/cassandra/CassandraFilterExpressionConverter.java b/vector-stores/spring-ai-cassandra-store/src/main/java/org/springframework/ai/vectorstore/cassandra/CassandraFilterExpressionConverter.java index 4437a5fc8..d4c78d69c 100644 --- a/vector-stores/spring-ai-cassandra-store/src/main/java/org/springframework/ai/vectorstore/cassandra/CassandraFilterExpressionConverter.java +++ b/vector-stores/spring-ai-cassandra-store/src/main/java/org/springframework/ai/vectorstore/cassandra/CassandraFilterExpressionConverter.java @@ -23,7 +23,9 @@ import java.util.function.Function; import java.util.stream.Collectors; import com.datastax.oss.driver.api.core.metadata.schema.ColumnMetadata; +import com.datastax.oss.driver.api.core.type.DataType; import com.datastax.oss.driver.api.core.type.DataTypes; +import com.datastax.oss.driver.api.core.type.ListType; import com.datastax.oss.driver.api.core.type.codec.registry.CodecRegistry; import com.datastax.oss.driver.shaded.guava.common.base.Preconditions; @@ -118,10 +120,19 @@ class CassandraFilterExpressionConverter extends AbstractFilterExpressionConvert } private void doValue(ColumnMetadata column, Object v, StringBuilder context) { + + DataType dataType = column.getType(); + + // Check if we're handling an element inside a collection for an IN clause + if ((dataType instanceof ListType) && !(v instanceof Collection)) { + // Extract the element type from the collection type + dataType = ((ListType) dataType).getElementType(); + } + if (DataTypes.SMALLINT.equals(column.getType())) { v = ((Number) v).shortValue(); } - context.append(CodecRegistry.DEFAULT.codecFor(column.getType()).format(v)); + context.append(CodecRegistry.DEFAULT.codecFor(dataType).format(v)); } private Optional getColumn(String name) { diff --git a/vector-stores/spring-ai-cassandra-store/src/test/java/org/springframework/ai/vectorstore/cassandra/CassandraVectorStoreIT.java b/vector-stores/spring-ai-cassandra-store/src/test/java/org/springframework/ai/vectorstore/cassandra/CassandraVectorStoreIT.java index a5993b9c2..040f222e7 100644 --- a/vector-stores/spring-ai-cassandra-store/src/test/java/org/springframework/ai/vectorstore/cassandra/CassandraVectorStoreIT.java +++ b/vector-stores/spring-ai-cassandra-store/src/test/java/org/springframework/ai/vectorstore/cassandra/CassandraVectorStoreIT.java @@ -522,6 +522,61 @@ class CassandraVectorStoreIT extends BaseVectorStoreTests { }); } + @Test + void searchWithCollectionFilter() { + this.contextRunner.run(context -> { + try (CassandraVectorStore store = createTestStore(context, + new SchemaColumn("currencies", DataTypes.listOf(DataTypes.TEXT), SchemaColumnTags.INDEXED))) { + + // Create test documents with different currency lists + var btcDocument = new Document("BTC_doc", "Bitcoin document", Map.of("currencies", List.of("BTC"))); + var ethDocument = new Document("ETH_doc", "Ethereum document", Map.of("currencies", List.of("ETH"))); + var multiCurrencyDocument = new Document("MULTI_doc", "Multi-currency document", + Map.of("currencies", List.of("BTC", "ETH", "SOL"))); + + store.add(List.of(btcDocument, ethDocument, multiCurrencyDocument)); + + // Verify initial state + List results = store + .similaritySearch(SearchRequest.builder().query("document").topK(5).build()); + assertThat(results).hasSize(3); + + try { + // Test filtering with IN operator on a collection field + Filter.Expression filterExpression = new Filter.Expression(Filter.ExpressionType.IN, + new Filter.Key("currencies"), new Filter.Value(List.of("BTC"))); + + // Search using programmatic filter + store.similaritySearch(SearchRequest.builder() + .query("document") + .topK(5) + .similarityThresholdAll() + .filterExpression(filterExpression) + .build()); + + // If we get here without an exception, it means Cassandra + // unexpectedly accepted the query, + // which is surprising since Cassandra doesn't support the IN operator + // on collection columns. + // This would indicate a potential change in Cassandra's behavior. + Assertions.fail("Expected InvalidQueryException from Cassandra"); + } + catch (InvalidQueryException e) { + // This is the expected outcome: Cassandra rejects the query with a + // specific error + // indicating that collection columns cannot be used with IN + // operators, which is + // a documented limitation of Cassandra's query language. Support for + // collection + // filtering via CONTAINS would be needed for this type of query to + // work. + assertThat(e.getMessage()).contains("Collection column 'currencies'"); + assertThat(e.getMessage()).contains("cannot be restricted by a 'IN' relation"); + } + } + }); + } + @Test void throwsExceptionOnInvalidIndexNameWithSchemaValidation() { this.contextRunner.run(context -> {