1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16 package org.codelibs.fess.helper;
17
18 import static org.codelibs.core.stream.StreamUtil.split;
19 import static org.codelibs.core.stream.StreamUtil.stream;
20
21 import java.time.LocalDateTime;
22 import java.time.ZoneId;
23 import java.time.format.DateTimeFormatter;
24 import java.util.ArrayList;
25 import java.util.Arrays;
26 import java.util.Collections;
27 import java.util.HashMap;
28 import java.util.HashSet;
29 import java.util.List;
30 import java.util.Map;
31 import java.util.Set;
32 import java.util.function.Consumer;
33
34 import org.apache.logging.log4j.LogManager;
35 import org.apache.logging.log4j.Logger;
36 import org.codelibs.core.lang.StringUtil;
37 import org.codelibs.core.lang.ThreadUtil;
38 import org.codelibs.core.misc.Pair;
39 import org.codelibs.fess.Constants;
40 import org.codelibs.fess.mylasta.direction.FessConfig;
41 import org.codelibs.fess.opensearch.client.SearchEngineClient;
42 import org.codelibs.fess.opensearch.config.exbhv.BadWordBhv;
43 import org.codelibs.fess.opensearch.config.exbhv.ElevateWordBhv;
44 import org.codelibs.fess.opensearch.config.exentity.BadWord;
45 import org.codelibs.fess.opensearch.config.exentity.ElevateWord;
46 import org.codelibs.fess.opensearch.log.exbhv.SearchLogBhv;
47 import org.codelibs.fess.opensearch.log.exentity.SearchLog;
48 import org.codelibs.fess.suggest.Suggester;
49 import org.codelibs.fess.suggest.constants.FieldNames;
50 import org.codelibs.fess.suggest.entity.SuggestItem;
51 import org.codelibs.fess.suggest.exception.SuggestSettingsException;
52 import org.codelibs.fess.suggest.index.SuggestDeleteResponse;
53 import org.codelibs.fess.suggest.index.contents.document.ESSourceReader;
54 import org.codelibs.fess.suggest.settings.SuggestSettings;
55 import org.codelibs.fess.suggest.settings.SuggestSettingsBuilder;
56 import org.codelibs.fess.suggest.util.SuggestUtil;
57 import org.codelibs.fess.util.ComponentUtil;
58 import org.opensearch.common.lucene.search.function.CombineFunction;
59 import org.opensearch.index.query.BoolQueryBuilder;
60 import org.opensearch.index.query.QueryBuilders;
61 import org.opensearch.index.query.functionscore.FunctionScoreQueryBuilder;
62 import org.opensearch.index.query.functionscore.ScoreFunctionBuilders;
63 import org.opensearch.search.sort.SortBuilders;
64
65 import jakarta.annotation.PostConstruct;
66
67
68
69
70
71
72 public class SuggestHelper {
73
74
75
76 public SuggestHelper() {
77
78 }
79
80 private static final Logger logger = LogManager.getLogger(SuggestHelper.class);
81
82
83 protected static final String TEXT_SEP = " ";
84
85
86 protected Suggester suggester;
87
88
89 protected FessConfig fessConfig;
90
91
92 protected final Set<String> contentFieldNameSet = new HashSet<>();
93
94
95 protected final Set<String> tagFieldNameSet = new HashSet<>();
96
97
98 protected final Set<String> roleFieldNameSet = new HashSet<>();
99
100
101 protected List<String> contentFieldList;
102
103
104 protected PopularWordHelper popularWordHelper = null;
105
106
107 protected long searchStoreInterval = 1;
108
109
110
111
112
113
114 @PostConstruct
115 public void init() {
116 if (logger.isDebugEnabled()) {
117 logger.debug("Initializing {}", this.getClass().getSimpleName());
118 }
119 fessConfig = ComponentUtil.getFessConfig();
120 split(fessConfig.getSuggestFieldContents(), ",")
121 .of(stream -> stream.filter(StringUtil::isNotBlank).forEach(contentFieldNameSet::add));
122 split(fessConfig.getSuggestFieldTags(), ",").of(stream -> stream.filter(StringUtil::isNotBlank).forEach(tagFieldNameSet::add));
123 split(fessConfig.getSuggestFieldRoles(), ",").of(stream -> stream.filter(StringUtil::isNotBlank).forEach(roleFieldNameSet::add));
124 contentFieldList = Arrays.asList(stream(fessConfig.getSuggestFieldContents()).get(stream -> stream.toArray(n -> new String[n])));
125
126 final SearchEngineClient searchEngineClient = ComponentUtil.getSearchEngineClient();
127 searchEngineClient.admin()
128 .cluster()
129 .prepareHealth()
130 .setWaitForYellowStatus()
131 .execute()
132 .actionGet(fessConfig.getIndexHealthTimeout());
133
134 final SuggestSettingsBuilder settingsBuilder = SuggestSettings.builder();
135 settingsBuilder.addInitialSettings("search_engine.type", fessConfig.getFesenType());
136 settingsBuilder.bulkTimeout(fessConfig.getIndexBulkTimeout());
137 settingsBuilder.clusterTimeout(fessConfig.getIndexHealthTimeout());
138 settingsBuilder.indexTimeout(fessConfig.getIndexIndexTimeout());
139 settingsBuilder.indicesTimeout(fessConfig.getIndexIndicesTimeout());
140 settingsBuilder.searchTimeout(fessConfig.getIndexSearchTimeout());
141 settingsBuilder.setSettingsIndexName(fessConfig.getIndexDocumentSuggestIndex() + "_suggest");
142 suggester = Suggester.builder().settings(settingsBuilder).build(searchEngineClient, fessConfig.getIndexDocumentSuggestIndex());
143 if (ComponentUtil.hasPopularWordHelper()) {
144 popularWordHelper = ComponentUtil.getPopularWordHelper();
145 }
146 try {
147 suggester.settings().array().delete(SuggestSettings.DefaultKeys.SUPPORTED_FIELDS);
148 split(fessConfig.getSuggestFieldIndexContents(), ",").of(stream -> stream.filter(StringUtil::isNotBlank).forEach(field -> {
149 try {
150 suggester.settings().array().add(SuggestSettings.DefaultKeys.SUPPORTED_FIELDS, field);
151 } catch (final SuggestSettingsException e) {
152 logger.warn("Failed to add suggest field: field={}", field, e);
153 }
154 }));
155 suggester.createIndexIfNothing();
156 } catch (final Exception e) {
157 logger.warn("Failed to initialize Suggester.", e);
158 }
159 }
160
161
162
163
164
165
166 public Suggester suggester() {
167 return suggester;
168 }
169
170
171
172
173 public void storeSearchLog() {
174 final SearchLogBhv searchLogBhv = ComponentUtil.getComponent(SearchLogBhv.class);
175
176 searchLogBhv.selectBulk(cb -> {
177 final String from = LocalDateTime.now().minusDays(fessConfig.getPurgeSuggestSearchLogDay()).format(DateTimeFormatter.ISO_DATE);
178 cb.query().addQuery(QueryBuilders.rangeQuery("requestedAt").gte(from));
179 cb.query().addOrderBy_RequestedAt_Asc();
180 cb.specify().columnAccessType();
181 cb.specify().columnUserSessionId();
182 cb.specify().columnHitCount();
183 cb.specify().columnClientIp();
184 cb.specify().columnSearchWord();
185 cb.specify().columnRequestedAt();
186 cb.specify().columnHitCount();
187 cb.specify().columnVirtualHost();
188 cb.specify().columnLanguages();
189 cb.specify().columnRoles();
190 cb.specify().doColumn("searchField.*");
191 }, this::indexFromSearchLog);
192 }
193
194
195
196
197
198
199 public void indexFromSearchLog(final List<SearchLog> searchLogList) {
200 final Map<String, LocalDateTime> duplicateSessionMap = new HashMap<>();
201 searchLogList.stream().forEach(searchLog -> {
202 if (searchLog.getHitCount() == null
203 || searchLog.getHitCount().longValue() < fessConfig.getSuggestMinHitCountAsInteger().longValue()) {
204 return;
205 }
206
207 final String sessionId;
208 if (searchLog.getUserSessionId() != null) {
209 sessionId = searchLog.getUserSessionId();
210 } else if (Constants.SEARCH_LOG_ACCESS_TYPE_WEB.equals(searchLog.getAccessType())) {
211 sessionId = searchLog.getClientIp();
212 } else {
213 sessionId = searchLog.getClientIp() + '_' + searchLog.getSearchWord();
214 }
215
216 final LocalDateTime requestedAt = searchLog.getRequestedAt();
217 if (sessionId == null || duplicateSessionMap.containsKey(sessionId)
218 && duplicateSessionMap.get(sessionId).plusMinutes(searchStoreInterval).isAfter(requestedAt)) {
219 return;
220 }
221
222 final StringBuilder sb = new StringBuilder(100);
223 final List<String> fields = new ArrayList<>();
224 final List<String> tags = new ArrayList<>();
225 final List<String> roles = new ArrayList<>();
226
227 for (final Pair<String, String> searchFieldLog : searchLog.getSearchFieldLogList()) {
228 final String name = searchFieldLog.getFirst();
229 if (contentFieldNameSet.contains(name)) {
230 if (sb.length() > 0) {
231 sb.append(TEXT_SEP);
232 }
233 sb.append(searchFieldLog.getSecond());
234 fields.add(name);
235 } else if (tagFieldNameSet.contains(name)) {
236 tags.add(searchFieldLog.getSecond());
237 } else if (roleFieldNameSet.contains(name)) {
238 roles.add(searchFieldLog.getSecond());
239 }
240 }
241
242 final String virtualHost = searchLog.getVirtualHost();
243 if (virtualHost != null) {
244 tags.add(virtualHost);
245 }
246
247 if (sb.length() > 0) {
248 final String[] langs = searchLog.getLanguages() == null ? new String[] {} : searchLog.getLanguages().split(",");
249 stream(searchLog.getRoles()).of(stream -> stream.forEach(role -> roles.add(role)));
250 if (fessConfig.isValidSearchLogPermissions(roles.toArray(new String[roles.size()]))) {
251 suggester.indexer()
252 .indexFromSearchWord(sb.toString(), fields.toArray(new String[fields.size()]),
253 tags.toArray(new String[tags.size()]), roles.toArray(new String[roles.size()]), 1, langs);
254 duplicateSessionMap.put(sessionId, requestedAt);
255 }
256 }
257 });
258 refresh();
259 }
260
261
262
263
264
265
266
267 public void indexFromDocuments(final Consumer<Boolean> success, final Consumer<Throwable> error) {
268 final FessConfig fessConfig = ComponentUtil.getFessConfig();
269 final long interval = fessConfig.getSuggestUpdateRequestIntervalAsInteger().longValue();
270 final int docPerReq = fessConfig.getSuggestUpdateDocPerRequestAsInteger();
271 final SystemHelper systemHelper = ComponentUtil.getSystemHelper();
272 suggester.indexer().indexFromDocument(() -> {
273 final ESSourceReader reader = new ESSourceReader(ComponentUtil.getSearchEngineClient(), suggester.settings(),
274 fessConfig.getIndexDocumentSearchIndex());
275 reader.setScrollSize(fessConfig.getSuggestSourceReaderScrollSizeAsInteger());
276 reader.setLimitDocNumPercentage(fessConfig.getSuggestUpdateContentsLimitNumPercentage());
277 reader.setLimitNumber(fessConfig.getSuggestUpdateContentsLimitNumAsInteger());
278 reader.setLimitOfDocumentSize(fessConfig.getSuggestUpdateContentsLimitDocSizeAsInteger());
279
280 final List<FunctionScoreQueryBuilder.FilterFunctionBuilder> flist = new ArrayList<>();
281 flist.add(new FunctionScoreQueryBuilder.FilterFunctionBuilder(ScoreFunctionBuilders.randomFunction()
282 .seed(ComponentUtil.getSystemHelper().getCurrentTimeAsLong())
283 .setField(fessConfig.getIndexFieldDocId())));
284 reader.setQuery(QueryBuilders
285 .functionScoreQuery(QueryBuilders.matchAllQuery(),
286 flist.toArray(new FunctionScoreQueryBuilder.FilterFunctionBuilder[flist.size()]))
287 .boostMode(CombineFunction.MULTIPLY));
288 reader.addSort(SortBuilders.fieldSort(fessConfig.getIndexFieldClickCount()));
289 reader.addSort(SortBuilders.scoreSort());
290 return reader;
291 }, docPerReq, () -> {
292 systemHelper.calibrateCpuLoad();
293 ThreadUtil.sleep(interval);
294 }).then(response -> {
295 refresh();
296 success.accept(true);
297 }).error(t -> error.accept(t));
298 }
299
300
301
302
303
304
305 public void purgeDocumentSuggest(final LocalDateTime time) {
306 final BoolQueryBuilder boolQueryBuilder = QueryBuilders.boolQuery();
307 boolQueryBuilder
308 .must(QueryBuilders.rangeQuery(FieldNames.TIMESTAMP).lt(time.atZone(ZoneId.systemDefault()).toInstant().toEpochMilli()));
309
310 boolQueryBuilder.must(QueryBuilders.termQuery(FieldNames.KINDS, SuggestItem.Kind.DOCUMENT.toString()));
311 boolQueryBuilder.mustNot(QueryBuilders.termQuery(FieldNames.KINDS, SuggestItem.Kind.QUERY.toString()));
312 boolQueryBuilder.mustNot(QueryBuilders.termQuery(FieldNames.KINDS, SuggestItem.Kind.USER.toString()));
313
314 SuggestUtil.deleteByQuery(ComponentUtil.getSearchEngineClient(), suggester.settings(), suggester.getIndex(), boolQueryBuilder);
315 }
316
317
318
319
320
321
322 public void purgeSearchlogSuggest(final LocalDateTime time) {
323 final BoolQueryBuilder boolQueryBuilder = QueryBuilders.boolQuery();
324 boolQueryBuilder
325 .must(QueryBuilders.rangeQuery(FieldNames.TIMESTAMP).lt(time.atZone(ZoneId.systemDefault()).toInstant().toEpochMilli()));
326
327 boolQueryBuilder.mustNot(QueryBuilders.termQuery(FieldNames.KINDS, SuggestItem.Kind.DOCUMENT.toString()));
328 boolQueryBuilder.must(QueryBuilders.termQuery(FieldNames.KINDS, SuggestItem.Kind.QUERY.toString()));
329 boolQueryBuilder.mustNot(QueryBuilders.termQuery(FieldNames.KINDS, SuggestItem.Kind.USER.toString()));
330
331 SuggestUtil.deleteByQuery(ComponentUtil.getSearchEngineClient(), suggester.settings(), suggester.getIndex(), boolQueryBuilder);
332 }
333
334
335
336
337
338
339 public long getAllWordsNum() {
340 return suggester.getAllWordsNum();
341 }
342
343
344
345
346
347
348 public long getDocumentWordsNum() {
349 return suggester.getDocumentWordsNum();
350 }
351
352
353
354
355
356
357 public long getQueryWordsNum() {
358 return suggester.getQueryWordsNum();
359 }
360
361
362
363
364
365
366 public boolean deleteAllWords() {
367 final SuggestDeleteResponse response = suggester.indexer().deleteAll();
368 if (response.hasError()) {
369 logger.warn("Failed to delete all words.", response.getErrors().get(0));
370 return false;
371 }
372 refresh();
373 return true;
374 }
375
376
377
378
379
380
381 public boolean deleteDocumentWords() {
382 final SuggestDeleteResponse response = suggester.indexer().deleteDocumentWords();
383 if (response.hasError()) {
384 logger.warn("Failed to delete document words.", response.getErrors().get(0));
385 return false;
386 }
387 refresh();
388 return true;
389 }
390
391
392
393
394
395
396 public boolean deleteQueryWords() {
397 final SuggestDeleteResponse response = suggester.indexer().deleteQueryWords();
398 if (response.hasError()) {
399 logger.warn("Failed to delete query words.", response.getErrors().get(0));
400 return false;
401 }
402 refresh();
403 return true;
404 }
405
406
407
408
409
410
411 public void storeAllElevateWords(final boolean apply) {
412 deleteAllElevateWord(apply);
413
414 final List<ElevateWord> list = ComponentUtil.getComponent(ElevateWordBhv.class).selectList(cb -> {
415 cb.query().matchAll();
416 cb.fetchFirst(ComponentUtil.getFessConfig().getPageElevateWordMaxFetchSizeAsInteger());
417 });
418
419 for (final ElevateWord elevateWord : list) {
420 addElevateWord(elevateWord.getSuggestWord(), elevateWord.getReading(), elevateWord.getLabelTypeValues(),
421 elevateWord.getPermissions(), elevateWord.getBoost(), apply);
422 }
423 refresh();
424 }
425
426
427
428
429
430
431 public void deleteAllElevateWord(final boolean apply) {
432 final List<ElevateWord> list = ComponentUtil.getComponent(ElevateWordBhv.class).selectList(cb -> {
433 cb.query().matchAll();
434 cb.fetchFirst(ComponentUtil.getFessConfig().getPageElevateWordMaxFetchSizeAsInteger());
435 });
436
437 for (final ElevateWord elevateWord : list) {
438 suggester.indexer().deleteElevateWord(elevateWord.getSuggestWord(), apply);
439 }
440 refresh();
441 }
442
443
444
445
446
447
448
449 public void deleteElevateWord(final String word, final boolean apply) {
450 suggester.indexer().deleteElevateWord(word, apply);
451 refresh();
452 }
453
454
455
456
457
458
459
460
461
462
463
464 public void addElevateWord(final String word, final String reading, final String[] tags, final String[] permissions, final Float boost,
465 final boolean apply) {
466 final String[] readings;
467 if (StringUtil.isBlank(reading)) {
468 readings = word.replace(" ", TEXT_SEP).replaceAll(TEXT_SEP + "+", TEXT_SEP).split(TEXT_SEP);
469 } else {
470 readings = reading.replace(" ", TEXT_SEP).replaceAll(TEXT_SEP + "+", TEXT_SEP).split(TEXT_SEP);
471 }
472
473 final List<String> labelList = new ArrayList<>();
474 if (tags != null) {
475 Collections.addAll(labelList, tags);
476 }
477 final List<String> roleList = new ArrayList<>();
478 if (permissions != null) {
479 Collections.addAll(roleList, permissions);
480 }
481
482 suggester.indexer()
483 .addElevateWord(new org.codelibs.fess.suggest.entity.ElevateWord(word, boost, Arrays.asList(readings), contentFieldList,
484 labelList, roleList), apply);
485
486 refresh();
487 }
488
489
490
491
492 protected void deleteAllBadWords() {
493 suggester.settings().badword().deleteAll();
494 }
495
496
497
498
499
500
501 public void storeAllBadWords(final boolean apply) {
502 deleteAllBadWords();
503 final List<BadWord> list = ComponentUtil.getComponent(BadWordBhv.class).selectList(cb -> {
504 cb.query().matchAll();
505 cb.fetchFirst(ComponentUtil.getFessConfig().getPageBadWordMaxFetchSizeAsInteger());
506 });
507 for (final BadWord badWord : list) {
508 final String word = badWord.getSuggestWord();
509 suggester.indexer().addBadWord(word, apply);
510 }
511 refresh();
512 }
513
514
515
516
517
518
519
520 public void addBadWord(final String badWord, final boolean apply) {
521 suggester.indexer().addBadWord(badWord, apply);
522 refresh();
523 }
524
525
526
527
528
529
530 public void deleteBadWord(final String badWord) {
531 suggester.indexer().deleteBadWord(badWord);
532 refresh();
533 }
534
535
536
537
538 public synchronized void refresh() {
539 suggester.refresh();
540 if (popularWordHelper != null) {
541 popularWordHelper.clearCache();
542 }
543 }
544
545
546
547
548
549
550 public void setSearchStoreInterval(final long searchStoreInterval) {
551 this.searchStoreInterval = searchStoreInterval;
552 }
553 }