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.time.LocalDateTime;
21  import java.util.ArrayList;
22  import java.util.Arrays;
23  import java.util.Collections;
24  import java.util.HashMap;
25  import java.util.List;
26  import java.util.Locale;
27  import java.util.Map;
28  import java.util.Queue;
29  import java.util.concurrent.ConcurrentLinkedQueue;
30  import java.util.concurrent.ExecutionException;
31  import java.util.concurrent.TimeUnit;
32  import java.util.stream.Collectors;
33  
34  import org.apache.commons.lang3.StringUtils;
35  import org.apache.logging.log4j.LogManager;
36  import org.apache.logging.log4j.Logger;
37  import org.codelibs.core.concurrent.CommonPoolUtil;
38  import org.codelibs.core.lang.StringUtil;
39  import org.codelibs.fess.Constants;
40  import org.codelibs.fess.entity.SearchLogEvent;
41  import org.codelibs.fess.entity.SearchRequestParams;
42  import org.codelibs.fess.entity.SearchRequestParams.SearchRequestType;
43  import org.codelibs.fess.mylasta.action.FessUserBean;
44  import org.codelibs.fess.mylasta.direction.FessConfig;
45  import org.codelibs.fess.opensearch.log.exbhv.ClickLogBhv;
46  import org.codelibs.fess.opensearch.log.exbhv.FavoriteLogBhv;
47  import org.codelibs.fess.opensearch.log.exbhv.SearchLogBhv;
48  import org.codelibs.fess.opensearch.log.exbhv.UserInfoBhv;
49  import org.codelibs.fess.opensearch.log.exentity.ClickLog;
50  import org.codelibs.fess.opensearch.log.exentity.SearchLog;
51  import org.codelibs.fess.opensearch.log.exentity.UserInfo;
52  import org.codelibs.fess.util.ComponentUtil;
53  import org.codelibs.fess.util.DocumentUtil;
54  import org.codelibs.fess.util.QueryResponseList;
55  import org.dbflute.optional.OptionalEntity;
56  import org.dbflute.optional.OptionalThing;
57  import org.lastaflute.web.util.LaRequestUtil;
58  import org.opensearch.action.update.UpdateRequest;
59  import org.opensearch.script.Script;
60  
61  import com.fasterxml.jackson.core.JsonProcessingException;
62  import com.fasterxml.jackson.databind.ObjectMapper;
63  import com.google.common.base.CaseFormat;
64  import com.google.common.cache.CacheBuilder;
65  import com.google.common.cache.CacheLoader;
66  import com.google.common.cache.LoadingCache;
67  
68  import jakarta.annotation.PostConstruct;
69  import jakarta.servlet.http.HttpServletRequest;
70  
71  /**
72   * Helper class for managing search logs.
73   */
74  public class SearchLogHelper {
75      private static final Logger logger = LogManager.getLogger(SearchLogHelper.class);
76  
77      /**
78       * Default constructor for SearchLogHelper.
79       */
80      public SearchLogHelper() {
81          // Default constructor
82      }
83  
84      /** Interval for checking user information in milliseconds (default: 10 minutes). */
85      protected long userCheckInterval = 10 * 60 * 1000L; // 10 min
86  
87      /** Maximum size of the user information cache. */
88      protected int userInfoCacheSize = 10000;
89  
90      /** Queue for storing search logs. */
91      protected Queue<SearchLog> searchLogQueue = new ConcurrentLinkedQueue<>();
92  
93      /** Queue for storing click logs. */
94      protected Queue<ClickLog> clickLogQueue = new ConcurrentLinkedQueue<>();
95  
96      /** Cache for storing user information. */
97      protected LoadingCache<String, UserInfo> userInfoCache;
98  
99      /** Name of the logger for search logs. */
100     protected String loggerName = "fess.log.searchlog";
101 
102     /** Logger for search logs. */
103     protected Logger searchLogLogger = null;
104 
105     /** ObjectMapper for JSON processing. */
106     protected ObjectMapper objectMapper = new ObjectMapper();
107 
108     /**
109      * Initializes the SearchLogHelper.
110      */
111     @PostConstruct
112     public void init() {
113         if (logger.isDebugEnabled()) {
114             logger.debug("Initializing {}", this.getClass().getSimpleName());
115         }
116         userInfoCache = CacheBuilder.newBuilder()//
117                 .maximumSize(userInfoCacheSize)//
118                 .expireAfterWrite(userCheckInterval, TimeUnit.MILLISECONDS)//
119                 .build(new CacheLoader<String, UserInfo>() {
120                     @Override
121                     public UserInfo load(final String key) throws Exception {
122                         return storeUserInfo(key);
123                     }
124                 });
125         searchLogLogger = LogManager.getLogger(loggerName);
126     }
127 
128     /** Holds resolved dependencies for search log creation, decoupled from ComponentUtil. */
129     protected static class SearchLogContext {
130         final FessConfig fessConfig;
131         final String[] roles;
132         final String userCode;
133         final String userId;
134         final HttpServletRequest request;
135         final String clientIp;
136         final String virtualHostKey;
137 
138         SearchLogContext(final FessConfig fessConfig, final String[] roles, final String userCode, final String userId,
139                 final HttpServletRequest request, final String clientIp, final String virtualHostKey) {
140             this.fessConfig = fessConfig;
141             this.roles = roles;
142             this.userCode = userCode;
143             this.userId = userId;
144             this.request = request;
145             this.clientIp = clientIp;
146             this.virtualHostKey = virtualHostKey;
147         }
148     }
149 
150     /**
151      * Adds a search log to the queue.
152      *
153      * @param params The search request parameters.
154      * @param requestedTime The time the search was requested.
155      * @param queryId The ID of the search query.
156      * @param query The search query.
157      * @param pageStart The starting page number.
158      * @param pageSize The size of the page.
159      * @param queryResponseList The list of query responses.
160      */
161     public void addSearchLog(final SearchRequestParams params, final LocalDateTime requestedTime, final String queryId, final String query,
162             final int pageStart, final int pageSize, final QueryResponseList queryResponseList) {
163         final FessConfig fessConfig = ComponentUtil.getFessConfig();
164         if (searchLogQueue.size() > fessConfig.getLoggingSearchMaxQueueSizeAsInteger()) {
165             logger.warn("[{}] The search log queue size is too large. Skipped the search log: {}", queryId, query);
166             return;
167         }
168 
169         final SearchLogContext context = createSearchLogContext(params, fessConfig);
170         createSearchLog(params, requestedTime, queryId, query, pageStart, pageSize, queryResponseList, context);
171     }
172 
173     /**
174      * Resolves the runtime dependencies needed to build a SearchLog.
175      *
176      * @param params The search request parameters.
177      * @param fessConfig The Fess configuration.
178      * @return The resolved search log context.
179      */
180     protected SearchLogContext createSearchLogContext(final SearchRequestParams params, final FessConfig fessConfig) {
181         final String[] roles = ComponentUtil.getRoleQueryHelper().build(params.getType()).stream().toArray(n -> new String[n]);
182         final String userCode = fessConfig.isUserInfo() ? ComponentUtil.getUserInfoHelper().getUserCode() : null;
183         final String userId = ComponentUtil.getRequestManager().findUserBean(FessUserBean.class).map(FessUserBean::getUserId).orElse(null);
184         final HttpServletRequest request = LaRequestUtil.getOptionalRequest().orElse(null);
185         final String clientIp = request != null ? ComponentUtil.getViewHelper().getClientIp(request) : null;
186         final String virtualHostKey = ComponentUtil.getVirtualHostHelper().getVirtualHostKey();
187 
188         return new SearchLogContext(fessConfig, roles, userCode, userId, request, clientIp, virtualHostKey);
189     }
190 
191     /**
192      * Builds a SearchLog from the given parameters and context, then adds it to the queue.
193      *
194      * @param params The search request parameters.
195      * @param requestedTime The time the search was requested.
196      * @param queryId The ID of the search query.
197      * @param query The search query string.
198      * @param pageStart The start position of the page.
199      * @param pageSize The size of the page.
200      * @param queryResponseList The list of query responses.
201      * @param context The search log context holding resolved dependencies.
202      */
203     protected void createSearchLog(final SearchRequestParams params, final LocalDateTime requestedTime, final String queryId,
204             final String query, final int pageStart, final int pageSize, final QueryResponseList queryResponseList,
205             final SearchLogContext context) {
206         final SearchLog searchLog = new SearchLog();
207 
208         if (context.userCode != null) {
209             searchLog.setUserSessionId(context.userCode);
210             searchLog.setUserInfo(getUserInfo(context.userCode));
211         }
212 
213         searchLog.setRoles(context.roles);
214         searchLog.setQueryId(queryId);
215         searchLog.setHitCount(queryResponseList.getAllRecordCount());
216         searchLog.setHitCountRelation(queryResponseList.getAllRecordCountRelation());
217         searchLog.setResponseTime(queryResponseList.getExecTime());
218         searchLog.setQueryTime(queryResponseList.getQueryTime());
219         searchLog.setSearchWord(StringUtils.abbreviate(query, 1000));
220         searchLog.setRequestedAt(requestedTime);
221         searchLog.setSearchQuery(StringUtils.abbreviate(queryResponseList.getSearchQuery(), 1000));
222         searchLog.setQueryOffset(pageStart);
223         searchLog.setQueryPageSize(pageSize);
224 
225         if (context.userId != null) {
226             searchLog.setUser(context.userId);
227         }
228 
229         if (context.request != null) {
230             searchLog.setClientIp(StringUtils.abbreviate(context.clientIp, 100));
231             searchLog.setReferer(StringUtils.abbreviate(context.request.getHeader("referer"), 1000));
232             searchLog.setUserAgent(StringUtils.abbreviate(context.request.getHeader("user-agent"), 255));
233 
234             searchLog.setAccessType(determineAccessType(context.request.getAttribute(Constants.SEARCH_LOG_ACCESS_TYPE)));
235 
236             final Object languages = context.request.getAttribute(Constants.REQUEST_LANGUAGES);
237             if (languages != null) {
238                 searchLog.setLanguages(StringUtils.join((String[]) languages, ","));
239             } else {
240                 searchLog.setLanguages(StringUtil.EMPTY);
241             }
242 
243             @SuppressWarnings("unchecked")
244             final Map<String, List<String>> fieldLogMap = (Map<String, List<String>>) context.request.getAttribute(Constants.FIELD_LOGS);
245             if (fieldLogMap != null) {
246                 final int queryMaxLength = context.fessConfig.getQueryMaxLengthAsInteger();
247                 for (final Map.Entry<String, List<String>> logEntry : fieldLogMap.entrySet()) {
248                     for (final String value : logEntry.getValue()) {
249                         searchLog.addSearchFieldLogValue(logEntry.getKey(), StringUtils.abbreviate(value, queryMaxLength));
250                     }
251                 }
252             }
253 
254             for (final String s : context.fessConfig.getSearchlogRequestHeadersAsArray()) {
255                 final String key = s.replace('-', '_').toLowerCase(Locale.ENGLISH);
256                 Collections.list(context.request.getHeaders(s)).stream().forEach(v -> {
257                     searchLog.addRequestHeaderValue(key, v);
258                 });
259             }
260         }
261 
262         if (StringUtil.isNotBlank(context.virtualHostKey)) {
263             searchLog.setVirtualHost(context.virtualHostKey);
264         } else {
265             searchLog.setVirtualHost(StringUtil.EMPTY);
266         }
267 
268         addDocumentsInResponse(queryResponseList, searchLog);
269         searchLogQueue.add(searchLog);
270     }
271 
272     /**
273      * Returns the access type string from the given request attribute value, defaulting to web.
274      *
275      * @param accessType The access type attribute value from the request.
276      * @return The access type string.
277      */
278     protected String determineAccessType(final Object accessType) {
279         if (Constants.SEARCH_LOG_ACCESS_TYPE_JSON.equals(accessType)) {
280             return Constants.SEARCH_LOG_ACCESS_TYPE_JSON;
281         } else if (Constants.SEARCH_LOG_ACCESS_TYPE_GSA.equals(accessType)) {
282             return Constants.SEARCH_LOG_ACCESS_TYPE_GSA;
283         } else if (Constants.SEARCH_LOG_ACCESS_TYPE_OTHER.equals(accessType)) {
284             return Constants.SEARCH_LOG_ACCESS_TYPE_OTHER;
285         } else if (Constants.SEARCH_LOG_ACCESS_TYPE_ADMIN.equals(accessType)) {
286             return Constants.SEARCH_LOG_ACCESS_TYPE_ADMIN;
287         } else if (accessType instanceof String && StringUtil.isNotBlank((String) accessType)) {
288             return (String) accessType;
289         }
290         return Constants.SEARCH_LOG_ACCESS_TYPE_WEB;
291     }
292 
293     /**
294      * Adds documents in the response to the search log.
295      *
296      * @param queryResponseList The list of query responses.
297      * @param searchLog The search log.
298      */
299     protected void addDocumentsInResponse(final QueryResponseList queryResponseList, final SearchLog searchLog) {
300         if (ComponentUtil.getFessConfig().isLoggingSearchDocsEnabled()) {
301             queryResponseList.stream().forEach(res -> {
302                 final Map<String, Object> map = new HashMap<>();
303                 Arrays.stream(ComponentUtil.getFessConfig().getLoggingSearchDocsFieldsAsArray()).forEach(s -> map.put(s, res.get(s)));
304                 searchLog.addDocument(map);
305             });
306         }
307     }
308 
309     /**
310      * Adds a click log to the queue.
311      *
312      * @param clickLog The click log.
313      */
314     public void addClickLog(final ClickLog clickLog) {
315         final FessConfig fessConfig = ComponentUtil.getFessConfig();
316         if (clickLogQueue.size() > fessConfig.getLoggingClickMaxQueueSizeAsInteger()) {
317             logger.warn("Click log queue size exceeded: queueSize={}, limit={}. Skipped.", clickLogQueue.size(),
318                     fessConfig.getLoggingClickMaxQueueSizeAsInteger());
319             return;
320         }
321         clickLogQueue.add(clickLog);
322     }
323 
324     /**
325      * Stores search logs from the queue.
326      */
327     public void storeSearchLog() {
328         storeSearchLogFromQueue();
329         storeClickLogFromQueue();
330     }
331 
332     /**
333      * Stores click logs from the queue.
334      */
335     protected void storeClickLogFromQueue() {
336         if (!clickLogQueue.isEmpty()) {
337             processClickLogQueue(clickLogQueue);
338         }
339     }
340 
341     /**
342      * Stores search logs from the queue.
343      */
344     protected void storeSearchLogFromQueue() {
345         if (!searchLogQueue.isEmpty()) {
346             processSearchLogQueue(searchLogQueue);
347         }
348     }
349 
350     /**
351     * Gets the click count for a URL.
352     *
353     * @param url The URL.
354     * @return The click count.
355     */
356     public int getClickCount(final String url) {
357         final ClickLogBhv clickLogBhv = ComponentUtil.getComponent(ClickLogBhv.class);
358         return clickLogBhv.selectCount(cb -> {
359             cb.query().setUrl_Equal(url);
360         });
361     }
362 
363     /**
364      * Gets the favorite count for a URL.
365      *
366      * @param url The URL.
367      * @return The favorite count.
368      */
369     public long getFavoriteCount(final String url) {
370         final FavoriteLogBhv favoriteLogBhv = ComponentUtil.getComponent(FavoriteLogBhv.class);
371         return favoriteLogBhv.selectCount(cb -> {
372             cb.query().setUrl_Equal(url);
373         });
374     }
375 
376     /**
377      * Stores user information.
378      *
379      * @param userCode The user code.
380      * @return The user information.
381      */
382     protected UserInfo storeUserInfo(final String userCode) {
383         final UserInfoBhv userInfoBhv = ComponentUtil.getComponent(UserInfoBhv.class);
384 
385         final LocalDateTime now = ComponentUtil.getSystemHelper().getCurrentTimeAsLocalDateTime();
386         final UserInfo userInfo = userInfoBhv.selectByPK(userCode).map(e -> {
387             e.setUpdatedAt(now);
388             return e;
389         }).orElseGet(() -> {
390             final UserInfo e = new UserInfo();
391             e.setId(userCode);
392             e.setCreatedAt(now);
393             e.setUpdatedAt(now);
394             return e;
395         });
396         CommonPoolUtil.execute(() -> userInfoBhv.insertOrUpdate(userInfo));
397         return userInfo;
398     }
399 
400     /**
401      * Gets user information.
402      *
403      * @param userCode The user code.
404      * @return The user information.
405      */
406     public OptionalEntity<UserInfo> getUserInfo(final String userCode) {
407         if (StringUtil.isNotBlank(userCode)) {
408             try {
409                 return OptionalEntity.of(userInfoCache.get(userCode));
410             } catch (final ExecutionException e) {
411                 if (logger.isDebugEnabled()) {
412                     logger.debug("Failed to access UserInfo cache.", e);
413                 }
414             }
415         }
416         return OptionalEntity.empty();
417     }
418 
419     /**
420      * Processes the search log queue.
421      *
422      * @param queue The search log queue.
423      */
424     protected void processSearchLogQueue(final Queue<SearchLog> queue) {
425         final FessConfig fessConfig = ComponentUtil.getFessConfig();
426         final String value = fessConfig.getPurgeByBots();
427         String[] botNames;
428         if (StringUtil.isBlank(value)) {
429             botNames = StringUtil.EMPTY_STRINGS;
430         } else {
431             botNames = value.split(",");
432         }
433 
434         final int batchSize = fessConfig.getSearchlogProcessBatchSizeAsInteger();
435 
436         final List<SearchLog> searchLogList = new ArrayList<>();
437         final Map<String, UserInfo> userInfoMap = new HashMap<>();
438         while (!queue.isEmpty()) {
439             final SearchLog searchLog = queue.poll();
440             if (searchLog != null) {
441                 final String userAgent = searchLog.getUserAgent();
442                 final boolean isBot =
443                         userAgent != null && stream(botNames).get(stream -> stream.anyMatch(botName -> userAgent.indexOf(botName) >= 0));
444                 if (!isBot) {
445                     searchLog.getUserInfo().ifPresent(userInfo -> {
446                         final String code = userInfo.getId();
447                         final UserInfo oldUserInfo = userInfoMap.get(code);
448                         if (oldUserInfo != null) {
449                             userInfo.setCreatedAt(oldUserInfo.getCreatedAt());
450                         }
451                         userInfoMap.put(code, userInfo);
452                     });
453                     searchLogList.add(searchLog);
454                 }
455             }
456             if (searchLogList.size() >= batchSize) {
457                 processUserInfoLog(searchLogList, userInfoMap);
458                 processSearchLog(searchLogList);
459                 searchLogList.clear();
460                 userInfoMap.clear();
461             }
462         }
463 
464         if (!searchLogList.isEmpty()) {
465             processUserInfoLog(searchLogList, userInfoMap);
466             processSearchLog(searchLogList);
467         }
468     }
469 
470     /**
471      * Processes the search log list.
472      *
473      * @param searchLogList The search log list.
474      */
475     protected void processSearchLog(final List<SearchLog> searchLogList) {
476         if (!searchLogList.isEmpty()) {
477             final FessConfig fessConfig = ComponentUtil.getFessConfig();
478             // write log
479             if (fessConfig.isLoggingSearchUseLogfile()) {
480                 searchLogList.forEach(this::writeSearchLogEvent);
481             }
482             // insert search log
483             storeSearchLogList(searchLogList);
484             // update suggest index
485             if (fessConfig.isSuggestSearchLog()) {
486                 final SuggestHelper suggestHelper = ComponentUtil.getSuggestHelper();
487                 suggestHelper.indexFromSearchLog(searchLogList);
488             }
489         }
490     }
491 
492     /**
493      * Processes user information logs.
494      *
495      * @param searchLogList The search log list.
496      * @param userInfoMap The user information map.
497      */
498     protected void processUserInfoLog(final List<SearchLog> searchLogList, final Map<String, UserInfo> userInfoMap) {
499         if (!userInfoMap.isEmpty()) {
500             final FessConfig fessConfig = ComponentUtil.getFessConfig();
501             final List<UserInfo> insertList = new ArrayList<>(userInfoMap.values());
502             final List<UserInfo> updateList = new ArrayList<>();
503             final UserInfoBhv userInfoBhv = ComponentUtil.getComponent(UserInfoBhv.class);
504             userInfoBhv.selectList(cb -> {
505                 cb.query().setId_InScope(userInfoMap.keySet());
506                 cb.fetchFirst(userInfoMap.size());
507             }).forEach(userInfo -> {
508                 final String code = userInfo.getId();
509                 final UserInfo entity = userInfoMap.get(code);
510                 entity.setId(userInfo.getId());
511                 entity.setCreatedAt(userInfo.getCreatedAt());
512                 updateList.add(entity);
513                 insertList.remove(entity);
514             });
515             // write log
516             if (fessConfig.isLoggingSearchUseLogfile()) {
517                 insertList.forEach(this::writeSearchLogEvent);
518                 updateList.forEach(this::writeSearchLogEvent);
519             }
520             // insert/update user info
521             userInfoBhv.batchInsert(insertList);
522             userInfoBhv.batchUpdate(updateList);
523             // update search log
524             searchLogList.stream().forEach(searchLog -> {
525                 searchLog.getUserInfo().ifPresent(userInfo -> {
526                     searchLog.setUserInfoId(userInfo.getId());
527                 });
528             });
529         }
530     }
531 
532     /**
533     * Stores a list of search logs.
534     *
535     * @param searchLogList The search log list.
536     */
537     protected void storeSearchLogList(final List<SearchLog> searchLogList) {
538         final SearchLogBhv searchLogBhv = ComponentUtil.getComponent(SearchLogBhv.class);
539         searchLogBhv.batchUpdate(searchLogList, op -> {
540             op.setRefreshPolicy(Constants.TRUE);
541         });
542     }
543 
544     /**
545      * Processes the click log queue.
546      *
547      * @param queue The click log queue.
548      */
549     protected void processClickLogQueue(final Queue<ClickLog> queue) {
550         final FessConfig fessConfig = ComponentUtil.getFessConfig();
551         final int batchSize = fessConfig.getSearchlogProcessBatchSizeAsInteger();
552         final Map<String, Integer> clickCountMap = new HashMap<>();
553         final List<ClickLog> clickLogList = new ArrayList<>();
554         while (!queue.isEmpty()) {
555             final ClickLog clickLog = queue.poll();
556             if (clickLog != null) {
557                 try {
558                     final SearchLogBhv searchLogBhv = ComponentUtil.getComponent(SearchLogBhv.class);
559                     searchLogBhv.selectEntity(cb -> {
560                         cb.query().setQueryId_Equal(clickLog.getQueryId());
561                     }).ifPresent(entity -> {
562                         clickLogList.add(clickLog);
563                         final String docId = clickLog.getDocId();
564                         Integer countObj = clickCountMap.get(docId);
565                         if (countObj == null) {
566                             countObj = 1;
567                         } else {
568                             countObj = countObj.intValue() + 1;
569                         }
570                         clickCountMap.put(docId, countObj);
571                     }).orElse(() -> {
572                         logger.warn("Not Found for SearchLog: {}", clickLog);
573                     });
574                 } catch (final Exception e) {
575                     logger.warn("Failed to process: {}", clickLog, e);
576                 }
577             }
578             if (clickLogList.size() >= batchSize) {
579                 processClickLog(clickLogList);
580                 updateClickFieldInIndex(clickCountMap);
581                 clickLogList.clear();
582                 clickCountMap.clear();
583             }
584         }
585 
586         if (!clickLogList.isEmpty()) {
587             processClickLog(clickLogList);
588             updateClickFieldInIndex(clickCountMap);
589         }
590     }
591 
592     /**
593      * Updates the click field in the index.
594      *
595      * @param clickCountMap The click count map.
596      */
597     protected void updateClickFieldInIndex(final Map<String, Integer> clickCountMap) {
598         if (!clickCountMap.isEmpty()) {
599             final SearchHelper searchHelper = ComponentUtil.getSearchHelper();
600             final FessConfig fessConfig = ComponentUtil.getFessConfig();
601             try {
602                 final UpdateRequest[] updateRequests =
603                         searchHelper
604                                 .getDocumentListByDocIds(clickCountMap.keySet().toArray(new String[clickCountMap.size()]),
605                                         new String[] { fessConfig.getIndexFieldDocId(), fessConfig.getIndexFieldLang() },
606                                         OptionalThing.of(FessUserBean.empty()), SearchRequestType.ADMIN_SEARCH)
607                                 .stream()
608                                 .map(doc -> {
609                                     final String id = DocumentUtil.getValue(doc, fessConfig.getIndexFieldId(), String.class);
610                                     final String docId = DocumentUtil.getValue(doc, fessConfig.getIndexFieldDocId(), String.class);
611                                     if (id != null && docId != null && clickCountMap.containsKey(docId)) {
612                                         final Integer count = clickCountMap.get(docId);
613                                         final Script script = ComponentUtil.getLanguageHelper()
614                                                 .createScript(doc,
615                                                         "ctx._source." + fessConfig.getIndexFieldClickCount() + "+=" + count.toString());
616                                         final Map<String, Object> upsertMap = new HashMap<>();
617                                         upsertMap.put(fessConfig.getIndexFieldClickCount(), count);
618                                         return new UpdateRequest(fessConfig.getIndexDocumentUpdateIndex(), id).script(script)
619                                                 .upsert(upsertMap);
620                                     }
621                                     return null;
622                                 })
623                                 .filter(req -> req != null)
624                                 .toArray(n -> new UpdateRequest[n]);
625                 if (updateRequests.length > 0) {
626                     searchHelper.bulkUpdate(builder -> {
627                         for (final UpdateRequest req : updateRequests) {
628                             builder.add(req);
629                         }
630                     });
631                 }
632             } catch (final Exception e) {
633                 logger.warn("Failed to update clickCounts", e);
634             }
635         }
636     }
637 
638     /**
639      * Processes a list of click logs.
640      *
641      * @param clickLogList The click log list.
642      */
643     protected void processClickLog(final List<ClickLog> clickLogList) {
644         if (!clickLogList.isEmpty()) {
645             final FessConfig fessConfig = ComponentUtil.getFessConfig();
646             if (fessConfig.isLoggingSearchUseLogfile()) {
647                 clickLogList.forEach(this::writeSearchLogEvent);
648             }
649             try {
650                 ComponentUtil.getComponent(ClickLogBhv.class).batchInsert(clickLogList);
651             } catch (final Exception e) {
652                 logger.warn("Failed to insert: {}", clickLogList, e);
653             }
654         }
655     }
656 
657     /**
658      * Writes a search log event.
659      *
660      * @param event The search log event.
661      */
662     public void writeSearchLogEvent(final SearchLogEvent event) {
663         try {
664             final Map<String, Object> source = toSource(event);
665             searchLogLogger.info(objectMapper.writeValueAsString(source));
666         } catch (final JsonProcessingException e) {
667             logger.warn("Failed to write {}", event, e);
668         }
669     }
670 
671     /**
672     * Converts a search log event to a source map.
673     *
674     * @param searchLogEvent The search log event.
675     * @return The source map.
676     */
677     protected Map<String, Object> toSource(final SearchLogEvent searchLogEvent) {
678         final Map<String, Object> source = toLowerHyphen(searchLogEvent.toSource());
679         source.put("_id", searchLogEvent.getId());
680         // source.put("version_no", searchLogEvent.getVersionNo());
681         source.put("event_type", searchLogEvent.getEventType());
682         return source;
683     }
684 
685     /**
686      * Converts a map to lower hyphen case.
687      *
688      * @param source The source map.
689      * @return The converted map.
690      */
691     protected Map<String, Object> toLowerHyphen(final Map<String, Object> source) {
692         return source.entrySet()
693                 .stream()
694                 .collect(Collectors.toMap(e -> CaseFormat.UPPER_CAMEL.to(CaseFormat.LOWER_UNDERSCORE, e.getKey()), e -> {
695                     final Object value = e.getValue();
696                     if (value instanceof Map) {
697                         @SuppressWarnings("unchecked")
698                         final Map<String, Object> mapValue = (Map<String, Object>) value;
699                         return toLowerHyphen(mapValue);
700                     }
701                     return e.getValue();
702                 }));
703     }
704 
705     /**
706      * Sets the user check interval.
707      *
708      * @param userCheckInterval The user check interval.
709      */
710     public void setUserCheckInterval(final long userCheckInterval) {
711         this.userCheckInterval = userCheckInterval;
712     }
713 
714     /**
715      * Sets the user information cache size.
716      *
717      * @param userInfoCacheSize The user information cache size.
718      */
719     public void setUserInfoCacheSize(final int userInfoCacheSize) {
720         this.userInfoCacheSize = userInfoCacheSize;
721     }
722 
723     /**
724      * Sets the logger name.
725      *
726      * @param loggerName The logger name.
727      */
728     public void setLoggerName(final String loggerName) {
729         this.loggerName = loggerName;
730     }
731 }