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.ds.callback;
17  
18  import java.util.HashSet;
19  import java.util.Map;
20  import java.util.Set;
21  import java.util.concurrent.atomic.AtomicLong;
22  
23  import org.apache.logging.log4j.LogManager;
24  import org.apache.logging.log4j.Logger;
25  import org.codelibs.core.stream.StreamUtil;
26  import org.codelibs.fess.entity.DataStoreParams;
27  import org.codelibs.fess.exception.DataStoreException;
28  import org.codelibs.fess.helper.CrawlingInfoHelper;
29  import org.codelibs.fess.helper.IndexingHelper;
30  import org.codelibs.fess.helper.SearchLogHelper;
31  import org.codelibs.fess.helper.SystemHelper;
32  import org.codelibs.fess.ingest.IngestFactory;
33  import org.codelibs.fess.ingest.Ingester;
34  import org.codelibs.fess.mylasta.direction.FessConfig;
35  import org.codelibs.fess.opensearch.client.SearchEngineClient;
36  import org.codelibs.fess.util.ComponentUtil;
37  import org.codelibs.fess.util.DocList;
38  import org.codelibs.fess.util.DocumentUtil;
39  import org.codelibs.fess.util.MemoryUtil;
40  
41  import jakarta.annotation.PostConstruct;
42  
43  /**
44   * Implementation of IndexUpdateCallback for handling document indexing operations.
45   * This class manages the process of updating the search index with documents from
46   * data stores, including bulk operations, document transformation, and error handling.
47   */
48  public class IndexUpdateCallbackImpl implements IndexUpdateCallback {
49      private static final Logger logger = LogManager.getLogger(IndexUpdateCallbackImpl.class);
50  
51      /**
52       * Default constructor for index update callback implementation.
53       * Creates a new instance with default values.
54       */
55      public IndexUpdateCallbackImpl() {
56          // Default constructor
57      }
58  
59      /** Atomic counter for the number of documents processed */
60      protected AtomicLong documentSize = new AtomicLong(0);
61  
62      /** Total execution time for all operations */
63      protected volatile long executeTime = 0;
64  
65      /** List of documents waiting to be indexed */
66      protected final DocList docList = new DocList();
67  
68      /** Maximum size of document requests in bytes */
69      protected long maxDocumentRequestSize;
70  
71      /** Maximum number of documents to cache before indexing */
72      protected int maxDocumentCacheSize;
73  
74      /** Factory for creating ingesters to process documents */
75      private IngestFactory ingestFactory = null;
76  
77      /**
78       * Initializes the callback implementation after dependency injection.
79       * Sets up configuration values and initializes the ingest factory if available.
80       */
81      @PostConstruct
82      public void init() {
83          if (logger.isDebugEnabled()) {
84              logger.debug("Initializing {}", this.getClass().getSimpleName());
85          }
86          maxDocumentRequestSize = Long.parseLong(ComponentUtil.getFessConfig().getIndexerDataMaxDocumentRequestSize());
87          maxDocumentCacheSize = ComponentUtil.getFessConfig().getIndexerDataMaxDocumentCacheSizeAsInteger();
88          if (ComponentUtil.hasIngestFactory()) {
89              ingestFactory = ComponentUtil.getIngestFactory();
90          }
91      }
92  
93      /**
94       * Stores a document in the index after processing and validation.
95       * Handles document transformation, field addition, and batched indexing.
96       *
97       * @param paramMap the data store parameters
98       * @param dataMap the document data to store
99       * @throws DataStoreException if required fields are missing or other errors occur
100      */
101     @Override
102     public void store(final DataStoreParams paramMap, final Map<String, Object> dataMap) {
103         final SystemHelper systemHelper = ComponentUtil.getSystemHelper();
104         systemHelper.calibrateCpuLoad();
105 
106         final long startTime = systemHelper.getCurrentTimeAsLong();
107         final FessConfig fessConfig = ComponentUtil.getFessConfig();
108         final SearchEngineClient searchEngineClient = ComponentUtil.getSearchEngineClient();
109 
110         if (logger.isDebugEnabled()) {
111             logger.debug("Adding document: url={}", dataMap.get(fessConfig.getIndexFieldUrl()));
112         }
113 
114         //   required check
115         final Object urlObj = dataMap.get(fessConfig.getIndexFieldUrl());
116         if (urlObj == null) {
117             final Object configId = dataMap.get(fessConfig.getIndexFieldConfigId());
118             throw new DataStoreException("URL field is null in dataMap. Cannot index document without a URL. configId: " + configId);
119         }
120 
121         final IndexingHelper indexingHelper = ComponentUtil.getIndexingHelper();
122         final CrawlingInfoHelper crawlingInfoHelper = ComponentUtil.getCrawlingInfoHelper();
123         dataMap.put(fessConfig.getIndexFieldId(), crawlingInfoHelper.generateId(dataMap));
124 
125         final String url = dataMap.get(fessConfig.getIndexFieldUrl()).toString();
126 
127         if (fessConfig.getIndexerClickCountEnabledAsBoolean()) {
128             addClickCountField(dataMap, url, fessConfig.getIndexFieldClickCount());
129         }
130 
131         if (fessConfig.getIndexerFavoriteCountEnabledAsBoolean()) {
132             addFavoriteCountField(dataMap, url, fessConfig.getIndexFieldFavoriteCount());
133         }
134 
135         final Set<String> matchedLabelSet = ComponentUtil.getLabelTypeHelper().getMatchedLabelValueSet(url);
136         if (!matchedLabelSet.isEmpty()) {
137             final Set<String> newLabelSet = new HashSet<>();
138             final String[] oldLabels = DocumentUtil.getValue(dataMap, fessConfig.getIndexFieldLabel(), String[].class);
139             StreamUtil.stream(oldLabels).of(stream -> stream.forEach(newLabelSet::add));
140             matchedLabelSet.stream().forEach(newLabelSet::add);
141             dataMap.put(fessConfig.getIndexFieldLabel(), newLabelSet.toArray(new String[newLabelSet.size()]));
142         }
143 
144         if (!dataMap.containsKey(fessConfig.getIndexFieldDocId())) {
145             dataMap.put(fessConfig.getIndexFieldDocId(), systemHelper.generateDocId(dataMap));
146         }
147 
148         ComponentUtil.getLanguageHelper().updateDocument(dataMap);
149 
150         synchronized (docList) {
151             docList.add(ingest(paramMap, dataMap));
152             final long contentSize = indexingHelper.calculateDocumentSize(dataMap);
153             docList.addContentSize(contentSize);
154             final long processingTime = systemHelper.getCurrentTimeAsLong() - startTime;
155             docList.addProcessingTime(processingTime);
156             if (logger.isDebugEnabled()) {
157                 logger.debug("Added document (size={}, time={}ms). Document cache count: {}",
158                         MemoryUtil.byteCountToDisplaySize(contentSize), processingTime, docList.size());
159             }
160 
161             if (docList.getContentSize() >= maxDocumentRequestSize || docList.size() >= maxDocumentCacheSize) {
162                 indexingHelper.sendDocuments(searchEngineClient, docList);
163             }
164             executeTime += processingTime;
165         }
166 
167         documentSize.getAndIncrement();
168 
169         if (logger.isDebugEnabled()) {
170             logger.debug("Total documents added: {}", documentSize.get());
171         }
172 
173     }
174 
175     /**
176      * Processes a document through the ingest pipeline.
177      * Applies all available ingesters to transform the document data.
178      *
179      * @param paramMap the data store parameters
180      * @param dataMap the document data to process
181      * @return the processed document data
182      */
183     protected Map<String, Object> ingest(final DataStoreParams paramMap, final Map<String, Object> dataMap) {
184         if (ingestFactory == null) {
185             return dataMap;
186         }
187         Map<String, Object> target = dataMap;
188         for (final Ingester ingester : ingestFactory.getIngesters()) {
189             try {
190                 target = ingester.process(target, paramMap);
191             } catch (final Exception e) {
192                 logger.warn("[{}] Failed to process ingester", ingester.getClass().getSimpleName(), e);
193             }
194         }
195         return target;
196     }
197 
198     /**
199      * Commits any remaining documents in the cache to the index.
200      * This method ensures all pending documents are processed.
201      */
202     @Override
203     public void commit() {
204         synchronized (docList) {
205             if (!docList.isEmpty()) {
206                 final IndexingHelper indexingHelper = ComponentUtil.getIndexingHelper();
207                 final SearchEngineClient searchEngineClient = ComponentUtil.getSearchEngineClient();
208                 indexingHelper.sendDocuments(searchEngineClient, docList);
209             }
210         }
211     }
212 
213     /**
214      * Adds click count information to the document.
215      *
216      * @param doc the document to update
217      * @param url the URL to get click count for
218      * @param clickCountField the field name to store click count
219      */
220     protected void addClickCountField(final Map<String, Object> doc, final String url, final String clickCountField) {
221         final SearchLogHelper searchLogHelper = ComponentUtil.getSearchLogHelper();
222         final int count = searchLogHelper.getClickCount(url);
223         doc.put(clickCountField, count);
224         if (logger.isDebugEnabled()) {
225             logger.debug("Updated click count: count={}, url={}", count, url);
226         }
227     }
228 
229     /**
230      * Adds favorite count information to the document.
231      *
232      * @param doc the document to update
233      * @param url the URL to get favorite count for
234      * @param favoriteCountField the field name to store favorite count
235      */
236     protected void addFavoriteCountField(final Map<String, Object> doc, final String url, final String favoriteCountField) {
237         final SearchLogHelper searchLogHelper = ComponentUtil.getSearchLogHelper();
238         final long count = searchLogHelper.getFavoriteCount(url);
239         doc.put(favoriteCountField, count);
240         if (logger.isDebugEnabled()) {
241             logger.debug("Updated favorite count: count={}, url={}", count, url);
242         }
243     }
244 
245     /**
246      * Returns the total number of documents processed.
247      *
248      * @return the number of documents processed
249      */
250     @Override
251     public long getDocumentSize() {
252         return documentSize.get();
253     }
254 
255     /**
256      * Returns the total execution time for all operations.
257      *
258      * @return the total execution time in milliseconds
259      */
260     @Override
261     public long getExecuteTime() {
262         return executeTime;
263     }
264 
265 }