Browse Source

fix #2828 Add configurable DisMax query support for default field searches

Shinsuke Sugaya 11 months ago
parent
commit
7ee6b77a60

+ 69 - 0
src/main/java/org/codelibs/fess/mylasta/direction/FessConfig.java

@@ -1110,6 +1110,15 @@ public interface FessConfig extends FessEnv, org.codelibs.fess.mylasta.direction
     /** The key of the configuration. e.g. true */
     String QUERY_BOOST_FUZZY_CONTENT_TRANSPOSITIONS = "query.boost.fuzzy.content.transpositions";
 
+    /** The key of the configuration. e.g. bool */
+    String QUERY_DEFAULT_query_type = "query.default.query_type";
+
+    /** The key of the configuration. e.g. 0.1 */
+    String QUERY_DISMAX_tie_breaker = "query.dismax.tie_breaker";
+
+    /** The key of the configuration. e.g.  */
+    String QUERY_BOOL_minimum_should_match = "query.bool.minimum_should_match";
+
     /** The key of the configuration. e.g. 50 */
     String QUERY_PREFIX_EXPANSIONS = "query.prefix.expansions";
 
@@ -5244,6 +5253,43 @@ public interface FessConfig extends FessEnv, org.codelibs.fess.mylasta.direction
      */
     boolean isQueryBoostFuzzyContentTranspositions();
 
