1
2
3
4
5
6
7
8
9
10
11
12
13
14
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
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82 public class IndexUpdater extends Thread {
83
84 private static final Logger logger = LogManager.getLogger(IndexUpdater.class);
85
86
87 protected List<String> sessionIdList;
88
89
90 @Resource
91 protected SearchEngineClient searchEngineClient;
92
93
94 @Resource
95 protected DataService<OpenSearchAccessResult> dataService;
96
97
98 @Resource
99 protected UrlQueueService<OpenSearchUrlQueue> urlQueueService;
100
101
102 @Resource
103 protected UrlFilterService urlFilterService;
104
105
106 @Resource
107 protected ClickLogBhv clickLogBhv;
108
109
110 @Resource
111 protected FavoriteLogBhv favoriteLogBhv;
112
113
114 @Resource
115 protected SystemHelper systemHelper;
116
117
118 @Resource
119 protected IndexingHelper indexingHelper;
120
121
122 protected boolean finishCrawling = false;
123
124
125 protected long executeTime;
126
127
128 protected long documentSize;
129
130
131 protected int maxIndexerErrorCount = 0;
132
133
134 protected int maxErrorCount = 2;
135
136
137 protected List<String> finishedSessionIdList = new ArrayList<>();
138
139
140 private final List<DocBoostMatcher> docBoostMatcherList = new ArrayList<>();
141
142
143 private List<Crawler> crawlerList;
144
145
146 private IngestFactory ingestFactory = null;
147
148
149
150
151
152 public IndexUpdater() {
153 super();
154 }
155
156
157
158
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
172
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
186
187
188
189
190 public void addFinishedSessionId(final String sessionId) {
191 synchronized (finishedSessionIdList) {
192 finishedSessionIdList.add(sessionId);
193 }
194 }
195
196
197
198
199
200
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
222
223
224
225
226
227
228
229
230
231
232
233
234
235
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
286 ThreadUtil.sleep(interval);
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;
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
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
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
403
404
405
406
407
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
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
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
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
486
487
488
489
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
502
503
504
505
506
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
525
526
527
528
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
562
563
564
565
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
577
578
579
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
596
597
598
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
615
616
617
618
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
637
638
639
640
641
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
689
690
691 private void cleanupFinishedSessionData() {
692 final long execTime = systemHelper.getCurrentTimeAsLong();
693
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
713
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
726
727
728
729 public long getExecuteTime() {
730 return executeTime;
731 }
732
733
734
735
736
737
738 public List<String> getSessionIdList() {
739 return sessionIdList;
740 }
741
742
743
744
745
746
747 public void setSessionIdList(final List<String> sessionIdList) {
748 this.sessionIdList = sessionIdList;
749 }
750
751
752
753
754
755
756 public void setFinishCrawling(final boolean finishCrawling) {
757 this.finishCrawling = finishCrawling;
758 }
759
760
761
762
763
764
765 public long getDocumentSize() {
766 return documentSize;
767 }
768
769
770
771
772
773
774 @Override
775 public void setUncaughtExceptionHandler(final UncaughtExceptionHandler eh) {
776 super.setUncaughtExceptionHandler(eh);
777 }
778
779
780
781
782
783
784 public static void setDefaultUncaughtExceptionHandler(final UncaughtExceptionHandler eh) {
785 Thread.setDefaultUncaughtExceptionHandler(eh);
786 }
787
788
789
790
791
792
793 public void setMaxIndexerErrorCount(final int maxIndexerErrorCount) {
794 this.maxIndexerErrorCount = maxIndexerErrorCount;
795 }
796
797
798
799
800
801
802 public void addDocBoostMatcher(final DocBoostMatcher rule) {
803 docBoostMatcherList.add(rule);
804 }
805
806
807
808
809
810
811 public void setCrawlerList(final List<Crawler> crawlerList) {
812 this.crawlerList = crawlerList;
813 }
814 }