Java tutorial
/******************************************************************************* * Copyright 2013 Matias Surdi * * Licensed 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 io.github.msurdi.redeye.core.lucene; import com.google.common.base.Optional; import com.google.common.collect.Lists; import io.github.msurdi.redeye.api.Indexable; import io.github.msurdi.redeye.core.Index; import org.apache.lucene.analysis.Analyzer; import org.apache.lucene.document.Document; import org.apache.lucene.index.DirectoryReader; import org.apache.lucene.index.IndexWriter; import org.apache.lucene.index.IndexWriterConfig; import org.apache.lucene.index.Term; import org.apache.lucene.queryparser.classic.ParseException; import org.apache.lucene.queryparser.classic.QueryParser; import org.apache.lucene.queryparser.simple.SimpleQueryParser; import org.apache.lucene.search.*; import org.apache.lucene.store.Directory; import org.apache.lucene.util.Version; import java.io.IOException; import java.util.ArrayList; import java.util.List; /** * This class encapsulates the Lucene API calls. All indexing is done using {@link io.github.msurdi.redeye.core.lucene.RedeyeAnalyzer}. * * @param <T> the type of objects stored in the lucene. {@code <T>} must implement {@link Indexable} */ public abstract class AbstractIndex<T extends Indexable> implements Index<T> { final static String DEFAULT_FIELD = Indexable.PRIVATE_PREFIX + "default"; private final static Version LUCENE_VERSION = Version.LUCENE_47; private final static String MATCH_ALL = "*"; private final Directory index; private final Analyzer analyzer = new RedeyeAnalyzer(LUCENE_VERSION); private final IndexWriterConfig config = new IndexWriterConfig(LUCENE_VERSION, analyzer); private IndexWriter writer; private SearcherManager searcherManager; private boolean opened = false; protected AbstractIndex(Directory index) { this.index = index; } /** * This method must convert a Lucene {@link Document} object into a {@code <T>} object. * * @param document the lucene {@link Document} * @return The {@code <T>} object constructed from the lucene {@link org.apache.lucene.document.Document} */ protected abstract T buildEntity(Document document); /** * This method must convert an instance of {@code <T>} into a Lucene {@code Document} * * @param id the id of this entry in the lucene * @param indexable the {@code <T>} object implementing {@link io.github.msurdi.redeye.api.Indexable} * @return a lucene {@link org.apache.lucene.document.Document} */ protected abstract Document buildDocument(String id, T indexable); /** * This method provides a very simplistic view of the lucene status. * * @return true if the lucene exists, false otherwise. * @throws IOException */ @Override public boolean isOk() throws IOException { return DirectoryReader.indexExists(index); } /** * This method opens the lucene and ensures it is ready for use. * * @throws IOException */ @Override public void open() throws IOException { ensureClosed(); writer = new IndexWriter(index, config); writer.commit(); searcherManager = new SearcherManager(writer, true, null); this.opened = true; } /** * This method closes the lucene guaranteeing its consistency. * * @throws IOException */ @Override public void close() throws IOException { ensureOpened(); searcherManager.close(); writer.close(); index.close(); this.opened = false; } private void ensureOpened() { if (!opened) { throw new IllegalStateException("The lucene is not ready"); } } private void ensureClosed() { if (opened) { throw new IllegalStateException("The lucene is already closed"); } } /** * Add or replace a document by id * * @param id the id used as the key in the lucene * @param indexable the {@link io.github.msurdi.redeye.api.Indexable} instance to store * @return the entity as an exact representation of how it will be returned when retrieved from the lucene * @throws IOException */ @Override public T put(String id, T indexable) throws IOException { ensureOpened(); Document doc = buildDocument(id, indexable); // Add or replace in the lucene Term idTerm = new Term(Indexable.ID_FIELD, id); writer.updateDocument(idTerm, doc); return buildEntity(doc); } /** * Retrieve a list of documents matching given query. The query must be a valid lucene query or '*' * for matching all documents. If the query is not valid, a best effort search is done. * * @param q a query string * @return a list of the {@link io.github.msurdi.redeye.api.Indexable} documents matching. * @throws IOException */ @Override public List<T> query(String q) throws IOException { ensureOpened(); ArrayList<T> results = Lists.newArrayList(); Query query; try { if (MATCH_ALL.equals(q)) { query = new MatchAllDocsQuery(); } else { query = new QueryParser(LUCENE_VERSION, DEFAULT_FIELD, analyzer).parse(q); } } catch (ParseException e) { query = new SimpleQueryParser(analyzer, DEFAULT_FIELD).parse(q); } IndexSearcher searcher = null; try { searcherManager.maybeRefresh(); searcher = searcherManager.acquire(); TopDocs docs = searcher.search(query, Math.max(1, searcher.getIndexReader().maxDoc())); for (ScoreDoc scoreDoc : docs.scoreDocs) { Document document = searcher.doc(scoreDoc.doc); results.add(buildEntity(document)); } } finally { searcherManager.release(searcher); } return results; } /** * Get a document from the lucene by its id * * @param id the id of the document to retrieve * @return An {@link com.google.common.base.Optional} from the document instance. * @throws IOException */ @Override public Optional<T> get(String id) throws IOException { ensureOpened(); final Query query = new TermQuery(new Term(Indexable.ID_FIELD, id)); IndexSearcher searcher = null; try { searcherManager.maybeRefresh(); searcher = searcherManager.acquire(); TopDocs docs = searcher.search(query, 1); if (docs.totalHits < 1) { return Optional.absent(); } else { return Optional.of(buildEntity(searcher.doc(docs.scoreDocs[0].doc))); } } finally { searcherManager.release(searcher); } } /** * Delete a document from the lucene by its id * * @param id the id of the document to remove * @throws IOException */ @Override public void delete(String id) throws IOException { ensureOpened(); final Query query = new TermQuery(new Term(Indexable.ID_FIELD, id)); writer.deleteDocuments(query); } /** * Get the number of documents in the lucene * * @return the number of documents in the lucene * @throws IOException */ @Override public long getCount() throws IOException { ensureOpened(); IndexSearcher searcher = null; try { searcherManager.maybeRefresh(); searcher = searcherManager.acquire(); return searcher.getIndexReader().maxDoc(); } finally { searcherManager.release(searcher); } } }