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.getSearcher(
109 searchContext.getCompanyId(), true);
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 LuceneHelperUtil.cleanUp(indexSearcher);
314 }
315
316 if (_log.isDebugEnabled()) {
317 _log.debug(
318 "Search found " + hits.getLength() + " results in " +
319 hits.getSearchTime() + "ms");
320 }
321
322 return hits;
323 }
324
325 @Override
326 public Hits search(
327 String searchEngineId, long companyId, Query query, Sort[] sorts,
328 int start, int end)
329 throws SearchException {
330
331 if (_log.isDebugEnabled()) {
332 _log.debug("Query " + query);
333 }
334
335 Hits hits = null;
336
337 IndexSearcher indexSearcher = null;
338 org.apache.lucene.search.Sort luceneSort = null;
339
340 try {
341 indexSearcher = LuceneHelperUtil.getSearcher(companyId, true);
342
343 if (sorts != null) {
344 SortField[] sortFields = new SortField[sorts.length];
345
346 for (int i = 0; i < sorts.length; i++) {
347 Sort sort = sorts[i];
348
349 sortFields[i] = new SortField(
350 sort.getFieldName(), sort.getType(), sort.isReverse());
351 }
352
353 luceneSort = new org.apache.lucene.search.Sort(sortFields);
354 }
355 else {
356 luceneSort = new org.apache.lucene.search.Sort();
357 }
358
359 long startTime = System.currentTimeMillis();
360
361 TopFieldDocs topFieldDocs = indexSearcher.search(
362 (org.apache.lucene.search.Query)QueryTranslatorUtil.translate(
363 query),
364 null, PropsValues.INDEX_SEARCH_LIMIT, luceneSort);
365
366 long endTime = System.currentTimeMillis();
367
368 float searchTime = (float)(endTime - startTime) / Time.SECOND;
369
370 hits = toHits(
371 indexSearcher, new HitDocs(topFieldDocs), query, startTime,
372 searchTime, start, end);
373 }
374 catch (BooleanQuery.TooManyClauses tmc) {
375 int maxClauseCount = BooleanQuery.getMaxClauseCount();
376
377 BooleanQuery.setMaxClauseCount(Integer.MAX_VALUE);
378
379 try {
380 long startTime = System.currentTimeMillis();
381
382 TopFieldDocs topFieldDocs = indexSearcher.search(
383 (org.apache.lucene.search.Query)
384 QueryTranslatorUtil.translate(query),
385 null, PropsValues.INDEX_SEARCH_LIMIT, luceneSort);
386
387 long endTime = System.currentTimeMillis();
388
389 float searchTime = (float)(endTime - startTime) / Time.SECOND;
390
391 hits = toHits(
392 indexSearcher, new HitDocs(topFieldDocs), query, startTime,
393 searchTime, start, end);
394 }
395 catch (Exception e) {
396 throw new SearchException(e);
397 }
398 finally {
399 BooleanQuery.setMaxClauseCount(maxClauseCount);
400 }
401 }
402 catch (ParseException pe) {
403 _log.error("Query " + query, pe);
404
405 return new HitsImpl();
406 }
407 catch (Exception e) {
408 throw new SearchException(e);
409 }
410 finally {
411 LuceneHelperUtil.cleanUp(indexSearcher);
412 }
413
414 if (_log.isDebugEnabled()) {
415 _log.debug(
416 "Search found " + hits.getLength() + " results in " +
417 hits.getSearchTime() + "ms");
418 }
419
420 return hits;
421 }
422
423 protected void cleanUp(BoboBrowser boboBrowser) {
424 if (boboBrowser == null) {
425 return;
426 }
427
428 try {
429 boboBrowser.close();
430 }
431 catch (IOException ioe) {
432 _log.error(ioe, ioe);
433 }
434
435 Browsable[] browsables = boboBrowser.getSubBrowsers();
436
437 for (Browsable browsable : browsables) {
438 if (!(browsable instanceof BoboSubBrowser)) {
439 continue;
440 }
441
442 BoboSubBrowser boboSubBrowser = (BoboSubBrowser)browsable;
443
444 BoboIndexReader boboIndexReader = boboSubBrowser.getIndexReader();
445
446 try {
447 ThreadLocal<?> threadLocal =
448 (ThreadLocal<?>)_runtimeFacetDataMapField.get(
449 boboIndexReader);
450
451 threadLocal.remove();
452
453 _runtimeFacetDataMapField.set(boboIndexReader, null);
454 }
455 catch (Exception e) {
456 _log.error(
457 "Unable to clean up BoboIndexReader#_runtimeFacetDataMap",
458 e);
459 }
460
461 try {
462 ThreadLocal<?> threadLocal =
463 (ThreadLocal<?>)_runtimeFacetHandlerMapField.get(
464 boboIndexReader);
465
466 threadLocal.remove();
467
468 _runtimeFacetHandlerMapField.set(boboIndexReader, null);
469 }
470 catch (Exception e) {
471 _log.error(
472 "Unable to clean up BoboIndexReader#" +
473 "_runtimeFacetHandlerMap",
474 e);
475 }
476 }
477 }
478
479 protected DocumentImpl getDocument(
480 org.apache.lucene.document.Document oldDocument) {
481
482 DocumentImpl newDocument = new DocumentImpl();
483
484 List<org.apache.lucene.document.Fieldable> oldFieldables =
485 oldDocument.getFields();
486
487 for (org.apache.lucene.document.Fieldable oldFieldable :
488 oldFieldables) {
489
490 Field newField = null;
491
492 String[] values = oldDocument.getValues(oldFieldable.name());
493
494 if ((values != null) && (values.length > 1)) {
495 newField = new Field(oldFieldable.name(), values);
496 }
497 else {
498 newField = new Field(
499 oldFieldable.name(), oldFieldable.stringValue());
500 }
501
502 newField.setNumeric(oldFieldable instanceof NumericField);
503 newField.setTokenized(oldFieldable.isTokenized());
504
505 newDocument.add(newField);
506 }
507
508 return newDocument;
509 }
510
511 protected Set<String> getQueryTerms(Query query) {
512 Set<String> queryTerms = new HashSet<String>();
513
514 try {
515 queryTerms = LuceneHelperUtil.getQueryTerms(
516 (org.apache.lucene.search.Query)QueryTranslatorUtil.translate(
517 query));
518 }
519 catch (ParseException pe) {
520 _log.error("Query " + query, pe);
521 }
522
523 return queryTerms;
524 }
525
526 protected String getSnippet(
527 org.apache.lucene.document.Document doc, Query query, String field,
528 Locale locale, Document hitDoc, Set<String> matchingTerms)
529 throws IOException {
530
531 String snippetField = DocumentImpl.getLocalizedName(locale, field);
532 String snippet = null;
533
534 try {
535 org.apache.lucene.search.Query luceneQuery =
536 (org.apache.lucene.search.Query)QueryTranslatorUtil.translate(
537 query);
538
539 String[] values = doc.getValues(snippetField);
540
541 TermCollectingFormatter termCollectingFormatter =
542 new TermCollectingFormatter();
543
544 if (ArrayUtil.isNotEmpty(values)) {
545 snippet = LuceneHelperUtil.getSnippet(
546 luceneQuery, snippetField, StringUtil.merge(values),
547 termCollectingFormatter);
548 }
549
550 if (ArrayUtil.isEmpty(values) || Validator.isNull(snippet)) {
551 snippetField = field;
552
553 values = doc.getValues(snippetField);
554
555 if (ArrayUtil.isEmpty(values)) {
556 return StringPool.BLANK;
557 }
558
559 snippet = LuceneHelperUtil.getSnippet(
560 luceneQuery, field, StringUtil.merge(values),
561 termCollectingFormatter);
562 }
563
564 if (Validator.isNull(snippet)) {
565 return StringPool.BLANK;
566 }
567
568 matchingTerms.addAll(termCollectingFormatter.getTerms());
569 }
570 catch (ParseException pe) {
571 _log.error("Query " + query, pe);
572 }
573
574 hitDoc.addText(
575 Field.SNIPPET.concat(StringPool.UNDERLINE).concat(snippetField),
576 snippet);
577
578 return snippet;
579 }
580
581 protected Hits toHits(
582 IndexSearcher indexSearcher, HitDocs hitDocs, Query query,
583 long startTime, float searchTime, int start, int end)
584 throws IOException, ParseException {
585
586 int total = hitDocs.getTotalHits();
587
588 if ((start == QueryUtil.ALL_POS) && (end == QueryUtil.ALL_POS)) {
589 start = 0;
590 end = total;
591 }
592
593 int[] startAndEnd = SearchPaginationUtil.calculateStartAndEnd(
594 start, end, total);
595
596 start = startAndEnd[0];
597 end = startAndEnd[1];
598
599 Set<String> queryTerms = new HashSet<String>();
600
601 IndexReader indexReader = indexSearcher.getIndexReader();
602
603 List<String> indexedFieldNames = new ArrayList<String> (
604 indexReader.getFieldNames(IndexReader.FieldOption.INDEXED));
605
606 org.apache.lucene.search.Query luceneQuery =
607 (org.apache.lucene.search.Query)QueryTranslatorUtil.translate(
608 query);
609
610 int scoredFieldNamesCount = LuceneHelperUtil.countScoredFieldNames(
611 luceneQuery, ArrayUtil.toStringArray(indexedFieldNames.toArray()));
612
613 Hits hits = new HitsImpl();
614
615 if ((start < 0) || (start > end)) {
616 return hits;
617 }
618
619 int subsetTotal = end - start;
620
621 if (subsetTotal > PropsValues.INDEX_SEARCH_LIMIT) {
622 subsetTotal = PropsValues.INDEX_SEARCH_LIMIT;
623 }
624
625 List<Document> subsetDocs = new ArrayList<Document>(subsetTotal);
626 List<Float> subsetScores = new ArrayList<Float>(subsetTotal);
627
628 QueryConfig queryConfig = query.getQueryConfig();
629
630 for (int i = start; i < start + subsetTotal; i++) {
631 int docId = hitDocs.getDocId(i);
632
633 org.apache.lucene.document.Document document = indexSearcher.doc(
634 docId);
635
636 Document subsetDocument = getDocument(document);
637
638 if (queryConfig.isHighlightEnabled()) {
639 Locale locale = queryConfig.getLocale();
640
641 getSnippet(
642 document, query, Field.CONTENT, locale, subsetDocument,
643 queryTerms);
644 getSnippet(
645 document, query, Field.DESCRIPTION, locale, subsetDocument,
646 queryTerms);
647 getSnippet(
648 document, query, Field.TITLE, locale, subsetDocument,
649 queryTerms);
650 }
651
652 subsetDocs.add(subsetDocument);
653
654 Float subsetScore = hitDocs.getScore(i);
655
656 if (scoredFieldNamesCount > 0) {
657 subsetScore = subsetScore / scoredFieldNamesCount;
658 }
659
660 subsetScores.add(subsetScore);
661
662 if (_log.isDebugEnabled()) {
663 try {
664 Explanation explanation = indexSearcher.explain(
665 luceneQuery, docId);
666
667 _log.debug(explanation.toString());
668 }
669 catch (Exception e) {
670 }
671 }
672 }
673
674 if (!queryConfig.isHighlightEnabled()) {
675 queryTerms = getQueryTerms(query);
676 }
677
678 hits.setDocs(subsetDocs.toArray(new Document[subsetDocs.size()]));
679 hits.setLength(total);
680 hits.setQuery(query);
681 hits.setQueryTerms(queryTerms.toArray(new String[queryTerms.size()]));
682 hits.setScores(subsetScores.toArray(new Float[subsetScores.size()]));
683 hits.setSearchTime(searchTime);
684 hits.setStart(startTime);
685
686 return hits;
687 }
688
689 private static Log _log = LogFactoryUtil.getLog(LuceneIndexSearcher.class);
690
691 private static java.lang.reflect.Field _runtimeFacetDataMapField;
692 private static java.lang.reflect.Field _runtimeFacetHandlerMapField;
693
694 static {
695 try {
696 _runtimeFacetDataMapField = ReflectionUtil.getDeclaredField(
697 BoboIndexReader.class, "_runtimeFacetDataMap");
698 _runtimeFacetHandlerMapField = ReflectionUtil.getDeclaredField(
699 BoboIndexReader.class, "_runtimeFacetHandlerMap");
700 }
701 catch (Exception e) {
702 throw new ExceptionInInitializerError(e);
703 }
704 }
705
706 private class HitDocs {
707
708 public HitDocs(BrowseHit[] browseHits) {
709 _browseHits = browseHits;
710 }
711
712 public HitDocs(TopFieldDocs topFieldDocs) {
713 _topFieldDocs = topFieldDocs;
714 }
715
716 public int getDocId(int i) {
717 if (_topFieldDocs != null) {
718 ScoreDoc scoreDoc = _topFieldDocs.scoreDocs[i];
719
720 return scoreDoc.doc;
721 }
722 else if (_browseHits != null) {
723 return _browseHits[i].getDocid();
724 }
725
726 throw new IllegalStateException();
727 }
728
729 public float getScore(int i) {
730 if (_topFieldDocs != null) {
731 ScoreDoc scoreDoc = _topFieldDocs.scoreDocs[i];
732
733 return scoreDoc.score;
734 }
735 else if (_browseHits != null) {
736 return _browseHits[i].getScore();
737 }
738
739 throw new IllegalStateException();
740 }
741
742 public int getTotalHits() {
743 if (_topFieldDocs != null) {
744 return _topFieldDocs.totalHits;
745 }
746 else if (_browseHits != null) {
747 return _browseHits.length;
748 }
749
750 throw new IllegalStateException();
751 }
752
753 private BrowseHit[] _browseHits;
754 private TopFieldDocs _topFieldDocs;
755
756 }
757
758 private class TermCollectingFormatter implements Formatter {
759
760 public Set<String> getTerms() {
761 return _terms;
762 }
763
764 @Override
765 public String highlightTerm(
766 String originalText, TokenGroup tokenGroup) {
767
768 if (tokenGroup.getTotalScore() > 0) {
769 _terms.add(originalText);
770 }
771
772 return originalText;
773 }
774
775 private Set<String> _terms = new HashSet<String>();
776
777 }
778
779 }