This is an automated email from the ASF dual-hosted git repository. btellier pushed a commit to branch master in repository https://gitbox.apache.org/repos/asf/james-project.git
The following commit(s) were added to refs/heads/master by this push: new 103d13846a JAMES-4057 Enable Query String to be used on OpenSearch (#2704) 103d13846a is described below commit 103d13846aaf7067fe71698db642678b747f9ee0 Author: Benoit TELLIER <btell...@linagora.com> AuthorDate: Mon May 5 20:48:52 2025 +0200 JAMES-4057 Enable Query String to be used on OpenSearch (#2704) This allows eg the user that both the terms "avocado" and "banana" are expected when searching the string "avocado banana" Amongst overs. --- .../opensearch/OpenSearchMailboxConfiguration.java | 23 +++- .../opensearch/query/CriterionConverter.java | 149 +++++++++++++-------- .../OpenSearchNoIndexBodyIntegrationTest.java | 2 +- ...ionTest.java => OpenSearchQueryStringTest.java} | 39 +++--- 4 files changed, 130 insertions(+), 83 deletions(-) diff --git a/mailbox/opensearch/src/main/java/org/apache/james/mailbox/opensearch/OpenSearchMailboxConfiguration.java b/mailbox/opensearch/src/main/java/org/apache/james/mailbox/opensearch/OpenSearchMailboxConfiguration.java index c90d8ad806..6113ebcacd 100644 --- a/mailbox/opensearch/src/main/java/org/apache/james/mailbox/opensearch/OpenSearchMailboxConfiguration.java +++ b/mailbox/opensearch/src/main/java/org/apache/james/mailbox/opensearch/OpenSearchMailboxConfiguration.java @@ -38,6 +38,7 @@ public class OpenSearchMailboxConfiguration { private Optional<IndexHeaders> indexHeaders; private Optional<Boolean> optimiseMoves; private Optional<Boolean> textFuzzinessSearch; + private Optional<Boolean> useQueryStringQuery; private Optional<IndexBody> indexBody; private Optional<IndexUser> indexUser; @@ -49,6 +50,7 @@ public class OpenSearchMailboxConfiguration { indexHeaders = Optional.empty(); optimiseMoves = Optional.empty(); textFuzzinessSearch = Optional.empty(); + useQueryStringQuery = Optional.empty(); indexBody = Optional.empty(); indexUser = Optional.empty(); } @@ -88,6 +90,12 @@ public class OpenSearchMailboxConfiguration { return this; } + + public Builder useQueryStringQuery(Boolean useQueryStringQuery) { + this.useQueryStringQuery = Optional.ofNullable(useQueryStringQuery); + return this; + } + public Builder indexBody(IndexBody indexBody) { this.indexBody = Optional.ofNullable(indexBody); return this; @@ -107,6 +115,7 @@ public class OpenSearchMailboxConfiguration { indexHeaders.orElse(IndexHeaders.YES), optimiseMoves.orElse(DEFAULT_OPTIMIZE_MOVES), textFuzzinessSearch.orElse(DEFAULT_TEXT_FUZZINESS_SEARCH), + useQueryStringQuery.orElse(DEFAULT_USE_SIMPLE_TEXT_QUERY), indexBody.orElse(IndexBody.YES), indexUser.orElse(IndexUser.NO)); } @@ -126,12 +135,14 @@ public class OpenSearchMailboxConfiguration { private static final String OPENSEARCH_INDEX_HEADERS = "opensearch.indexHeaders"; private static final String OPENSEARCH_MESSAGE_INDEX_OPTIMIZE_MOVE = "opensearch.message.index.optimize.move"; private static final String OPENSEARCH_TEXT_FUZZINESS_SEARCH = "opensearch.text.fuzziness.search"; + private static final String OPENSEARCH_TEXT_STRING_QUERY = "opensearch.text.string.query"; private static final String OPENSEARCH_INDEX_BODY = "opensearch.indexBody"; private static final String OPENSEARCH_INDEX_USER = "opensearch.indexUser"; private static final boolean DEFAULT_INDEX_ATTACHMENTS = true; private static final boolean DEFAULT_INDEX_HEADERS = true; public static final boolean DEFAULT_OPTIMIZE_MOVES = false; public static final boolean DEFAULT_TEXT_FUZZINESS_SEARCH = false; + public static final boolean DEFAULT_USE_SIMPLE_TEXT_QUERY = false; public static final boolean DEFAULT_INDEX_BODY = true; public static final boolean DEFAULT_INDEX_USER = false; public static final OpenSearchMailboxConfiguration DEFAULT_CONFIGURATION = builder().build(); @@ -145,6 +156,7 @@ public class OpenSearchMailboxConfiguration { .indexHeaders(provideIndexHeaders(configuration)) .optimiseMoves(configuration.getBoolean(OPENSEARCH_MESSAGE_INDEX_OPTIMIZE_MOVE, null)) .textFuzzinessSearch(configuration.getBoolean(OPENSEARCH_TEXT_FUZZINESS_SEARCH, null)) + .useQueryStringQuery(configuration.getBoolean(OPENSEARCH_TEXT_STRING_QUERY, null)) .indexBody(provideIndexBody(configuration)) .indexUser(provideIndexUser(configuration)) .build(); @@ -206,12 +218,13 @@ public class OpenSearchMailboxConfiguration { private final IndexHeaders indexHeaders; private final boolean optimiseMoves; private final boolean textFuzzinessSearch; + private final boolean useQueryStringQuery; private final IndexBody indexBody; private final IndexUser indexUser; private OpenSearchMailboxConfiguration(IndexName indexMailboxName, ReadAliasName readAliasMailboxName, WriteAliasName writeAliasMailboxName, IndexAttachments indexAttachment, - IndexHeaders indexHeaders, boolean optimiseMoves, boolean textFuzzinessSearch, + IndexHeaders indexHeaders, boolean optimiseMoves, boolean textFuzzinessSearch, boolean useSimpleTextQuery, IndexBody indexBody, IndexUser indexUser) { this.indexMailboxName = indexMailboxName; this.readAliasMailboxName = readAliasMailboxName; @@ -220,6 +233,7 @@ public class OpenSearchMailboxConfiguration { this.indexHeaders = indexHeaders; this.optimiseMoves = optimiseMoves; this.textFuzzinessSearch = textFuzzinessSearch; + this.useQueryStringQuery = useSimpleTextQuery; this.indexBody = indexBody; this.indexUser = indexUser; } @@ -260,6 +274,10 @@ public class OpenSearchMailboxConfiguration { return indexUser; } + public boolean isUseQueryStringQuery() { + return useQueryStringQuery; + } + @Override public final boolean equals(Object o) { if (o instanceof OpenSearchMailboxConfiguration) { @@ -271,6 +289,7 @@ public class OpenSearchMailboxConfiguration { && Objects.equals(this.readAliasMailboxName, that.readAliasMailboxName) && Objects.equals(this.optimiseMoves, that.optimiseMoves) && Objects.equals(this.textFuzzinessSearch, that.textFuzzinessSearch) + && Objects.equals(this.useQueryStringQuery, that.useQueryStringQuery) && Objects.equals(this.writeAliasMailboxName, that.writeAliasMailboxName) && Objects.equals(this.indexBody, that.indexBody) && Objects.equals(this.indexUser, that.indexUser); @@ -281,6 +300,6 @@ public class OpenSearchMailboxConfiguration { @Override public final int hashCode() { return Objects.hash(indexMailboxName, readAliasMailboxName, writeAliasMailboxName, indexAttachment, indexHeaders, - writeAliasMailboxName, optimiseMoves, textFuzzinessSearch, indexBody, indexUser); + writeAliasMailboxName, optimiseMoves, textFuzzinessSearch, useQueryStringQuery, indexBody, indexUser); } } diff --git a/mailbox/opensearch/src/main/java/org/apache/james/mailbox/opensearch/query/CriterionConverter.java b/mailbox/opensearch/src/main/java/org/apache/james/mailbox/opensearch/query/CriterionConverter.java index 714d53bee3..a85a1c050b 100644 --- a/mailbox/opensearch/src/main/java/org/apache/james/mailbox/opensearch/query/CriterionConverter.java +++ b/mailbox/opensearch/src/main/java/org/apache/james/mailbox/opensearch/query/CriterionConverter.java @@ -49,22 +49,27 @@ import org.opensearch.client.opensearch._types.query_dsl.MatchQuery; import org.opensearch.client.opensearch._types.query_dsl.NestedQuery; import org.opensearch.client.opensearch._types.query_dsl.Operator; import org.opensearch.client.opensearch._types.query_dsl.Query; +import org.opensearch.client.opensearch._types.query_dsl.QueryStringQuery; import org.opensearch.client.opensearch._types.query_dsl.RangeQuery; +import org.opensearch.client.opensearch._types.query_dsl.SimpleQueryStringQuery; import org.opensearch.client.opensearch._types.query_dsl.TermQuery; import com.google.common.annotations.VisibleForTesting; +import com.google.common.collect.ImmutableList; public class CriterionConverter { private final Map<Class<?>, Function<Criterion, Query>> criterionConverterMap; private final Map<Class<?>, BiFunction<String, HeaderOperator, Query>> headerOperatorConverterMap; private final String textFuzzinessSearchValue; + private final boolean useQueryStringQuery; @Inject public CriterionConverter(OpenSearchMailboxConfiguration openSearchMailboxConfiguration) { this.criterionConverterMap = new HashMap<>(); this.headerOperatorConverterMap = new HashMap<>(); this.textFuzzinessSearchValue = evaluateFuzzinessValue(openSearchMailboxConfiguration.textFuzzinessSearchEnable()); + this.useQueryStringQuery = openSearchMailboxConfiguration.isUseQueryStringQuery(); registerCriterionConverters(); registerHeaderOperatorConverters(); @@ -75,6 +80,7 @@ public class CriterionConverter { this.criterionConverterMap = new HashMap<>(); this.headerOperatorConverterMap = new HashMap<>(); this.textFuzzinessSearchValue = evaluateFuzzinessValue(DEFAULT_TEXT_FUZZINESS_SEARCH); + this.useQueryStringQuery = false; registerCriterionConverters(); registerHeaderOperatorConverters(); @@ -218,59 +224,80 @@ public class CriterionConverter { private Query convertTextCriterion(SearchQuery.TextCriterion textCriterion) { switch (textCriterion.getType()) { case BODY: - return new BoolQuery.Builder() - .should(new MatchQuery.Builder() - .field(JsonMessageConstants.TEXT_BODY) - .query(new FieldValue.Builder().stringValue(textCriterion.getOperator().getValue()).build()) - .fuzziness(textFuzzinessSearchValue) - .operator(Operator.And) - .build() - .toQuery()) - .should(new MatchQuery.Builder() - .field(JsonMessageConstants.HTML_BODY) - .query(new FieldValue.Builder().stringValue(textCriterion.getOperator().getValue()).build()) - .fuzziness(textFuzzinessSearchValue) - .operator(Operator.And) + if (useQueryStringQuery) { + return new SimpleQueryStringQuery.Builder() + .fields(ImmutableList.of(JsonMessageConstants.TEXT_BODY, JsonMessageConstants.HTML_BODY)) + .query(textCriterion.getOperator().getValue()) + .build().toQuery(); + } else { + return new BoolQuery.Builder() + .should(new MatchQuery.Builder() + .field(JsonMessageConstants.TEXT_BODY) + .query(new FieldValue.Builder().stringValue(textCriterion.getOperator().getValue()).build()) + .fuzziness(textFuzzinessSearchValue) + .operator(Operator.And) + .build() + .toQuery()) + .should(new MatchQuery.Builder() + .field(JsonMessageConstants.HTML_BODY) + .query(new FieldValue.Builder().stringValue(textCriterion.getOperator().getValue()).build()) + .fuzziness(textFuzzinessSearchValue) + .operator(Operator.And) + .build() + .toQuery()) .build() - .toQuery()) - .build() - .toQuery(); + .toQuery(); + } case FULL: - return new BoolQuery.Builder() - .should(new MatchQuery.Builder() - .field(JsonMessageConstants.TEXT_BODY) - .query(new FieldValue.Builder().stringValue(textCriterion.getOperator().getValue()).build()) - .fuzziness(textFuzzinessSearchValue) - .operator(Operator.And) - .build() - .toQuery()) - .should(new MatchQuery.Builder() - .field(JsonMessageConstants.HTML_BODY) - .query(new FieldValue.Builder().stringValue(textCriterion.getOperator().getValue()).build()) - .fuzziness(textFuzzinessSearchValue) - .operator(Operator.And) - .build() - .toQuery()) - .should(new MatchQuery.Builder() - .field(JsonMessageConstants.ATTACHMENTS + "." + JsonMessageConstants.Attachment.TEXT_CONTENT) - .query(new FieldValue.Builder().stringValue(textCriterion.getOperator().getValue()).build()) - .fuzziness(textFuzzinessSearchValue) - .operator(Operator.And) + if (useQueryStringQuery) { + return new SimpleQueryStringQuery.Builder() + .fields(ImmutableList.of(JsonMessageConstants.TEXT_BODY, JsonMessageConstants.HTML_BODY, JsonMessageConstants.ATTACHMENTS + "." + JsonMessageConstants.Attachment.TEXT_CONTENT)) + .query(textCriterion.getOperator().getValue()) + .build().toQuery(); + } else { + return new BoolQuery.Builder() + .should(new MatchQuery.Builder() + .field(JsonMessageConstants.TEXT_BODY) + .query(new FieldValue.Builder().stringValue(textCriterion.getOperator().getValue()).build()) + .fuzziness(textFuzzinessSearchValue) + .operator(Operator.And) + .build() + .toQuery()) + .should(new MatchQuery.Builder() + .field(JsonMessageConstants.HTML_BODY) + .query(new FieldValue.Builder().stringValue(textCriterion.getOperator().getValue()).build()) + .fuzziness(textFuzzinessSearchValue) + .operator(Operator.And) + .build() + .toQuery()) + .should(new MatchQuery.Builder() + .field(JsonMessageConstants.ATTACHMENTS + "." + JsonMessageConstants.Attachment.TEXT_CONTENT) + .query(new FieldValue.Builder().stringValue(textCriterion.getOperator().getValue()).build()) + .fuzziness(textFuzzinessSearchValue) + .operator(Operator.And) + .build() + .toQuery()) .build() - .toQuery()) - .build() - .toQuery(); + .toQuery(); + } case ATTACHMENTS: - return new BoolQuery.Builder() - .should(new MatchQuery.Builder() - .field(JsonMessageConstants.ATTACHMENTS + "." + JsonMessageConstants.Attachment.TEXT_CONTENT) - .query(new FieldValue.Builder().stringValue(textCriterion.getOperator().getValue()).build()) - .fuzziness(textFuzzinessSearchValue) - .operator(Operator.And) + if (useQueryStringQuery) { + return new SimpleQueryStringQuery.Builder() + .fields(ImmutableList.of(JsonMessageConstants.ATTACHMENTS + "." + JsonMessageConstants.Attachment.TEXT_CONTENT)) + .query(textCriterion.getOperator().getValue()) + .build().toQuery(); + } else { + return new BoolQuery.Builder() + .should(new MatchQuery.Builder() + .field(JsonMessageConstants.ATTACHMENTS + "." + JsonMessageConstants.Attachment.TEXT_CONTENT) + .query(new FieldValue.Builder().stringValue(textCriterion.getOperator().getValue()).build()) + .fuzziness(textFuzzinessSearchValue) + .operator(Operator.And) + .build() + .toQuery()) .build() - .toQuery()) - .build() - .toQuery(); + .toQuery(); + } case ATTACHMENT_FILE_NAME: return new BoolQuery.Builder() .should(new MatchQuery.Builder() @@ -466,15 +493,23 @@ public class CriterionConverter { } private Query convertSubject(SearchQuery.SubjectCriterion headerCriterion) { - return new MatchQuery.Builder() - .field(JsonMessageConstants.SUBJECT) - .query(new FieldValue.Builder() - .stringValue(headerCriterion.getSubject()) - .build()) - .fuzziness(textFuzzinessSearchValue) - .operator(Operator.And) - .build() - .toQuery(); + if (useQueryStringQuery) { + return new QueryStringQuery.Builder() + .fields(ImmutableList.of(JsonMessageConstants.SUBJECT)) + .query(headerCriterion.getSubject()) + .fuzziness(textFuzzinessSearchValue) + .build().toQuery(); + } else { + return new MatchQuery.Builder() + .field(JsonMessageConstants.SUBJECT) + .query(new FieldValue.Builder() + .stringValue(headerCriterion.getSubject()) + .build()) + .fuzziness(textFuzzinessSearchValue) + .operator(Operator.And) + .build() + .toQuery(); + } } private Query manageAddressFields(String headerName, String value) { diff --git a/mailbox/opensearch/src/test/java/org/apache/james/mailbox/opensearch/OpenSearchNoIndexBodyIntegrationTest.java b/mailbox/opensearch/src/test/java/org/apache/james/mailbox/opensearch/OpenSearchNoIndexBodyIntegrationTest.java index 8673fe901f..72b42ec897 100644 --- a/mailbox/opensearch/src/test/java/org/apache/james/mailbox/opensearch/OpenSearchNoIndexBodyIntegrationTest.java +++ b/mailbox/opensearch/src/test/java/org/apache/james/mailbox/opensearch/OpenSearchNoIndexBodyIntegrationTest.java @@ -71,7 +71,7 @@ import com.google.common.collect.ImmutableSet; import reactor.core.publisher.Mono; -public class OpenSearchNoIndexBodyIntegrationTest { +class OpenSearchNoIndexBodyIntegrationTest { static final int SEARCH_SIZE = 1; private static final Username USERNAME = Username.of("user"); private static final ConditionFactory CALMLY_AWAIT = Awaitility diff --git a/mailbox/opensearch/src/test/java/org/apache/james/mailbox/opensearch/OpenSearchNoIndexBodyIntegrationTest.java b/mailbox/opensearch/src/test/java/org/apache/james/mailbox/opensearch/OpenSearchQueryStringTest.java similarity index 89% copy from mailbox/opensearch/src/test/java/org/apache/james/mailbox/opensearch/OpenSearchNoIndexBodyIntegrationTest.java copy to mailbox/opensearch/src/test/java/org/apache/james/mailbox/opensearch/OpenSearchQueryStringTest.java index 8673fe901f..74b3f41a25 100644 --- a/mailbox/opensearch/src/test/java/org/apache/james/mailbox/opensearch/OpenSearchNoIndexBodyIntegrationTest.java +++ b/mailbox/opensearch/src/test/java/org/apache/james/mailbox/opensearch/OpenSearchQueryStringTest.java @@ -71,8 +71,8 @@ import com.google.common.collect.ImmutableSet; import reactor.core.publisher.Mono; -public class OpenSearchNoIndexBodyIntegrationTest { - static final int SEARCH_SIZE = 1; +class OpenSearchQueryStringTest { + static final int SEARCH_SIZE = 10; private static final Username USERNAME = Username.of("user"); private static final ConditionFactory CALMLY_AWAIT = Awaitility .with().pollInterval(ONE_HUNDRED_MILLISECONDS) @@ -113,6 +113,10 @@ public class OpenSearchNoIndexBodyIntegrationTest { MailboxIndexCreationUtil.prepareClient(client, readAliasName, writeAliasName, indexName, openSearch.getDockerOpenSearch().configuration()); + OpenSearchMailboxConfiguration openSearchMailboxConfiguration = OpenSearchMailboxConfiguration.builder() + .indexBody(IndexBody.YES) + .useQueryStringQuery(true) + .build(); InMemoryIntegrationResources resources = InMemoryIntegrationResources.builder() .preProvisionnedFakeAuthenticator() .fakeAuthorizator() @@ -123,10 +127,10 @@ public class OpenSearchNoIndexBodyIntegrationTest { preInstanciationStage.getMapperFactory(), ImmutableSet.of(), new OpenSearchIndexer(client, writeAliasName), - new OpenSearchSearcher(client, new QueryConverter(new CriterionConverter()), SEARCH_SIZE, readAliasName, routingKeyFactory), - new MessageToOpenSearchJson(textExtractor, ZoneId.of("Europe/Paris"), IndexAttachments.YES, IndexHeaders.YES, IndexBody.NO), + new OpenSearchSearcher(client, new QueryConverter(new CriterionConverter(openSearchMailboxConfiguration)), SEARCH_SIZE, readAliasName, routingKeyFactory), + new MessageToOpenSearchJson(textExtractor, ZoneId.of("Europe/Paris"), IndexAttachments.YES, IndexHeaders.YES, IndexBody.YES), preInstanciationStage.getSessionProvider(), routingKeyFactory, messageIdFactory, - OpenSearchMailboxConfiguration.builder().indexBody(IndexBody.NO).build(), new RecordingMetricFactory(), + openSearchMailboxConfiguration, new RecordingMetricFactory(), ImmutableSet.of())) .noPreDeletionHooks() .storeQuotaManager() @@ -150,37 +154,26 @@ public class OpenSearchNoIndexBodyIntegrationTest { @Test void searchingByBodyContentShouldNotReturnMessageWhenNoIndexBody() throws Exception { - addMessage(session, inboxPath).block(); - - awaitForOpenSearch(QueryBuilders.matchAll().build().toQuery(), 1L); - - SearchQuery searchQuery = SearchQuery.of(SearchQuery.bodyContains("Hello")); - - assertThat(messageSearchIndex.search(session, mailbox, searchQuery).collectList().block()) - .isEmpty(); - } - - @Test - void searchingAllShoulReturnMessageWhenNoIndexBody() throws Exception { - ComposedMessageId composedMessageId = addMessage(session, inboxPath).block(); + ComposedMessageId expectedId = addMessage(session, inboxPath, "Lucas love avocado banana smoothie").block(); + addMessage(session, inboxPath, "Avocado grows in Mexico").block(); - awaitForOpenSearch(QueryBuilders.matchAll().build().toQuery(), 1L); + awaitForOpenSearch(QueryBuilders.matchAll().build().toQuery(), 2L); - SearchQuery searchQuery = SearchQuery.of(SearchQuery.all()); + SearchQuery searchQuery = SearchQuery.of(SearchQuery.bodyContains("\"love avocado\"")); assertThat(messageSearchIndex.search(session, mailbox, searchQuery).collectList().block()) - .containsExactly(composedMessageId.getUid()); + .containsOnly(expectedId.getUid()); } - private Mono<ComposedMessageId> addMessage(MailboxSession session, MailboxPath mailboxPath) throws Exception { + private Mono<ComposedMessageId> addMessage(MailboxSession session, MailboxPath mailboxPath, String message) throws Exception { MessageManager messageManager = storeMailboxManager.getMailbox(mailboxPath, session); String recipient = "u...@example.com"; return Mono.from(messageManager.appendMessageReactive(MessageManager.AppendCommand.from( Message.Builder.of() .setTo(recipient) - .setBody("Hello", StandardCharsets.UTF_8)), + .setBody(message, StandardCharsets.UTF_8)), session)) .map(MessageManager.AppendResult::getId); } --------------------------------------------------------------------- To unsubscribe, e-mail: notifications-unsubscr...@james.apache.org For additional commands, e-mail: notifications-h...@james.apache.org