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 static org.codelibs.core.stream.StreamUtil.stream;
19  
20  import java.util.ArrayList;
21  import java.util.Collections;
22  import java.util.List;
23  import java.util.concurrent.ExecutionException;
24  import java.util.concurrent.TimeUnit;
25  
26  import org.apache.logging.log4j.LogManager;
27  import org.apache.logging.log4j.Logger;
28  import org.codelibs.core.lang.StringUtil;
29  import org.codelibs.fess.entity.SearchRequestParams.SearchRequestType;
30  import org.codelibs.fess.mylasta.direction.FessConfig;
31  import org.codelibs.fess.suggest.exception.SuggesterException;
32  import org.codelibs.fess.suggest.request.popularwords.PopularWordsRequestBuilder;
33  import org.codelibs.fess.util.ComponentUtil;
34  
35  import com.google.common.cache.Cache;
36  import com.google.common.cache.CacheBuilder;
37  
38  import jakarta.annotation.PostConstruct;
39  
40  /**
41   * Helper class for managing popular words and suggestions.
42   * Provides functionality to retrieve popular words based on search parameters
43   * and manages caching for improved performance.
44   */
45  public class PopularWordHelper {
46      /** Logger instance for this class */
47      private static final Logger logger = LogManager.getLogger(PopularWordHelper.class);
48  
49      /** Character used to separate cache key components */
50      protected static final char CACHE_KEY_SPLITTER = '\n';
51  
52      /** Cache for storing popular word lists */
53      protected Cache<String, List<String>> cache;
54  
55      /** Fess configuration instance */
56      protected FessConfig fessConfig;
57  
58      /**
59       * Default constructor.
60       */
61      public PopularWordHelper() {
62          // Default constructor
63      }
64  
65      /**
66       * Initializes the PopularWordHelper after dependency injection.
67       * Sets up the cache with configured size and expiration settings.
68       */
69      @PostConstruct
70      public void init() {
71          if (logger.isDebugEnabled()) {
72              logger.debug("Initializing {}", this.getClass().getSimpleName());
73          }
74          fessConfig = ComponentUtil.getFessConfig();
75          cache = CacheBuilder.newBuilder()
76                  .maximumSize(fessConfig.getSuggestPopularWordCacheSizeAsInteger().longValue())
77                  .expireAfterWrite(fessConfig.getSuggestPopularWordCacheExpireAsInteger().longValue(), TimeUnit.MINUTES)
78                  .build();
79      }
80  
81      /**
82       * Retrieves a list of popular words based on the specified search parameters.
83       * Uses caching to improve performance for repeated requests.
84       *
85       * @param searchRequestType the type of search request
86       * @param seed the seed value for popular word generation
87       * @param tags array of tags to filter results
88       * @param roles array of roles to filter results
89       * @param fields array of fields to search in
90       * @param excludes array of words to exclude from results
91       * @return list of popular words matching the criteria
92       */
93      public List<String> getWordList(final SearchRequestType searchRequestType, final String seed, final String[] tags, final String[] roles,
94              final String[] fields, final String[] excludes) {
95          final String baseSeed = seed != null ? seed : fessConfig.getSuggestPopularWordSeed();
96          final String[] baseTags = tags != null ? tags : fessConfig.getSuggestPopularWordTagsAsArray();
97          final String[] baseRoles = roles != null ? roles
98                  : ComponentUtil.getRoleQueryHelper()
99                          .build(searchRequestType)
100                         .stream()
101                         .filter(StringUtil::isNotBlank)
102                         .toArray(n -> new String[n]);
103         final String[] baseFields = fields != null ? fields : fessConfig.getSuggestPopularWordFieldsAsArray();
104         final String[] baseExcludes = excludes != null ? excludes : fessConfig.getSuggestPopularWordExcludesAsArray();
105         try {
106             return cache.get(getCacheKey(baseSeed, baseTags, baseRoles, baseFields, baseExcludes), () -> {
107                 final List<String> wordList = new ArrayList<>();
108                 final SuggestHelper suggestHelper = ComponentUtil.getSuggestHelper();
109                 final PopularWordsRequestBuilder popularWordsRequestBuilder = suggestHelper.suggester()
110                         .popularWords()
111                         .setSize(fessConfig.getSuggestPopularWordSizeAsInteger())
112                         .setWindowSize(fessConfig.getSuggestPopularWordWindowSizeAsInteger())
113                         .setQueryFreqThreshold(fessConfig.getSuggestPopularWordQueryFreqAsInteger());
114                 popularWordsRequestBuilder.setSeed(baseSeed);
115                 stream(baseTags).of(stream -> stream.forEach(tag -> popularWordsRequestBuilder.addTag(tag)));
116                 stream(baseRoles).of(stream -> stream.forEach(role -> popularWordsRequestBuilder.addRole(role)));
117                 stream(baseFields).of(stream -> stream.forEach(field -> popularWordsRequestBuilder.addField(field)));
118                 stream(baseExcludes).of(stream -> stream.forEach(exclude -> popularWordsRequestBuilder.addExcludeWord(exclude)));
119                 try {
120                     popularWordsRequestBuilder.execute().getResponse().getItems().stream().forEach(item -> wordList.add(item.getText()));
121                 } catch (final SuggesterException e) {
122                     logger.warn("Failed to generate popular words.", e);
123                 }
124 
125                 return wordList;
126             });
127         } catch (final ExecutionException e) {
128             logger.warn("Failed to load popular words.", e);
129         }
130         return Collections.emptyList();
131     }
132 
133     /**
134      * Clears all cached popular word lists.
135      */
136     public void clearCache() {
137         cache.invalidateAll();
138     }
139 
140     /**
141      * Generates a cache key based on the provided parameters.
142      *
143      * @param seed the seed value
144      * @param tags array of tags
145      * @param roles array of roles
146      * @param fields array of fields
147      * @param excludes array of excluded words
148      * @return cache key string
149      */
150     protected String getCacheKey(final String seed, final String[] tags, final String[] roles, final String[] fields,
151             final String[] excludes) {
152         final StringBuilder buf = new StringBuilder(100);
153         buf.append(seed).append(CACHE_KEY_SPLITTER);
154         stream(tags).of(stream -> stream.sorted().reduce((l, r) -> l + r).ifPresent(v -> buf.append(v)));
155         buf.append(CACHE_KEY_SPLITTER);
156         stream(roles).of(stream -> stream.sorted().reduce((l, r) -> l + r).ifPresent(v -> buf.append(v)));
157         buf.append(CACHE_KEY_SPLITTER);
158         stream(fields).of(stream -> stream.sorted().reduce((l, r) -> l + r).ifPresent(v -> buf.append(v)));
159         buf.append(CACHE_KEY_SPLITTER);
160         stream(excludes).of(stream -> stream.sorted().reduce((l, r) -> l + r).ifPresent(v -> buf.append(v)));
161         return buf.toString();
162     }
163 
164 }