diff --git a/spring-ai-vector-store/src/main/java/org/springframework/ai/vectorstore/filter/converter/SimpleVectorStoreFilterExpressionConverter.java b/spring-ai-vector-store/src/main/java/org/springframework/ai/vectorstore/filter/converter/SimpleVectorStoreFilterExpressionConverter.java index 266dd4d6a0c..7cb9e7bea3b 100644 --- a/spring-ai-vector-store/src/main/java/org/springframework/ai/vectorstore/filter/converter/SimpleVectorStoreFilterExpressionConverter.java +++ b/spring-ai-vector-store/src/main/java/org/springframework/ai/vectorstore/filter/converter/SimpleVectorStoreFilterExpressionConverter.java @@ -16,11 +16,11 @@ package org.springframework.ai.vectorstore.filter.converter; -import java.text.ParseException; -import java.text.SimpleDateFormat; +import java.time.ZoneOffset; +import java.time.format.DateTimeFormatter; +import java.time.format.DateTimeParseException; import java.util.Date; import java.util.List; -import java.util.TimeZone; import java.util.regex.Pattern; import org.springframework.ai.vectorstore.filter.Filter; @@ -36,11 +36,10 @@ public class SimpleVectorStoreFilterExpressionConverter extends AbstractFilterEx private static final Pattern DATE_FORMAT_PATTERN = Pattern.compile("\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}Z"); - private final SimpleDateFormat dateFormat; + private final DateTimeFormatter dateFormat; public SimpleVectorStoreFilterExpressionConverter() { - this.dateFormat = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'"); - this.dateFormat.setTimeZone(TimeZone.getTimeZone("UTC")); + this.dateFormat = DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ss'Z'").withZone(ZoneOffset.UTC); } @Override @@ -113,17 +112,17 @@ private void appendSpELContains(StringBuilder formattedList, StringBuilder conte protected void doSingleValue(Object value, StringBuilder context) { if (value instanceof Date date) { context.append("'"); - context.append(this.dateFormat.format(date)); + context.append(this.dateFormat.format(date.toInstant())); context.append("'"); } else if (value instanceof String text) { context.append("'"); if (DATE_FORMAT_PATTERN.matcher(text).matches()) { try { - Date date = this.dateFormat.parse(text); + var date = this.dateFormat.parse(text); context.append(this.dateFormat.format(date)); } - catch (ParseException e) { + catch (DateTimeParseException e) { throw new IllegalArgumentException("Invalid date type:" + text, e); } } diff --git a/vector-stores/spring-ai-azure-store/src/main/java/org/springframework/ai/vectorstore/azure/AzureAiSearchFilterExpressionConverter.java b/vector-stores/spring-ai-azure-store/src/main/java/org/springframework/ai/vectorstore/azure/AzureAiSearchFilterExpressionConverter.java index 545edf3594e..4ded51fc1c6 100644 --- a/vector-stores/spring-ai-azure-store/src/main/java/org/springframework/ai/vectorstore/azure/AzureAiSearchFilterExpressionConverter.java +++ b/vector-stores/spring-ai-azure-store/src/main/java/org/springframework/ai/vectorstore/azure/AzureAiSearchFilterExpressionConverter.java @@ -16,11 +16,11 @@ package org.springframework.ai.vectorstore.azure; -import java.text.ParseException; -import java.text.SimpleDateFormat; +import java.time.ZoneOffset; +import java.time.format.DateTimeFormatter; +import java.time.format.DateTimeParseException; import java.util.Date; import java.util.List; -import java.util.TimeZone; import java.util.regex.Pattern; import org.springframework.ai.vectorstore.azure.AzureVectorStore.MetadataField; @@ -40,9 +40,9 @@ */ public class AzureAiSearchFilterExpressionConverter extends AbstractFilterExpressionConverter { - private static Pattern DATE_FORMAT_PATTERN = Pattern.compile("\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}Z"); + private static final Pattern DATE_FORMAT_PATTERN = Pattern.compile("\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}Z"); - private final SimpleDateFormat dateFormat; + private final DateTimeFormatter dateFormat; private List allowedIdentifierNames; @@ -50,8 +50,7 @@ public AzureAiSearchFilterExpressionConverter(List filterMetadata Assert.notNull(filterMetadataFields, "The filterMetadataFields can not null."); this.allowedIdentifierNames = filterMetadataFields.stream().map(MetadataField::name).toList(); - this.dateFormat = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'"); - this.dateFormat.setTimeZone(TimeZone.getTimeZone("UTC")); + this.dateFormat = DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ss'Z'").withZone(ZoneOffset.UTC); } @Override @@ -137,15 +136,15 @@ protected void doValue(Filter.Value filterValue, StringBuilder context) { @Override protected void doSingleValue(Object value, StringBuilder context) { if (value instanceof Date date) { - context.append(this.dateFormat.format(date)); + context.append(this.dateFormat.format(date.toInstant())); } else if (value instanceof String text) { if (DATE_FORMAT_PATTERN.matcher(text).matches()) { try { - Date date = this.dateFormat.parse(text); + var date = this.dateFormat.parse(text); context.append(this.dateFormat.format(date)); } - catch (ParseException e) { + catch (DateTimeParseException e) { throw new IllegalArgumentException("Invalid date type:" + text, e); } } diff --git a/vector-stores/spring-ai-elasticsearch-store/src/main/java/org/springframework/ai/vectorstore/elasticsearch/ElasticsearchAiSearchFilterExpressionConverter.java b/vector-stores/spring-ai-elasticsearch-store/src/main/java/org/springframework/ai/vectorstore/elasticsearch/ElasticsearchAiSearchFilterExpressionConverter.java index eb8b505a8df..75729c38f18 100644 --- a/vector-stores/spring-ai-elasticsearch-store/src/main/java/org/springframework/ai/vectorstore/elasticsearch/ElasticsearchAiSearchFilterExpressionConverter.java +++ b/vector-stores/spring-ai-elasticsearch-store/src/main/java/org/springframework/ai/vectorstore/elasticsearch/ElasticsearchAiSearchFilterExpressionConverter.java @@ -16,11 +16,11 @@ package org.springframework.ai.vectorstore.elasticsearch; -import java.text.ParseException; -import java.text.SimpleDateFormat; +import java.time.ZoneOffset; +import java.time.format.DateTimeFormatter; +import java.time.format.DateTimeParseException; import java.util.Date; import java.util.List; -import java.util.TimeZone; import java.util.regex.Pattern; import org.springframework.ai.vectorstore.filter.Filter; @@ -40,11 +40,10 @@ public class ElasticsearchAiSearchFilterExpressionConverter extends AbstractFilt private static final Pattern DATE_FORMAT_PATTERN = Pattern.compile("\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}Z"); - private final SimpleDateFormat dateFormat; + private final DateTimeFormatter dateFormat; public ElasticsearchAiSearchFilterExpressionConverter() { - this.dateFormat = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'"); - this.dateFormat.setTimeZone(TimeZone.getTimeZone("UTC")); + this.dateFormat = DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ss'Z'").withZone(ZoneOffset.UTC); } @Override @@ -121,15 +120,15 @@ protected void doValue(Filter.Value filterValue, StringBuilder context) { @Override protected void doSingleValue(Object value, StringBuilder context) { if (value instanceof Date date) { - context.append(this.dateFormat.format(date)); + context.append(this.dateFormat.format(date.toInstant())); } else if (value instanceof String text) { if (DATE_FORMAT_PATTERN.matcher(text).matches()) { try { - Date date = this.dateFormat.parse(text); + var date = this.dateFormat.parse(text); context.append(this.dateFormat.format(date)); } - catch (ParseException e) { + catch (DateTimeParseException e) { throw new IllegalArgumentException("Invalid date type:" + text, e); } } diff --git a/vector-stores/spring-ai-elasticsearch-store/src/test/java/org/springframework/ai/vectorstore/elasticsearch/ElasticsearchAiSearchFilterExpressionConverterTest.java b/vector-stores/spring-ai-elasticsearch-store/src/test/java/org/springframework/ai/vectorstore/elasticsearch/ElasticsearchAiSearchFilterExpressionConverterTest.java index 8a366533a98..0d08231dee4 100644 --- a/vector-stores/spring-ai-elasticsearch-store/src/test/java/org/springframework/ai/vectorstore/elasticsearch/ElasticsearchAiSearchFilterExpressionConverterTest.java +++ b/vector-stores/spring-ai-elasticsearch-store/src/test/java/org/springframework/ai/vectorstore/elasticsearch/ElasticsearchAiSearchFilterExpressionConverterTest.java @@ -18,6 +18,7 @@ import java.util.Date; import java.util.List; +import java.util.stream.IntStream; import org.junit.jupiter.api.Test; @@ -49,6 +50,18 @@ public void testDate() { assertThat(vectorExpr).isEqualTo("metadata.activationDate:1970-01-01T00:00:02Z"); } + @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 diff --git a/vector-stores/spring-ai-opensearch-store/src/main/java/org/springframework/ai/vectorstore/opensearch/OpenSearchAiSearchFilterExpressionConverter.java b/vector-stores/spring-ai-opensearch-store/src/main/java/org/springframework/ai/vectorstore/opensearch/OpenSearchAiSearchFilterExpressionConverter.java index 9b5be81e759..c10efaa79a0 100644 --- a/vector-stores/spring-ai-opensearch-store/src/main/java/org/springframework/ai/vectorstore/opensearch/OpenSearchAiSearchFilterExpressionConverter.java +++ b/vector-stores/spring-ai-opensearch-store/src/main/java/org/springframework/ai/vectorstore/opensearch/OpenSearchAiSearchFilterExpressionConverter.java @@ -16,11 +16,11 @@ package org.springframework.ai.vectorstore.opensearch; -import java.text.ParseException; -import java.text.SimpleDateFormat; +import java.time.ZoneOffset; +import java.time.format.DateTimeFormatter; +import java.time.format.DateTimeParseException; import java.util.Date; import java.util.List; -import java.util.TimeZone; import java.util.regex.Pattern; import org.springframework.ai.vectorstore.filter.Filter; @@ -38,11 +38,10 @@ public class OpenSearchAiSearchFilterExpressionConverter extends AbstractFilterE private static final Pattern DATE_FORMAT_PATTERN = Pattern.compile("\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}Z"); - private final SimpleDateFormat dateFormat; + private final DateTimeFormatter dateFormat; public OpenSearchAiSearchFilterExpressionConverter() { - this.dateFormat = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'"); - this.dateFormat.setTimeZone(TimeZone.getTimeZone("UTC")); + this.dateFormat = DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ss'Z'").withZone(ZoneOffset.UTC); } @Override @@ -119,15 +118,15 @@ protected void doValue(Filter.Value filterValue, StringBuilder context) { @Override protected void doSingleValue(Object value, StringBuilder context) { if (value instanceof Date date) { - context.append(this.dateFormat.format(date)); + context.append(this.dateFormat.format(date.toInstant())); } else if (value instanceof String text) { if (DATE_FORMAT_PATTERN.matcher(text).matches()) { try { - Date date = this.dateFormat.parse(text); + var date = this.dateFormat.parse(text); context.append(this.dateFormat.format(date)); } - catch (ParseException e) { + catch (DateTimeParseException e) { throw new IllegalArgumentException("Invalid date type:" + text, e); } }