001
014
015 package com.liferay.portal.search.lucene;
016
017 import com.browseengine.bobo.api.BoboBrowser;
018 import com.browseengine.bobo.api.BoboIndexReader;
019 import com.browseengine.bobo.api.BoboSubBrowser;
020 import com.browseengine.bobo.api.Browsable;
021 import com.browseengine.bobo.api.BrowseHit;
022 import com.browseengine.bobo.api.BrowseRequest;
023 import com.browseengine.bobo.api.BrowseResult;
024 import com.browseengine.bobo.api.FacetAccessible;
025 import com.browseengine.bobo.api.FacetSpec;
026 import com.browseengine.bobo.api.FacetSpec.FacetSortSpec;
027 import com.browseengine.bobo.facets.FacetHandler;
028 import com.browseengine.bobo.facets.FacetHandler.TermCountSize;
029 import com.browseengine.bobo.facets.impl.MultiValueFacetHandler;
030 import com.browseengine.bobo.facets.impl.RangeFacetHandler;
031 import com.browseengine.bobo.facets.impl.SimpleFacetHandler;
032
033 import com.liferay.portal.kernel.dao.orm.QueryUtil;
034 import com.liferay.portal.kernel.dao.search.SearchPaginationUtil;
035 import com.liferay.portal.kernel.json.JSONArray;
036 import com.liferay.portal.kernel.json.JSONObject;
037 import com.liferay.portal.kernel.log.Log;
038 import com.liferay.portal.kernel.log.LogFactoryUtil;
039 import com.liferay.portal.kernel.search.BaseIndexSearcher;
040 import com.liferay.portal.kernel.search.Document;
041 import com.liferay.portal.kernel.search.DocumentImpl;
042 import com.liferay.portal.kernel.search.Field;
043 import com.liferay.portal.kernel.search.Hits;
044 import com.liferay.portal.kernel.search.HitsImpl;
045 import com.liferay.portal.kernel.search.ParseException;
046 import com.liferay.portal.kernel.search.Query;
047 import com.liferay.portal.kernel.search.QueryConfig;
048 import com.liferay.portal.kernel.search.QueryTranslatorUtil;
049 import com.liferay.portal.kernel.search.SearchContext;
050 import com.liferay.portal.kernel.search.SearchException;
051 import com.liferay.portal.kernel.search.Sort;
052 import com.liferay.portal.kernel.search.facet.Facet;
053 import com.liferay.portal.kernel.search.facet.MultiValueFacet;
054 import com.liferay.portal.kernel.search.facet.RangeFacet;
055 import com.liferay.portal.kernel.search.facet.SimpleFacet;
056 import com.liferay.portal.kernel.search.facet.collector.FacetCollector;
057 import com.liferay.portal.kernel.search.facet.config.FacetConfiguration;
058 import com.liferay.portal.kernel.util.ArrayUtil;
059 import com.liferay.portal.kernel.util.ReflectionUtil;
060 import com.liferay.portal.kernel.util.StringPool;
061 import com.liferay.portal.kernel.util.StringUtil;
062 import com.liferay.portal.kernel.util.Time;
063 import com.liferay.portal.kernel.util.Validator;
064 import com.liferay.portal.search.BoboFacetCollector;
065 import com.liferay.portal.util.PropsValues;
066
067 import java.io.IOException;
068
069 import java.util.ArrayList;
070 import java.util.HashSet;
071 import java.util.List;
072 import java.util.Locale;
073 import java.util.Map;
074 import java.util.Set;
075
076 import org.apache.lucene.document.NumericField;
077 import org.apache.lucene.index.IndexReader;
078 import org.apache.lucene.search.BooleanQuery;
079 import org.apache.lucene.search.Explanation;
080 import org.apache.lucene.search.IndexSearcher;
081 import org.apache.lucene.search.ScoreDoc;
082 import org.apache.lucene.search.SortField;
083 import org.apache.lucene.search.TopFieldDocs;
084 import org.apache.lucene.search.highlight.Formatter;
085 import org.apache.lucene.search.highlight.TokenGroup;
086
087
090 public class LuceneIndexSearcher extends BaseIndexSearcher {
091
092 @Override
093 public Hits search(SearchContext searchContext, Query query)
094 throws SearchException {
095
096 if (_log.isDebugEnabled()) {
097 _log.debug("Query " + query);
098 }
099
100 Hits hits = null;
101
102 IndexSearcher indexSearcher = null;
103 Map<String, Facet> facets = null;
104 BoboBrowser boboBrowser = null;
105 BrowseRequest browseRequest = null;
106
107 try {
108 indexSearcher = LuceneHelperUtil.getIndexSearcher(
109 searchContext.getCompanyId());
110
111 List<FacetHandler<?>> facetHandlers =
112 new ArrayList<FacetHandler<?>>();
113
114 facets = searchContext.getFacets();
115
116 for (Facet facet : facets.values()) {
117 if (facet.isStatic()) {
118 continue;
119 }
120
121 FacetConfiguration facetConfiguration =
122 facet.getFacetConfiguration();
123
124 if (facet instanceof MultiValueFacet) {
125 MultiValueFacetHandler multiValueFacetHandler =
126 new MultiValueFacetHandler(
127 facetConfiguration.getFieldName(),
128 facetConfiguration.getFieldName());
129
130 JSONObject dataJSONObject = facetConfiguration.getData();
131
132 if (dataJSONObject.has("maxTerms")) {
133 multiValueFacetHandler.setMaxItems(
134 dataJSONObject.getInt("maxTerms"));
135 }
136
137 facetHandlers.add(multiValueFacetHandler);
138 }
139 else if (facet instanceof RangeFacet) {
140 List<String> ranges = new ArrayList<String>();
141
142 JSONObject dataJSONObject = facetConfiguration.getData();
143
144 JSONArray rangesJSONArray = dataJSONObject.getJSONArray(
145 "ranges");
146
147 if (rangesJSONArray != null) {
148 for (int i = 0; i < rangesJSONArray.length(); i++) {
149 JSONObject rangeJSONObject =
150 rangesJSONArray.getJSONObject(i);
151
152 ranges.add(rangeJSONObject.getString("range"));
153 }
154 }
155
156 RangeFacetHandler rangeFacetHandler = new RangeFacetHandler(
157 facetConfiguration.getFieldName(),
158 facetConfiguration.getFieldName(), ranges);
159
160 rangeFacetHandler.setTermCountSize(TermCountSize.large);
161
162 facetHandlers.add(rangeFacetHandler);
163 }
164 else if (facet instanceof SimpleFacet) {
165 SimpleFacetHandler simpleFacetHandler =
166 new SimpleFacetHandler(
167 facetConfiguration.getFieldName(),
168 facetConfiguration.getFieldName());
169
170 facetHandlers.add(simpleFacetHandler);
171 }
172 }
173
174 BoboIndexReader boboIndexReader = BoboIndexReader.getInstance(
175 indexSearcher.getIndexReader(), facetHandlers);
176
177 SortField[] sortFields = new SortField[0];
178
179 Sort[] sorts = searchContext.getSorts();
180
181 if (sorts != null) {
182 sortFields = new SortField[sorts.length];
183
184 for (int i = 0; i < sorts.length; i++) {
185 Sort sort = sorts[i];
186
187 if ((sort.getType() == Sort.STRING_TYPE) &&
188 (searchContext.getLocale() != null)) {
189
190 sortFields[i] = new SortField(
191 sort.getFieldName(), searchContext.getLocale(),
192 sort.isReverse());
193 }
194 else {
195 sortFields[i] = new SortField(
196 sort.getFieldName(), sort.getType(),
197 sort.isReverse());
198 }
199 }
200 }
201
202 browseRequest = new BrowseRequest();
203
204 for (Facet facet : facets.values()) {
205 if (facet.isStatic()) {
206 continue;
207 }
208
209 FacetConfiguration facetConfiguration =
210 facet.getFacetConfiguration();
211
212 FacetSpec facetSpec = new FacetSpec();
213
214 facetSpec.setOrderBy(
215 FacetSortSpec.valueOf(facetConfiguration.getOrder()));
216
217 browseRequest.setFacetSpec(facet.getFieldName(), facetSpec);
218 }
219
220 browseRequest.setCount(PropsValues.INDEX_SEARCH_LIMIT);
221 browseRequest.setOffset(0);
222 browseRequest.setQuery(
223 (org.apache.lucene.search.Query)QueryTranslatorUtil.translate(
224 query));
225 browseRequest.setSort(sortFields);
226
227 boboBrowser = new BoboBrowser(boboIndexReader);
228
229 long startTime = System.currentTimeMillis();
230
231 BrowseResult browseResult = boboBrowser.browse(browseRequest);
232
233 BrowseHit[] browseHits = browseResult.getHits();
234
235 long endTime = System.currentTimeMillis();
236
237 float searchTime = (float)(endTime - startTime) / Time.SECOND;
238
239 hits = toHits(
240 indexSearcher, new HitDocs(browseHits), query, startTime,
241 searchTime, searchContext.getStart(), searchContext.getEnd());
242
243 Map<String, FacetAccessible> facetMap = browseResult.getFacetMap();
244
245 for (Map.Entry<String, FacetAccessible> entry :
246 facetMap.entrySet()) {
247
248 Facet facet = facets.get(entry.getKey());
249
250 FacetAccessible facetAccessible = entry.getValue();
251
252 FacetCollector facetCollector = new BoboFacetCollector(
253 entry.getKey(), facetAccessible);
254
255 facet.setFacetCollector(facetCollector);
256 }
257 }
258 catch (BooleanQuery.TooManyClauses tmc) {
259 int maxClauseCount = BooleanQuery.getMaxClauseCount();
260
261 BooleanQuery.setMaxClauseCount(Integer.MAX_VALUE);
262
263 try {
264 long startTime = System.currentTimeMillis();
265
266 BrowseResult browseResult = boboBrowser.browse(browseRequest);
267
268 BrowseHit[] browseHits = browseResult.getHits();
269
270 long endTime = System.currentTimeMillis();
271
272 float searchTime = (float)(endTime - startTime) / Time.SECOND;
273
274 hits = toHits(
275 indexSearcher, new HitDocs(browseHits), query, startTime,
276 searchTime, searchContext.getStart(),
277 searchContext.getEnd());
278
279 Map<String, FacetAccessible> facetMap =
280 browseResult.getFacetMap();
281
282 for (Map.Entry<String, FacetAccessible> entry :
283 facetMap.entrySet()) {
284
285 Facet facet = facets.get(entry.getKey());
286
287 FacetAccessible facetAccessible = entry.getValue();
288
289 FacetCollector facetCollector = new BoboFacetCollector(
290 entry.getKey(), facetAccessible);
291
292 facet.setFacetCollector(facetCollector);
293 }
294 }
295 catch (Exception e) {
296 throw new SearchException(e);
297 }
298 finally {
299 BooleanQuery.setMaxClauseCount(maxClauseCount);
300 }
301 }
302 catch (ParseException pe) {
303 _log.error("Query " + query, pe);
304
305 return new HitsImpl();
306 }
307 catch (Exception e) {
308 throw new SearchException(e);
309 }
310 finally {
311 cleanUp(boboBrowser);
312
313 try {
314 LuceneHelperUtil.releaseIndexSearcher(
315 searchContext.getCompanyId(), indexSearcher);
316 }
317 catch (IOException ioe) {
318 _log.error("Unable to release searcher", ioe);
319 }
320 }
321
322 if (_log.isDebugEnabled()) {
323 _log.debug(
324 "Search found " + hits.getLength() + " results in " +
325 hits.getSearchTime() + "ms");
326 }
327
328 return hits;
329 }
330
331 @Override
332 public Hits search(
333 String searchEngineId, long companyId, Query query, Sort[] sorts,
334 int start, int end)
335 throws SearchException {
336
337 if (_log.isDebugEnabled()) {
338 _log.debug("Query " + query);
339 }
340
341 Hits hits = null;
342
343 IndexSearcher indexSearcher = null;
344 org.apache.lucene.search.Sort luceneSort = null;
345
346 try {
347 indexSearcher = LuceneHelperUtil.getSearcher(companyId, true);
348
349 if (sorts != null) {
350 SortField[] sortFields = new SortField[sorts.length];
351
352 for (int i = 0; i < sorts.length; i++) {
353 Sort sort = sorts[i];
354
355 sortFields[i] = new SortField(
356 sort.getFieldName(), sort.getType(), sort.isReverse());
357 }
358
359 luceneSort = new org.apache.lucene.search.Sort(sortFields);
360 }
361 else {
362 luceneSort = new org.apache.lucene.search.Sort();
363 }
364
365 long startTime = System.currentTimeMillis();
366
367 TopFieldDocs topFieldDocs = indexSearcher.search(
368 (org.apache.lucene.search.Query)QueryTranslatorUtil.translate(
369 query),
370 null, PropsValues.INDEX_SEARCH_LIMIT, luceneSort);
371
372 long endTime = System.currentTimeMillis();
373
374 float searchTime = (float)(endTime - startTime) / Time.SECOND;
375
376 hits = toHits(
377 indexSearcher, new HitDocs(topFieldDocs), query, startTime,
378 searchTime, start, end);
379 }
380 catch (BooleanQuery.TooManyClauses tmc) {
381 int maxClauseCount = BooleanQuery.getMaxClauseCount();
382
383 BooleanQuery.setMaxClauseCount(Integer.MAX_VALUE);
384
385 try {
386 long startTime = System.currentTimeMillis();
387
388 TopFieldDocs topFieldDocs = indexSearcher.search(
389 (org.apache.lucene.search.Query)
390 QueryTranslatorUtil.translate(query),
391 null, PropsValues.INDEX_SEARCH_LIMIT, luceneSort);
392
393 long endTime = System.currentTimeMillis();
394
395 float searchTime = (float)(endTime - startTime) / Time.SECOND;
396
397 hits = toHits(
398 indexSearcher, new HitDocs(topFieldDocs), query, startTime,
399 searchTime, start, end);
400 }
401 catch (Exception e) {
402 throw new SearchException(e);
403 }
404 finally {
405 BooleanQuery.setMaxClauseCount(maxClauseCount);
406 }
407 }
408 catch (ParseException pe) {
409 _log.error("Query " + query, pe);
410
411 return new HitsImpl();
412 }
413 catch (Exception e) {
414 throw new SearchException(e);
415 }
416 finally {
417 LuceneHelperUtil.cleanUp(indexSearcher);
418 }
419
420 if (_log.isDebugEnabled()) {
421 _log.debug(
422 "Search found " + hits.getLength() + " results in " +
423 hits.getSearchTime() + "ms");
424 }
425
426 return hits;
427 }
428
429 protected void cleanUp(BoboBrowser boboBrowser) {
430 if (boboBrowser == null) {
431 return;
432 }
433
434 try {
435 boboBrowser.close();
436 }
437 catch (IOException ioe) {
438 _log.error(ioe, ioe);
439 }
440
441 Browsable[] browsables = boboBrowser.getSubBrowsers();
442
443 for (Browsable browsable : browsables) {
444 if (!(browsable instanceof BoboSubBrowser)) {
445 continue;
446 }
447
448 BoboSubBrowser boboSubBrowser = (BoboSubBrowser)browsable;
449
450 BoboIndexReader boboIndexReader = boboSubBrowser.getIndexReader();
451
452 try {
453 ThreadLocal<?> threadLocal =
454 (ThreadLocal<?>)_runtimeFacetDataMapField.get(
455 boboIndexReader);
456
457 threadLocal.remove();
458
459 _runtimeFacetDataMapField.set(boboIndexReader, null);
460 }
461 catch (Exception e) {
462 _log.error(
463 "Unable to clean up BoboIndexReader#_runtimeFacetDataMap",
464 e);
465 }
466
467 try {
468 ThreadLocal<?> threadLocal =
469 (ThreadLocal<?>)_runtimeFacetHandlerMapField.get(
470 boboIndexReader);
471
472 threadLocal.remove();
473
474 _runtimeFacetHandlerMapField.set(boboIndexReader, null);
475 }
476 catch (Exception e) {
477 _log.error(
478 "Unable to clean up BoboIndexReader#" +
479 "_runtimeFacetHandlerMap",
480 e);
481 }
482 }
483 }
484
485 protected DocumentImpl getDocument(
486 org.apache.lucene.document.Document oldDocument) {
487
488 DocumentImpl newDocument = new DocumentImpl();
489
490 List<org.apache.lucene.document.Fieldable> oldFieldables =
491 oldDocument.getFields();
492
493 for (org.apache.lucene.document.Fieldable oldFieldable :
494 oldFieldables) {
495
496 Field newField = null;
497
498 String[] values = oldDocument.getValues(oldFieldable.name());
499
500 if ((values != null) && (values.length > 1)) {
501 newField = new Field(oldFieldable.name(), values);
502 }
503 else {
504 newField = new Field(
505 oldFieldable.name(), oldFieldable.stringValue());
506 }
507
508 newField.setNumeric(oldFieldable instanceof NumericField);
509 newField.setTokenized(oldFieldable.isTokenized());
510
511 newDocument.add(newField);
512 }
513
514 return newDocument;
515 }
516
517 protected Set<String> getQueryTerms(Query query) {
518 Set<String> queryTerms = new HashSet<String>();
519
520 try {
521 queryTerms = LuceneHelperUtil.getQueryTerms(
522 (org.apache.lucene.search.Query)QueryTranslatorUtil.translate(
523 query));
524 }
525 catch (ParseException pe) {
526 _log.error("Query " + query, pe);
527 }
528
529 return queryTerms;
530 }
531
532 protected String getSnippet(
533 org.apache.lucene.document.Document doc, Query query, String field,
534 Locale locale, Document hitDoc, Set<String> matchingTerms)
535 throws IOException {
536
537 String snippetField = DocumentImpl.getLocalizedName(locale, field);
538 String snippet = null;
539
540 try {
541 org.apache.lucene.search.Query luceneQuery =
542 (org.apache.lucene.search.Query)QueryTranslatorUtil.translate(
543 query);
544
545 String[] values = doc.getValues(snippetField);
546
547 TermCollectingFormatter termCollectingFormatter =
548 new TermCollectingFormatter();
549
550 if (ArrayUtil.isNotEmpty(values)) {
551 snippet = LuceneHelperUtil.getSnippet(
552 luceneQuery, snippetField, StringUtil.merge(values),
553 termCollectingFormatter);
554 }
555
556 if (ArrayUtil.isEmpty(values) || Validator.isNull(snippet)) {
557 snippetField = field;
558
559 values = doc.getValues(snippetField);
560
561 if (ArrayUtil.isEmpty(values)) {
562 return StringPool.BLANK;
563 }
564
565 snippet = LuceneHelperUtil.getSnippet(
566 luceneQuery, field, StringUtil.merge(values),
567 termCollectingFormatter);
568 }
569
570 if (Validator.isNull(snippet)) {
571 return StringPool.BLANK;
572 }
573
574 matchingTerms.addAll(termCollectingFormatter.getTerms());
575 }
576 catch (ParseException pe) {
577 _log.error("Query " + query, pe);
578 }
579
580 hitDoc.addText(
581 Field.SNIPPET.concat(StringPool.UNDERLINE).concat(snippetField),
582 snippet);
583
584 return snippet;
585 }
586
587 protected Hits toHits(
588 IndexSearcher indexSearcher, HitDocs hitDocs, Query query,
589 long startTime, float searchTime, int start, int end)
590 throws IOException, ParseException {
591
592 int total = hitDocs.getTotalHits();
593
594 if ((start == QueryUtil.ALL_POS) && (end == QueryUtil.ALL_POS)) {
595 start = 0;
596 end = total;
597 }
598
599 int[] startAndEnd = SearchPaginationUtil.calculateStartAndEnd(
600 start, end, total);
601
602 start = startAndEnd[0];
603 end = startAndEnd[1];
604
605 Set<String> queryTerms = new HashSet<String>();
606
607 IndexReader indexReader = indexSearcher.getIndexReader();
608
609 List<String> indexedFieldNames = new ArrayList<String> (
610 indexReader.getFieldNames(IndexReader.FieldOption.INDEXED));
611
612 org.apache.lucene.search.Query luceneQuery =
613 (org.apache.lucene.search.Query)QueryTranslatorUtil.translate(
614 query);
615
616 int scoredFieldNamesCount = LuceneHelperUtil.countScoredFieldNames(
617 luceneQuery, ArrayUtil.toStringArray(indexedFieldNames.toArray()));
618
619 Hits hits = new HitsImpl();
620
621 if ((start < 0) || (start > end)) {
622 return hits;
623 }
624
625 int subsetTotal = end - start;
626
627 if (subsetTotal > PropsValues.INDEX_SEARCH_LIMIT) {
628 subsetTotal = PropsValues.INDEX_SEARCH_LIMIT;
629 }
630
631 List<Document> subsetDocs = new ArrayList<Document>(subsetTotal);
632 List<Float> subsetScores = new ArrayList<Float>(subsetTotal);
633
634 QueryConfig queryConfig = query.getQueryConfig();
635
636 for (int i = start; i < start + subsetTotal; i++) {
637 int docId = hitDocs.getDocId(i);
638
639 org.apache.lucene.document.Document document = indexSearcher.doc(
640 docId);
641
642 Document subsetDocument = getDocument(document);
643
644 if (queryConfig.isHighlightEnabled()) {
645 Locale locale = queryConfig.getLocale();
646
647 getSnippet(
648 document, query, Field.CONTENT, locale, subsetDocument,
649 queryTerms);
650 getSnippet(
651 document, query, Field.DESCRIPTION, locale, subsetDocument,
652 queryTerms);
653 getSnippet(
654 document, query, Field.TITLE, locale, subsetDocument,
655 queryTerms);
656 }
657
658 subsetDocs.add(subsetDocument);
659
660 Float subsetScore = hitDocs.getScore(i);
661
662 if (scoredFieldNamesCount > 0) {
663 subsetScore = subsetScore / scoredFieldNamesCount;
664 }
665
666 subsetScores.add(subsetScore);
667
668 if (_log.isDebugEnabled()) {
669 try {
670 Explanation explanation = indexSearcher.explain(
671 luceneQuery, docId);
672
673 _log.debug(explanation.toString());
674 }
675 catch (Exception e) {
676 }
677 }
678 }
679
680 if (!queryConfig.isHighlightEnabled()) {
681 queryTerms = getQueryTerms(query);
682 }
683
684 hits.setDocs(subsetDocs.toArray(new Document[subsetDocs.size()]));
685 hits.setLength(total);
686 hits.setQuery(query);
687 hits.setQueryTerms(queryTerms.toArray(new String[queryTerms.size()]));
688 hits.setScores(subsetScores.toArray(new Float[subsetScores.size()]));
689 hits.setSearchTime(searchTime);
690 hits.setStart(startTime);
691
692 return hits;
693 }
694
695 private static Log _log = LogFactoryUtil.getLog(LuceneIndexSearcher.class);
696
697 private static java.lang.reflect.Field _runtimeFacetDataMapField;
698 private static java.lang.reflect.Field _runtimeFacetHandlerMapField;
699
700 static {
701 try {
702 _runtimeFacetDataMapField = ReflectionUtil.getDeclaredField(
703 BoboIndexReader.class, "_runtimeFacetDataMap");
704 _runtimeFacetHandlerMapField = ReflectionUtil.getDeclaredField(
705 BoboIndexReader.class, "_runtimeFacetHandlerMap");
706 }
707 catch (Exception e) {
708 throw new ExceptionInInitializerError(e);
709 }
710 }
711
712 private class HitDocs {
713
714 public HitDocs(BrowseHit[] browseHits) {
715 _browseHits = browseHits;
716 }
717
718 public HitDocs(TopFieldDocs topFieldDocs) {
719 _topFieldDocs = topFieldDocs;
720 }
721
722 public int getDocId(int i) {
723 if (_topFieldDocs != null) {
724 ScoreDoc scoreDoc = _topFieldDocs.scoreDocs[i];
725
726 return scoreDoc.doc;
727 }
728 else if (_browseHits != null) {
729 return _browseHits[i].getDocid();
730 }
731
732 throw new IllegalStateException();
733 }
734
735 public float getScore(int i) {
736 if (_topFieldDocs != null) {
737 ScoreDoc scoreDoc = _topFieldDocs.scoreDocs[i];
738
739 return scoreDoc.score;
740 }
741 else if (_browseHits != null) {
742 return _browseHits[i].getScore();
743 }
744
745 throw new IllegalStateException();
746 }
747
748 public int getTotalHits() {
749 if (_topFieldDocs != null) {
750 return _topFieldDocs.totalHits;
751 }
752 else if (_browseHits != null) {
753 return _browseHits.length;
754 }
755
756 throw new IllegalStateException();
757 }
758
759 private BrowseHit[] _browseHits;
760 private TopFieldDocs _topFieldDocs;
761
762 }
763
764 private class TermCollectingFormatter implements Formatter {
765
766 public Set<String> getTerms() {
767 return _terms;
768 }
769
770 @Override
771 public String highlightTerm(
772 String originalText, TokenGroup tokenGroup) {
773
774 if (tokenGroup.getTotalScore() > 0) {
775 _terms.add(originalText);
776 }
777
778 return originalText;
779 }
780
781 private Set<String> _terms = new HashSet<String>();
782
783 }
784
785 }