From af6f9df17128e2e30d8a87543a9daa363ceb7811 Mon Sep 17 00:00:00 2001 From: Lassi Immonen Date: Thu, 23 Dec 2010 00:44:51 +0000 Subject: [PATCH] Initial LuceneSession with callbacks --- querydsl-lucene/pom.xml | 2 +- .../query/lucene/session/LuceneSession.java | 45 ++++++ .../lucene/session/LuceneSessionImpl.java | 146 ++++++++++++++++++ .../query/lucene/session/QueryCallback.java | 12 ++ .../query/lucene/session/WriteCallback.java | 18 +++ .../lucene/session/LuceneSessionImplTest.java | 130 ++++++++++++++++ 6 files changed, 352 insertions(+), 1 deletion(-) create mode 100644 querydsl-lucene/src/main/java/com/mysema/query/lucene/session/LuceneSession.java create mode 100644 querydsl-lucene/src/main/java/com/mysema/query/lucene/session/LuceneSessionImpl.java create mode 100644 querydsl-lucene/src/main/java/com/mysema/query/lucene/session/QueryCallback.java create mode 100644 querydsl-lucene/src/main/java/com/mysema/query/lucene/session/WriteCallback.java create mode 100644 querydsl-lucene/src/test/java/com/mysema/query/lucene/session/LuceneSessionImplTest.java diff --git a/querydsl-lucene/pom.xml b/querydsl-lucene/pom.xml index f03600a4b..1abeda788 100644 --- a/querydsl-lucene/pom.xml +++ b/querydsl-lucene/pom.xml @@ -18,7 +18,7 @@ org.apache.lucene lucene-core - 3.0.0 + 3.0.3 provided diff --git a/querydsl-lucene/src/main/java/com/mysema/query/lucene/session/LuceneSession.java b/querydsl-lucene/src/main/java/com/mysema/query/lucene/session/LuceneSession.java new file mode 100644 index 000000000..0720c55ae --- /dev/null +++ b/querydsl-lucene/src/main/java/com/mysema/query/lucene/session/LuceneSession.java @@ -0,0 +1,45 @@ +package com.mysema.query.lucene.session; + +import java.io.IOException; +import java.util.List; + +import org.apache.lucene.document.Document; +import org.apache.lucene.index.CorruptIndexException; + +/** + * General interface on using Lucene. + * + * @author laimw + */ +public interface LuceneSession { + + /** + * Lucene query callback for querying + * + * @param clazz + * @param callback + * @return + * @throws IOException + * @throws CorruptIndexException + */ + T query(QueryCallback callback) throws CorruptIndexException, IOException; + + /** + * Creates a new index, adds updates to it and publishes the new index to + * all readers after callback finishes. + * + * @param callback + * @throws IOException + */ + void updateNew(WriteCallback callback) throws IOException; + + /** + * Updates the current index and publishes it to all readers after callback + * finishes. + * + * @param callback + * @throws IOException + */ + void update(WriteCallback callback) throws IOException; + +} diff --git a/querydsl-lucene/src/main/java/com/mysema/query/lucene/session/LuceneSessionImpl.java b/querydsl-lucene/src/main/java/com/mysema/query/lucene/session/LuceneSessionImpl.java new file mode 100644 index 000000000..bfc9665dd --- /dev/null +++ b/querydsl-lucene/src/main/java/com/mysema/query/lucene/session/LuceneSessionImpl.java @@ -0,0 +1,146 @@ +package com.mysema.query.lucene.session; + +import java.io.File; +import java.io.IOException; +import java.util.List; +import java.util.concurrent.atomic.AtomicReference; + +import org.apache.lucene.analysis.standard.StandardAnalyzer; +import org.apache.lucene.document.Document; +import org.apache.lucene.index.CorruptIndexException; +import org.apache.lucene.index.IndexWriter; +import org.apache.lucene.index.IndexWriter.MaxFieldLength; +import org.apache.lucene.search.IndexSearcher; +import org.apache.lucene.store.Directory; +import org.apache.lucene.store.SimpleFSDirectory; +import org.apache.lucene.util.Version; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.mysema.query.lucene.LuceneQuery; +import com.mysema.query.lucene.LuceneSerializer; + +/** + * Lucene session implementation for single dto per index. + * + * @author laimw + * + */ +public class LuceneSessionImpl implements LuceneSession { + + + private final Logger logger = LoggerFactory.getLogger(LuceneSessionImpl.class); + + private Directory directory; + + private final AtomicReference searcher = new AtomicReference(); + + private LuceneSerializer serializer = new LuceneSerializer(true, true); + + public LuceneSessionImpl(String indexPath) throws IOException { + File folder = new File(indexPath); + if (!folder.exists() && !folder.mkdirs()) { + throw new IOException("Could not create directory: " + folder.getAbsolutePath()); + } + + try { + directory = new SimpleFSDirectory(folder); + } catch (IOException e) { + logger.error("Could not create lucene directory to " + folder.getAbsolutePath()); + throw e; + } + } + + public LuceneSessionImpl(Directory directory) { + this.directory = directory; + } + + @Override + public T query(QueryCallback callback) throws CorruptIndexException, IOException { + + IndexSearcher is = getSearcher(); + T results = null; + try { + // Incrementing the reference count + is.getIndexReader().incRef(); + results = callback.query(new LuceneQuery(serializer, is)); + + } finally { + // Releasing the reference count + // This can be the last to actually close the reader + is.getIndexReader().decRef(); + } + + return results; + } + + private IndexSearcher getSearcher() throws CorruptIndexException, IOException { + if (searcher.get() == null) { + createNewSearcher(null); + } + + // Checking do we need to refresh the reader + IndexSearcher is = searcher.get(); + if (!is.getIndexReader().isCurrent()) { + // Underlying index has changed + + // Decreasing the reference counter so that + // count can go to zero either here or + // when final searcher has done it's job + is.getIndexReader().decRef(); + + createNewSearcher(is); + } + + return searcher.get(); + } + + private IndexSearcher createNewSearcher(IndexSearcher expected) throws IOException { + IndexSearcher is = new IndexSearcher(directory); + if (!searcher.compareAndSet(expected, is)) { + // Some thread already created a new one so just close this + is.close(); + } else { + // Incrementing the reference count first time + // We want to keep using the same reader until the index is changed + is.getIndexReader().incRef(); + } + return searcher.get(); + } + + @Override + public void updateNew(WriteCallback callback) throws IOException { + update(callback, true); + + } + + @Override + public void update(WriteCallback callback) throws IOException { + update(callback, false); + } + + private void update(WriteCallback callback, boolean create) throws IOException { + IndexWriter writer = + new IndexWriter(directory, new StandardAnalyzer(Version.LUCENE_CURRENT), create, + MaxFieldLength.LIMITED); + + try { + callback.write(writer); + } finally { + try { + writer.close(); + } catch (IOException e) { + logger.error("Writer close failed", e); + try { + if (IndexWriter.isLocked(directory)) { + IndexWriter.unlock(directory); + } + } catch (IOException e1) { + logger.error("Lock release failed", e1); + } + } + } + + } + +} diff --git a/querydsl-lucene/src/main/java/com/mysema/query/lucene/session/QueryCallback.java b/querydsl-lucene/src/main/java/com/mysema/query/lucene/session/QueryCallback.java new file mode 100644 index 000000000..b229d9536 --- /dev/null +++ b/querydsl-lucene/src/main/java/com/mysema/query/lucene/session/QueryCallback.java @@ -0,0 +1,12 @@ +package com.mysema.query.lucene.session; + +import com.mysema.query.lucene.LuceneQuery; + +/** + * + * @author laimw + * + */ +public interface QueryCallback { + T query(LuceneQuery query); +} diff --git a/querydsl-lucene/src/main/java/com/mysema/query/lucene/session/WriteCallback.java b/querydsl-lucene/src/main/java/com/mysema/query/lucene/session/WriteCallback.java new file mode 100644 index 000000000..81f5e45f1 --- /dev/null +++ b/querydsl-lucene/src/main/java/com/mysema/query/lucene/session/WriteCallback.java @@ -0,0 +1,18 @@ +package com.mysema.query.lucene.session; + +import java.io.IOException; + +import org.apache.lucene.index.CorruptIndexException; +import org.apache.lucene.index.IndexWriter; + +/** + * Callback which has Lucene Writer instance. + * + * @author laimw + * + */ +public interface WriteCallback { + + void write(IndexWriter writer) throws CorruptIndexException, IOException; + +} diff --git a/querydsl-lucene/src/test/java/com/mysema/query/lucene/session/LuceneSessionImplTest.java b/querydsl-lucene/src/test/java/com/mysema/query/lucene/session/LuceneSessionImplTest.java new file mode 100644 index 000000000..c09dab183 --- /dev/null +++ b/querydsl-lucene/src/test/java/com/mysema/query/lucene/session/LuceneSessionImplTest.java @@ -0,0 +1,130 @@ +package com.mysema.query.lucene.session; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; + +import java.io.IOException; +import java.util.List; + +import org.apache.lucene.document.Document; +import org.apache.lucene.document.Field; +import org.apache.lucene.document.Field.Index; +import org.apache.lucene.document.Field.Store; +import org.apache.lucene.document.NumericField; +import org.apache.lucene.index.CorruptIndexException; +import org.apache.lucene.index.IndexWriter; +import org.apache.lucene.search.IndexSearcher; +import org.apache.lucene.store.Directory; +import org.apache.lucene.store.RAMDirectory; +import org.junit.Before; +import org.junit.Test; + +import com.mysema.query.lucene.LuceneQuery; +import com.mysema.query.lucene.LuceneSerializer; +import com.mysema.query.types.PathMetadataFactory; +import com.mysema.query.types.path.EntityPathBase; +import com.mysema.query.types.path.NumberPath; +import com.mysema.query.types.path.StringPath; + +public class LuceneSessionImplTest { + + public class QDocument extends EntityPathBase { + + private static final long serialVersionUID = -4872833626508344081L; + + public QDocument(final String var) { + super(Document.class, PathMetadataFactory.forVariable(var)); + } + + public final NumberPath year = createNumber("year", Integer.class); + + public final StringPath title = createString("title"); + + public final NumberPath gross = createNumber("gross", Double.class); + } + + private LuceneSession session; + + private Directory directory; + + private StringPath title; + + private NumberPath year; + + private NumberPath gross; + + @Before + public void before() throws IOException { + directory = new RAMDirectory(); + session = new LuceneSessionImpl(directory); + final QDocument entityPath = new QDocument("doc"); + title = entityPath.title; + year = entityPath.year; + gross = entityPath.gross; + } + + @Test + public void testCreate() throws IOException { + + session.updateNew(new WriteCallback() { + public void write(IndexWriter writer) throws CorruptIndexException, IOException { + + writer.addDocument(createDocument( + "Jurassic Park", + "Michael Crichton", + "It's a UNIX system! I know this!", + 1990, + 90.00)); + writer.addDocument(createDocument( + "Nummisuutarit", + "Aleksis Kivi", + "ESKO. Ja iloitset ja riemuitset?", + 1864, + 10.00)); + + } + }); + + List results = session.query(new QueryCallback>() { + public List query(LuceneQuery query) { + + return query.where(title.eq("Jurassic Park")).list(); + + } + }); + + assertEquals(1, results.size()); + assertEquals("Jurassic Park", results.get(0).getField("title").stringValue()); + + Long count = session.query(new QueryCallback() { + public Long query(LuceneQuery query) { + + //TODO Tästä tulee 0 eikä 2!! + //return query.where(title.ne("AA")).count(); + + return query.where(title.startsWith("Nummi")).count(); + + } + }); + + assertEquals(1, (long) count); + } + + private Document createDocument( + final String docTitle, + final String docAuthor, + final String docText, + final int docYear, + final double docGross) { + final Document doc = new Document(); + + doc.add(new Field("title", docTitle, Store.YES, Index.ANALYZED)); + doc.add(new Field("author", docAuthor, Store.YES, Index.ANALYZED)); + doc.add(new Field("text", docText, Store.YES, Index.ANALYZED)); + doc.add(new NumericField("year", Store.YES, true).setIntValue(docYear)); + doc.add(new NumericField("gross", Store.YES, true).setDoubleValue(docGross)); + + return doc; + } + +}