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.indexer;
17  
18  import java.util.ArrayList;
19  import java.util.List;
20  import java.util.Map;
21  import java.util.function.Consumer;
22  
23  import org.apache.logging.log4j.LogManager;
24  import org.apache.logging.log4j.Logger;
25  import org.codelibs.core.lang.StringUtil;
26  import org.codelibs.core.lang.ThreadUtil;
27  import org.codelibs.fess.Constants;
28  import org.codelibs.fess.crawler.Crawler;
29  import org.codelibs.fess.crawler.entity.AccessResult;
30  import org.codelibs.fess.crawler.entity.AccessResultData;
31  import org.codelibs.fess.crawler.entity.OpenSearchAccessResult;
32  import org.codelibs.fess.crawler.entity.OpenSearchUrlQueue;
33  import org.codelibs.fess.crawler.service.DataService;
34  import org.codelibs.fess.crawler.service.UrlFilterService;
35  import org.codelibs.fess.crawler.service.UrlQueueService;
36  import org.codelibs.fess.crawler.service.impl.OpenSearchDataService;
37  import org.codelibs.fess.crawler.transformer.Transformer;
38  import org.codelibs.fess.crawler.util.OpenSearchResultList;
39  import org.codelibs.fess.exception.ContainerNotAvailableException;
40  import org.codelibs.fess.exception.FessSystemException;
41  import org.codelibs.fess.helper.IndexingHelper;
42  import org.codelibs.fess.helper.IntervalControlHelper;
43  import org.codelibs.fess.helper.SearchLogHelper;
44  import org.codelibs.fess.helper.SystemHelper;
45  import org.codelibs.fess.ingest.IngestFactory;
46  import org.codelibs.fess.ingest.Ingester;
47  import org.codelibs.fess.mylasta.direction.FessConfig;
48  import org.codelibs.fess.opensearch.client.SearchEngineClient;
49  import org.codelibs.fess.opensearch.log.exbhv.ClickLogBhv;
50  import org.codelibs.fess.opensearch.log.exbhv.FavoriteLogBhv;
51  import org.codelibs.fess.util.ComponentUtil;
52  import org.codelibs.fess.util.DocList;
53  import org.codelibs.fess.util.MemoryUtil;
54  import org.codelibs.fess.util.ThreadDumpUtil;
55  import org.opensearch.action.search.SearchRequestBuilder;
56  import org.opensearch.index.query.QueryBuilder;
57  import org.opensearch.index.query.QueryBuilders;
58  import org.opensearch.search.sort.SortOrder;
59  
60  import jakarta.annotation.PostConstruct;
61  import jakarta.annotation.PreDestroy;
62  import jakarta.annotation.Resource;
63  
64  /**
65   * IndexUpdater is responsible for updating the search index with crawled document data.
66   * This class extends Thread and continuously processes access results from the crawler,
67   * transforms them into indexed documents, and updates the OpenSearch index.
68   *
69   * <p>The updater performs the following key operations:
70   * <ul>
71   * <li>Retrieves crawled documents from the data service</li>
72   * <li>Transforms document data using appropriate transformers</li>
73   * <li>Applies document boosting rules and click/favorite count enhancements</li>
74   * <li>Sends processed documents to the search engine for indexing</li>
75   * <li>Manages cleanup of processed crawler session data</li>
76   * </ul>
77   *
78   * <p>The updater runs continuously until crawling is finished and all documents are processed.
79   * It includes error handling, retry logic, and performance monitoring capabilities.
80   *
81   */
82  public class IndexUpdater extends Thread {
83      /** Logger for this class */
84      private static final Logger logger = LogManager.getLogger(IndexUpdater.class);
85  
86      /** List of crawler session IDs to process */
87      protected List<String> sessionIdList;
88  
89      /** OpenSearch client for index operations */
90      @Resource
91      protected SearchEngineClient searchEngineClient;
92  
93      /** Service for managing crawled document data */
94      @Resource
95      protected DataService<OpenSearchAccessResult> dataService;
96  
97      /** Service for managing URL crawling queue */
98      @Resource
99      protected UrlQueueService<OpenSearchUrlQueue> urlQueueService;
100 
101     /** Service for URL filtering operations */
102     @Resource
103     protected UrlFilterService urlFilterService;
104 
105     /** Behavior class for click log operations */
106     @Resource
107     protected ClickLogBhv clickLogBhv;
108 
109     /** Behavior class for favorite log operations */
110     @Resource
111     protected FavoriteLogBhv favoriteLogBhv;
112 
113     /** Helper for system-level operations */
114     @Resource
115     protected SystemHelper systemHelper;
116 
117     /** Helper for document indexing operations */
118     @Resource
119     protected IndexingHelper indexingHelper;
120 
121     /** Flag indicating if crawling should be finished */
122     protected boolean finishCrawling = false;
123 
124     /** Total execution time in milliseconds */
125     protected long executeTime;
126 
127     /** Total number of processed documents */
128     protected long documentSize;
129 
130     /** Maximum number of indexer errors allowed */
131     protected int maxIndexerErrorCount = 0;
132 
133     /** Maximum number of general errors allowed before termination */
134     protected int maxErrorCount = 2;
135 
136     /** List of finished crawler session IDs for cleanup */
137     protected List<String> finishedSessionIdList = new ArrayList<>();
138 
139     /** List of document boost matchers for scoring enhancement */
140     private final List<DocBoostMatcher> docBoostMatcherList = new ArrayList<>();
141 
142     /** List of active crawler instances */
143     private List<Crawler> crawlerList;
144 
145     /** Factory for creating document ingesters */
146     private IngestFactory ingestFactory = null;
147 
148     /**
149      * Default constructor for IndexUpdater.
150      * Initializes a new instance with default settings.
151      */
152     public IndexUpdater() {
153         super();
154     }
155 
156     /**
157      * Initializes the IndexUpdater after dependency injection.
158      * Sets up the ingest factory if available in the component container.
159      */
160     @PostConstruct
161     public void init() {
162         if (logger.isDebugEnabled()) {
163             logger.debug("Initializing {}", this.getClass().getSimpleName());
164         }
165         if (ComponentUtil.hasIngestFactory()) {
166             ingestFactory = ComponentUtil.getIngestFactory();
167         }
168     }
169 
170     /**
171      * Destroys the IndexUpdater when the container is shutting down.
172      * Stops all crawler instances if crawling is still in progress.
173      */
174     @PreDestroy
175     public void destroy() {
176         if (!finishCrawling) {
177             if (logger.isInfoEnabled()) {
178                 logger.info("Stopping all crawlers.");
179             }
180             forceStop();
181         }
182     }
183 
184     /**
185      * Adds a finished session ID to the cleanup list.
186      * This method is thread-safe and adds the session ID to be cleaned up later.
187      *
188      * @param sessionId the crawler session ID that has finished processing
189      */
190     public void addFinishedSessionId(final String sessionId) {
191         synchronized (finishedSessionIdList) {
192             finishedSessionIdList.add(sessionId);
193         }
194     }
195 
196     /**
197      * Deletes all data associated with a specific crawler session.
198      * Removes URL filters, URL queues, and access result data for the session.
199      *
200      * @param sessionId the session ID whose data should be deleted
201      */
202     private void deleteBySessionId(final String sessionId) {
203         try {
204             urlFilterService.delete(sessionId);
205         } catch (final Exception e) {
206             logger.warn("Failed to delete UrlFilter: sessionId={}", sessionId, e);
207         }
208         try {
209             urlQueueService.delete(sessionId);
210         } catch (final Exception e) {
211             logger.warn("Failed to delete UrlQueue: sessionId={}", sessionId, e);
212         }
213         try {
214             dataService.delete(sessionId);
215         } catch (final Exception e) {
216             logger.warn("Failed to delete AccessResult: sessionId={}", sessionId, e);
217         }
218     }
219 
220     /**
221      * Main execution method that runs the index updating process.
222      * Continuously processes crawled documents from the data service, transforms them,
223      * and updates the search index until crawling is finished and all documents are processed.
224      *
225      * <p>The method performs the following operations in a loop:
226      * <ul>
227      * <li>Retrieves access results from the data service</li>
228      * <li>Processes each document through transformers</li>
229      * <li>Applies document boosting and metadata enhancements</li>
230      * <li>Sends processed documents to the search engine</li>
231      * <li>Cleans up processed data and manages crawler sessions</li>
232      * </ul>
233      *
234      * <p>The method includes error handling, retry logic, and will terminate
235      * if too many empty results are encountered or if a system shutdown is requested.
236      */
237     @Override
238     public void run() {
239         if (dataService == null) {
240             throw new FessSystemException("DataService is null. IndexUpdater cannot proceed without a DataService instance.");
241         }
242 
243         if (logger.isDebugEnabled()) {
244             logger.debug("Starting indexUpdater.");
245         }
246 
247         executeTime = 0;
248         documentSize = 0;
249 
250         final FessConfig fessConfig = ComponentUtil.getFessConfig();
251         final long updateInterval = fessConfig.getIndexerWebfsUpdateIntervalAsInteger().longValue();
252         final int maxEmptyListCount = fessConfig.getIndexerWebfsMaxEmptyListCountAsInteger();
253         final IntervalControlHelper intervalControlHelper = ComponentUtil.getIntervalControlHelper();
254         try {
255             final Consumer<SearchRequestBuilder> cb = builder -> {
256                 final QueryBuilder queryBuilder = QueryBuilders.boolQuery()
257                         .filter(QueryBuilders.termsQuery(OpenSearchAccessResult.SESSION_ID, sessionIdList))
258                         .filter(QueryBuilders.termQuery(OpenSearchAccessResult.STATUS, org.codelibs.fess.crawler.Constants.OK_STATUS));
259                 builder.setQuery(queryBuilder);
260                 builder.setFrom(0);
261                 final int maxDocumentCacheSize = fessConfig.getIndexerWebfsMaxDocumentCacheSizeAsInteger();
262                 builder.setSize(maxDocumentCacheSize <= 0 ? 1 : maxDocumentCacheSize);
263                 builder.addSort(OpenSearchAccessResult.CREATE_TIME, SortOrder.ASC);
264             };
265 
266             final DocList docList = new DocList();
267             final List<OpenSearchAccessResult> accessResultList = new ArrayList<>();
268 
269             long updateTime = systemHelper.getCurrentTimeAsLong();
270             int errorCount = 0;
271             int emptyListCount = 0;
272             long cleanupTime = -1;
273             while (!finishCrawling || !accessResultList.isEmpty()) {
274                 try {
275                     final int sessionIdListSize = finishedSessionIdList.size();
276                     intervalControlHelper.setCrawlerRunning(true);
277 
278                     docList.clear();
279                     accessResultList.clear();
280 
281                     updateTime = systemHelper.getCurrentTimeAsLong() - updateTime;
282 
283                     final long interval = updateInterval - updateTime;
284                     if (interval > 0) {
285                         // sleep
286                         ThreadUtil.sleep(interval); // 10 sec (default)
287                     }
288 
289                     systemHelper.calibrateCpuLoad();
290                     systemHelper.waitForNoWaitingThreads();
291 
292                     intervalControlHelper.delayByRules();
293 
294                     if (logger.isDebugEnabled()) {
295                         logger.debug("Processing documents in IndexUpdater queue.");
296                     }
297 
298                     updateTime = systemHelper.getCurrentTimeAsLong();
299 
300                     List<OpenSearchAccessResult> arList = getAccessResultList(cb, cleanupTime);
301                     if (arList.isEmpty()) {
302                         emptyListCount++;
303                     } else {
304                         emptyListCount = 0; // reset
305                     }
306                     long hitCount = ((OpenSearchResultList<OpenSearchAccessResult>) arList).getTotalHits();
307                     while (hitCount > 0) {
308                         if (arList.isEmpty()) {
309                             ThreadUtil.sleep(fessConfig.getIndexerWebfsCommitMarginTimeAsInteger().longValue());
310                             cleanupTime = -1;
311                         } else {
312                             processAccessResults(docList, accessResultList, arList);
313                             cleanupTime = cleanupAccessResults(accessResultList);
314                         }
315                         arList = getAccessResultList(cb, cleanupTime);
316                         hitCount = ((OpenSearchResultList<OpenSearchAccessResult>) arList).getTotalHits();
317                     }
318                     if (!docList.isEmpty()) {
319                         indexingHelper.sendDocuments(searchEngineClient, docList);
320                     }
321 
322                     synchronized (finishedSessionIdList) {
323                         if (sessionIdListSize != 0 && sessionIdListSize == finishedSessionIdList.size()) {
324                             cleanupFinishedSessionData();
325                         }
326                     }
327                     executeTime += systemHelper.getCurrentTimeAsLong() - updateTime;
328 
329                     if (logger.isDebugEnabled()) {
330                         logger.debug("Processed documents in IndexUpdater queue.");
331                     }
332 
333                     // reset count
334                     errorCount = 0;
335                 } catch (final Exception e) {
336                     if (errorCount > maxErrorCount) {
337                         throw e;
338                     }
339                     errorCount++;
340                     logger.warn("Failed to access AccessResult data. Retrying... (attempt={}/{})", errorCount, maxErrorCount, e);
341                 } finally {
342                     if (systemHelper.isForceStop()) {
343                         finishCrawling = true;
344                         if (logger.isDebugEnabled()) {
345                             logger.debug("Stopped indexUpdater.");
346                         }
347                     }
348                 }
349 
350                 if (emptyListCount >= maxEmptyListCount) {
351                     if (logger.isInfoEnabled()) {
352                         logger.info("Terminating indexUpdater. emptyListCount is over {}.", maxEmptyListCount);
353                     }
354                     // terminate crawling
355                     finishCrawling = true;
356                     forceStop();
357                     if (fessConfig.getIndexerThreadDumpEnabledAsBoolean()) {
358                         ThreadDumpUtil.printThreadDump();
359                     }
360                     org.codelibs.fess.exec.Crawler.addError("QueueTimeout");
361                 }
362 
363                 if (!ComponentUtil.available()) {
364                     logger.info("IndexUpdater is terminated.");
365                     forceStop();
366                     break;
367                 }
368             }
369 
370             if (logger.isDebugEnabled()) {
371                 logger.debug("Finished indexUpdater.");
372             }
373         } catch (final ContainerNotAvailableException e) {
374             if (logger.isDebugEnabled()) {
375                 logger.error("IndexUpdater is terminated.", e);
376             } else if (logger.isInfoEnabled()) {
377                 logger.info("IndexUpdater is terminated.");
378             }
379             forceStop();
380         } catch (final Throwable t) {
381             if (ComponentUtil.available()) {
382                 logger.error("IndexUpdater is terminated.", t);
383             } else if (logger.isDebugEnabled()) {
384                 logger.error("IndexUpdater is terminated.", t);
385                 org.codelibs.fess.exec.Crawler.addError(t.getClass().getSimpleName());
386             } else if (logger.isInfoEnabled()) {
387                 logger.info("IndexUpdater is terminated.");
388                 org.codelibs.fess.exec.Crawler.addError(t.getClass().getSimpleName());
389             }
390             forceStop();
391         } finally {
392             intervalControlHelper.setCrawlerRunning(true);
393         }
394 
395         if (logger.isInfoEnabled()) {
396             logger.info("[EXEC TIME] index update time: {}ms", executeTime);
397         }
398 
399     }
400 
401     /**
402      * Processes a list of access results and converts them into indexable documents.
403      * Each access result is transformed into a document map and added to the document list.
404      *
405      * @param docList the document list to add processed documents to
406      * @param accessResultList the list to track processed access results for cleanup
407      * @param arList the list of access results to process
408      */
409     private void processAccessResults(final DocList docList, final List<OpenSearchAccessResult> accessResultList,
410             final List<OpenSearchAccessResult> arList) {
411         final FessConfig fessConfig = ComponentUtil.getFessConfig();
412         final long maxDocumentRequestSize = Long.parseLong(fessConfig.getIndexerWebfsMaxDocumentRequestSize());
413         for (final OpenSearchAccessResult accessResult : arList) {
414             if (logger.isDebugEnabled()) {
415                 logger.debug("Indexing: url={}", accessResult.getUrl());
416             }
417             accessResult.setStatus(Constants.DONE_STATUS);
418             accessResultList.add(accessResult);
419 
420             if (accessResult.getHttpStatusCode() != 200) {
421                 // invalid page
422                 if (logger.isDebugEnabled()) {
423                     logger.debug("Skipped: httpStatusCode={}", accessResult.getHttpStatusCode());
424                 }
425                 continue;
426             }
427 
428             final long startTime = systemHelper.getCurrentTimeAsLong();
429             final AccessResultData<?> accessResultData = getAccessResultData(accessResult);
430             if (accessResultData != null) {
431                 accessResult.setAccessResultData(null);
432                 try {
433                     final Transformer transformer = ComponentUtil.getComponent(accessResultData.getTransformerName());
434                     if (transformer == null) {
435                         // no transformer
436                         logger.warn("Transformer not found: name={}, url={}", accessResultData.getTransformerName(), accessResult.getUrl());
437                         continue;
438                     }
439                     @SuppressWarnings("unchecked")
440                     final Map<String, Object> map = (Map<String, Object>) transformer.getData(accessResultData);
441                     if (map.isEmpty()) {
442                         // no transformer
443                         logger.warn("No data: url={}", accessResult.getUrl());
444                         continue;
445                     }
446 
447                     if (Constants.FALSE.equals(map.get(Constants.INDEXING_TARGET))) {
448                         if (logger.isDebugEnabled()) {
449                             logger.debug("Skipped indexing (not a target): url={}", accessResult.getUrl());
450                         }
451                         continue;
452                     }
453                     map.remove(Constants.INDEXING_TARGET);
454 
455                     updateDocument(map);
456 
457                     docList.add(ingest(accessResult, map));
458                     final long contentSize = indexingHelper.calculateDocumentSize(map);
459                     docList.addContentSize(contentSize);
460                     final long processingTime = systemHelper.getCurrentTimeAsLong() - startTime;
461                     docList.addProcessingTime(processingTime);
462                     if (logger.isDebugEnabled()) {
463                         logger.debug("Added the document({}, {}ms). The number of a document cache is {} (size: {}).",
464                                 MemoryUtil.byteCountToDisplaySize(contentSize), processingTime, docList.size(), docList.getContentSize());
465                     }
466 
467                     if (docList.getContentSize() >= maxDocumentRequestSize) {
468                         indexingHelper.sendDocuments(searchEngineClient, docList);
469                     }
470                     documentSize++;
471                     if (logger.isDebugEnabled()) {
472                         logger.debug("Added documents: count={}", documentSize);
473                     }
474                 } catch (final Exception e) {
475                     logger.warn("Failed to add document: url={}", accessResult.getUrl(), e);
476                 }
477             } else if (logger.isDebugEnabled()) {
478                 logger.debug("Skipped indexing (no content): url={}", accessResult.getUrl());
479             }
480 
481         }
482     }
483 
484     /**
485      * Retrieves the access result data from an OpenSearch access result.
486      * Handles exceptions that may occur during data retrieval.
487      *
488      * @param accessResult the access result to extract data from
489      * @return the access result data, or null if retrieval fails
490      */
491     private AccessResultData<?> getAccessResultData(final OpenSearchAccessResult accessResult) {
492         try {
493             return accessResult.getAccessResultData();
494         } catch (final Exception e) {
495             logger.warn("Failed to get data: url={}", accessResult.getUrl(), e);
496         }
497         return null;
498     }
499 
500     /**
501      * Processes a document through the ingest pipeline if an ingest factory is available.
502      * Applies all configured ingesters to transform and enrich the document data.
503      *
504      * @param accessResult the access result containing document metadata
505      * @param map the document data map to process
506      * @return the processed document map after applying all ingesters
507      */
508     protected Map<String, Object> ingest(final AccessResult<String> accessResult, final Map<String, Object> map) {
509         if (ingestFactory == null) {
510             return map;
511         }
512         Map<String, Object> target = map;
513         for (final Ingester ingester : ingestFactory.getIngesters()) {
514             try {
515                 target = ingester.process(target, accessResult);
516             } catch (final Exception e) {
517                 logger.warn("Failed to process Ingest[{}]", ingester.getClass().getSimpleName(), e);
518             }
519         }
520         return target;
521     }
522 
523     /**
524      * Updates a document with additional metadata and enhancements.
525      * Adds click counts, favorite counts, document boosting, and generates document ID.
526      * Also applies language-specific updates through the language helper.
527      *
528      * @param map the document data map to update with additional metadata
529      */
530     protected void updateDocument(final Map<String, Object> map) {
531         final FessConfig fessConfig = ComponentUtil.getFessConfig();
532 
533         if (fessConfig.getIndexerClickCountEnabledAsBoolean()) {
534             addClickCountField(map);
535         }
536 
537         if (fessConfig.getIndexerFavoriteCountEnabledAsBoolean()) {
538             addFavoriteCountField(map);
539         }
540 
541         float documentBoost = 0.0f;
542         for (final DocBoostMatcher docBoostMatcher : docBoostMatcherList) {
543             if (docBoostMatcher.match(map)) {
544                 documentBoost = docBoostMatcher.getValue(map);
545                 break;
546             }
547         }
548 
549         if (documentBoost > 0) {
550             addBoostValue(map, documentBoost);
551         }
552 
553         if (!map.containsKey(fessConfig.getIndexFieldDocId())) {
554             map.put(fessConfig.getIndexFieldDocId(), systemHelper.generateDocId(map));
555         }
556 
557         ComponentUtil.getLanguageHelper().updateDocument(map);
558     }
559 
560     /**
561      * Adds a boost value to the document for search relevance scoring.
562      * The boost value affects how highly the document will rank in search results.
563      *
564      * @param map the document data map to add the boost value to
565      * @param documentBoost the boost value to apply to the document
566      */
567     protected void addBoostValue(final Map<String, Object> map, final float documentBoost) {
568         final FessConfig fessConfig = ComponentUtil.getFessConfig();
569         map.put(fessConfig.getIndexFieldBoost(), documentBoost);
570         if (logger.isDebugEnabled()) {
571             logger.debug("Document boost applied: boost={}, url={}", documentBoost, map.get(fessConfig.getIndexFieldUrl()));
572         }
573     }
574 
575     /**
576      * Adds a click count field to the document based on search log data.
577      * The click count represents how many times users have clicked on this document in search results.
578      *
579      * @param doc the document data map to add the click count to
580      */
581     protected void addClickCountField(final Map<String, Object> doc) {
582         final FessConfig fessConfig = ComponentUtil.getFessConfig();
583         final String url = (String) doc.get(fessConfig.getIndexFieldUrl());
584         if (StringUtil.isNotBlank(url)) {
585             final SearchLogHelper searchLogHelper = ComponentUtil.getSearchLogHelper();
586             final int count = searchLogHelper.getClickCount(url);
587             doc.put(fessConfig.getIndexFieldClickCount(), count);
588             if (logger.isDebugEnabled()) {
589                 logger.debug("Click count: count={}, url={}", count, url);
590             }
591         }
592     }
593 
594     /**
595      * Adds a favorite count field to the document based on user favorite data.
596      * The favorite count represents how many users have marked this document as a favorite.
597      *
598      * @param map the document data map to add the favorite count to
599      */
600     protected void addFavoriteCountField(final Map<String, Object> map) {
601         final FessConfig fessConfig = ComponentUtil.getFessConfig();
602         final String url = (String) map.get(fessConfig.getIndexFieldUrl());
603         if (StringUtil.isNotBlank(url)) {
604             final SearchLogHelper searchLogHelper = ComponentUtil.getSearchLogHelper();
605             final long count = searchLogHelper.getFavoriteCount(url);
606             map.put(fessConfig.getIndexFieldFavoriteCount(), count);
607             if (logger.isDebugEnabled()) {
608                 logger.debug("Favorite count: count={}, url={}", count, url);
609             }
610         }
611     }
612 
613     /**
614      * Cleans up processed access results by updating their status in the data service.
615      * This marks the access results as processed and clears the list.
616      *
617      * @param accessResultList the list of access results to clean up
618      * @return the time taken for the cleanup operation in milliseconds, or -1 if no cleanup was needed
619      */
620     private long cleanupAccessResults(final List<OpenSearchAccessResult> accessResultList) {
621         if (!accessResultList.isEmpty()) {
622             final long execTime = systemHelper.getCurrentTimeAsLong();
623             final int size = accessResultList.size();
624             dataService.update(accessResultList);
625             accessResultList.clear();
626             final long time = systemHelper.getCurrentTimeAsLong() - execTime;
627             if (logger.isDebugEnabled()) {
628                 logger.debug("Updated access results: count={}, time={}ms", size, time);
629             }
630             return time;
631         }
632         return -1;
633     }
634 
635     /**
636      * Retrieves a list of access results from the data service for processing.
637      * Filters out results that are too recent based on commit margin time and manages crawler throttling.
638      *
639      * @param cb the consumer to customize the search request
640      * @param cleanupTime the time taken for the last cleanup operation
641      * @return the list of access results ready for processing
642      */
643     private List<OpenSearchAccessResult> getAccessResultList(final Consumer<SearchRequestBuilder> cb, final long cleanupTime) {
644         if (logger.isDebugEnabled()) {
645             logger.debug("Getting documents in IndexUpdater queue.");
646         }
647         final long execTime = systemHelper.getCurrentTimeAsLong();
648         final List<OpenSearchAccessResult> arList = ((OpenSearchDataService) dataService).getAccessResultList(cb);
649         final FessConfig fessConfig = ComponentUtil.getFessConfig();
650         if (!arList.isEmpty()) {
651             final long commitMarginTime = fessConfig.getIndexerWebfsCommitMarginTimeAsInteger().longValue();
652             for (final AccessResult<?> ar : arList.toArray(new AccessResult[arList.size()])) {
653                 if (ar.getCreateTime().longValue() > execTime - commitMarginTime) {
654                     arList.remove(ar);
655                 }
656             }
657         }
658         final long totalHits = ((OpenSearchResultList<OpenSearchAccessResult>) arList).getTotalHits();
659         if (logger.isInfoEnabled()) {
660             final StringBuilder buf = new StringBuilder(100);
661             buf.append("Processing ");
662             if (totalHits > 0) {
663                 buf.append(arList.size()).append('/').append(totalHits).append(" docs (Doc:{access ");
664             } else {
665                 buf.append("no docs in indexing queue (Doc:{access ");
666             }
667             buf.append(systemHelper.getCurrentTimeAsLong() - execTime).append("ms");
668             if (cleanupTime >= 0) {
669                 buf.append(", cleanup ").append(cleanupTime).append("ms");
670             }
671             buf.append("}, ");
672             buf.append(MemoryUtil.getMemoryUsageLog());
673             buf.append(')');
674             logger.info(buf.toString());
675         }
676         final long unprocessedDocumentSize = fessConfig.getIndexerUnprocessedDocumentSizeAsInteger().longValue();
677         final IntervalControlHelper intervalControlHelper = ComponentUtil.getIntervalControlHelper();
678         if (totalHits > unprocessedDocumentSize && intervalControlHelper.isCrawlerRunning()) {
679             if (logger.isInfoEnabled()) {
680                 logger.info("Stopped all crawler threads. Unprocessed documents: count={}, limit={}", totalHits, unprocessedDocumentSize);
681             }
682             intervalControlHelper.setCrawlerRunning(false);
683         }
684         return arList;
685     }
686 
687     /**
688      * Cleans up data for all finished crawler sessions.
689      * Deletes URL filters, URL queues, and access result data for each finished session.
690      */
691     private void cleanupFinishedSessionData() {
692         final long execTime = systemHelper.getCurrentTimeAsLong();
693         // cleanup
694         for (final String sessionId : finishedSessionIdList) {
695             final long execTime2 = systemHelper.getCurrentTimeAsLong();
696             if (logger.isDebugEnabled()) {
697                 logger.debug("Deleting document data: sessionId={}", sessionId);
698             }
699             deleteBySessionId(sessionId);
700             if (logger.isDebugEnabled()) {
701                 logger.debug("Deleted session data: sessionId={}, time={}ms", sessionId, systemHelper.getCurrentTimeAsLong() - execTime2);
702             }
703         }
704         finishedSessionIdList.clear();
705 
706         if (logger.isInfoEnabled()) {
707             logger.info("Deleted completed document data: time={}ms", systemHelper.getCurrentTimeAsLong() - execTime);
708         }
709     }
710 
711     /**
712      * Forces all crawlers to stop immediately.
713      * Sets the force stop flag and stops all active crawler instances.
714      */
715     private void forceStop() {
716         systemHelper.setForceStop(true);
717         if (crawlerList != null) {
718             for (final Crawler crawler : crawlerList) {
719                 crawler.stop();
720             }
721         }
722     }
723 
724     /**
725      * Gets the total execution time for index updates.
726      *
727      * @return the total execution time in milliseconds
728      */
729     public long getExecuteTime() {
730         return executeTime;
731     }
732 
733     /**
734      * Gets the list of crawler session IDs being processed.
735      *
736      * @return the list of session IDs
737      */
738     public List<String> getSessionIdList() {
739         return sessionIdList;
740     }
741 
742     /**
743      * Sets the list of crawler session IDs to process.
744      *
745      * @param sessionIdList the list of session IDs to set
746      */
747     public void setSessionIdList(final List<String> sessionIdList) {
748         this.sessionIdList = sessionIdList;
749     }
750 
751     /**
752      * Sets the flag indicating whether crawling should be finished.
753      *
754      * @param finishCrawling true if crawling should be finished, false otherwise
755      */
756     public void setFinishCrawling(final boolean finishCrawling) {
757         this.finishCrawling = finishCrawling;
758     }
759 
760     /**
761      * Gets the total number of documents processed.
762      *
763      * @return the total document count
764      */
765     public long getDocumentSize() {
766         return documentSize;
767     }
768 
769     /**
770      * Sets the uncaught exception handler for this IndexUpdater thread.
771      *
772      * @param eh the uncaught exception handler to set
773      */
774     @Override
775     public void setUncaughtExceptionHandler(final UncaughtExceptionHandler eh) {
776         super.setUncaughtExceptionHandler(eh);
777     }
778 
779     /**
780      * Sets the default uncaught exception handler for all threads.
781      *
782      * @param eh the default uncaught exception handler to set
783      */
784     public static void setDefaultUncaughtExceptionHandler(final UncaughtExceptionHandler eh) {
785         Thread.setDefaultUncaughtExceptionHandler(eh);
786     }
787 
788     /**
789      * Sets the maximum number of indexer errors allowed.
790      *
791      * @param maxIndexerErrorCount the maximum error count to set
792      */
793     public void setMaxIndexerErrorCount(final int maxIndexerErrorCount) {
794         this.maxIndexerErrorCount = maxIndexerErrorCount;
795     }
796 
797     /**
798      * Adds a document boost matcher rule for enhancing document relevance scores.
799      *
800      * @param rule the document boost matcher rule to add
801      */
802     public void addDocBoostMatcher(final DocBoostMatcher rule) {
803         docBoostMatcherList.add(rule);
804     }
805 
806     /**
807      * Sets the list of crawler instances that this updater will manage.
808      *
809      * @param crawlerList the list of crawlers to set
810      */
811     public void setCrawlerList(final List<Crawler> crawlerList) {
812         this.crawlerList = crawlerList;
813     }
814 }