View Javadoc
1   /*
2    * Copyright 2012-2025 CodeLibs Project and the Others.
3    *
4    * Licensed under the Apache License, Version 2.0 (the "License");
5    * you may not use this file except in compliance with the License.
6    * You may obtain a copy of the License at
7    *
8    *     http://www.apache.org/licenses/LICENSE-2.0
9    *
10   * Unless required by applicable law or agreed to in writing, software
11   * distributed under the License is distributed on an "AS IS" BASIS,
12   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND,
13   * either express or implied. See the License for the specific language
14   * governing permissions and limitations under the License.
15   */
16  package org.codelibs.fess.query;
17  
18  import static org.codelibs.core.stream.StreamUtil.stream;
19  
20  import java.lang.Character.UnicodeBlock;
21  
22  import org.apache.lucene.search.Query;
23  import org.codelibs.core.lang.StringUtil;
24  import org.codelibs.fess.Constants;
25  import org.codelibs.fess.entity.QueryContext;
26  import org.codelibs.fess.mylasta.direction.FessConfig;
27  import org.codelibs.fess.util.ComponentUtil;
28  import org.dbflute.optional.OptionalThing;
29  import org.lastaflute.web.util.LaRequestUtil;
30  import org.opensearch.index.query.BoolQueryBuilder;
31  import org.opensearch.index.query.DisMaxQueryBuilder;
32  import org.opensearch.index.query.QueryBuilder;
33  import org.opensearch.index.query.QueryBuilders;
34  import org.opensearch.search.sort.SortBuilder;
35  import org.opensearch.search.sort.SortBuilders;
36  import org.opensearch.search.sort.SortOrder;
37  
38  /**
39   * Abstract base class for query command implementations.
40   * Provides common functionality for processing and executing search queries.
41   */
42  public abstract class QueryCommand {
43  
44      /**
45       * Default constructor for QueryCommand.
46       * Creates a new instance of the query command with default settings.
47       */
48      public QueryCommand() {
49          // Default constructor
50      }
51  
52      /**
53       * Executes the query command and returns a QueryBuilder.
54       * @param context The query context containing search parameters.
55       * @param query The Lucene query to execute.
56       * @param boost The boost factor to apply.
57       * @return The executed QueryBuilder.
58       */
59      public abstract QueryBuilder execute(final QueryContext context, final Query query, final float boost);
60  
61      /**
62       * Gets the class name of the query this command handles.
63       * @return The query class name.
64       */
65      protected abstract String getQueryClassName();
66  
67      /**
68       * Registers this query command with the query processor.
69       * Associates this command with its query class name in the processor.
70       */
71      public void register() {
72          ComponentUtil.getQueryProcessor().add(getQueryClassName(), this);
73      }
74  
75      /**
76       * Gets the query field configuration.
77       * @return The query field configuration instance.
78       */
79      protected QueryFieldConfig getQueryFieldConfig() {
80          return ComponentUtil.getQueryFieldConfig();
81      }
82  
83      /**
84       * Gets the query processor instance.
85       * @return The query processor instance.
86       */
87      protected QueryProcessor getQueryProcessor() {
88          return ComponentUtil.getQueryProcessor();
89      }
90  
91      /**
92       * Creates a sort builder for the specified field and order.
93       * @param field The field name to sort by.
94       * @param order The sort order (ascending or descending).
95       * @return The appropriate sort builder for the field.
96       */
97      protected SortBuilder<?> createFieldSortBuilder(final String field, final SortOrder order) {
98          if (QueryFieldConfig.SCORE_FIELD.equals(field) || QueryFieldConfig.DOC_SCORE_FIELD.equals(field)) {
99              return SortBuilders.scoreSort().order(order);
100         }
101         return SortBuilders.fieldSort(field).order(order);
102     }
103 
104     /**
105      * Checks if the specified field is a search field.
106      * Uses O(1) Set lookup for improved performance.
107      * @param field The field name to check.
108      * @return True if the field is a search field, false otherwise.
109      */
110     protected boolean isSearchField(final String field) {
111         final QueryFieldConfig config = getQueryFieldConfig();
112         return config.searchFieldSet != null && config.searchFieldSet.contains(field);
113     }
114 
115     /**
116      * Gets the query languages from the current request.
117      * @return An optional containing the query languages array, or empty if not available.
118      */
119     protected OptionalThing<String[]> getQueryLanguages() {
120         return LaRequestUtil.getOptionalRequest()
121                 .map(request -> ComponentUtil.getFessConfig()
122                         .getQueryLanguages(request.getLocales(), (String[]) request.getAttribute(Constants.REQUEST_LANGUAGES)));
123     }
124 
125     /**
126      * Builds a default query builder with configured fields and boost values.
127      * @param fessConfig The Fess configuration.
128      * @param context The query context.
129      * @param builder The function to build individual field queries.
130      * @return The constructed default query builder.
131      */
132     protected DefaultQueryBuilder buildDefaultQueryBuilder(final FessConfig fessConfig, final QueryContext context,
133             final DefaultQueryBuilderFunction builder) {
134         final DefaultQueryBuilder defaultQuery = createDefaultQueryBuilder();
135         defaultQuery.add(builder.apply(fessConfig.getIndexFieldTitle(), fessConfig.getQueryBoostTitleAsDecimal().floatValue()));
136         defaultQuery.add(builder.apply(fessConfig.getIndexFieldContent(), fessConfig.getQueryBoostContentAsDecimal().floatValue()));
137         final float importantContentBoost = fessConfig.getQueryBoostImportantContentAsDecimal().floatValue();
138         if (importantContentBoost >= 0.0f) {
139             defaultQuery.add(builder.apply(fessConfig.getIndexFieldImportantContent(), importantContentBoost));
140         }
141         final float importantContantLangBoost = fessConfig.getQueryBoostImportantContentLangAsDecimal().floatValue();
142         getQueryLanguages().ifPresent(langs -> stream(langs).of(stream -> stream.forEach(lang -> {
143             defaultQuery.add(
144                     builder.apply(fessConfig.getIndexFieldTitle() + "_" + lang, fessConfig.getQueryBoostTitleLangAsDecimal().floatValue()));
145             defaultQuery.add(builder.apply(fessConfig.getIndexFieldContent() + "_" + lang,
146                     fessConfig.getQueryBoostContentLangAsDecimal().floatValue()));
147             if (importantContantLangBoost >= 0.0f) {
148                 defaultQuery.add(builder.apply(fessConfig.getIndexFieldImportantContent() + "_" + lang, importantContantLangBoost));
149             }
150         })));
151         getQueryFieldConfig().additionalDefaultList.stream().forEach(f -> {
152             final QueryBuilder query = builder.apply(f.getFirst(), f.getSecond());
153             defaultQuery.add(query);
154         });
155         return defaultQuery;
156     }
157 
158     /**
159      * Creates a default query builder based on the configured query type.
160      * @return The default query builder (either dismax or bool query).
161      */
162     protected DefaultQueryBuilder createDefaultQueryBuilder() {
163         final FessConfig fessConfig = ComponentUtil.getFessConfig();
164 
165         if ("dismax".equals(fessConfig.getQueryDefaultQueryType())) {
166             final DisMaxQueryBuilder disMaxQuery = QueryBuilders.disMaxQuery();
167             disMaxQuery.tieBreaker(fessConfig.getQueryDismaxTieBreakerAsDecimal().floatValue());
168             return new DefaultQueryBuilder(disMaxQuery);
169         }
170 
171         final BoolQueryBuilder boolQuery = QueryBuilders.boolQuery();
172         final String minimumShouldMatch = fessConfig.getQueryBoolMinimumShouldMatch();
173         if (StringUtil.isNotBlank(minimumShouldMatch)) {
174             boolQuery.minimumShouldMatch(minimumShouldMatch);
175         }
176         return new DefaultQueryBuilder(boolQuery);
177     }
178 
179     /**
180      * Builds a match phrase query, with special handling for single CJK characters.
181      * For single CJK characters in title or content fields, uses prefix query instead.
182      * @param f The field name.
183      * @param text The text to search for.
184      * @return The appropriate query builder.
185      */
186     protected QueryBuilder buildMatchPhraseQuery(final String f, final String text) {
187         final FessConfig fessConfig = ComponentUtil.getFessConfig();
188         if (text == null || text.length() != 1
189                 || !fessConfig.getIndexFieldTitle().equals(f) && !fessConfig.getIndexFieldContent().equals(f)) {
190             return QueryBuilders.matchPhraseQuery(f, text);
191         }
192 
193         final UnicodeBlock block = UnicodeBlock.of(text.codePointAt(0));
194         if (block == UnicodeBlock.CJK_UNIFIED_IDEOGRAPHS //
195                 || block == UnicodeBlock.HIRAGANA //
196                 || block == UnicodeBlock.KATAKANA //
197                 || block == UnicodeBlock.HANGUL_SYLLABLES //
198         ) {
199             return QueryBuilders.prefixQuery(f, text);
200         }
201         return QueryBuilders.matchPhraseQuery(f, text);
202     }
203 
204     /**
205      * Gets the actual search field, replacing default field placeholder if needed.
206      * @param defaultField The default field to use if field is the default placeholder.
207      * @param field The field name to check.
208      * @return The actual field name to use for searching.
209      */
210     protected String getSearchField(final String defaultField, final String field) {
211         if (Constants.DEFAULT_FIELD.equals(field) && defaultField != null) {
212             return defaultField;
213         }
214         return field;
215     }
216 
217     /**
218      * Functional interface for building query builders with field and boost parameters.
219      */
220     protected interface DefaultQueryBuilderFunction {
221         /**
222          * Applies the function to create a query builder for the specified field and boost.
223          * @param field The field name.
224          * @param boost The boost value.
225          * @return The created query builder.
226          */
227         QueryBuilder apply(String field, float boost);
228     }
229 
230     /**
231      * Functional interface for building field-specific query builders.
232      */
233     protected interface FieldQueryBuilder {
234         /**
235          * Builds a query builder for the specified field and text.
236          * @param field The field name.
237          * @param text The query text.
238          * @param boost The boost value.
239          * @return The created query builder.
240          */
241         QueryBuilder buildQuery(String field, String text, float boost);
242     }
243 
244     /**
245      * Template method that handles the common pattern of query conversion:
246      * 1. Check if field is DEFAULT_FIELD and apply default query builder
247      * 2. Check if field is a search field and apply field-specific query
248      * 3. Fall back to default query builder for unsupported fields
249      *
250      * This reduces code duplication across query command implementations.
251      *
252      * @param fessConfig the Fess configuration
253      * @param context the query context
254      * @param field the field name
255      * @param text the query text
256      * @param boost the boost value
257      * @param defaultBuilder function to build default queries
258      * @param fieldBuilder function to build field-specific queries
259      * @return the constructed query builder
260      */
261     protected QueryBuilder convertWithFieldCheck(final FessConfig fessConfig, final QueryContext context, final String field,
262             final String text, final float boost, final DefaultQueryBuilderFunction defaultBuilder, final FieldQueryBuilder fieldBuilder) {
263 
264         context.addFieldLog(field, text);
265         context.addHighlightedQuery(text);
266 
267         if (Constants.DEFAULT_FIELD.equals(field)) {
268             return buildDefaultQueryBuilder(fessConfig, context, defaultBuilder);
269         }
270 
271         if (isSearchField(field)) {
272             return fieldBuilder.buildQuery(field, text, boost);
273         }
274 
275         // Fallback: treat as default field query
276         context.addFieldLog(Constants.DEFAULT_FIELD, text);
277         return buildDefaultQueryBuilder(fessConfig, context, defaultBuilder);
278     }
279 }