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.split;
19  import static org.codelibs.fess.Constants.DEFAULT_FIELD;
20  import static org.codelibs.fess.query.QueryFieldConfig.INURL_FIELD;
21  import static org.codelibs.fess.query.QueryFieldConfig.SITE_FIELD;
22  
23  import org.apache.logging.log4j.LogManager;
24  import org.apache.logging.log4j.Logger;
25  import org.apache.lucene.index.Term;
26  import org.apache.lucene.search.PrefixQuery;
27  import org.apache.lucene.search.Query;
28  import org.apache.lucene.search.TermQuery;
29  import org.codelibs.core.lang.StringUtil;
30  import org.codelibs.fess.Constants;
31  import org.codelibs.fess.entity.QueryContext;
32  import org.codelibs.fess.exception.InvalidQueryException;
33  import org.codelibs.fess.mylasta.direction.FessConfig;
34  import org.codelibs.fess.util.ComponentUtil;
35  import org.lastaflute.core.message.UserMessages;
36  import org.opensearch.common.unit.Fuzziness;
37  import org.opensearch.index.query.QueryBuilder;
38  import org.opensearch.index.query.QueryBuilders;
39  import org.opensearch.search.sort.SortOrder;
40  
41  /**
42   * Command class for handling term query execution and conversion.
43   * This class processes Lucene TermQuery objects and converts them to OpenSearch QueryBuilder instances.
44   */
45  public class TermQueryCommand extends QueryCommand {
46      private static final Logger logger = LogManager.getLogger(TermQueryCommand.class);
47  
48      /**
49       * Default constructor for TermQueryCommand.
50       */
51      public TermQueryCommand() {
52          super();
53      }
54  
55      private static final String SORT_FIELD = "sort";
56  
57      @Override
58      protected String getQueryClassName() {
59          return TermQuery.class.getSimpleName();
60      }
61  
62      @Override
63      public QueryBuilder execute(final QueryContext context, final Query query, final float boost) {
64          if (query instanceof final TermQuery termQuery) {
65              if (logger.isDebugEnabled()) {
66                  logger.debug("TermQuery: query={}, boost={}", query, boost);
67              }
68              return convertTermQuery(context, termQuery, boost);
69          }
70          throw new InvalidQueryException(messages -> messages.addErrorsInvalidQueryUnknown(UserMessages.GLOBAL_PROPERTY_KEY),
71                  "Unknown q: " + query.getClass() + " => " + query);
72      }
73  
74      /**
75       * Converts a TermQuery to a QueryBuilder with the given boost value.
76       *
77       * @param context the query context
78       * @param termQuery the term query to convert
79       * @param boost the boost value to apply
80       * @return the converted QueryBuilder
81       */
82      protected QueryBuilder convertTermQuery(final QueryContext context, final TermQuery termQuery, final float boost) {
83          final String field = getSearchField(context.getDefaultField(), termQuery.getTerm().field());
84          final String text = termQuery.getTerm().text();
85          final FessConfig fessConfig = ComponentUtil.getFessConfig();
86          return convertTermQuery(fessConfig, context, termQuery, boost, field, text);
87      }
88  
89      /**
90       * Converts a TermQuery to a QueryBuilder with field-specific handling.
91       *
92       * @param fessConfig the Fess configuration
93       * @param context the query context
94       * @param termQuery the term query to convert
95       * @param boost the boost value to apply
96       * @param field the field name
97       * @param text the query text
98       * @return the converted QueryBuilder
99       */
100     protected QueryBuilder convertTermQuery(final FessConfig fessConfig, final QueryContext context, final TermQuery termQuery,
101             final float boost, final String field, final String text) {
102         if (fessConfig.getQueryReplaceTermWithPrefixQueryAsBoolean() && text.length() > 1 && text.endsWith("*")) {
103             return convertPrefixQuery(fessConfig, context, termQuery, boost, field, text);
104         }
105         if (DEFAULT_FIELD.equals(field)) {
106             return convertDefaultTermQuery(fessConfig, context, termQuery, boost, field, text);
107         }
108         if (SORT_FIELD.equals(field)) {
109             return convertSortQuery(fessConfig, context, termQuery, boost, field, text);
110         }
111         if (SITE_FIELD.equals(field)) {
112             return convertSiteQuery(fessConfig, context, termQuery, boost, field, text);
113         }
114         if (INURL_FIELD.equals(field)
115                 || StringUtil.equals(field, context.getDefaultField()) && fessConfig.getIndexFieldUrl().equals(context.getDefaultField())) {
116             return convertWildcardQuery(fessConfig, context, termQuery, boost, field, text);
117         }
118         if (!isSearchField(field)) {
119             final String origQuery = termQuery.toString();
120             return convertDefaultTermQuery(fessConfig, context, termQuery, boost, DEFAULT_FIELD, origQuery);
121         }
122         if (getQueryFieldConfig().notAnalyzedFieldSet.contains(field)) {
123             return convertKeywordQuery(fessConfig, context, termQuery, boost, field, text);
124         }
125         return convertTextQuery(fessConfig, context, termQuery, boost, field, text);
126     }
127 
128     /**
129      * Converts a term query to a text-based match phrase query.
130      *
131      * @param fessConfig the Fess configuration
132      * @param context the query context
133      * @param termQuery the term query to convert
134      * @param boost the boost value to apply
135      * @param field the field name
136      * @param text the query text
137      * @return the converted QueryBuilder
138      */
139     protected QueryBuilder convertTextQuery(final FessConfig fessConfig, final QueryContext context, final TermQuery termQuery,
140             final float boost, final String field, final String text) {
141         context.addFieldLog(field, text);
142         context.addHighlightedQuery(text);
143         return buildMatchPhraseQuery(field, text).boost(boost);
144     }
145 
146     /**
147      * Converts a term query to a keyword-based exact term query.
148      *
149      * @param fessConfig the Fess configuration
150      * @param context the query context
151      * @param termQuery the term query to convert
152      * @param boost the boost value to apply
153      * @param field the field name
154      * @param text the query text
155      * @return the converted QueryBuilder
156      */
157     protected QueryBuilder convertKeywordQuery(final FessConfig fessConfig, final QueryContext context, final TermQuery termQuery,
158             final float boost, final String field, final String text) {
159         context.addFieldLog(field, text);
160         context.addHighlightedQuery(text);
161         return QueryBuilders.termQuery(field, text).boost(boost);
162     }
163 
164     /**
165      * Converts a term query to a wildcard query for URL field matching.
166      *
167      * @param fessConfig the Fess configuration
168      * @param context the query context
169      * @param termQuery the term query to convert
170      * @param boost the boost value to apply
171      * @param field the field name
172      * @param text the query text
173      * @return the converted QueryBuilder
174      */
175     protected QueryBuilder convertWildcardQuery(final FessConfig fessConfig, final QueryContext context, final TermQuery termQuery,
176             final float boost, final String field, final String text) {
177         final String urlField = fessConfig.getIndexFieldUrl();
178         final String queryString = "*" + text + "*";
179         context.addFieldLog(urlField, queryString);
180         context.addHighlightedQuery(text);
181         return QueryBuilders.wildcardQuery(urlField, queryString).boost(boost);
182     }
183 
184     /**
185      * Converts a term query ending with asterisk to a prefix query.
186      *
187      * @param fessConfig the Fess configuration
188      * @param context the query context
189      * @param termQuery the term query to convert
190      * @param boost the boost value to apply
191      * @param field the field name
192      * @param text the query text
193      * @return the converted QueryBuilder
194      */
195     protected QueryBuilder convertPrefixQuery(final FessConfig fessConfig, final QueryContext context, final TermQuery termQuery,
196             final float boost, final String field, final String text) {
197         return getQueryProcessor().execute(context, new PrefixQuery(new Term(field, text.substring(0, text.length() - 1))), boost);
198     }
199 
200     /**
201      * Converts a sort field query to add sort criteria to the context.
202      *
203      * @param fessConfig the Fess configuration
204      * @param context the query context
205      * @param termQuery the term query to convert
206      * @param boost the boost value to apply
207      * @param field the field name
208      * @param text the query text
209      * @return null as this method only adds sort criteria
210      */
211     protected QueryBuilder convertSortQuery(final FessConfig fessConfig, final QueryContext context, final TermQuery termQuery,
212             final float boost, final String field, final String text) {
213         split(text, ",").of(stream -> stream.filter(StringUtil::isNotBlank).forEach(t -> {
214             final String[] values = t.split("\\.");
215             if (values.length > 2) {
216                 throw new InvalidQueryException(messages -> messages.addErrorsInvalidQuerySortValue(UserMessages.GLOBAL_PROPERTY_KEY, text),
217                         "Invalid sort field: " + termQuery);
218             }
219             final String sortField = values[0];
220             if (!getQueryFieldConfig().isSortField(sortField)) {
221                 throw new InvalidQueryException(
222                         messages -> messages.addErrorsInvalidQueryUnsupportedSortField(UserMessages.GLOBAL_PROPERTY_KEY, sortField),
223                         "Unsupported sort field: " + termQuery);
224             }
225             final SortOrder sortOrder;
226             if (values.length == 2) {
227                 if (SortOrder.DESC.toString().equalsIgnoreCase(values[1])) {
228                     sortOrder = SortOrder.DESC;
229                 } else if (SortOrder.ASC.toString().equalsIgnoreCase(values[1])) {
230                     sortOrder = SortOrder.ASC;
231                 } else {
232                     throw new InvalidQueryException(
233                             messages -> messages.addErrorsInvalidQueryUnsupportedSortOrder(UserMessages.GLOBAL_PROPERTY_KEY, values[1]),
234                             "Invalid sort order: " + termQuery);
235                 }
236             } else {
237                 sortOrder = SortOrder.ASC;
238             }
239             context.addSorts(createFieldSortBuilder(sortField, sortOrder));
240         }));
241         return null;
242     }
243 
244     /**
245      * Converts a term query for the default field with fuzzy matching support.
246      *
247      * @param fessConfig the Fess configuration
248      * @param context the query context
249      * @param termQuery the term query to convert
250      * @param boost the boost value to apply
251      * @param field the field name
252      * @param text the query text
253      * @return the converted QueryBuilder
254      */
255     protected QueryBuilder convertDefaultTermQuery(final FessConfig fessConfig, final QueryContext context, final TermQuery termQuery,
256             final float boost, final String field, final String text) {
257         context.addFieldLog(field, text);
258         context.addHighlightedQuery(text);
259         final DefaultQueryBuilder defaultQuery =
260                 buildDefaultQueryBuilder(fessConfig, context, (f, b) -> buildMatchPhraseQuery(f, text).boost(b * boost));
261         final Integer fuzzyMinLength = fessConfig.getQueryBoostFuzzyMinLengthAsInteger();
262         if (fuzzyMinLength >= 0 && text.length() >= fuzzyMinLength) {
263             defaultQuery.add(QueryBuilders.fuzzyQuery(fessConfig.getIndexFieldTitle(), text)
264                     .boost(fessConfig.getQueryBoostFuzzyTitleAsDecimal().floatValue())
265                     .prefixLength(fessConfig.getQueryBoostFuzzyTitlePrefixLengthAsInteger())
266                     .transpositions(Constants.TRUE.equalsIgnoreCase(fessConfig.getQueryBoostFuzzyTitleTranspositions()))
267                     .fuzziness(Fuzziness.build(fessConfig.getQueryBoostFuzzyTitleFuzziness()))
268                     .maxExpansions(fessConfig.getQueryBoostFuzzyTitleExpansionsAsInteger()));
269             defaultQuery.add(QueryBuilders.fuzzyQuery(fessConfig.getIndexFieldContent(), text)
270                     .prefixLength(fessConfig.getQueryBoostFuzzyContentPrefixLengthAsInteger())
271                     .transpositions(Constants.TRUE.equalsIgnoreCase(fessConfig.getQueryBoostFuzzyContentTranspositions()))
272                     .boost(fessConfig.getQueryBoostFuzzyContentAsDecimal().floatValue())
273                     .fuzziness(Fuzziness.build(fessConfig.getQueryBoostFuzzyContentFuzziness()))
274                     .maxExpansions(fessConfig.getQueryBoostFuzzyContentExpansionsAsInteger()));
275         }
276         return defaultQuery;
277     }
278 
279     /**
280      * Converts a site field query to a prefix query for site filtering.
281      *
282      * @param fessConfig the Fess configuration
283      * @param context the query context
284      * @param termQuery the term query to convert
285      * @param boost the boost value to apply
286      * @param field the field name
287      * @param text the query text
288      * @return the converted QueryBuilder
289      */
290     protected QueryBuilder convertSiteQuery(final FessConfig fessConfig, final QueryContext context, final TermQuery termQuery,
291             final float boost, final String field, final String text) {
292         final String siteField = fessConfig.getIndexFieldSite();
293         context.addFieldLog(siteField, text + "*");
294         context.addHighlightedQuery(text);
295         return QueryBuilders.prefixQuery(siteField, text).boost(boost);
296     }
297 }