001: /*
002: * Copyright 2004-2006 the original author or authors.
003: *
004: * Licensed under the Apache License, Version 2.0 (the "License");
005: * you may not use this file except in compliance with the License.
006: * You may obtain a copy of the License at
007: *
008: * http://www.apache.org/licenses/LICENSE-2.0
009: *
010: * Unless required by applicable law or agreed to in writing, software
011: * distributed under the License is distributed on an "AS IS" BASIS,
012: * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
013: * See the License for the specific language governing permissions and
014: * limitations under the License.
015: */
016:
017: package org.compass.core.lucene.engine.spellcheck;
018:
019: import java.io.IOException;
020: import java.util.ArrayList;
021: import java.util.Arrays;
022: import java.util.HashMap;
023: import java.util.HashSet;
024: import java.util.List;
025: import java.util.Map;
026: import java.util.Set;
027: import java.util.concurrent.Callable;
028: import java.util.concurrent.Future;
029: import java.util.concurrent.ScheduledFuture;
030: import java.util.concurrent.TimeUnit;
031: import java.util.concurrent.atomic.AtomicBoolean;
032:
033: import org.apache.commons.logging.Log;
034: import org.apache.commons.logging.LogFactory;
035: import org.apache.lucene.analysis.WhitespaceAnalyzer;
036: import org.apache.lucene.index.IndexReader;
037: import org.apache.lucene.index.IndexWriter;
038: import org.apache.lucene.index.LuceneSubIndexInfo;
039: import org.apache.lucene.index.MultiReader;
040: import org.apache.lucene.index.Term;
041: import org.apache.lucene.search.IndexSearcher;
042: import org.apache.lucene.search.MultiSearcher;
043: import org.apache.lucene.search.Query;
044: import org.apache.lucene.search.Searchable;
045: import org.apache.lucene.search.spell.CompassSpellChecker;
046: import org.apache.lucene.search.spell.HighFrequencyDictionary;
047: import org.apache.lucene.store.Directory;
048: import org.apache.lucene.store.IndexInput;
049: import org.apache.lucene.store.IndexOutput;
050: import org.apache.lucene.store.LockObtainFailedException;
051: import org.compass.core.CompassException;
052: import org.compass.core.CompassQuery;
053: import org.compass.core.config.CompassEnvironment;
054: import org.compass.core.config.CompassSettings;
055: import org.compass.core.engine.SearchEngineException;
056: import org.compass.core.engine.spellcheck.SearchEngineSpellCheckSuggestBuilder;
057: import org.compass.core.impl.DefaultCompassQuery;
058: import org.compass.core.lucene.LuceneEnvironment;
059: import org.compass.core.lucene.engine.LuceneSearchEngineFactory;
060: import org.compass.core.lucene.engine.LuceneSearchEngineInternalSearch;
061: import org.compass.core.lucene.engine.LuceneSearchEngineQuery;
062: import org.compass.core.lucene.engine.queryparser.QueryParserUtils;
063: import org.compass.core.lucene.engine.store.DefaultLuceneSearchEngineStore;
064: import org.compass.core.lucene.engine.store.LuceneSearchEngineStore;
065: import org.compass.core.mapping.CompassMapping;
066: import org.compass.core.mapping.ResourceMapping;
067: import org.compass.core.mapping.ResourcePropertyMapping;
068: import org.compass.core.mapping.SpellCheckType;
069: import org.compass.core.transaction.InternalCompassTransaction;
070: import org.compass.core.transaction.context.TransactionContextCallback;
071: import org.compass.core.transaction.context.TransactionalRunnable;
072: import org.compass.core.util.StringUtils;
073:
074: /**
075: * The default implementation of the search engine spell check manager. Uses Lucene (modified) spell check
076: * support. Only activated if the spell jar exists.
077: *
078: * @author kimchy
079: */
080: public class DefaultLuceneSpellCheckManager implements
081: InternalLuceneSearchEngineSpellCheckManager {
082:
083: private static final Log log = LogFactory
084: .getLog(DefaultLuceneSpellCheckManager.class);
085:
086: private LuceneSearchEngineFactory searchEngineFactory;
087:
088: private LuceneSearchEngineStore indexStore;
089:
090: private LuceneSearchEngineStore spellCheckStore;
091:
092: private String spellIndexSubContext = "spellcheck";
093:
094: private CompassSettings spellCheckSettings;
095:
096: private Map<String, IndexReader> readerMap = new HashMap<String, IndexReader>();
097:
098: private Map<String, IndexSearcher> searcherMap = new HashMap<String, IndexSearcher>();
099:
100: private Map<String, Object> indexLocks = new HashMap<String, Object>();
101:
102: private String defaultProperty;
103:
104: private Map<String, Set<String>> properties = new HashMap<String, Set<String>>();
105:
106: private float defaultAccuracy = 0.5f;
107:
108: private float defaultDictionaryThreshold;
109:
110: private String spellCheckVersionFileName = "spellcheck.version";
111:
112: private volatile boolean started = false;
113:
114: private boolean closeStore;
115:
116: private ScheduledFuture refreshCacheFuture;
117:
118: private ScheduledFuture rebuildFuture;
119:
120: public void configure(
121: LuceneSearchEngineFactory searchEngineFactory,
122: CompassSettings settings, CompassMapping mapping) {
123: this .searchEngineFactory = searchEngineFactory;
124: this .indexStore = searchEngineFactory.getLuceneIndexManager()
125: .getStore();
126: this .spellCheckSettings = settings.copy();
127: for (Object key1 : settings.getProperties().keySet()) {
128: String key = (String) key1;
129: String value = settings.getSetting(key);
130: if (key.startsWith(LuceneEnvironment.SpellCheck.PREFIX)) {
131: key = "compass."
132: + key
133: .substring(LuceneEnvironment.SpellCheck.PREFIX
134: .length());
135: spellCheckSettings.setSetting(key, value);
136: }
137: }
138: spellCheckSettings
139: .setIntSetting(
140: LuceneEnvironment.SearchEngineIndex.MERGE_FACTOR,
141: spellCheckSettings
142: .getSettingAsInt(
143: LuceneEnvironment.SearchEngineIndex.MERGE_FACTOR,
144: 3000));
145:
146: if (spellCheckSettings
147: .getSetting(CompassEnvironment.CONNECTION)
148: .equals(
149: settings
150: .getSetting(CompassEnvironment.CONNECTION))) {
151: spellCheckStore = searchEngineFactory
152: .getLuceneIndexManager().getStore();
153: closeStore = false;
154: if (log.isDebugEnabled()) {
155: log
156: .debug("Spell index uses Compass store ["
157: + spellCheckSettings
158: .getSetting(CompassEnvironment.CONNECTION)
159: + "]");
160: }
161: } else {
162: spellCheckStore = new DefaultLuceneSearchEngineStore();
163: spellCheckStore.configure(searchEngineFactory,
164: spellCheckSettings, mapping);
165: closeStore = true;
166: if (log.isDebugEnabled()) {
167: log
168: .debug("Spell index uses specialized store ["
169: + spellCheckSettings
170: .getSetting(CompassEnvironment.CONNECTION)
171: + "]");
172: }
173: }
174:
175: String[] sharedProps = new String[0];
176: String sSharedProps = spellCheckSettings
177: .getSetting(LuceneEnvironment.SpellCheck.GLOBAL_INCLUDE_PROPERTIES);
178: if (sSharedProps != null) {
179: sharedProps = StringUtils.tokenizeToStringArray(
180: sSharedProps, ",");
181: }
182: String[] sharedExcludeProps = new String[0];
183: String sSharedExcludeProps = spellCheckSettings
184: .getSetting(LuceneEnvironment.SpellCheck.GLOBAL_EXCLUDE_PROPERTY);
185: if (sSharedExcludeProps != null) {
186: sharedExcludeProps = StringUtils.tokenizeToStringArray(
187: sSharedExcludeProps, ",");
188: }
189:
190: for (String subIndex : indexStore.getSubIndexes()) {
191: Set<String> subIndexProps = properties.get(subIndex);
192: if (subIndexProps == null) {
193: subIndexProps = new HashSet<String>();
194: properties.put(subIndex, subIndexProps);
195: }
196:
197: subIndexProps.addAll(Arrays.asList(sharedProps));
198:
199: for (String alias : spellCheckStore
200: .getAliasesBySubIndex(subIndex)) {
201: ResourceMapping resourceMapping = searchEngineFactory
202: .getMapping().getMappingByAlias(alias);
203: for (ResourcePropertyMapping resourcePropertyMapping : resourceMapping
204: .getResourcePropertyMappings()) {
205: if (resourcePropertyMapping.isInternal()) {
206: continue;
207: }
208: if (resourceMapping.getSpellCheck() == SpellCheckType.INCLUDE
209: && resourcePropertyMapping.getSpellCheck() != SpellCheckType.EXCLUDE) {
210: subIndexProps.add(resourcePropertyMapping
211: .getPath().getPath());
212: }
213: if (resourceMapping.getSpellCheck() == SpellCheckType.EXCLUDE
214: && resourcePropertyMapping.getSpellCheck() == SpellCheckType.INCLUDE) {
215: subIndexProps.add(resourcePropertyMapping
216: .getPath().getPath());
217: }
218: }
219: if (resourceMapping.getAllMapping().getSpellCheck() == SpellCheckType.INCLUDE) {
220: subIndexProps.add(resourceMapping.getAllMapping()
221: .getProperty());
222: }
223:
224: if (subIndexProps.size() == 0
225: && resourceMapping.getSpellCheck() == SpellCheckType.NA
226: && resourceMapping.getAllMapping()
227: .getSpellCheck() != SpellCheckType.EXCLUDE) {
228: subIndexProps.add(settings.getSetting(
229: CompassEnvironment.All.NAME,
230: CompassEnvironment.All.DEFAULT_NAME));
231: }
232: }
233:
234: for (String excludeProperty : sharedExcludeProps) {
235: subIndexProps.remove(excludeProperty);
236: }
237:
238: if (log.isDebugEnabled()) {
239: log.debug("Sub index [" + subIndex
240: + "] includes the following properties "
241: + subIndexProps);
242: }
243: }
244: defaultProperty = settings.getSetting(
245: LuceneEnvironment.SpellCheck.DEFAULT_PROPERTY, settings
246: .getSetting(CompassEnvironment.All.NAME,
247: CompassEnvironment.All.DEFAULT_NAME));
248:
249: this .defaultAccuracy = spellCheckSettings.getSettingAsFloat(
250: LuceneEnvironment.SpellCheck.ACCURACY, 0.5f);
251: this .defaultDictionaryThreshold = spellCheckSettings
252: .getSettingAsFloat(
253: LuceneEnvironment.SpellCheck.DICTIONARY_THRESHOLD,
254: 0.0f);
255:
256: for (final String subIndex : indexStore.getSubIndexes()) {
257: indexLocks.put(subIndex, new Object());
258: }
259: }
260:
261: public void start() {
262: if (started) {
263: return;
264: }
265: started = true;
266:
267: for (final String subIndex : indexStore.getSubIndexes()) {
268: searchEngineFactory.getTransactionContext().execute(
269: new TransactionContextCallback<Object>() {
270: public Object doInTransaction(
271: InternalCompassTransaction tr)
272: throws CompassException {
273: Directory dir = spellCheckStore
274: .openDirectory(
275: spellIndexSubContext,
276: subIndex);
277: close(subIndex);
278: try {
279: if (!IndexReader.indexExists(dir)) {
280: IndexWriter writer = new IndexWriter(
281: dir,
282: new WhitespaceAnalyzer(),
283: true);
284: writer.close();
285: }
286: } catch (IOException e) {
287: throw new SearchEngineException(
288: "Failed to verify spell index for sub index ["
289: + subIndex + "]", e);
290: }
291: refresh(subIndex);
292: return null;
293: }
294: });
295: }
296:
297: // schedule a refresh task
298: long cacheRefreshInterval = spellCheckSettings
299: .getSettingAsLong(
300: LuceneEnvironment.SearchEngineIndex.CACHE_INTERVAL_INVALIDATION,
301: 5000);
302: refreshCacheFuture = searchEngineFactory.getExecutorManager()
303: .scheduleWithFixedDelay(
304: new TransactionalRunnable(searchEngineFactory
305: .getTransactionContext(),
306: new Runnable() {
307: public void run() {
308: refresh();
309: }
310: }), cacheRefreshInterval,
311: cacheRefreshInterval, TimeUnit.MILLISECONDS);
312:
313: if (spellCheckSettings.getSettingAsBoolean(
314: LuceneEnvironment.SpellCheck.SCHEDULE, true)) {
315: rebuildFuture = searchEngineFactory
316: .getExecutorManager()
317: .scheduleWithFixedDelay(
318: new Runnable() {
319: public void run() {
320: rebuild();
321: }
322: },
323: spellCheckSettings
324: .getSettingAsLong(
325: LuceneEnvironment.SpellCheck.SCHEDULE_INITIAL_DELAY,
326: 10),
327: spellCheckSettings
328: .getSettingAsLong(
329: LuceneEnvironment.SpellCheck.SCHEDULE_INTERVAL,
330: 10) * 60, TimeUnit.SECONDS);
331: }
332: }
333:
334: public void stop() {
335: if (!started) {
336: return;
337: }
338: started = false;
339: if (refreshCacheFuture != null) {
340: refreshCacheFuture.cancel(true);
341: }
342: if (rebuildFuture != null) {
343: rebuildFuture.cancel(true);
344: }
345: }
346:
347: public void close() {
348: stop();
349: for (String subIndex : indexStore.getSubIndexes()) {
350: close(subIndex);
351: }
352: if (closeStore) {
353: spellCheckStore.close();
354: }
355: }
356:
357: private void close(String subIndex) {
358: synchronized (indexLocks.get(subIndex)) {
359: IndexSearcher searcher = searcherMap.remove(subIndex);
360: if (searcher != null) {
361: try {
362: searcher.close();
363: } catch (IOException e) {
364: // ignore
365: }
366: }
367: IndexReader reader = readerMap.remove(subIndex);
368: if (reader != null) {
369: try {
370: reader.close();
371: } catch (IOException e) {
372: // ignore
373: }
374: }
375: }
376: }
377:
378: public String getDefaultProperty() {
379: return this .defaultProperty;
380: }
381:
382: public float getDefaultAccuracy() {
383: return defaultAccuracy;
384: }
385:
386: public CompassMapping getMapping() {
387: return searchEngineFactory.getMapping();
388: }
389:
390: public void concurrentRefresh() throws SearchEngineException {
391: checkIfStarted();
392: ArrayList<Callable<Object>> rebuildTasks = new ArrayList<Callable<Object>>();
393: for (String subIndex : indexStore.getSubIndexes()) {
394: rebuildTasks.add(new RefreshTask(subIndex));
395: }
396: searchEngineFactory.getExecutorManager()
397: .invokeAllWithLimitBailOnException(rebuildTasks,
398: Integer.MAX_VALUE);
399: }
400:
401: public void refresh() throws SearchEngineException {
402: checkIfStarted();
403: for (String subIndex : indexStore.getSubIndexes()) {
404: refresh(subIndex);
405: }
406: }
407:
408: public void refresh(String subIndex) throws SearchEngineException {
409: checkIfStarted();
410: synchronized (indexLocks.get(subIndex)) {
411: IndexReader reader = readerMap.get(subIndex);
412: if (reader != null) {
413: try {
414: if (reader.isCurrent()) {
415: return;
416: }
417: } catch (IOException e) {
418: throw new SearchEngineException(
419: "Failed to check if spell index is current for sub index ["
420: + subIndex + "]", e);
421: }
422: }
423: try {
424: reader = IndexReader.open(spellCheckStore
425: .openDirectory(spellIndexSubContext, subIndex));
426: readerMap.put(subIndex, reader);
427: searcherMap.put(subIndex, new IndexSearcher(reader));
428: } catch (IOException e) {
429: throw new SearchEngineException(
430: "Failed to open spell index searcher for sub index ["
431: + subIndex + "]", e);
432: }
433: }
434: }
435:
436: public boolean isRebuildNeeded() throws SearchEngineException {
437: checkIfStarted();
438: boolean rebulidRequired = false;
439: for (String subIndex : indexStore.getSubIndexes()) {
440: rebulidRequired |= isRebuildNeeded(subIndex);
441: }
442: return rebulidRequired;
443: }
444:
445: public boolean isRebuildNeeded(final String subIndex)
446: throws SearchEngineException {
447: checkIfStarted();
448: return searchEngineFactory.getTransactionContext().execute(
449: new TransactionContextCallback<Boolean>() {
450: public Boolean doInTransaction(
451: InternalCompassTransaction tr)
452: throws CompassException {
453: try {
454: long spellCheckVersion = readSpellCheckIndexVersion(subIndex);
455: long indexVerion = LuceneSubIndexInfo
456: .getIndexInfo(subIndex, indexStore)
457: .version();
458: return indexVerion != spellCheckVersion;
459: } catch (IOException e) {
460: throw new SearchEngineException(
461: "Failed to read index version for sub index ["
462: + subIndex + "]");
463: }
464: }
465: });
466: }
467:
468: public boolean concurrentRebuild() throws SearchEngineException {
469: checkIfStarted();
470: ArrayList<Callable<Boolean>> rebuildTasks = new ArrayList<Callable<Boolean>>();
471: for (String subIndex : indexStore.getSubIndexes()) {
472: rebuildTasks.add(new RebuildTask(subIndex));
473: }
474: List<Future<Boolean>> rebuildResults = searchEngineFactory
475: .getExecutorManager()
476: .invokeAllWithLimitBailOnException(rebuildTasks,
477: Integer.MAX_VALUE);
478: boolean rebuilt = false;
479: for (Future<Boolean> rebuildResult : rebuildResults) {
480: try {
481: rebuilt |= rebuildResult.get();
482: } catch (Exception e) {
483: // will not happen
484: }
485: }
486: return rebuilt;
487: }
488:
489: public boolean rebuild() throws SearchEngineException {
490: checkIfStarted();
491: boolean rebuilt = false;
492: for (String subIndex : indexStore.getSubIndexes()) {
493: rebuilt |= rebuild(subIndex);
494: }
495: return rebuilt;
496: }
497:
498: public synchronized boolean rebuild(final String subIndex)
499: throws SearchEngineException {
500: checkIfStarted();
501: return searchEngineFactory.getTransactionContext().execute(
502: new TransactionContextCallback<Boolean>() {
503: public Boolean doInTransaction(
504: InternalCompassTransaction tr)
505: throws CompassException {
506: long version = readSpellCheckIndexVersion(subIndex);
507: long indexVersion;
508: try {
509: indexVersion = LuceneSubIndexInfo
510: .getIndexInfo(subIndex, indexStore)
511: .version();
512: } catch (IOException e) {
513: throw new SearchEngineException(
514: "Failed to read actual index version for sub index ["
515: + subIndex + "]", e);
516: }
517: if (version == indexVersion) {
518: if (log.isDebugEnabled()) {
519: log
520: .debug("No need to rebuild spell check index, sub index ["
521: + subIndex
522: + "] has not changed");
523: }
524: return false;
525: }
526:
527: if (log.isDebugEnabled()) {
528: log
529: .debug("Rebuilding spell index for sub index ["
530: + subIndex + "]");
531: }
532: Directory dir = spellCheckStore.openDirectory(
533: spellIndexSubContext, subIndex);
534: CompassSpellChecker spellChecker;
535: try {
536: spellChecker = new CompassSpellChecker(dir,
537: true);
538: spellChecker.clearIndex();
539: } catch (IOException e) {
540: throw new SearchEngineException(
541: "Failed to create spell checker for sub index ["
542: + subIndex + "]", e);
543: }
544: IndexWriter writer = null;
545: try {
546: LuceneSearchEngineInternalSearch search = (LuceneSearchEngineInternalSearch) tr
547: .getSearchEngine().internalSearch(
548: new String[] { subIndex },
549: null);
550: if (search.getSearcher() != null) {
551: writer = searchEngineFactory
552: .getLuceneIndexManager()
553: .openIndexWriter(
554: spellCheckSettings,
555: dir,
556: true,
557: true,
558: null,
559: new WhitespaceAnalyzer());
560: for (String property : properties
561: .get(subIndex)) {
562: spellChecker
563: .indexDictionary(
564: writer,
565: new HighFrequencyDictionary(
566: search
567: .getReader(),
568: property,
569: defaultDictionaryThreshold));
570: }
571: writer.optimize();
572: } else {
573: if (log.isDebugEnabled()) {
574: log
575: .debug("No data found in sub index ["
576: + subIndex
577: + "], skipping building spell index");
578: }
579: }
580: } catch (LockObtainFailedException e) {
581: log
582: .debug("Failed to obtain lock, assuming indexing of spell index is in process for sub index ["
583: + subIndex + "]");
584: return null;
585: } catch (IOException e) {
586: throw new SearchEngineException(
587: "Failed to index spell index for sub index ["
588: + subIndex + "]", e);
589: } finally {
590: if (writer != null) {
591: try {
592: writer.close();
593: } catch (IOException e) {
594: log
595: .warn(
596: "Failed to close specll check index writer for sub index ["
597: + subIndex
598: + "]", e);
599: }
600: }
601: }
602: // refresh the readers and searchers
603: closeAndRefresh(subIndex);
604: writeSpellCheckIndexVersion(subIndex,
605: indexVersion);
606:
607: if (log.isDebugEnabled()) {
608: log
609: .debug("Finished rebuilding spell index for sub index ["
610: + subIndex + "]");
611: }
612: return true;
613: }
614: });
615: }
616:
617: public void deleteIndex() throws SearchEngineException {
618: // no need to check if started
619: for (String subIndex : indexStore.getSubIndexes()) {
620: deleteIndex(subIndex);
621: }
622: }
623:
624: public void deleteIndex(String subIndex)
625: throws SearchEngineException {
626: // no need to check if started
627: close(subIndex);
628: spellCheckStore.deleteIndex(spellIndexSubContext, subIndex);
629: }
630:
631: public SearchEngineSpellCheckSuggestBuilder suggestBuilder(
632: String word) {
633: return new DefaultLuceneSearchEngineSpellCheckSuggestBuilder(
634: word, this );
635: }
636:
637: public CompassQuery suggest(CompassQuery query) {
638: DefaultCompassQuery defaultCompassQuery = (DefaultCompassQuery) query;
639: LuceneSearchEngineQuery searchEngineQuery = (LuceneSearchEngineQuery) defaultCompassQuery
640: .getSearchEngineQuery();
641: final CompassSpellChecker spellChecker = createSpellChecker(
642: searchEngineQuery.getSubIndexes(), searchEngineQuery
643: .getAliases());
644:
645: if (spellChecker == null) {
646: return query;
647: }
648:
649: final AtomicBoolean suggestedQuery = new AtomicBoolean(false);
650: try {
651: Query replacedQ = QueryParserUtils.visit(searchEngineQuery
652: .getQuery(),
653: new QueryParserUtils.QueryTermVisitor() {
654: public Term replaceTerm(Term term)
655: throws SearchEngineException {
656: try {
657: if (spellChecker.exist(term.text())) {
658: return term;
659: }
660: String[] similarWords = spellChecker
661: .suggestSimilar(term.text(), 1);
662: if (similarWords.length == 0) {
663: return term;
664: }
665: suggestedQuery.set(true);
666: return term.createTerm(similarWords[0]);
667: } catch (IOException e) {
668: throw new SearchEngineException(
669: "Failed to suggest for query",
670: e);
671: }
672: }
673: });
674:
675: if (!suggestedQuery.get()) {
676: return query;
677: }
678:
679: try {
680: CompassQuery suggested = (CompassQuery) query.clone();
681: LuceneSearchEngineQuery suggestedSEQ = (LuceneSearchEngineQuery) ((DefaultCompassQuery) suggested)
682: .getSearchEngineQuery();
683: suggestedSEQ.setQuery(replacedQ);
684: suggestedSEQ.setSuggested(suggestedQuery.get());
685: return suggested;
686: } catch (CloneNotSupportedException e) {
687: throw new SearchEngineException(
688: "Failed to clone query", e);
689: }
690:
691: } finally {
692: spellChecker.close();
693: }
694: }
695:
696: public <T> T execute(final String[] subIndexes,
697: final String[] aliases,
698: final SpellCheckerCallback<T> callback) {
699: return searchEngineFactory.getTransactionContext().execute(
700: new TransactionContextCallback<T>() {
701: public T doInTransaction(
702: InternalCompassTransaction tr)
703: throws CompassException {
704: CompassSpellChecker spellChecker = createSpellChecker(
705: subIndexes, aliases);
706: if (spellChecker == null) {
707: return callback.execute(null, null);
708: }
709: try {
710: LuceneSearchEngineInternalSearch search = (LuceneSearchEngineInternalSearch) tr
711: .getSearchEngine().internalSearch(
712: subIndexes, aliases);
713: return callback.execute(spellChecker,
714: search.getReader());
715: } finally {
716: spellChecker.close();
717: }
718: }
719: });
720: }
721:
722: public CompassSpellChecker createSpellChecker(
723: final String[] subIndexes, final String[] aliases) {
724: String[] calcSubIndexes = indexStore.calcSubIndexes(subIndexes,
725: aliases);
726: ArrayList<Searchable> searchers = new ArrayList<Searchable>(
727: calcSubIndexes.length);
728: ArrayList<IndexReader> readers = new ArrayList<IndexReader>(
729: calcSubIndexes.length);
730: for (String subIndex : calcSubIndexes) {
731: synchronized (indexLocks.get(subIndex)) {
732: IndexSearcher searcher = searcherMap.get(subIndex);
733: if (searcher != null) {
734: searchers.add(searcher);
735: readers.add(searcher.getIndexReader());
736: }
737: }
738: }
739:
740: if (searchers.isEmpty()) {
741: return null;
742: }
743:
744: MultiSearcher searcher;
745: try {
746: searcher = new MultiSearcher(searchers
747: .toArray(new Searchable[searchers.size()]));
748: } catch (IOException e) {
749: throw new SearchEngineException(
750: "Failed to open searcher for spell check", e);
751: }
752: MultiReader reader = new MultiReader(readers
753: .toArray(new IndexReader[readers.size()]), false);
754:
755: return new CompassSpellChecker(searcher, reader);
756: }
757:
758: private void writeSpellCheckIndexVersion(String subIndex,
759: long version) {
760: Directory dir = spellCheckStore.openDirectory(
761: spellIndexSubContext, subIndex);
762: try {
763: if (dir.fileExists(spellCheckVersionFileName)) {
764: dir.deleteFile(spellCheckVersionFileName);
765: }
766: IndexOutput indexOutput = dir
767: .createOutput(spellCheckVersionFileName);
768: indexOutput.writeLong(version);
769: indexOutput.close();
770: } catch (IOException e) {
771: throw new SearchEngineException(
772: "Failed to write spell check index version for sub index ["
773: + subIndex + "]", e);
774: }
775: }
776:
777: private long readSpellCheckIndexVersion(String subIndex) {
778: Directory dir = spellCheckStore.openDirectory(
779: spellIndexSubContext, subIndex);
780: IndexInput input = null;
781: try {
782: if (!dir.fileExists(spellCheckVersionFileName)) {
783: return -1;
784: }
785: input = dir.openInput(spellCheckVersionFileName);
786: return input.readLong();
787: } catch (IOException e) {
788: throw new SearchEngineException(
789: "Failed to read spell check index version for sub index ["
790: + subIndex + "]", e);
791: } finally {
792: try {
793: if (input != null) {
794: input.close();
795: }
796: } catch (IOException e) {
797: // ignore
798: }
799: }
800: }
801:
802: private void closeAndRefresh() {
803: for (String subIndex : indexStore.getSubIndexes()) {
804: closeAndRefresh(subIndex);
805: }
806: }
807:
808: private void closeAndRefresh(String subIndex) {
809: synchronized (indexLocks.get(subIndex)) {
810: close(subIndex);
811: refresh(subIndex);
812: }
813: }
814:
815: private void checkIfStarted()
816: throws java.lang.IllegalStateException {
817: if (!started) {
818: throw new IllegalStateException(
819: "Spell check manager must be started to perform this operation");
820: }
821: }
822:
823: private class RebuildTask implements Callable<Boolean> {
824:
825: private String subIndex;
826:
827: public RebuildTask(String subIndex) {
828: this .subIndex = subIndex;
829: }
830:
831: public Boolean call() throws Exception {
832: return rebuild(subIndex);
833: }
834: }
835:
836: private class RefreshTask implements Callable<Object> {
837:
838: private String subIndex;
839:
840: public RefreshTask(String subIndex) {
841: this .subIndex = subIndex;
842: }
843:
844: public Object call() throws Exception {
845: refresh(subIndex);
846: return null;
847: }
848: }
849: }
|