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.helper;
17  
18  import java.util.ArrayList;
19  import java.util.Collections;
20  import java.util.HashMap;
21  import java.util.List;
22  import java.util.Locale;
23  import java.util.Map;
24  
25  import org.apache.logging.log4j.LogManager;
26  import org.apache.logging.log4j.Logger;
27  import org.codelibs.core.lang.StringUtil;
28  import org.codelibs.core.misc.Tuple3;
29  import org.codelibs.fess.Constants;
30  import org.codelibs.fess.entity.SearchRequestParams.SearchRequestType;
31  import org.codelibs.fess.mylasta.direction.FessConfig;
32  import org.codelibs.fess.opensearch.client.SearchEngineClient;
33  import org.codelibs.fess.opensearch.client.SearchEngineClient.SearchConditionBuilder;
34  import org.codelibs.fess.opensearch.config.exbhv.KeyMatchBhv;
35  import org.codelibs.fess.opensearch.config.exentity.KeyMatch;
36  import org.codelibs.fess.util.ComponentUtil;
37  import org.codelibs.fess.util.DocumentUtil;
38  import org.opensearch.index.query.BoolQueryBuilder;
39  import org.opensearch.index.query.QueryBuilder;
40  import org.opensearch.index.query.QueryBuilders;
41  import org.opensearch.index.query.functionscore.FunctionScoreQueryBuilder.FilterFunctionBuilder;
42  import org.opensearch.index.query.functionscore.ScoreFunctionBuilder;
43  import org.opensearch.index.query.functionscore.ScoreFunctionBuilders;
44  
45  import jakarta.annotation.PostConstruct;
46  
47  /**
48   * KeyMatchHelper is a helper class for KeyMatch feature.
49   * It manages KeyMatch instances and provides methods to build queries for boosting documents.
50   */
51  public class KeyMatchHelper extends AbstractConfigHelper {
52      private static final Logger logger = LogManager.getLogger(KeyMatchHelper.class);
53  
54      /**
55       * Default constructor.
56       */
57      public KeyMatchHelper() {
58          super();
59      }
60  
61      /**
62       * A map containing query information for KeyMatch.
63       * The key is a virtual host, and the value is a map of terms and boost information.
64       */
65      protected volatile Map<String, Map<String, List<Tuple3<String, QueryBuilder, ScoreFunctionBuilder<?>>>>> keyMatchQueryMap =
66              Collections.emptyMap();
67  
68      /**
69       * Initializes the helper.
70       * It loads KeyMatch settings from the database.
71       */
72      @PostConstruct
73      public void init() {
74          if (logger.isDebugEnabled()) {
75              logger.debug("Initializing {}", this.getClass().getSimpleName());
76          }
77          load();
78      }
79  
80      /**
81       * Returns a list of available KeyMatch instances.
82       *
83       * @return A list of KeyMatch instances.
84       */
85      public List<KeyMatch> getAvailableKeyMatchList() {
86          return ComponentUtil.getComponent(KeyMatchBhv.class).selectList(cb -> {
87              cb.query().matchAll();
88              cb.fetchFirst(ComponentUtil.getFessConfig().getPageKeymatchMaxFetchSizeAsInteger());
89          });
90      }
91  
92      /**
93       * Loads KeyMatch settings from the database and builds a query map.
94       *
95       * @return The number of loaded KeyMatch settings.
96       */
97      @Override
98      public int load() {
99          final FessConfig fessConfig = ComponentUtil.getFessConfig();
100         final Map<String, Map<String, List<Tuple3<String, QueryBuilder, ScoreFunctionBuilder<?>>>>> keyMatchQueryMap = new HashMap<>();
101         getAvailableKeyMatchList().stream().forEach(keyMatch -> {
102             try {
103                 final BoolQueryBuilder boolQuery = QueryBuilders.boolQuery();
104                 if (logger.isDebugEnabled()) {
105                     logger.debug("Loading KeyMatch Query: {}, Size: {}", keyMatch.getQuery(), keyMatch.getMaxSize());
106                 }
107                 getDocumentList(keyMatch).stream().map(doc -> {
108                     if (logger.isDebugEnabled()) {
109                         logger.debug("Loaded KeyMatch doc: {}", doc);
110                     }
111                     return DocumentUtil.getValue(doc, fessConfig.getIndexFieldDocId(), String.class);
112                 }).forEach(docId -> {
113                     boolQuery.should(QueryBuilders.termQuery(fessConfig.getIndexFieldDocId(), docId));
114                 });
115 
116                 if (boolQuery.hasClauses()) {
117                     if (logger.isDebugEnabled()) {
118                         logger.debug("Loaded KeyMatch Boost Query: {}", boolQuery);
119                     }
120                     String virtualHost = keyMatch.getVirtualHost();
121                     if (StringUtil.isBlank(virtualHost)) {
122                         virtualHost = StringUtil.EMPTY;
123                     }
124                     Map<String, List<Tuple3<String, QueryBuilder, ScoreFunctionBuilder<?>>>> queryMap = keyMatchQueryMap.get(virtualHost);
125                     if (queryMap == null) {
126                         queryMap = new HashMap<>();
127                         keyMatchQueryMap.put(virtualHost, queryMap);
128                     }
129                     final String termKey = toLowerCase(keyMatch.getTerm());
130                     List<Tuple3<String, QueryBuilder, ScoreFunctionBuilder<?>>> boostList = queryMap.get(termKey);
131                     if (boostList == null) {
132                         boostList = new ArrayList<>();
133                         queryMap.put(termKey, boostList);
134                     }
135                     boostList.add(
136                             new Tuple3<>(keyMatch.getId(), boolQuery, ScoreFunctionBuilders.weightFactorFunction(keyMatch.getBoost())));
137                 } else if (logger.isDebugEnabled()) {
138                     logger.debug("No KeyMatch boost docs");
139                 }
140 
141                 waitForNext();
142             } catch (final Exception e) {
143                 logger.warn("Failed to load KeyMatch: id={}, term={}", keyMatch.getId(), keyMatch.getTerm(), e);
144             }
145         });
146         this.keyMatchQueryMap = keyMatchQueryMap;
147         return keyMatchQueryMap.size();
148     }
149 
150     /**
151      * Retrieves a list of documents based on the KeyMatch query.
152      *
153      * @param keyMatch The KeyMatch instance.
154      * @return A list of documents.
155      */
156     protected List<Map<String, Object>> getDocumentList(final KeyMatch keyMatch) {
157         final SearchEngineClient searchEngineClient = ComponentUtil.getSearchEngineClient();
158         final FessConfig fessConfig = ComponentUtil.getFessConfig();
159         return searchEngineClient.getDocumentList(fessConfig.getIndexDocumentSearchIndex(),
160                 searchRequestBuilder -> SearchConditionBuilder
161                         .builder(searchRequestBuilder.setPreference(Constants.SEARCH_PREFERENCE_LOCAL))
162                         .searchRequestType(SearchRequestType.ADMIN_SEARCH)
163                         .size(keyMatch.getMaxSize())
164                         .query(keyMatch.getQuery())
165                         .responseFields(new String[] { fessConfig.getIndexFieldDocId() })
166                         .build());
167     }
168 
169     /**
170      * Returns a query map for the specified virtual host.
171      *
172      * @param key The virtual host key.
173      * @return A map of terms and boost information.
174      */
175     protected Map<String, List<Tuple3<String, QueryBuilder, ScoreFunctionBuilder<?>>>> getQueryMap(final String key) {
176         final Map<String, List<Tuple3<String, QueryBuilder, ScoreFunctionBuilder<?>>>> map = keyMatchQueryMap.get(key);
177         if (map != null) {
178             return map;
179         }
180         return Collections.emptyMap();
181     }
182 
183     /**
184      * Builds a query for boosting documents based on the keyword list.
185      *
186      * @param keywordList The list of keywords.
187      * @param list The list of filter function builders to add to.
188      */
189     public void buildQuery(final List<String> keywordList, final List<FilterFunctionBuilder> list) {
190         final String key = ComponentUtil.getVirtualHostHelper().getVirtualHostKey();
191         keywordList.stream().forEach(keyword -> {
192             final List<Tuple3<String, QueryBuilder, ScoreFunctionBuilder<?>>> boostList = getQueryMap(key).get(toLowerCase(keyword));
193             if (boostList != null) {
194                 boostList.forEach(pair -> list.add(new FilterFunctionBuilder(pair.getValue2(), pair.getValue3())));
195             }
196         });
197     }
198 
199     /**
200      * Retrieves a list of boosted documents for the specified KeyMatch.
201      *
202      * @param keyMatch The KeyMatch instance.
203      * @return A list of boosted documents.
204      */
205     public List<Map<String, Object>> getBoostedDocumentList(final KeyMatch keyMatch) {
206         final SearchEngineClient searchEngineClient = ComponentUtil.getSearchEngineClient();
207         String virtualHost = keyMatch.getVirtualHost();
208         if (StringUtil.isBlank(virtualHost)) {
209             virtualHost = StringUtil.EMPTY;
210         }
211         final List<Tuple3<String, QueryBuilder, ScoreFunctionBuilder<?>>> boostList =
212                 getQueryMap(virtualHost).get(toLowerCase(keyMatch.getTerm()));
213         if (boostList == null) {
214             return Collections.emptyList();
215         }
216         for (final Tuple3<String, QueryBuilder, ScoreFunctionBuilder<?>> pair : boostList) {
217             if (!keyMatch.getId().equals(pair.getValue1())) {
218                 continue;
219             }
220             final FessConfig fessConfig = ComponentUtil.getFessConfig();
221             return searchEngineClient.getDocumentList(fessConfig.getIndexDocumentSearchIndex(), searchRequestBuilder -> {
222                 searchRequestBuilder.setPreference(Constants.SEARCH_PREFERENCE_LOCAL)
223                         .setQuery(pair.getValue2())
224                         .setSize(keyMatch.getMaxSize());
225                 return true;
226             });
227         }
228         return Collections.emptyList();
229     }
230 
231     /**
232      * Converts a string to lowercase.
233      *
234      * @param term The string to convert.
235      * @return The lowercase string.
236      */
237     private String toLowerCase(final String term) {
238         return term != null ? term.toLowerCase(Locale.ROOT) : term;
239     }
240 
241 }