+    /**
+     * Get the value for the key 'query.default.query_type'. <br>
+     * The value is, e.g. bool <br>
+     * @return The value of found property. (NotNull: if not found, exception but basically no way)
+     */
+    String getQueryDefaultQueryType();
+
+    /**
+     * Get the value for the key 'query.dismax.tie_breaker'. <br>
+     * The value is, e.g. 0.1 <br>
+     * @return The value of found property. (NotNull: if not found, exception but basically no way)
+     */
+    String getQueryDismaxTieBreaker();
+
+    /**
+     * Get the value for the key 'query.dismax.tie_breaker' as {@link java.math.BigDecimal}. <br>
+     * The value is, e.g. 0.1 <br>
+     * @return The value of found property. (NotNull: if not found, exception but basically no way)
+     * @throws NumberFormatException When the property is not decimal.
+     */
+    java.math.BigDecimal getQueryDismaxTieBreakerAsDecimal();
+
+    /**
+     * Get the value for the key 'query.bool.minimum_should_match'. <br>
+     * The value is, e.g.  <br>
+     * @return The value of found property. (NotNull: if not found, exception but basically no way)
+     */
+    String getQueryBoolMinimumShouldMatch();
+
+    /**
+     * Get the value for the key 'query.bool.minimum_should_match' as {@link Integer}. <br>
+     * The value is, e.g.  <br>
+     * @return The value of found property. (NotNull: if not found, exception but basically no way)
+     * @throws NumberFormatException When the property is not integer.
+     */
+    Integer getQueryBoolMinimumShouldMatchAsInteger();
+
     /**
      * Get the value for the key 'query.prefix.expansions'. <br>
      * The value is, e.g. 50 <br>
@@ -9489,6 +9535,26 @@ public interface FessConfig extends FessEnv, org.codelibs.fess.mylasta.direction
             return is(FessConfig.QUERY_BOOST_FUZZY_CONTENT_TRANSPOSITIONS);
         }
 
+        public String getQueryDefaultQueryType() {
+            return get(FessConfig.QUERY_DEFAULT_query_type);
+        }
+
+        public String getQueryDismaxTieBreaker() {
+            return get(FessConfig.QUERY_DISMAX_tie_breaker);
+        }
+
+        public java.math.BigDecimal getQueryDismaxTieBreakerAsDecimal() {
+            return getAsDecimal(FessConfig.QUERY_DISMAX_tie_breaker);
+        }
+
+        public String getQueryBoolMinimumShouldMatch() {
+            return get(FessConfig.QUERY_BOOL_minimum_should_match);
+        }
+
+        public Integer getQueryBoolMinimumShouldMatchAsInteger() {
+            return getAsInteger(FessConfig.QUERY_BOOL_minimum_should_match);
+        }
+
         public String getQueryPrefixExpansions() {
             return get(FessConfig.QUERY_PREFIX_EXPANSIONS);
         }
@@ -11158,6 +11224,9 @@ public interface FessConfig extends FessEnv, org.codelibs.fess.mylasta.direction
             defaultMap.put(FessConfig.QUERY_BOOST_FUZZY_CONTENT_EXPANSIONS, "10");
             defaultMap.put(FessConfig.QUERY_BOOST_FUZZY_CONTENT_prefix_length, "0");
             defaultMap.put(FessConfig.QUERY_BOOST_FUZZY_CONTENT_TRANSPOSITIONS, "true");
+            defaultMap.put(FessConfig.QUERY_DEFAULT_query_type, "bool");
+            defaultMap.put(FessConfig.QUERY_DISMAX_tie_breaker, "0.1");
+            defaultMap.put(FessConfig.QUERY_BOOL_minimum_should_match, "");
             defaultMap.put(FessConfig.QUERY_PREFIX_EXPANSIONS, "50");
             defaultMap.put(FessConfig.QUERY_PREFIX_SLOP, "0");
             defaultMap.put(FessConfig.QUERY_FUZZY_prefix_length, "0");

+ 148 - 0
src/main/java/org/codelibs/fess/query/DefaultQueryBuilder.java

@@ -0,0 +1,148 @@
+/*
+ * Copyright 2012-2024 CodeLibs Project and the Others.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND,
+ * either express or implied. See the License for the specific language
+ * governing permissions and limitations under the License.
+ */
+package org.codelibs.fess.query;
+
+import java.io.IOException;
+import java.util.Objects;
+
+import org.apache.lucene.search.Query;
+import org.opensearch.core.common.io.stream.StreamOutput;
+import org.opensearch.core.xcontent.XContentBuilder;
+import org.opensearch.index.query.BoolQueryBuilder;
+import org.opensearch.index.query.DisMaxQueryBuilder;
+import org.opensearch.index.query.QueryBuilder;
+import org.opensearch.index.query.QueryBuilderVisitor;
+import org.opensearch.index.query.QueryRewriteContext;
+import org.opensearch.index.query.QueryShardContext;
+
+public class DefaultQueryBuilder implements QueryBuilder {
+
+    private final QueryBuilder queryBuilder;
+
+    private final QueryType queryType;
+
+    public DefaultQueryBuilder(final QueryBuilder queryBuilder) {
+        this.queryBuilder = queryBuilder;
+        if (queryBuilder instanceof BoolQueryBuilder) {
+            queryType = QueryType.BOOL;
+        } else if (queryBuilder instanceof DisMaxQueryBuilder) {
+            queryType = QueryType.DISMAX;
+        } else {
+            throw new IllegalArgumentException("Unknown query builder: " + queryBuilder);
+        }
+    }
+
+    public DefaultQueryBuilder add(final QueryBuilder innerQueryBuilder) {
+        switch (queryType) {
+        case BOOL:
+            ((BoolQueryBuilder) queryBuilder).should(innerQueryBuilder);
+            break;
+        case DISMAX:
+            ((DisMaxQueryBuilder) queryBuilder).add(innerQueryBuilder);
+            break;
+        default:
+            break;
+        }
+        return this;
+    }
+
+    enum QueryType {
+        BOOL, DISMAX;
+    }
+
+    @Override
+    public String getWriteableName() {
+        return queryBuilder.getWriteableName();
+    }
+
+    @Override
+    public Query toQuery(final QueryShardContext context) throws IOException {
+        return queryBuilder.toQuery(context);
+    }
+
+    @Override
+    public boolean isFragment() {
+        return queryBuilder.isFragment();
+    }
+
+    @Override
+    public QueryBuilder queryName(final String queryName) {
+        return queryBuilder.queryName(queryName);
+    }
+
+    @Override
+    public String queryName() {
+        return queryBuilder.queryName();
+    }
+
+    @Override
+    public float boost() {
+        return queryBuilder.boost();
+    }
+
+    @Override
+    public QueryBuilder boost(final float boost) {
+        return queryBuilder.boost(boost);
+    }
+
+    @Override
+    public String getName() {
+        return queryBuilder.getName();
+    }
+
+    @Override
+    public QueryBuilder rewrite(final QueryRewriteContext queryShardContext) throws IOException {
+        return queryBuilder.rewrite(queryShardContext);
+    }
+
+    @Override
+    public void visit(final QueryBuilderVisitor visitor) {
+        queryBuilder.visit(visitor);
+    }
+
+    @Override
+    public XContentBuilder toXContent(final XContentBuilder builder, final Params params) throws IOException {
+        return queryBuilder.toXContent(builder, params);
+    }
+
+    @Override
+    public void writeTo(final StreamOutput out) throws IOException {
+        queryBuilder.writeTo(out);
+    }
+
+    @Override
+    public int hashCode() {
+        return queryBuilder.hashCode();
+    }
+
+    @Override
+    public boolean equals(final Object obj) {
+        if (this == obj) {
+            return true;
+        }
+        if ((obj == null) || (getClass() != obj.getClass())) {
+            return false;
+        }
+        final DefaultQueryBuilder other = (DefaultQueryBuilder) obj;
+        return Objects.equals(queryBuilder, other.queryBuilder);
+    }
+
+    @Override
+    public String toString() {
+        return queryBuilder.toString();
+    }
+
+}

+ 29 - 10
src/main/java/org/codelibs/fess/query/QueryCommand.java

@@ -20,6 +20,7 @@ import static org.codelibs.core.stream.StreamUtil.stream;
 import java.lang.Character.UnicodeBlock;
 
 import org.apache.lucene.search.Query;
+import org.codelibs.core.lang.StringUtil;
 import org.codelibs.fess.Constants;
 import org.codelibs.fess.entity.QueryContext;
 import org.codelibs.fess.mylasta.direction.FessConfig;
