diff --git a/langchain4j-milvus/src/main/java/dev/langchain4j/store/embedding/milvus/MilvusMetadataFilterMapper.java b/langchain4j-milvus/src/main/java/dev/langchain4j/store/embedding/milvus/MilvusMetadataFilterMapper.java index e772e89143..b2e728eb16 100644 --- a/langchain4j-milvus/src/main/java/dev/langchain4j/store/embedding/milvus/MilvusMetadataFilterMapper.java +++ b/langchain4j-milvus/src/main/java/dev/langchain4j/store/embedding/milvus/MilvusMetadataFilterMapper.java @@ -46,10 +46,29 @@ class MilvusMetadataFilterMapper { } private static String mapContains(ContainsString containsString, String metadataFieldName) { + // ContainsString is a literal substring match (see Filter / ContainsString#test, which uses + // String#contains). Milvus LIKE treats % and _ as wildcards, so any % or _ in the user-supplied + // value must be escaped to be matched literally; only the surrounding % we add are real wildcards. return format( "%s LIKE %s", formatKey(containsString.key(), metadataFieldName), - formatValue("%" + containsString.comparisonValue() + "%")); + formatLikePattern(containsString.comparisonValue())); + } + + /** + * Builds a quoted Milvus LIKE pattern that matches the given value as a literal substring. The value's + * own LIKE wildcards ({@code %} and {@code _}) are escaped with a backslash so they are matched + * literally, while the surrounding {@code %} characters remain wildcards for the "contains" semantics. + * Backslash and double quote are escaped as in {@link #formatValue(Object)} so the value stays inside + * the string literal. + */ + private static String formatLikePattern(String value) { + // Escape backslash first, then the LIKE wildcards % and _, then the string-literal double quote. + String escaped = value.replace("\\", "\\\\") + .replace("%", "\\%") + .replace("_", "\\_") + .replace("\"", "\\\""); + return "\"%" + escaped + "%\""; } private static String mapEqual(IsEqualTo isEqualTo, String metadataFieldName) { diff --git a/langchain4j-milvus/src/test/java/dev/langchain4j/store/embedding/milvus/MilvusMetadataFilterMapperTest.java b/langchain4j-milvus/src/test/java/dev/langchain4j/store/embedding/milvus/MilvusMetadataFilterMapperTest.java index db6c90712b..b38efb9bba 100644 --- a/langchain4j-milvus/src/test/java/dev/langchain4j/store/embedding/milvus/MilvusMetadataFilterMapperTest.java +++ b/langchain4j-milvus/src/test/java/dev/langchain4j/store/embedding/milvus/MilvusMetadataFilterMapperTest.java @@ -43,4 +43,42 @@ class MilvusMetadataFilterMapperTest { assertThat(expr).isEqualTo("metadata[\"key\"] == \"foo\""); } + + @Test + void contains_should_wrap_plain_value_in_like_wildcards() { + Filter filter = metadataKey("key").containsString("foo"); + + String expr = MilvusMetadataFilterMapper.map(filter, "metadata"); + + assertThat(expr).isEqualTo("metadata[\"key\"] LIKE \"%foo%\""); + } + + @Test + void contains_should_escape_percent_wildcard_in_value() { + // "50%" must be matched as a literal substring; the user's '%' must NOT act as a LIKE wildcard. + Filter filter = metadataKey("key").containsString("50%"); + + String expr = MilvusMetadataFilterMapper.map(filter, "metadata"); + + assertThat(expr).isEqualTo("metadata[\"key\"] LIKE \"%50\\%%\""); + } + + @Test + void contains_should_escape_underscore_wildcard_in_value() { + // "a_b" must be matched literally; the user's '_' must NOT act as a single-character wildcard. + Filter filter = metadataKey("key").containsString("a_b"); + + String expr = MilvusMetadataFilterMapper.map(filter, "metadata"); + + assertThat(expr).isEqualTo("metadata[\"key\"] LIKE \"%a\\_b%\""); + } + + @Test + void contains_should_escape_backslash_percent_and_underscore_together() { + Filter filter = metadataKey("key").containsString("a\\b100%_done"); + + String expr = MilvusMetadataFilterMapper.map(filter, "metadata"); + + assertThat(expr).isEqualTo("metadata[\"key\"] LIKE \"%a\\\\b100\\%\\_done%\""); + } }