/** * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package org.waveprotocol.box.server.waveserver; import com.google.common.base.Preconditions; import com.google.common.collect.HashMultimap; import com.google.common.collect.Multimap; import com.google.common.util.concurrent.ListenableFuture; import com.google.common.util.concurrent.ListenableFutureTask; import com.google.common.util.concurrent.SettableFuture; import com.google.inject.Inject; import com.google.inject.Singleton; import com.google.inject.name.Named; import org.apache.lucene.analysis.standard.StandardAnalyzer; import org.apache.lucene.document.Document; import org.apache.lucene.document.Field; import org.apache.lucene.index.IndexWriter; import org.apache.lucene.index.IndexWriterConfig; import org.apache.lucene.index.IndexWriterConfig.OpenMode; import org.apache.lucene.index.Term; import org.apache.lucene.search.*; import org.apache.lucene.search.BooleanClause.Occur; import org.apache.lucene.store.AlreadyClosedException; import org.apache.lucene.util.Version; import org.waveprotocol.box.server.CoreSettingsNames; import org.waveprotocol.box.server.executor.ExecutorAnnotations.IndexExecutor; import org.waveprotocol.box.server.persistence.lucene.IndexDirectory; import org.waveprotocol.wave.model.id.WaveId; import org.waveprotocol.wave.model.id.WaveletId; import org.waveprotocol.wave.model.id.WaveletName; import org.waveprotocol.wave.model.wave.ParticipantId; import org.waveprotocol.wave.model.wave.ParticipantIdUtil; import org.waveprotocol.wave.model.wave.data.ReadableWaveletData; import java.io.Closeable; import java.io.IOException; import java.util.concurrent.Callable; import java.util.concurrent.Executor; import java.util.logging.Level; import java.util.logging.Logger; import static org.waveprotocol.box.server.waveserver.IndexFieldType.*; /** * Lucene based implementation of {@link PerUserWaveViewHandler}. * * @author yurize@apache.org (Yuri Zelikov) */ @Singleton public class LucenePerUserWaveViewHandlerImpl implements PerUserWaveViewHandler, Closeable { private static class WaveSearchWarmer implements SearcherWarmer { private final ParticipantId sharedDomainParticipantId; WaveSearchWarmer(String waveDomain) { sharedDomainParticipantId = ParticipantIdUtil.makeUnsafeSharedDomainParticipantId(waveDomain); } @Override public void warm(IndexSearcher searcher) throws IOException { // TODO (Yuri Z): Run some diverse searches, searching against all // fields. BooleanQuery participantQuery = new BooleanQuery(); participantQuery.add( new TermQuery(new Term(WITH.toString(), sharedDomainParticipantId.getAddress())), Occur.SHOULD); searcher.search(participantQuery, MAX_WAVES); } } private static final Logger LOG = Logger.getLogger(LucenePerUserWaveViewHandlerImpl.class .getName()); private static final Version LUCENE_VERSION = Version.LUCENE_35; /** The results will be returned in the ascending order according to last modified time. */ private static Sort LMT_ASC_SORT = new Sort(new SortField("title", SortField.LONG)); /** Minimum time until a new reader can be opened. */ private static final double MIN_STALE_SEC = 0.025; /** Maximum time until a new reader must be opened. */ private static final double MAX_STALE_SEC = 1.0; /** Defines the maximum number of waves returned by the search. */ private static final int MAX_WAVES = 10000; private final StandardAnalyzer analyzer; private final IndexWriter indexWriter; private final NRTManager nrtManager; private final NRTManagerReopenThread nrtManagerReopenThread; private final ReadableWaveletDataProvider waveletProvider; private final Executor executor; private boolean isClosed = false; @Inject public LucenePerUserWaveViewHandlerImpl(IndexDirectory directory, ReadableWaveletDataProvider waveletProvider, @Named(CoreSettingsNames.WAVE_SERVER_DOMAIN) String domain, @IndexExecutor Executor executor) { this.waveletProvider = waveletProvider; this.executor = executor; analyzer = new StandardAnalyzer(LUCENE_VERSION); try { IndexWriterConfig indexConfig = new IndexWriterConfig(LUCENE_VERSION, analyzer); indexConfig.setOpenMode(OpenMode.CREATE_OR_APPEND); indexWriter = new IndexWriter(directory.getDirectory(), indexConfig); nrtManager = new NRTManager(indexWriter, new WaveSearchWarmer(domain)); } catch (IOException ex) { throw new IndexException(ex); } nrtManagerReopenThread = new NRTManagerReopenThread(nrtManager, MAX_STALE_SEC, MIN_STALE_SEC); nrtManagerReopenThread.start(); } /** * Closes the handler, releases resources and flushes the recent index changes * to persistent storage. */ @Override public synchronized void close() { if (isClosed) { throw new AlreadyClosedException("Already closed"); } isClosed = true; try { nrtManager.close(); if (analyzer != null) { analyzer.close(); } nrtManagerReopenThread.close(); indexWriter.close(); } catch (IOException ex) { LOG.log(Level.SEVERE, "Failed to close the Lucene index", ex); } LOG.info("Successfully closed the Lucene index..."); } /** * Ensures that the index is up to date. Exits quickly if no changes were done * to the index. * * @throws IOException if something goes wrong. */ public void forceReopen() throws IOException { nrtManager.maybeReopen(true); } @Override public ListenableFuture<Void> onParticipantAdded(final WaveletName waveletName, ParticipantId participant) { Preconditions.checkNotNull(waveletName); Preconditions.checkNotNull(participant); ListenableFutureTask<Void> task = ListenableFutureTask.create(new Callable<Void>() { @Override public Void call() throws Exception { ReadableWaveletData waveletData; try { waveletData = waveletProvider.getReadableWaveletData(waveletName); updateIndex(waveletData); } catch (WaveServerException e) { LOG.log(Level.SEVERE, "Failed to update index for " + waveletName, e); throw e; } return null; } }); executor.execute(task); return task; } @Override public ListenableFuture<Void> onParticipantRemoved(final WaveletName waveletName, final ParticipantId participant) { Preconditions.checkNotNull(waveletName); Preconditions.checkNotNull(participant); ListenableFutureTask<Void> task = ListenableFutureTask.create(new Callable<Void>() { @Override public Void call() throws Exception { ReadableWaveletData waveletData; try { waveletData = waveletProvider.getReadableWaveletData(waveletName); try { removeParticipantfromIndex(waveletData, participant, nrtManager); } catch (IOException e) { LOG.log(Level.SEVERE, "Failed to update index for " + waveletName, e); throw e; } } catch (WaveServerException e) { LOG.log(Level.SEVERE, "Failed to update index for " + waveletName, e); throw e; } return null; } }); executor.execute(task); return task; } @Override public ListenableFuture<Void> onWaveInit(final WaveletName waveletName) { Preconditions.checkNotNull(waveletName); ListenableFutureTask<Void> task = ListenableFutureTask.create(new Callable<Void>() { @Override public Void call() throws Exception { ReadableWaveletData waveletData; try { waveletData = waveletProvider.getReadableWaveletData(waveletName); updateIndex(waveletData); } catch (WaveServerException e) { LOG.log(Level.SEVERE, "Failed to initialize index for " + waveletName, e); throw e; } return null; } }); executor.execute(task); return task; } @Override public ListenableFuture<Void> onWaveUpdated(final ReadableWaveletData waveletData) { // No op. SettableFuture<Void> task = SettableFuture.create(); task.set(null); return task; } private void updateIndex(ReadableWaveletData wavelet) throws IndexException { Preconditions.checkNotNull(wavelet); try { // TODO (Yuri Z): Update documents instead of totally removing and adding. removeIndex(wavelet, nrtManager); addIndex(wavelet, nrtManager); indexWriter.commit(); } catch (IOException e) { throw new IndexException(String.valueOf(wavelet.getWaveletId()), e); } } private static void addIndex(ReadableWaveletData wavelet, NRTManager nrtManager) throws IOException { Document doc = new Document(); addWaveletFieldsToIndex(wavelet, doc); nrtManager.addDocument(doc); } private static void addWaveletFieldsToIndex(ReadableWaveletData wavelet, Document doc) { doc.add(new Field(WAVEID.toString(), wavelet.getWaveId().serialise(), Field.Store.YES, Field.Index.NOT_ANALYZED)); doc.add(new Field(WAVELETID.toString(), wavelet.getWaveletId().serialise(), Field.Store.YES, Field.Index.NOT_ANALYZED)); doc.add(new Field(LMT.toString(), Long.toString(wavelet.getLastModifiedTime()), Field.Store.NO, Field.Index.NOT_ANALYZED)); for (ParticipantId participant : wavelet.getParticipants()) { doc.add(new Field(WITH.toString(), participant.toString(), Field.Store.YES, Field.Index.NOT_ANALYZED)); } } private static void removeIndex(ReadableWaveletData wavelet, NRTManager nrtManager) throws IOException { BooleanQuery query = new BooleanQuery(); query.add(new TermQuery(new Term(WAVEID.toString(), wavelet.getWaveId().serialise())), BooleanClause.Occur.MUST); query.add(new TermQuery(new Term(WAVELETID.toString(), wavelet.getWaveletId().serialise())), BooleanClause.Occur.MUST); nrtManager.deleteDocuments(query); } private static void removeParticipantfromIndex(ReadableWaveletData wavelet, ParticipantId participant, NRTManager nrtManager) throws IOException { BooleanQuery query = new BooleanQuery(); Term waveIdTerm = new Term(WAVEID.toString(), wavelet.getWaveId().serialise()); query.add(new TermQuery(waveIdTerm), BooleanClause.Occur.MUST); query.add(new TermQuery(new Term(WAVELETID.toString(), wavelet.getWaveletId().serialise())), BooleanClause.Occur.MUST); SearcherManager searcherManager = nrtManager.getSearcherManager(true); IndexSearcher indexSearcher = searcherManager.acquire(); try { TopDocs hints = indexSearcher.search(query, MAX_WAVES); for (ScoreDoc hint : hints.scoreDocs) { Document document = indexSearcher.doc(hint.doc); String[] participantValues = document.getValues(WITH.toString()); document.removeFields(WITH.toString()); for (String address : participantValues) { if (address.equals(participant.getAddress())) { continue; } document.add(new Field(WITH.toString(), address, Field.Store.YES, Field.Index.NOT_ANALYZED)); } nrtManager.updateDocument(waveIdTerm, document); } } catch (IOException e) { LOG.log(Level.WARNING, "Failed to fetch from index " + wavelet.toString(), e); } finally { try { searcherManager.release(indexSearcher); } catch (IOException e) { LOG.log(Level.WARNING, "Failed to close searcher. ", e); } } } @Override public Multimap<WaveId, WaveletId> retrievePerUserWaveView(ParticipantId user) { Preconditions.checkNotNull(user); Multimap<WaveId, WaveletId> userWavesViewMap = HashMultimap.create(); BooleanQuery participantQuery = new BooleanQuery(); participantQuery.add(new TermQuery(new Term(WITH.toString(), user.getAddress())), Occur.SHOULD); SearcherManager searcherManager = nrtManager.getSearcherManager(true); IndexSearcher indexSearcher = searcherManager.acquire(); try { TopDocs hints = indexSearcher.search(participantQuery, MAX_WAVES, LMT_ASC_SORT); for (ScoreDoc hint : hints.scoreDocs) { Document document = indexSearcher.doc(hint.doc); WaveId waveId = WaveId.deserialise(document.get(WAVEID.toString())); WaveletId waveletId = WaveletId.deserialise(document.get(WAVELETID.toString())); userWavesViewMap.put(waveId, waveletId); } } catch (IOException e) { LOG.log(Level.WARNING, "Search failed: " + user, e); } finally { try { searcherManager.release(indexSearcher); } catch (IOException e) { LOG.log(Level.WARNING, "Failed to close searcher. " + user, e); } } return userWavesViewMap; } }