@@ -27,6 +28,7 @@ import org.codelibs.fess.util.ComponentUtil;
 import org.dbflute.optional.OptionalThing;
 import org.lastaflute.web.util.LaRequestUtil;
 import org.opensearch.index.query.BoolQueryBuilder;
+import org.opensearch.index.query.DisMaxQueryBuilder;
 import org.opensearch.index.query.QueryBuilder;
 import org.opensearch.index.query.QueryBuilders;
 import org.opensearch.search.sort.SortBuilder;
@@ -72,30 +74,47 @@ public abstract class QueryCommand {
                 (String[]) request.getAttribute(Constants.REQUEST_LANGUAGES)));
     }
 
-    protected BoolQueryBuilder buildDefaultQueryBuilder(final FessConfig fessConfig, final QueryContext context,
+    protected DefaultQueryBuilder buildDefaultQueryBuilder(final FessConfig fessConfig, final QueryContext context,
             final DefaultQueryBuilderFunction builder) {
-        final BoolQueryBuilder boolQuery = QueryBuilders.boolQuery();
-        boolQuery.should(builder.apply(fessConfig.getIndexFieldTitle(), fessConfig.getQueryBoostTitleAsDecimal().floatValue()));
-        boolQuery.should(builder.apply(fessConfig.getIndexFieldContent(), fessConfig.getQueryBoostContentAsDecimal().floatValue()));
+        final DefaultQueryBuilder defaultQuery = createDefaultQueryBuilder();
+        defaultQuery.add(builder.apply(fessConfig.getIndexFieldTitle(), fessConfig.getQueryBoostTitleAsDecimal().floatValue()));
+        defaultQuery.add(builder.apply(fessConfig.getIndexFieldContent(), fessConfig.getQueryBoostContentAsDecimal().floatValue()));
         final float importantContentBoost = fessConfig.getQueryBoostImportantContentAsDecimal().floatValue();
         if (importantContentBoost >= 0.0f) {
-            boolQuery.should(builder.apply(fessConfig.getIndexFieldImportantContent(), importantContentBoost));
+            defaultQuery.add(builder.apply(fessConfig.getIndexFieldImportantContent(), importantContentBoost));
         }
         final float importantContantLangBoost = fessConfig.getQueryBoostImportantContentLangAsDecimal().floatValue();
         getQueryLanguages().ifPresent(langs -> stream(langs).of(stream -> stream.forEach(lang -> {
-            boolQuery.should(
+            defaultQuery.add(
                     builder.apply(fessConfig.getIndexFieldTitle() + "_" + lang, fessConfig.getQueryBoostTitleLangAsDecimal().floatValue()));
-            boolQuery.should(builder.apply(fessConfig.getIndexFieldContent() + "_" + lang,
+            defaultQuery.add(builder.apply(fessConfig.getIndexFieldContent() + "_" + lang,
                     fessConfig.getQueryBoostContentLangAsDecimal().floatValue()));
             if (importantContantLangBoost >= 0.0f) {
-                boolQuery.should(builder.apply(fessConfig.getIndexFieldImportantContent() + "_" + lang, importantContantLangBoost));
+                defaultQuery.add(builder.apply(fessConfig.getIndexFieldImportantContent() + "_" + lang, importantContantLangBoost));
             }
         })));
         getQueryFieldConfig().additionalDefaultList.stream().forEach(f -> {
             final QueryBuilder query = builder.apply(f.getFirst(), f.getSecond());
-            boolQuery.should(query);
+            defaultQuery.add(query);
         });
-        return boolQuery;
+        return defaultQuery;
+    }
+
+    protected DefaultQueryBuilder createDefaultQueryBuilder() {
+        final FessConfig fessConfig = ComponentUtil.getFessConfig();
+
+        if ("dismax".equals(fessConfig.getQueryDefaultQueryType())) {
+            final DisMaxQueryBuilder disMaxQuery = QueryBuilders.disMaxQuery();
+            disMaxQuery.tieBreaker(fessConfig.getQueryDismaxTieBreakerAsDecimal().floatValue());
+            return new DefaultQueryBuilder(disMaxQuery);
+        }
+
+        final BoolQueryBuilder boolQuery = QueryBuilders.boolQuery();
+        final String minimumShouldMatch = fessConfig.getQueryBoolMinimumShouldMatch();
+        if (StringUtil.isNotBlank(minimumShouldMatch)) {
+            boolQuery.minimumShouldMatch(minimumShouldMatch);
+        }
+        return new DefaultQueryBuilder(boolQuery);
     }
 
     protected QueryBuilder buildMatchPhraseQuery(final String f, final String text) {

+ 4 - 5
src/main/java/org/codelibs/fess/query/TermQueryCommand.java

@@ -34,7 +34,6 @@ import org.codelibs.fess.mylasta.direction.FessConfig;
 import org.codelibs.fess.util.ComponentUtil;
 import org.lastaflute.core.message.UserMessages;
 import org.opensearch.common.unit.Fuzziness;
-import org.opensearch.index.query.BoolQueryBuilder;
 import org.opensearch.index.query.QueryBuilder;
 import org.opensearch.index.query.QueryBuilders;
 import org.opensearch.search.sort.SortOrder;
@@ -161,24 +160,24 @@ public class TermQueryCommand extends QueryCommand {
             final float boost, final String field, final String text) {
         context.addFieldLog(field, text);
         context.addHighlightedQuery(text);
-        final BoolQueryBuilder boolQuery =
+        final DefaultQueryBuilder defaultQuery =
                 buildDefaultQueryBuilder(fessConfig, context, (f, b) -> buildMatchPhraseQuery(f, text).boost(b * boost));
         final Integer fuzzyMinLength = fessConfig.getQueryBoostFuzzyMinLengthAsInteger();
         if (fuzzyMinLength >= 0 && text.length() >= fuzzyMinLength) {
-            boolQuery.should(QueryBuilders.fuzzyQuery(fessConfig.getIndexFieldTitle(), text)
+            defaultQuery.add(QueryBuilders.fuzzyQuery(fessConfig.getIndexFieldTitle(), text)
                     .boost(fessConfig.getQueryBoostFuzzyTitleAsDecimal().floatValue())
                     .prefixLength(fessConfig.getQueryBoostFuzzyTitlePrefixLengthAsInteger())
                     .transpositions(Constants.TRUE.equalsIgnoreCase(fessConfig.getQueryBoostFuzzyTitleTranspositions()))
                     .fuzziness(Fuzziness.build(fessConfig.getQueryBoostFuzzyTitleFuzziness()))
                     .maxExpansions(fessConfig.getQueryBoostFuzzyTitleExpansionsAsInteger()));
-            boolQuery.should(QueryBuilders.fuzzyQuery(fessConfig.getIndexFieldContent(), text)
+            defaultQuery.add(QueryBuilders.fuzzyQuery(fessConfig.getIndexFieldContent(), text)
                     .prefixLength(fessConfig.getQueryBoostFuzzyContentPrefixLengthAsInteger())
                     .transpositions(Constants.TRUE.equalsIgnoreCase(fessConfig.getQueryBoostFuzzyContentTranspositions()))
                     .boost(fessConfig.getQueryBoostFuzzyContentAsDecimal().floatValue())
                     .fuzziness(Fuzziness.build(fessConfig.getQueryBoostFuzzyContentFuzziness()))
                     .maxExpansions(fessConfig.getQueryBoostFuzzyContentExpansionsAsInteger()));
         }
-        return boolQuery;
+        return defaultQuery;
     }
 
     protected QueryBuilder convertSiteQuery(final FessConfig fessConfig, final QueryContext context, final TermQuery termQuery,

+ 4 - 0
src/main/resources/fess_config.properties

@@ -570,6 +570,10 @@ query.boost.fuzzy.content.expansions=10
 query.boost.fuzzy.content.prefix_length=0
 query.boost.fuzzy.content.transpositions=true
 
+query.default.query_type=bool
+query.dismax.tie_breaker=0.1
+query.bool.minimum_should_match=
+
 query.prefix.expansions=50
 query.prefix.slop=0
 query.fuzzy.prefix_length=0

+ 65 - 0
src/test/java/org/codelibs/fess/helper/QueryHelperTest.java

@@ -16,6 +16,7 @@
 package org.codelibs.fess.helper;
 
 import java.io.File;
+import java.math.BigDecimal;
 import java.util.List;
 import java.util.Map;
 import java.util.Set;
@@ -27,6 +28,7 @@ import org.codelibs.fess.Constants;
 import org.codelibs.fess.entity.QueryContext;
 import org.codelibs.fess.entity.SearchRequestParams.SearchRequestType;
 import org.codelibs.fess.exception.InvalidQueryException;
+import org.codelibs.fess.mylasta.direction.FessConfig;
 import org.codelibs.fess.query.BooleanQueryCommand;
 import org.codelibs.fess.query.BoostQueryCommand;
 import org.codelibs.fess.query.FuzzyQueryCommand;
@@ -41,6 +43,7 @@ import org.codelibs.fess.query.WildcardQueryCommand;
 import org.codelibs.fess.query.parser.QueryParser;
 import org.codelibs.fess.unit.UnitFessTestCase;
 import org.codelibs.fess.util.ComponentUtil;
+import org.dbflute.util.DfTypeUtil;
 import org.opensearch.index.query.BoolQueryBuilder;
 import org.opensearch.index.query.QueryBuilder;
 import org.opensearch.index.query.QueryBuilders;
@@ -88,7 +91,54 @@ public class QueryHelperTest extends UnitFessTestCase {
         new WildcardQueryCommand().register();
     }
 
+    private void setQueryType(final String queryType) {
+        final FessConfig fessConfig = ComponentUtil.getFessConfig();
+        ComponentUtil.setFessConfig(new FessConfig.SimpleImpl() {
+            @Override
+            public String get(String propertyKey) {
+                return fessConfig.get(propertyKey);
+            }
+
+            @Override
+            public BigDecimal getAsDecimal(String propertyKey) {
+                return DfTypeUtil.toBigDecimal(get(propertyKey));
+            }
+
+            @Override
+            public Integer getAsInteger(String propertyKey) {
+                return DfTypeUtil.toInteger(get(propertyKey));
+            }
+
+            @Override
+            public String getQueryDefaultQueryType() {
+                return queryType;
+            }
+        });
+    }
+
     public void test_build_simple() {
+        setQueryType("bool");
+
+        float titleBoost = 0.5f;
+        float contentBoost = 0.05f;
+
+        assertQuery(functionScoreQuery(simpleQuery("QUERY", titleBoost, contentBoost)), //
+                Map.of("_default", List.of("QUERY")), //
+                Set.of("QUERY"), //
+                buildQuery("QUERY"));
+        assertQuery(functionScoreQuery(simpleQuery("QUERY", titleBoost, contentBoost)), //
+                Map.of("_default", List.of("QUERY")), //
+                Set.of("QUERY"), //
+                buildQuery(" QUERY"));
+        assertQuery(functionScoreQuery(simpleQuery("QUERY", titleBoost, contentBoost)), //
+                Map.of("_default", List.of("QUERY")), //
+                Set.of("QUERY"), //
+                buildQuery("QUERY "));
+    }
+
+    public void test_build_simple_dismax() {
+        setQueryType("dismax");
+
         float titleBoost = 0.5f;
         float contentBoost = 0.05f;
 
@@ -133,6 +183,7 @@ public class QueryHelperTest extends UnitFessTestCase {
     }
 
     public void test_build_boost() {
+        setQueryType("bool");
         assertQueryContext(
                 "{\"function_score\":{\"query\":{\"bool\":{\"should\":[{\"match_phrase\":{\"title\":{\"query\":\"QUERY1\",\"slop\":0,\"zero_terms_query\":\"NONE\",\"boost\":5.0}}},{\"match_phrase\":{\"content\":{\"query\":\"QUERY1\",\"slop\":0,\"zero_terms_query\":\"NONE\",\"boost\":0.5}}},{\"fuzzy\":{\"title\":{\"value\":\"QUERY1\",\"fuzziness\":\"AUTO\",\"prefix_length\":0,\"max_expansions\":10,\"transpositions\":true,\"boost\":0.01}}},{\"fuzzy\":{\"content\":{\"value\":\"QUERY1\",\"fuzziness\":\"AUTO\",\"prefix_length\":0,\"max_expansions\":10,\"transpositions\":true,\"boost\":0.005}}}],\"adjust_pure_negative\":true,\"boost\":1.0}},\"functions\":[{\"filter\":{\"match_all\":{\"boost\":1.0}},\"field_value_factor\":{\"field\":\"boost\",\"factor\":1.0,\"modifier\":\"none\"}}],\"score_mode\":\"multiply\",\"max_boost\":3.4028235E38,\"boost\":1.0}}",
                 Map.of("_default", List.of("QUERY1")), //
@@ -153,6 +204,7 @@ public class QueryHelperTest extends UnitFessTestCase {
     }
 
     public void test_build_wildcard() {
+        setQueryType("bool");
         assertQueryContext(
                 "{\"function_score\":{\"query\":{\"bool\":{\"should\":[{\"wildcard\":{\"title\":{\"wildcard\":\"*\",\"boost\":0.5}}},{\"wildcard\":{\"content\":{\"wildcard\":\"*\",\"boost\":0.05}}}],\"adjust_pure_negative\":true,\"boost\":1.0}},\"functions\":[{\"filter\":{\"match_all\":{\"boost\":1.0}},\"field_value_factor\":{\"field\":\"boost\",\"factor\":1.0,\"modifier\":\"none\"}}],\"score_mode\":\"multiply\",\"max_boost\":3.4028235E38,\"boost\":1.0}}",
                 Map.of("_default", List.of("*")), //
@@ -191,6 +243,7 @@ public class QueryHelperTest extends UnitFessTestCase {
     }
 
     public void test_build_phrase() {
+        setQueryType("bool");
         assertQueryContext(
                 "{\"function_score\":{\"query\":{\"bool\":{\"should\":[{\"match_phrase\":{\"title\":{\"query\":\"QUERY1QUERY2\",\"slop\":0,\"zero_terms_query\":\"NONE\",\"boost\":0.5}}},{\"match_phrase\":{\"content\":{\"query\":\"QUERY1QUERY2\",\"slop\":0,\"zero_terms_query\":\"NONE\",\"boost\":0.05}}}],\"adjust_pure_negative\":true,\"boost\":1.0}},\"functions\":[{\"filter\":{\"match_all\":{\"boost\":1.0}},\"field_value_factor\":{\"field\":\"boost\",\"factor\":1.0,\"modifier\":\"none\"}}],\"score_mode\":\"multiply\",\"max_boost\":3.4028235E38,\"boost\":1.0}}",
                 Map.of("_default", List.of("QUERY1 QUERY2")), //
@@ -207,6 +260,7 @@ public class QueryHelperTest extends UnitFessTestCase {
     }
 
     public void test_build_prefix() {
+        setQueryType("bool");
         assertQueryContext(
                 "{\"function_score\":{\"query\":{\"bool\":{\"should\":[{\"match_phrase_prefix\":{\"title\":{\"query\":\"query\",\"slop\":0,\"max_expansions\":50,\"zero_terms_query\":\"NONE\",\"boost\":0.5}}},{\"match_phrase_prefix\":{\"content\":{\"query\":\"query\",\"slop\":0,\"max_expansions\":50,\"zero_terms_query\":\"NONE\",\"boost\":0.05}}}],\"adjust_pure_negative\":true,\"boost\":1.0}},\"functions\":[{\"filter\":{\"match_all\":{\"boost\":1.0}},\"field_value_factor\":{\"field\":\"boost\",\"factor\":1.0,\"modifier\":\"none\"}}],\"score_mode\":\"multiply\",\"max_boost\":3.4028235E38,\"boost\":1.0}}",
                 Map.of("_default", List.of("QUERY*")), //
@@ -229,6 +283,7 @@ public class QueryHelperTest extends UnitFessTestCase {
     }
 
     public void test_build_escape() {
+        setQueryType("bool");
         assertQueryContext(
                 "{\"function_score\":{\"query\":{\"bool\":{\"should\":[{\"match_phrase\":{\"title\":{\"query\":\"aaa:bbb\",\"slop\":0,\"zero_terms_query\":\"NONE\",\"boost\":0.5}}},{\"match_phrase\":{\"content\":{\"query\":\"aaa:bbb\",\"slop\":0,\"zero_terms_query\":\"NONE\",\"boost\":0.05}}},{\"fuzzy\":{\"title\":{\"value\":\"aaa:bbb\",\"fuzziness\":\"AUTO\",\"prefix_length\":0,\"max_expansions\":10,\"transpositions\":true,\"boost\":0.01}}},{\"fuzzy\":{\"content\":{\"value\":\"aaa:bbb\",\"fuzziness\":\"AUTO\",\"prefix_length\":0,\"max_expansions\":10,\"transpositions\":true,\"boost\":0.005}}}],\"adjust_pure_negative\":true,\"boost\":1.0}},\"functions\":[{\"filter\":{\"match_all\":{\"boost\":1.0}},\"field_value_factor\":{\"field\":\"boost\",\"factor\":1.0,\"modifier\":\"none\"}}],\"score_mode\":\"multiply\",\"max_boost\":3.4028235E38,\"boost\":1.0}}",
                 Map.of("_default", List.of("aaa:bbb")), //
@@ -335,6 +390,7 @@ public class QueryHelperTest extends UnitFessTestCase {
     }
 
     public void test_build_fuzzy() {
+        setQueryType("bool");
         assertQueryContext(
                 "{\"function_score\":{\"query\":{\"bool\":{\"should\":[{\"fuzzy\":{\"title\":{\"value\":\"QUERY1\",\"fuzziness\":\"2\",\"prefix_length\":0,\"max_expansions\":50,\"transpositions\":true,\"boost\":0.5}}},{\"fuzzy\":{\"content\":{\"value\":\"QUERY1\",\"fuzziness\":\"2\",\"prefix_length\":0,\"max_expansions\":50,\"transpositions\":true,\"boost\":0.05}}}],\"adjust_pure_negative\":true,\"boost\":1.0}},\"functions\":[{\"filter\":{\"match_all\":{\"boost\":1.0}},\"field_value_factor\":{\"field\":\"boost\",\"factor\":1.0,\"modifier\":\"none\"}}],\"score_mode\":\"multiply\",\"max_boost\":3.4028235E38,\"boost\":1.0}}",
                 Map.of("_default", List.of("QUERY1")), //
@@ -357,6 +413,7 @@ public class QueryHelperTest extends UnitFessTestCase {
     }
 
     public void test_build_range() {
+        setQueryType("bool");
         assertQueryContext(
                 "{\"function_score\":{\"query\":{\"bool\":{\"should\":[{\"match_phrase\":{\"title\":{\"query\":\"[aaaTObbb]\",\"slop\":0,\"zero_terms_query\":\"NONE\",\"boost\":0.5}}},{\"match_phrase\":{\"content\":{\"query\":\"[aaaTObbb]\",\"slop\":0,\"zero_terms_query\":\"NONE\",\"boost\":0.05}}}],\"adjust_pure_negative\":true,\"boost\":1.0}},\"functions\":[{\"filter\":{\"match_all\":{\"boost\":1.0}},\"field_value_factor\":{\"field\":\"boost\",\"factor\":1.0,\"modifier\":\"none\"}}],\"score_mode\":\"multiply\",\"max_boost\":3.4028235E38,\"boost\":1.0}}",
                 Map.of("_default", List.of("[aaa TO bbb]")), //
@@ -439,6 +496,14 @@ public class QueryHelperTest extends UnitFessTestCase {
     }
 
     private QueryBuilder simpleQuery(String query, float titleBoost, float contentBoost) {
+        if ("dismax".equals(ComponentUtil.getFessConfig().getQueryDefaultQueryType())) {
+            return QueryBuilders.disMaxQuery().tieBreaker(0.1f)//
+                    .add(QueryBuilders.matchPhraseQuery("title", query).boost(titleBoost))//
+                    .add(QueryBuilders.matchPhraseQuery("content", query).boost(contentBoost))//
+                    .add(QueryBuilders.fuzzyQuery("title", query).boost(0.01f).maxExpansions(10))//
+                    .add(QueryBuilders.fuzzyQuery("content", query).boost(0.005f).maxExpansions(10))//
+            ;
+        }
         return QueryBuilders.boolQuery()//
                 .should(QueryBuilders.matchPhraseQuery("title", query).boost(titleBoost))//
                 .should(QueryBuilders.matchPhraseQuery("content", query).boost(contentBoost))//

+ 39 - 0
src/test/java/org/codelibs/fess/query/DefaultQueryBuilderTest.java

@@ -0,0 +1,39 @@
+/*
+ * Copyright 2012-2024 CodeLibs Project and the Others.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND,
+ * either express or implied. See the License for the specific language
+ * governing permissions and limitations under the License.
+ */
+package org.codelibs.fess.query;
+
+import org.codelibs.fess.unit.UnitFessTestCase;
+import org.opensearch.index.query.QueryBuilders;
+
+public class DefaultQueryBuilderTest extends UnitFessTestCase {
+    public void test_invalid_null() {
+        try {
+            new DefaultQueryBuilder(null);
+            fail();
+        } catch (IllegalArgumentException e) {
+            // nothing
+        }
+    }
+
+    public void test_invalid_query() {
+        try {
+            new DefaultQueryBuilder(QueryBuilders.matchAllQuery());
+            fail();
+        } catch (IllegalArgumentException e) {
+            // nothing
+        }
+    }
+}

+ 67 - 2
src/test/java/org/codelibs/fess/query/TermQueryCommandTest.java

@@ -15,6 +15,7 @@
  */
 package org.codelibs.fess.query;
 
+import java.math.BigDecimal;
 import java.util.List;
 
 import org.apache.logging.log4j.LogManager;
@@ -23,9 +24,11 @@ import org.apache.lucene.search.Query;
 import org.apache.lucene.search.TermQuery;
 import org.codelibs.fess.entity.QueryContext;
 import org.codelibs.fess.exception.InvalidQueryException;
+import org.codelibs.fess.mylasta.direction.FessConfig;
 import org.codelibs.fess.query.parser.QueryParser;
 import org.codelibs.fess.unit.UnitFessTestCase;
 import org.codelibs.fess.util.ComponentUtil;
+import org.dbflute.util.DfTypeUtil;
 import org.opensearch.index.query.BoolQueryBuilder;
 import org.opensearch.index.query.MatchPhraseQueryBuilder;
 import org.opensearch.index.query.PrefixQueryBuilder;
@@ -60,8 +63,34 @@ public class TermQueryCommandTest extends UnitFessTestCase {
         super.tearDown();
     }
 
+    private void setQueryType(final String queryType) {
+        final FessConfig fessConfig = ComponentUtil.getFessConfig();
+        ComponentUtil.setFessConfig(new FessConfig.SimpleImpl() {
+            @Override
+            public String get(String propertyKey) {
+                return fessConfig.get(propertyKey);
+            }
+
+            @Override
+            public BigDecimal getAsDecimal(String propertyKey) {
+                return DfTypeUtil.toBigDecimal(get(propertyKey));
+            }
+
+            @Override
+            public Integer getAsInteger(String propertyKey) {
+                return DfTypeUtil.toInteger(get(propertyKey));
+            }
+
+            @Override
+            public String getQueryDefaultQueryType() {
+                return queryType;
+            }
+        });
+    }
+
     public void test_convertTermQuery() throws Exception {
-        assertQueryBuilder(BoolQueryBuilder.class,
+        setQueryType("bool");
+        assertQueryBuilder(DefaultQueryBuilder.class,
                 "{\"bool\":{\"should\":[{\"match_phrase\":{\"title\":{\"query\":\"aaa\",\"slop\":0,\"zero_terms_query\":\"NONE\",\"boost\":0.5}}},{\"match_phrase\":{\"content\":{\"query\":\"aaa\",\"slop\":0,\"zero_terms_query\":\"NONE\",\"boost\":0.05}}}],\"adjust_pure_negative\":true,\"boost\":1.0}}",
                 "aaa");
         assertQueryBuilder(MatchPhraseQueryBuilder.class,
@@ -70,7 +99,7 @@ public class TermQueryCommandTest extends UnitFessTestCase {
         assertQueryBuilder(MatchPhraseQueryBuilder.class,
                 "{\"match_phrase\":{\"content\":{\"query\":\"aaa\",\"slop\":0,\"zero_terms_query\":\"NONE\",\"boost\":1.0}}}", //
                 "content:aaa");
-        assertQueryBuilder(BoolQueryBuilder.class,
+        assertQueryBuilder(DefaultQueryBuilder.class,
                 "{\"bool\":{\"should\":[{\"match_phrase\":{\"title\":{\"query\":\"xxx:aaa\",\"slop\":0,\"zero_terms_query\":\"NONE\",\"boost\":0.5}}},{\"match_phrase\":{\"content\":{\"query\":\"xxx:aaa\",\"slop\":0,\"zero_terms_query\":\"NONE\",\"boost\":0.05}}},{\"fuzzy\":{\"title\":{\"value\":\"xxx:aaa\",\"fuzziness\":\"AUTO\",\"prefix_length\":0,\"max_expansions\":10,\"transpositions\":true,\"boost\":0.01}}},{\"fuzzy\":{\"content\":{\"value\":\"xxx:aaa\",\"fuzziness\":\"AUTO\",\"prefix_length\":0,\"max_expansions\":10,\"transpositions\":true,\"boost\":0.005}}}],\"adjust_pure_negative\":true,\"boost\":1.0}}",
                 "xxx:aaa");
         assertQueryBuilder(WildcardQueryBuilder.class, //
@@ -95,6 +124,42 @@ public class TermQueryCommandTest extends UnitFessTestCase {
         }
     }
 
+    public void test_convertTermQuery_dismax() throws Exception {
+        setQueryType("dismax");
+        assertQueryBuilder(DefaultQueryBuilder.class,
+                "{\"dis_max\":{\"tie_breaker\":0.1,\"queries\":[{\"match_phrase\":{\"title\":{\"query\":\"aaa\",\"slop\":0,\"zero_terms_query\":\"NONE\",\"boost\":0.5}}},{\"match_phrase\":{\"content\":{\"query\":\"aaa\",\"slop\":0,\"zero_terms_query\":\"NONE\",\"boost\":0.05}}}],\"boost\":1.0}}",
+                "aaa");
+        assertQueryBuilder(MatchPhraseQueryBuilder.class,
+                "{\"match_phrase\":{\"title\":{\"query\":\"aaa\",\"slop\":0,\"zero_terms_query\":\"NONE\",\"boost\":1.0}}}", //
+                "title:aaa");
+        assertQueryBuilder(MatchPhraseQueryBuilder.class,
+                "{\"match_phrase\":{\"content\":{\"query\":\"aaa\",\"slop\":0,\"zero_terms_query\":\"NONE\",\"boost\":1.0}}}", //
+                "content:aaa");
+        assertQueryBuilder(DefaultQueryBuilder.class,
+                "{\"dis_max\":{\"tie_breaker\":0.1,\"queries\":[{\"match_phrase\":{\"title\":{\"query\":\"xxx:aaa\",\"slop\":0,\"zero_terms_query\":\"NONE\",\"boost\":0.5}}},{\"match_phrase\":{\"content\":{\"query\":\"xxx:aaa\",\"slop\":0,\"zero_terms_query\":\"NONE\",\"boost\":0.05}}},{\"fuzzy\":{\"title\":{\"value\":\"xxx:aaa\",\"fuzziness\":\"AUTO\",\"prefix_length\":0,\"max_expansions\":10,\"transpositions\":true,\"boost\":0.01}}},{\"fuzzy\":{\"content\":{\"value\":\"xxx:aaa\",\"fuzziness\":\"AUTO\",\"prefix_length\":0,\"max_expansions\":10,\"transpositions\":true,\"boost\":0.005}}}],\"boost\":1.0}}",
+                "xxx:aaa");
+        assertQueryBuilder(WildcardQueryBuilder.class, //
+                "{\"wildcard\":{\"url\":{\"wildcard\":\"*aaa*\",\"boost\":1.0}}}", //
+                "inurl:aaa");
+        assertQueryBuilder(TermQueryBuilder.class, //
+                "{\"term\":{\"url\":{\"value\":\"aaa\",\"boost\":1.0}}}", //
+                "url:aaa");
+        assertQueryBuilder(PrefixQueryBuilder.class, //
+                "{\"prefix\":{\"site\":{\"value\":\"aaa\",\"boost\":1.0}}}", //
+                "site:aaa");
+
+        assertQueryBuilder("{\"timestamp\":{\"order\":\"asc\"}}", "sort:timestamp");
+        assertQueryBuilder("{\"timestamp\":{\"order\":\"asc\"}}", "sort:timestamp.asc");
+        assertQueryBuilder("{\"timestamp\":{\"order\":\"desc\"}}", "sort:timestamp.desc");
+
+        try {
+            assertQueryBuilder("", "sort:xxx");
+            fail();
+        } catch (InvalidQueryException e) {
+            // nothing
+        }
+    }
+
     private void assertQueryBuilder(final String expect, final String text) throws Exception {
         QueryContext queryContext = assertQueryBuilder(null, null, text);
         List<SortBuilder<?>> sortBuilders = queryContext.sortBuilders();