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 java.util.Locale;
19  
20  import org.apache.commons.lang3.StringUtils;
21  import org.apache.logging.log4j.LogManager;
22  import org.apache.logging.log4j.Logger;
23  import org.apache.lucene.search.Query;
24  import org.apache.lucene.search.WildcardQuery;
25  import org.codelibs.core.lang.StringUtil;
26  import org.codelibs.fess.Constants;
27  import org.codelibs.fess.entity.QueryContext;
28  import org.codelibs.fess.exception.InvalidQueryException;
29  import org.codelibs.fess.mylasta.direction.FessConfig;
30  import org.codelibs.fess.util.ComponentUtil;
31  import org.lastaflute.core.message.UserMessages;
32  import org.opensearch.index.query.QueryBuilder;
33  import org.opensearch.index.query.QueryBuilders;
34  
35  /**
36   * Query command for handling wildcard queries.
37   * Converts Lucene WildcardQuery objects to OpenSearch QueryBuilder objects,
38   * supporting wildcard pattern matching with configurable case sensitivity.
39   */
40  public class WildcardQueryCommand extends QueryCommand {
41  
42      /**
43       * Default constructor.
44       */
45      public WildcardQueryCommand() {
46          super();
47      }
48  
49      private static final Logger logger = LogManager.getLogger(WildcardQueryCommand.class);
50  
51      /**
52       * Flag indicating whether wildcard terms should be converted to lowercase.
53       */
54      protected boolean lowercaseWildcard = true;
55  
56      @Override
57      protected String getQueryClassName() {
58          return WildcardQuery.class.getSimpleName();
59      }
60  
61      @Override
62      public QueryBuilder execute(final QueryContext context, final Query query, final float boost) {
63          if (query instanceof final WildcardQuery wildcardQuery) {
64              if (logger.isDebugEnabled()) {
65                  logger.debug("WildcardQuery: query={}, boost={}", query, boost);
66              }
67              return convertWildcardQuery(context, wildcardQuery, boost);
68          }
69          throw new InvalidQueryException(messages -> messages.addErrorsInvalidQueryUnknown(UserMessages.GLOBAL_PROPERTY_KEY),
70                  "Unknown q: " + query.getClass() + " => " + query);
71      }
72  
73      /**
74       * Converts a wildcard query to an appropriate OpenSearch QueryBuilder.
75       *
76       * @param context The query context containing field and search information
77       * @param wildcardQuery The Lucene wildcard query to convert
78       * @param boost The boost factor to apply to the query
79       * @return The converted OpenSearch QueryBuilder
80       */
81      protected QueryBuilder convertWildcardQuery(final QueryContext context, final WildcardQuery wildcardQuery, final float boost) {
82          final FessConfig fessConfig = ComponentUtil.getFessConfig();
83          final String field = getSearchField(context.getDefaultField(), wildcardQuery.getField());
84          if (Constants.DEFAULT_FIELD.equals(field)) {
85              final String text = wildcardQuery.getTerm().text();
86              context.addFieldLog(field, text);
87              final String highlightText = StringUtils.strip(text, "*");
88              if (StringUtil.isNotBlank(highlightText)) {
89                  context.addHighlightedQuery(highlightText);
90              }
91              return buildDefaultQueryBuilder(fessConfig, context,
92                      (f, b) -> QueryBuilders.wildcardQuery(f, toLowercaseWildcard(text)).boost(b * boost));
93          }
94  
95          if (isSearchField(field)) {
96              final String text = wildcardQuery.getTerm().text();
97              context.addFieldLog(field, text);
98              final String highlightText = StringUtils.strip(text, "*");
99              if (StringUtil.isNotBlank(highlightText)) {
100                 context.addHighlightedQuery(highlightText);
101             }
102             return QueryBuilders.wildcardQuery(field, toLowercaseWildcard(text)).boost(boost);
103         }
104 
105         final String query = wildcardQuery.getTerm().toString();
106         final StringBuilder queryBuf = new StringBuilder(query.length() + 2);
107         if (!query.startsWith("*")) {
108             queryBuf.append('*');
109         }
110         queryBuf.append(toLowercaseWildcard(query));
111         if (!query.endsWith("*")) {
112             queryBuf.append('*');
113         }
114         final String origQuery = queryBuf.toString();
115         context.addFieldLog(Constants.DEFAULT_FIELD, origQuery);
116         final String highlightText = StringUtils.strip(query, "*");
117         if (StringUtil.isNotBlank(highlightText)) {
118             context.addHighlightedQuery(highlightText);
119         }
120         return buildDefaultQueryBuilder(fessConfig, context, (f, b) -> QueryBuilders.wildcardQuery(f, origQuery).boost(b * boost));
121     }
122 
123     /**
124      * Converts a wildcard value to lowercase if configured to do so.
125      *
126      * @param value The wildcard value to potentially convert
127      * @return The value in lowercase if lowercaseWildcard is true, otherwise the original value
128      */
129     protected String toLowercaseWildcard(final String value) {
130         if (lowercaseWildcard) {
131             return value.toLowerCase(Locale.ROOT);
132         }
133         return value;
134     }
135 
136     /**
137      * Sets whether wildcard terms should be converted to lowercase.
138      *
139      * @param lowercaseWildcard True to convert wildcard terms to lowercase, false otherwise
140      */
141     public void setLowercaseWildcard(final boolean lowercaseWildcard) {
142         this.lowercaseWildcard = lowercaseWildcard;
143     }
144 }