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.entity;
17  
18  import static org.codelibs.core.stream.StreamUtil.stream;
19  
20  import java.util.ArrayList;
21  import java.util.Collections;
22  import java.util.HashMap;
23  import java.util.HashSet;
24  import java.util.List;
25  import java.util.Map;
26  import java.util.Set;
27  import java.util.function.Consumer;
28  
29  import org.codelibs.core.lang.StringUtil;
30  import org.codelibs.fess.Constants;
31  import org.codelibs.fess.util.ComponentUtil;
32  import org.lastaflute.web.util.LaRequestUtil;
33  import org.opensearch.index.query.BoolQueryBuilder;
34  import org.opensearch.index.query.MatchAllQueryBuilder;
35  import org.opensearch.index.query.QueryBuilder;
36  import org.opensearch.index.query.QueryBuilders;
37  import org.opensearch.index.query.functionscore.FunctionScoreQueryBuilder.FilterFunctionBuilder;
38  import org.opensearch.search.sort.SortBuilder;
39  
40  /**
41   * Context object that holds query-related information and state during search processing.
42   * Contains the query string, query builder, sort criteria, and various metadata.
43   */
44  public class QueryContext {
45  
46      /** Prefix for queries that search only in URL fields. */
47      protected static final String ALLINURL_FIELD_PREFIX = "allinurl:";
48  
49      /** Prefix for queries that search only in title fields. */
50      protected static final String ALLINTITLE_FIELD_PREFIX = "allintitle:";
51  
52      /** The OpenSearch query builder used for executing the search. */
53      protected QueryBuilder queryBuilder;
54  
55      /** List of sort builders to apply to the search query. */
56      protected final List<SortBuilder<?>> sortBuilderList = new ArrayList<>();
57  
58      /** The original query string provided by the user. */
59      protected String queryString;
60  
61      /** Set of query terms that should be highlighted in search results. */
62      protected Set<String> highlightedQuerySet = null;
63  
64      /** Map storing field names and their associated query terms for logging. */
65      protected Map<String, List<String>> fieldLogMap = null;
66  
67      /** Flag indicating whether role-based query filtering should be disabled. */
68      protected boolean disableRoleQuery = false;
69  
70      /** The default field to search in when no specific field is specified. */
71      protected String defaultField = null;
72  
73      /**
74       * Constructs a new QueryContext with the specified query string.
75       * Processes special query prefixes (allinurl:, allintitle:) and initializes
76       * request-scoped attributes for highlighting and field logging.
77       * @param queryString The query string to process.
78       * @param isQuery Whether this is a search query (enables highlighting and logging).
79       */
80      @SuppressWarnings("unchecked")
81      public QueryContext(final String queryString, final boolean isQuery) {
82          if (queryString != null) {
83              if (queryString.startsWith(ALLINURL_FIELD_PREFIX)) {
84                  defaultField = ComponentUtil.getFessConfig().getIndexFieldUrl();
85                  this.queryString = queryString.substring(ALLINURL_FIELD_PREFIX.length());
86              } else if (queryString.startsWith(ALLINTITLE_FIELD_PREFIX)) {
87                  defaultField = ComponentUtil.getFessConfig().getIndexFieldTitle();
88                  this.queryString = queryString.substring(ALLINTITLE_FIELD_PREFIX.length());
89              } else {
90                  this.queryString = queryString;
91              }
92          } else {
93              this.queryString = queryString;
94          }
95          if (StringUtil.isBlank(this.queryString)) {
96              this.queryString = "*";
97          }
98          if (isQuery) {
99              LaRequestUtil.getOptionalRequest().ifPresent(request -> {
100                 highlightedQuerySet = new HashSet<>();
101                 request.setAttribute(Constants.HIGHLIGHT_QUERIES, highlightedQuerySet);
102                 fieldLogMap = (Map<String, List<String>>) request.getAttribute(Constants.FIELD_LOGS);
103                 if (fieldLogMap == null) {
104                     fieldLogMap = new HashMap<>();
105                     request.setAttribute(Constants.FIELD_LOGS, fieldLogMap);
106                 }
107             });
108         }
109     }
110 
111     /**
112      * Adds function score configuration to the query builder.
113      * @param functionScoreQuery Consumer that configures the function score filters.
114      */
115     public void addFunctionScore(final Consumer<List<FilterFunctionBuilder>> functionScoreQuery) {
116         final List<FilterFunctionBuilder> list = new ArrayList<>();
117         functionScoreQuery.accept(list);
118         queryBuilder = QueryBuilders.functionScoreQuery(queryBuilder, list.toArray(new FilterFunctionBuilder[list.size()]));
119     }
120 
121     /**
122      * Adds additional query clauses using a boolean query builder.
123      * @param boolQuery Consumer that configures the boolean query.
124      */
125     public void addQuery(final Consumer<BoolQueryBuilder> boolQuery) {
126         BoolQueryBuilder builder;
127         if (queryBuilder instanceof MatchAllQueryBuilder) {
128             builder = QueryBuilders.boolQuery();
129         } else {
130             builder = QueryBuilders.boolQuery().must(queryBuilder);
131         }
132         boolQuery.accept(builder);
133         if (builder.hasClauses()) {
134             queryBuilder = builder;
135         }
136     }
137 
138     /**
139      * Sets the query builder for this context.
140      * @param queryBuilder The query builder to use.
141      */
142     public void setQueryBuilder(final QueryBuilder queryBuilder) {
143         this.queryBuilder = queryBuilder;
144     }
145 
146     /**
147      * Adds sort builders to the query context.
148      * @param sortBuilders Variable number of sort builders to add.
149      */
150     public void addSorts(final SortBuilder<?>... sortBuilders) {
151         stream(sortBuilders).of(stream -> stream.forEach(sortBuilder -> sortBuilderList.add(sortBuilder)));
152     }
153 
154     /**
155      * Checks if any sort builders have been added to this context.
156      * @return True if sort builders are present, false otherwise.
157      */
158     public boolean hasSorts() {
159         return !sortBuilderList.isEmpty();
160     }
161 
162     /**
163      * Gets the list of sort builders for this query context.
164      * @return The list of sort builders.
165      */
166     public List<SortBuilder<?>> sortBuilders() {
167         return sortBuilderList;
168     }
169 
170     /**
171      * Gets the query builder for this context.
172      * @return The query builder.
173      */
174     public QueryBuilder getQueryBuilder() {
175         return queryBuilder;
176     }
177 
178     /**
179      * Adds a field and text pair to the field log for tracking query terms.
180      * @param field The field name.
181      * @param text The query text for this field.
182      */
183     public void addFieldLog(final String field, final String text) {
184         if (fieldLogMap == null) {
185             return;
186         }
187 
188         List<String> list = fieldLogMap.get(field);
189         if (list == null) {
190             list = new ArrayList<>();
191             fieldLogMap.put(field, list);
192         }
193         list.add(text);
194     }
195 
196     /**
197      * Gets the default field keywords from the field log.
198      * @return List of keywords for the default field, or empty list if none.
199      */
200     public List<String> getDefaultKeyword() {
201         if (fieldLogMap != null) {
202             return fieldLogMap.getOrDefault(Constants.DEFAULT_FIELD, Collections.emptyList());
203         }
204         return Collections.emptyList();
205     }
206 
207     /**
208      * Adds a query term to the highlighted query set.
209      * @param text The query text to highlight.
210      */
211     public void addHighlightedQuery(final String text) {
212         if (highlightedQuerySet != null) {
213             highlightedQuerySet.add(text);
214         }
215     }
216 
217     /**
218      * Gets the processed query string.
219      * @return The query string.
220      */
221     public String getQueryString() {
222         return queryString;
223     }
224 
225     /**
226      * Checks if role-based query filtering is enabled.
227      * @return True if role query is enabled, false otherwise.
228      */
229     public boolean roleQueryEnabled() {
230         return !disableRoleQuery;
231     }
232 
233     /**
234      * Disables role-based query filtering for this context.
235      */
236     public void skipRoleQuery() {
237         disableRoleQuery = true;
238     }
239 
240     /**
241      * Gets the default field for this query context.
242      * @return The default field name, or null if not set.
243      */
244     public String getDefaultField() {
245         return defaultField;
246     }
247 
248     /**
249      * Sets the default field for this query context.
250      * @param defaultField The default field name to set.
251      */
252     public void setDefaultField(final String defaultField) {
253         this.defaultField = defaultField;
254     }
255 
256     /**
257      * Gets the set of highlighted query terms.
258      * @return The set of highlighted query terms, or empty set if not initialized.
259      */
260     public Set<String> getHighlightedQuerySet() {
261         return highlightedQuerySet != null ? highlightedQuerySet : new HashSet<>();
262     }
263 
264     /**
265      * Gets the field log map containing field names and their associated query terms.
266      * @return The field log map, or empty map if not initialized.
267      */
268     public Map<String, List<String>> getFieldLogMap() {
269         return fieldLogMap != null ? fieldLogMap : new HashMap<>();
270     }
271 }