فهرست منبع

Support minimum_should_match for terms_set query (#96082)

Support minimum_should_match for terms_set query.

Closes(#94095)

---------

Co-authored-by: Elastic Machine <elasticmachine@users.noreply.github.com>
Co-authored-by: Benjamin Trent <ben.w.trent@gmail.com>
Marantidis Kiriakos 2 سال پیش
والد
کامیت
5f5f9d26b7

+ 5 - 0
docs/changelog/96082.yaml

@@ -0,0 +1,5 @@
+pr: 96082
+summary: Support minimum_should_match field for terms_set query
+area: Search
+type: enhancement
+issues: []

+ 2 - 1
server/src/main/java/org/elasticsearch/TransportVersion.java

@@ -169,9 +169,10 @@ public record TransportVersion(int id) implements VersionId<TransportVersion> {
     public static final TransportVersion V_8_500_044 = registerTransportVersion(8_500_044, "96b83320-2317-4e9d-b735-356f18c1d76a");
     public static final TransportVersion V_8_500_045 = registerTransportVersion(8_500_045, "24a596dd-c843-4c0a-90b3-759697d74026");
     public static final TransportVersion V_8_500_046 = registerTransportVersion(8_500_046, "61666d4c-a4f0-40db-8a3d-4806718247c5");
+    public static final TransportVersion V_8_500_047 = registerTransportVersion(8_500_047, "4b1682fe-c37e-4184-80f6-7d57fcba9b3d");
 
     private static class CurrentHolder {
-        private static final TransportVersion CURRENT = findCurrent(V_8_500_046);
+        private static final TransportVersion CURRENT = findCurrent(V_8_500_047);
 
         // finds the pluggable current version, or uses the given fallback
         private static TransportVersion findCurrent(TransportVersion fallback) {

+ 47 - 7
server/src/main/java/org/elasticsearch/index/query/TermsSetQueryBuilder.java

@@ -44,15 +44,19 @@ public final class TermsSetQueryBuilder extends AbstractQueryBuilder<TermsSetQue
 
     public static final String NAME = "terms_set";
 
+    public static final TransportVersion MINIMUM_SHOULD_MATCH_ADDED_VERSION = TransportVersion.V_8_500_047;
+
     static final ParseField TERMS_FIELD = new ParseField("terms");
     static final ParseField MINIMUM_SHOULD_MATCH_FIELD = new ParseField("minimum_should_match_field");
     static final ParseField MINIMUM_SHOULD_MATCH_SCRIPT = new ParseField("minimum_should_match_script");
+    static final ParseField MINIMUM_SHOULD_MATCH = new ParseField("minimum_should_match");
 
     private final String fieldName;
     private final List<?> values;
 
     private String minimumShouldMatchField;
     private Script minimumShouldMatchScript;
+    private String minimumShouldMatch;
 
     public TermsSetQueryBuilder(String fieldName, List<?> values) {
         this(fieldName, values, true);
@@ -74,6 +78,9 @@ public final class TermsSetQueryBuilder extends AbstractQueryBuilder<TermsSetQue
         this.values = (List<?>) in.readGenericValue();
         this.minimumShouldMatchField = in.readOptionalString();
         this.minimumShouldMatchScript = in.readOptionalWriteable(Script::new);
+        if (in.getTransportVersion().onOrAfter(MINIMUM_SHOULD_MATCH_ADDED_VERSION)) {
+            this.minimumShouldMatch = in.readOptionalString();
+        }
     }
 
     @Override
@@ -82,6 +89,9 @@ public final class TermsSetQueryBuilder extends AbstractQueryBuilder<TermsSetQue
         out.writeGenericValue(values);
         out.writeOptionalString(minimumShouldMatchField);
         out.writeOptionalWriteable(minimumShouldMatchScript);
+        if (out.getTransportVersion().onOrAfter(MINIMUM_SHOULD_MATCH_ADDED_VERSION)) {
+            out.writeOptionalString(minimumShouldMatch);
+        }
     }
 
     // package protected for testing purpose
@@ -98,8 +108,10 @@ public final class TermsSetQueryBuilder extends AbstractQueryBuilder<TermsSetQue
     }
 
     public TermsSetQueryBuilder setMinimumShouldMatchField(String minimumShouldMatchField) {
-        if (minimumShouldMatchScript != null) {
-            throw new IllegalArgumentException("A script has already been specified. Cannot specify both a field and script");
+        if (minimumShouldMatchScript != null || minimumShouldMatch != null) {
+            throw new IllegalArgumentException(
+                "A script or value has already been specified. Cannot specify both a field and a script or value"
+            );
         }
         this.minimumShouldMatchField = minimumShouldMatchField;
         return this;
@@ -110,24 +122,41 @@ public final class TermsSetQueryBuilder extends AbstractQueryBuilder<TermsSetQue
     }
 
     public TermsSetQueryBuilder setMinimumShouldMatchScript(Script minimumShouldMatchScript) {
-        if (minimumShouldMatchField != null) {
-            throw new IllegalArgumentException("A field has already been specified. Cannot specify both a field and script");
+        if (minimumShouldMatchField != null || minimumShouldMatch != null) {
+            throw new IllegalArgumentException(
+                "A field or value has already been specified. Cannot specify both a script and a field or value"
+            );
         }
         this.minimumShouldMatchScript = minimumShouldMatchScript;
         return this;
     }
 
+    public String getMinimumShouldMatch() {
+        return minimumShouldMatch;
+    }
+
+    public TermsSetQueryBuilder setMinimumShouldMatch(String minimumShouldMatch) {
+        if (minimumShouldMatchField != null || minimumShouldMatchScript != null) {
+            throw new IllegalArgumentException(
+                "A field or script has already been specified. Cannot specify both a value and a script or field"
+            );
+        }
+        this.minimumShouldMatch = minimumShouldMatch;
+        return this;
+    }
+
     @Override
     protected boolean doEquals(TermsSetQueryBuilder other) {
         return Objects.equals(fieldName, other.fieldName)
             && Objects.equals(values, other.values)
             && Objects.equals(minimumShouldMatchField, other.minimumShouldMatchField)
-            && Objects.equals(minimumShouldMatchScript, other.minimumShouldMatchScript);
+            && Objects.equals(minimumShouldMatchScript, other.minimumShouldMatchScript)
+            && Objects.equals(minimumShouldMatch, other.minimumShouldMatch);
     }
 
     @Override
     protected int doHashCode() {
-        return Objects.hash(fieldName, values, minimumShouldMatchField, minimumShouldMatchScript);
+        return Objects.hash(fieldName, values, minimumShouldMatchField, minimumShouldMatchScript, minimumShouldMatch);
     }
 
     @Override
@@ -146,6 +175,9 @@ public final class TermsSetQueryBuilder extends AbstractQueryBuilder<TermsSetQue
         if (minimumShouldMatchScript != null) {
             builder.field(MINIMUM_SHOULD_MATCH_SCRIPT.getPreferredName(), minimumShouldMatchScript);
         }
+        if (minimumShouldMatch != null) {
+            builder.field(MINIMUM_SHOULD_MATCH.getPreferredName(), minimumShouldMatch);
+        }
         printBoostAndQueryName(builder);
         builder.endObject();
         builder.endObject();
@@ -166,6 +198,7 @@ public final class TermsSetQueryBuilder extends AbstractQueryBuilder<TermsSetQue
 
         List<Object> values = new ArrayList<>();
         String minimumShouldMatchField = null;
+        String minimumShouldMatch = null;
         Script minimumShouldMatchScript = null;
         String queryName = null;
         float boost = AbstractQueryBuilder.DEFAULT_BOOST;
@@ -194,6 +227,8 @@ public final class TermsSetQueryBuilder extends AbstractQueryBuilder<TermsSetQue
             } else if (token.isValue()) {
                 if (MINIMUM_SHOULD_MATCH_FIELD.match(currentFieldName, parser.getDeprecationHandler())) {
                     minimumShouldMatchField = parser.text();
+                } else if (MINIMUM_SHOULD_MATCH.match(currentFieldName, parser.getDeprecationHandler())) {
+                    minimumShouldMatch = parser.text();
                 } else if (AbstractQueryBuilder.BOOST_FIELD.match(currentFieldName, parser.getDeprecationHandler())) {
                     boost = parser.floatValue();
                 } else if (AbstractQueryBuilder.NAME_FIELD.match(currentFieldName, parser.getDeprecationHandler())) {
@@ -221,6 +256,9 @@ public final class TermsSetQueryBuilder extends AbstractQueryBuilder<TermsSetQue
         if (minimumShouldMatchField != null) {
             queryBuilder.setMinimumShouldMatchField(minimumShouldMatchField);
         }
+        if (minimumShouldMatch != null) {
+            queryBuilder.setMinimumShouldMatch(minimumShouldMatch);
+        }
         if (minimumShouldMatchScript != null) {
             queryBuilder.setMinimumShouldMatchScript(minimumShouldMatchScript);
         }
@@ -260,7 +298,9 @@ public final class TermsSetQueryBuilder extends AbstractQueryBuilder<TermsSetQue
 
     private LongValuesSource createValuesSource(SearchExecutionContext context) {
         LongValuesSource longValuesSource;
-        if (minimumShouldMatchField != null) {
+        if (minimumShouldMatch != null) {
+            longValuesSource = LongValuesSource.constant(Queries.calculateMinShouldMatch(values.size(), minimumShouldMatch));
+        } else if (minimumShouldMatchField != null) {
             MappedFieldType msmFieldType = context.getFieldType(minimumShouldMatchField);
             if (msmFieldType == null) {
                 throw new QueryShardException(context, "failed to find minimum_should_match field [" + minimumShouldMatchField + "]");

+ 58 - 13
server/src/test/java/org/elasticsearch/index/query/TermsSetQueryBuilderTests.java

@@ -78,10 +78,10 @@ public class TermsSetQueryBuilderTests extends AbstractQueryTestCase<TermsSetQue
         String fieldName = randomValueOtherThanMany(value -> value.equals(GEO_POINT_FIELD_NAME), () -> randomFrom(MAPPED_FIELD_NAMES));
         List<?> randomTerms = randomValues(fieldName);
         TermsSetQueryBuilder queryBuilder = new TermsSetQueryBuilder(TEXT_FIELD_NAME, randomTerms);
-        if (randomBoolean()) {
-            queryBuilder.setMinimumShouldMatchField("m_s_m");
-        } else {
-            queryBuilder.setMinimumShouldMatchScript(new Script(ScriptType.INLINE, MockScriptEngine.NAME, "_script", emptyMap()));
+        switch (randomIntBetween(0, 2)) {
+            case 0 -> queryBuilder.setMinimumShouldMatchField("m_s_m");
+            case 1 -> queryBuilder.setMinimumShouldMatchScript(new Script(ScriptType.INLINE, MockScriptEngine.NAME, "_script", emptyMap()));
+            case 2 -> queryBuilder.setMinimumShouldMatch("2");
         }
         return queryBuilder;
     }
@@ -104,7 +104,8 @@ public class TermsSetQueryBuilderTests extends AbstractQueryTestCase<TermsSetQue
     public void testCacheability() throws IOException {
         TermsSetQueryBuilder queryBuilder = createTestQueryBuilder();
         boolean isCacheable = queryBuilder.getMinimumShouldMatchField() != null
-            || (queryBuilder.getMinimumShouldMatchScript() != null && queryBuilder.getValues().isEmpty());
+            || (queryBuilder.getMinimumShouldMatchScript() != null && queryBuilder.getValues().isEmpty())
+            || queryBuilder.getMinimumShouldMatch() != null;
         SearchExecutionContext context = createSearchExecutionContext();
         rewriteQuery(queryBuilder, new SearchExecutionContext(context));
         assertNotNull(queryBuilder.doToQuery(context));
@@ -144,8 +145,9 @@ public class TermsSetQueryBuilderTests extends AbstractQueryTestCase<TermsSetQue
         List<?> values = instance.getValues();
         String minimumShouldMatchField = null;
         Script minimumShouldMatchScript = null;
+        String minimumShouldMatch = null;
 
-        switch (randomIntBetween(0, 3)) {
+        switch (randomIntBetween(0, 4)) {
             case 0 -> {
                 Predicate<String> predicate = s -> s.equals(instance.getFieldName()) == false && s.equals(GEO_POINT_FIELD_NAME) == false;
                 fieldName = randomValueOtherThanMany(predicate, () -> randomFrom(MAPPED_FIELD_NAMES));
@@ -154,6 +156,7 @@ public class TermsSetQueryBuilderTests extends AbstractQueryTestCase<TermsSetQue
             case 1 -> values = randomValues(fieldName);
             case 2 -> minimumShouldMatchField = randomAlphaOfLengthBetween(1, 10);
             case 3 -> minimumShouldMatchScript = new Script(ScriptType.INLINE, MockScriptEngine.NAME, randomAlphaOfLength(10), emptyMap());
+            case 4 -> minimumShouldMatch = "3";
         }
 
         TermsSetQueryBuilder newInstance = new TermsSetQueryBuilder(fieldName, values);
@@ -163,6 +166,9 @@ public class TermsSetQueryBuilderTests extends AbstractQueryTestCase<TermsSetQue
         if (minimumShouldMatchScript != null) {
             newInstance.setMinimumShouldMatchScript(minimumShouldMatchScript);
         }
+        if (minimumShouldMatch != null) {
+            newInstance.setMinimumShouldMatch(minimumShouldMatch);
+        }
         return newInstance;
     }
 
@@ -170,10 +176,18 @@ public class TermsSetQueryBuilderTests extends AbstractQueryTestCase<TermsSetQue
         TermsSetQueryBuilder queryBuilder = new TermsSetQueryBuilder("_field", Collections.emptyList());
         queryBuilder.setMinimumShouldMatchScript(new Script(""));
         expectThrows(IllegalArgumentException.class, () -> queryBuilder.setMinimumShouldMatchField("_field"));
+        expectThrows(IllegalArgumentException.class, () -> queryBuilder.setMinimumShouldMatch("2"));
 
         queryBuilder.setMinimumShouldMatchScript(null);
         queryBuilder.setMinimumShouldMatchField("_field");
         expectThrows(IllegalArgumentException.class, () -> queryBuilder.setMinimumShouldMatchScript(new Script("")));
+        expectThrows(IllegalArgumentException.class, () -> queryBuilder.setMinimumShouldMatch("2"));
+
+        queryBuilder.setMinimumShouldMatchField(null);
+        queryBuilder.setMinimumShouldMatch("2");
+        expectThrows(IllegalArgumentException.class, () -> queryBuilder.setMinimumShouldMatchField("_field"));
+        expectThrows(IllegalArgumentException.class, () -> queryBuilder.setMinimumShouldMatchScript(new Script("")));
+
     }
 
     public void testDoToQuery() throws Exception {
@@ -207,21 +221,52 @@ public class TermsSetQueryBuilderTests extends AbstractQueryTestCase<TermsSetQue
                 iw.addDocument(document);
 
                 document = new Document();
-                document.add(new TextField("message", "a b c d", Field.Store.NO));
+                document.add(new TextField("message", "a b c d f g", Field.Store.NO));
                 document.add(new SortedNumericDocValuesField("m_s_m", 3));
                 iw.addDocument(document);
             }
 
             try (IndexReader ir = DirectoryReader.open(directory)) {
                 SearchExecutionContext context = createSearchExecutionContext();
-                Query query = new TermsSetQueryBuilder("message", Arrays.asList("c", "d")).setMinimumShouldMatchField("m_s_m")
+                Query queryWithMinimumShouldMatchField = new TermsSetQueryBuilder("message", Arrays.asList("c", "d"))
+                    .setMinimumShouldMatchField("m_s_m")
                     .doToQuery(context);
                 IndexSearcher searcher = new IndexSearcher(ir);
-                TopDocs topDocs = searcher.search(query, 10, new Sort(SortField.FIELD_DOC));
-                assertThat(topDocs.totalHits.value, equalTo(3L));
-                assertThat(topDocs.scoreDocs[0].doc, equalTo(1));
-                assertThat(topDocs.scoreDocs[1].doc, equalTo(3));
-                assertThat(topDocs.scoreDocs[2].doc, equalTo(4));
+                TopDocs topDocsWithMinimumShouldMatchField = searcher.search(
+                    queryWithMinimumShouldMatchField,
+                    10,
+                    new Sort(SortField.FIELD_DOC)
+                );
+                assertThat(topDocsWithMinimumShouldMatchField.totalHits.value, equalTo(3L));
+                assertThat(topDocsWithMinimumShouldMatchField.scoreDocs[0].doc, equalTo(1));
+                assertThat(topDocsWithMinimumShouldMatchField.scoreDocs[1].doc, equalTo(3));
+                assertThat(topDocsWithMinimumShouldMatchField.scoreDocs[2].doc, equalTo(4));
+
+                context = createSearchExecutionContext();
+                Query queryWithMinimumShouldMatch = new TermsSetQueryBuilder("message", Arrays.asList("c", "d", "a")).setMinimumShouldMatch(
+                    "2"
+                ).doToQuery(context);
+                searcher = new IndexSearcher(ir);
+                TopDocs topDocsWithMinimumShouldMatch = searcher.search(queryWithMinimumShouldMatch, 10, new Sort(SortField.FIELD_DOC));
+                assertThat(topDocsWithMinimumShouldMatch.totalHits.value, equalTo(5L));
+                assertThat(topDocsWithMinimumShouldMatch.scoreDocs[0].doc, equalTo(1));
+                assertThat(topDocsWithMinimumShouldMatch.scoreDocs[1].doc, equalTo(2));
+                assertThat(topDocsWithMinimumShouldMatch.scoreDocs[2].doc, equalTo(3));
+                assertThat(topDocsWithMinimumShouldMatch.scoreDocs[3].doc, equalTo(4));
+                assertThat(topDocsWithMinimumShouldMatch.scoreDocs[4].doc, equalTo(5));
+
+                context = createSearchExecutionContext();
+                Query queryWithMinimumShouldMatchNegative = new TermsSetQueryBuilder("message", Arrays.asList("c", "g", "f"))
+                    .setMinimumShouldMatch("-1")
+                    .doToQuery(context);
+                searcher = new IndexSearcher(ir);
+                TopDocs topDocsWithMinimumShouldMatchNegative = searcher.search(
+                    queryWithMinimumShouldMatchNegative,
+                    10,
+                    new Sort(SortField.FIELD_DOC)
+                );
+                assertThat(topDocsWithMinimumShouldMatchNegative.totalHits.value, equalTo(1L));
+                assertThat(topDocsWithMinimumShouldMatchNegative.scoreDocs[0].doc, equalTo(5));
             }
         }
     }