diff --git a/lucene/CHANGES.txt b/lucene/CHANGES.txt
index 1e23b1943cd..f7ea50d177e 100644
--- a/lucene/CHANGES.txt
+++ b/lucene/CHANGES.txt
@@ -622,6 +622,13 @@ Changes in backwards compatibility policy
(instead of the old reader) if there are no changes in the index, to
prevent the common pitfall of accidentally closing the old reader.
+Changes in runtime behavior
+
+* LUCENE-3520: IndexReader.openIfChanged, when passed a near-real-time
+ reader, will now return null if there are no changes. The API has
+ always reserved the right to do this; it's just that in the past for
+ near-real-time readers it never did. (Mike McCandless)
+
Bug fixes
* LUCENE-3412: SloppyPhraseScorer was returning non-deterministic results
diff --git a/lucene/contrib/misc/src/java/org/apache/lucene/index/NRTManager.java b/lucene/contrib/misc/src/java/org/apache/lucene/index/NRTManager.java
index 715280232e3..9dc69a6f95b 100644
--- a/lucene/contrib/misc/src/java/org/apache/lucene/index/NRTManager.java
+++ b/lucene/contrib/misc/src/java/org/apache/lucene/index/NRTManager.java
@@ -93,10 +93,10 @@ public class NRTManager implements Closeable {
SearcherWarmer warmer, boolean alwaysApplyDeletes) throws IOException {
this.writer = writer;
if (alwaysApplyDeletes) {
- withoutDeletes = withDeletes = new SearcherManagerRef(true, 0, SearcherManager.open(writer, true, warmer, es));
+ withoutDeletes = withDeletes = new SearcherManagerRef(true, 0, new SearcherManager(writer, true, warmer, es));
} else {
- withDeletes = new SearcherManagerRef(true, 0, SearcherManager.open(writer, true, warmer, es));
- withoutDeletes = new SearcherManagerRef(false, 0, SearcherManager.open(writer, false, warmer, es));
+ withDeletes = new SearcherManagerRef(true, 0, new SearcherManager(writer, true, warmer, es));
+ withoutDeletes = new SearcherManagerRef(false, 0, new SearcherManager(writer, false, warmer, es));
}
indexingGen = new AtomicLong(1);
}
diff --git a/lucene/contrib/misc/src/java/org/apache/lucene/search/SearcherLifetimeManager.java b/lucene/contrib/misc/src/java/org/apache/lucene/search/SearcherLifetimeManager.java
index f69071867eb..e088dbae929 100644
--- a/lucene/contrib/misc/src/java/org/apache/lucene/search/SearcherLifetimeManager.java
+++ b/lucene/contrib/misc/src/java/org/apache/lucene/search/SearcherLifetimeManager.java
@@ -173,8 +173,8 @@ public class SearcherLifetimeManager implements Closeable {
// incRef done by SearcherTracker ctor:
tracker.close();
}
- } else {
- assert tracker.searcher == searcher;
+ } else if (tracker.searcher != searcher) {
+ throw new IllegalArgumentException("the provided searcher has the same underlying reader version yet the searcher instance differs from before (new=" + searcher + " vs old=" + tracker.searcher);
}
return version;
diff --git a/lucene/contrib/misc/src/java/org/apache/lucene/search/SearcherManager.java b/lucene/contrib/misc/src/java/org/apache/lucene/search/SearcherManager.java
index 13ace53ce6b..858b8994a93 100644
--- a/lucene/contrib/misc/src/java/org/apache/lucene/search/SearcherManager.java
+++ b/lucene/contrib/misc/src/java/org/apache/lucene/search/SearcherManager.java
@@ -64,23 +64,76 @@ import org.apache.lucene.store.Directory;
* @lucene.experimental
*/
-public abstract class SearcherManager {
+public final class SearcherManager {
- protected volatile IndexSearcher currentSearcher;
- protected final ExecutorService es;
- protected final SearcherWarmer warmer;
- protected final Semaphore reopenLock = new Semaphore(1);
+ private volatile IndexSearcher currentSearcher;
+ private final ExecutorService es;
+ private final SearcherWarmer warmer;
+ private final Semaphore reopenLock = new Semaphore(1);
- protected SearcherManager(IndexReader openedReader, SearcherWarmer warmer,
+ /**
+ * Creates and returns a new SearcherManager from the given {@link IndexWriter}.
+ * @param writer the IndexWriter to open the IndexReader from.
+ * @param applyAllDeletes If true
, all buffered deletes will
+ * be applied (made visible) in the {@link IndexSearcher} / {@link IndexReader}.
+ * If false
, the deletes may or may not be applied, but remain buffered
+ * (in IndexWriter) so that they will be applied in the future.
+ * Applying deletes can be costly, so if your app can tolerate deleted documents
+ * being returned you might gain some performance by passing false
.
+ * See {@link IndexReader#openIfChanged(IndexReader, IndexWriter, boolean)}.
+ * @param warmer An optional {@link SearcherWarmer}. Pass
+ * null
if you don't require the searcher to warmed
+ * before going live. If this is non-null
then a
+ * merged segment warmer is installed on the
+ * provided IndexWriter's config.
+ * @param es An optional {@link ExecutorService} so different segments can
+ * be searched concurrently (see {@link
+ * IndexSearcher#IndexSearcher(IndexReader,ExecutorService)}. Pass null
+ * to search segments sequentially.
+ *
+ * @throws IOException
+ */
+ public SearcherManager(IndexWriter writer, boolean applyAllDeletes,
+ final SearcherWarmer warmer, final ExecutorService es) throws IOException {
+ this.es = es;
+ this.warmer = warmer;
+ currentSearcher = new IndexSearcher(IndexReader.open(writer, applyAllDeletes));
+ if (warmer != null) {
+ writer.getConfig().setMergedSegmentWarmer(
+ new IndexWriter.IndexReaderWarmer() {
+ @Override
+ public void warm(IndexReader reader) throws IOException {
+ warmer.warm(new IndexSearcher(reader, es));
+ }
+ });
+ }
+ }
+
+ /**
+ * Creates and returns a new SearcherManager from the given {@link Directory}.
+ * @param dir the directory to open the IndexReader on.
+ * @param warmer An optional {@link SearcherWarmer}. Pass
+ * null
if you don't require the searcher to warmed
+ * before going live. If this is non-null
then a
+ * merged segment warmer is installed on the
+ * provided IndexWriter's config.
+ * @param es And optional {@link ExecutorService} so different segments can
+ * be searched concurrently (see {@link
+ * IndexSearcher#IndexSearcher(IndexReader,ExecutorService)}. Pass null
+ * to search segments sequentially.
+ *
+ * @throws IOException
+ */
+ public SearcherManager(Directory dir, SearcherWarmer warmer,
ExecutorService es) throws IOException {
this.es = es;
this.warmer = warmer;
- currentSearcher = new IndexSearcher(openedReader, es);
+ currentSearcher = new IndexSearcher(IndexReader.open(dir, true), es);
}
/**
* You must call this, periodically, to perform a reopen. This calls
- * {@link #openIfChanged(IndexReader)} with the underlying reader, and if that returns a
+ * {@link IndexReader#openIfChanged(IndexReader)} with the underlying reader, and if that returns a
* new reader, it's warmed (if you provided a {@link SearcherWarmer} and then
* swapped into production.
*
@@ -103,7 +156,9 @@ public abstract class SearcherManager {
// threads just return immediately:
if (reopenLock.tryAcquire()) {
try {
- final IndexReader newReader = openIfChanged(currentSearcher.getIndexReader());
+ // IR.openIfChanged preserves NRT and applyDeletes
+ // in the newly returned reader:
+ final IndexReader newReader = IndexReader.openIfChanged(currentSearcher.getIndexReader());
if (newReader != null) {
final IndexSearcher newSearcher = new IndexSearcher(newReader, es);
boolean success = false;
@@ -190,122 +245,10 @@ public abstract class SearcherManager {
}
}
- protected synchronized void swapSearcher(IndexSearcher newSearcher) throws IOException {
+ private synchronized void swapSearcher(IndexSearcher newSearcher) throws IOException {
ensureOpen();
final IndexSearcher oldSearcher = currentSearcher;
currentSearcher = newSearcher;
release(oldSearcher);
}
-
- protected abstract IndexReader openIfChanged(IndexReader oldReader)
- throws IOException;
-
- /**
- * Creates and returns a new SearcherManager from the given {@link IndexWriter}.
- * @param writer the IndexWriter to open the IndexReader from.
- * @param applyAllDeletes If true
, all buffered deletes will
- * be applied (made visible) in the {@link IndexSearcher} / {@link IndexReader}.
- * If false
, the deletes are not applied but remain buffered
- * (in IndexWriter) so that they will be applied in the future.
- * Applying deletes can be costly, so if your app can tolerate deleted documents
- * being returned you might gain some performance by passing false
.
- * @param warmer An optional {@link SearcherWarmer}. Pass
- * null
if you don't require the searcher to warmed
- * before going live. If this is non-null
then a
- * merged segment warmer is installed on the
- * provided IndexWriter's config.
- * @param es An optional {@link ExecutorService} so different segments can
- * be searched concurrently (see {@link
- * IndexSearcher#IndexSearcher(IndexReader,ExecutorService)}. Pass null
- * to search segments sequentially.
- *
- * @see IndexReader#openIfChanged(IndexReader, IndexWriter, boolean)
- * @throws IOException
- */
- public static SearcherManager open(IndexWriter writer, boolean applyAllDeletes,
- SearcherWarmer warmer, ExecutorService es) throws IOException {
- final IndexReader open = IndexReader.open(writer, true);
- boolean success = false;
- try {
- SearcherManager manager = new NRTSearcherManager(writer, applyAllDeletes,
- open, warmer, es);
- success = true;
- return manager;
- } finally {
- if (!success) {
- open.close();
- }
- }
- }
-
- /**
- * Creates and returns a new SearcherManager from the given {@link Directory}.
- * @param dir the directory to open the IndexReader on.
- * @param warmer An optional {@link SearcherWarmer}. Pass
- * null
if you don't require the searcher to warmed
- * before going live. If this is non-null
then a
- * merged segment warmer is installed on the
- * provided IndexWriter's config.
- * @param es And optional {@link ExecutorService} so different segments can
- * be searched concurrently (see {@link
- * IndexSearcher#IndexSearcher(IndexReader,ExecutorService)}. Pass null
- * to search segments sequentially.
- *
- * @throws IOException
- */
- public static SearcherManager open(Directory dir, SearcherWarmer warmer,
- ExecutorService es) throws IOException {
- final IndexReader open = IndexReader.open(dir, true);
- boolean success = false;
- try {
- SearcherManager manager = new DirectorySearchManager(open, warmer, es);
- success = true;
- return manager;
- } finally {
- if (!success) {
- open.close();
- }
- }
- }
-
- static final class NRTSearcherManager extends SearcherManager {
- private final IndexWriter writer;
- private final boolean applyDeletes;
-
- NRTSearcherManager(final IndexWriter writer, final boolean applyDeletes,
- final IndexReader openedReader, final SearcherWarmer warmer, final ExecutorService es)
- throws IOException {
- super(openedReader, warmer, es);
- this.writer = writer;
- this.applyDeletes = applyDeletes;
- if (warmer != null) {
- writer.getConfig().setMergedSegmentWarmer(
- new IndexWriter.IndexReaderWarmer() {
- @Override
- public void warm(IndexReader reader) throws IOException {
- warmer.warm(new IndexSearcher(reader, es));
- }
- });
- }
- }
-
- @Override
- protected IndexReader openIfChanged(IndexReader oldReader)
- throws IOException {
- return IndexReader.openIfChanged(oldReader, writer, applyDeletes);
- }
- }
-
- static final class DirectorySearchManager extends SearcherManager {
- DirectorySearchManager(IndexReader openedReader,
- SearcherWarmer warmer, ExecutorService es) throws IOException {
- super(openedReader, warmer, es);
- }
-
- @Override
- protected IndexReader openIfChanged(IndexReader oldReader)
- throws IOException {
- return IndexReader.openIfChanged(oldReader, true);
- }
- }
}
diff --git a/lucene/contrib/misc/src/test/org/apache/lucene/search/TestSearcherManager.java b/lucene/contrib/misc/src/test/org/apache/lucene/search/TestSearcherManager.java
index 28327643215..c06d8f188fd 100644
--- a/lucene/contrib/misc/src/test/org/apache/lucene/search/TestSearcherManager.java
+++ b/lucene/contrib/misc/src/test/org/apache/lucene/search/TestSearcherManager.java
@@ -72,12 +72,12 @@ public class TestSearcherManager extends ThreadedIndexingAndSearchingTestCase {
}
};
if (random.nextBoolean()) {
- mgr = SearcherManager.open(writer, true, warmer, es);
+ mgr = new SearcherManager(writer, true, warmer, es);
isNRT = true;
} else {
// SearcherManager needs to see empty commit:
writer.commit();
- mgr = SearcherManager.open(dir, warmer, es);
+ mgr = new SearcherManager(dir, warmer, es);
isNRT = false;
}
@@ -198,8 +198,8 @@ public class TestSearcherManager extends ThreadedIndexingAndSearchingTestCase {
}
}
};
- final SearcherManager searcherManager = random.nextBoolean() ? SearcherManager.open(dir,
- warmer, es) : SearcherManager.open(writer, random.nextBoolean(), warmer, es);
+ final SearcherManager searcherManager = random.nextBoolean() ? new SearcherManager(dir,
+ warmer, es) : new SearcherManager(writer, random.nextBoolean(), warmer, es);
IndexSearcher searcher = searcherManager.acquire();
try {
assertEquals(1, searcher.getIndexReader().numDocs());
diff --git a/lucene/src/java/org/apache/lucene/index/DirectoryReader.java b/lucene/src/java/org/apache/lucene/index/DirectoryReader.java
index 6c9313491be..fe64a210aee 100644
--- a/lucene/src/java/org/apache/lucene/index/DirectoryReader.java
+++ b/lucene/src/java/org/apache/lucene/index/DirectoryReader.java
@@ -406,8 +406,15 @@ class DirectoryReader extends IndexReader implements Cloneable {
return doOpenIfChanged(true, commit);
}
- // NOTE: always returns a non-null result (ie new reader)
- // but that could change someday
+ @Override
+ protected final IndexReader doOpenIfChanged(IndexWriter writer, boolean applyAllDeletes) throws CorruptIndexException, IOException {
+ if (writer == this.writer && applyAllDeletes == this.applyAllDeletes) {
+ return doOpenIfChanged();
+ } else {
+ return super.doOpenIfChanged(writer, applyAllDeletes);
+ }
+ }
+
private final IndexReader doOpenFromWriter(boolean openReadOnly, IndexCommit commit) throws CorruptIndexException, IOException {
assert readOnly;
@@ -419,10 +426,18 @@ class DirectoryReader extends IndexReader implements Cloneable {
throw new IllegalArgumentException("a reader obtained from IndexWriter.getReader() cannot currently accept a commit");
}
- // TODO: right now we *always* make a new reader; in
- // the future we could have write make some effort to
- // detect that no changes have occurred
+ if (writer.nrtIsCurrent(segmentInfos)) {
+ return null;
+ }
+
IndexReader reader = writer.getReader(applyAllDeletes);
+
+ // If in fact no changes took place, return null:
+ if (reader.getVersion() == getVersion()) {
+ reader.decRef();
+ return null;
+ }
+
reader.readerFinishedListeners = readerFinishedListeners;
return reader;
}
diff --git a/lucene/src/java/org/apache/lucene/index/IndexReader.java b/lucene/src/java/org/apache/lucene/index/IndexReader.java
index 6d20ec7e06f..07a8b9e59c5 100644
--- a/lucene/src/java/org/apache/lucene/index/IndexReader.java
+++ b/lucene/src/java/org/apache/lucene/index/IndexReader.java
@@ -561,10 +561,6 @@ public abstract class IndexReader implements Cloneable,Closeable {
* with the old reader uses "copy on write" semantics to
* ensure the changes are not seen by other readers.
*
- *
NOTE: If the provided reader is a near real-time - * reader, this method will return another near-real-time - * reader. - * * @throws CorruptIndexException if the index is corrupt * @throws IOException if there is a low-level IO error * @return null if there are no changes; else, a new diff --git a/lucene/src/java/org/apache/lucene/index/IndexWriter.java b/lucene/src/java/org/apache/lucene/index/IndexWriter.java index 75688919666..0f5b2849465 100644 --- a/lucene/src/java/org/apache/lucene/index/IndexWriter.java +++ b/lucene/src/java/org/apache/lucene/index/IndexWriter.java @@ -4073,6 +4073,7 @@ public class IndexWriter implements Closeable, TwoPhaseCommit { synchronized boolean nrtIsCurrent(SegmentInfos infos) { //System.out.println("IW.nrtIsCurrent " + (infos.version == segmentInfos.version && !docWriter.anyChanges() && !bufferedDeletesStream.any())); + ensureOpen(); return infos.version == segmentInfos.version && !docWriter.anyChanges() && !bufferedDeletesStream.any(); } diff --git a/lucene/src/test-framework/org/apache/lucene/index/ThreadedIndexingAndSearchingTestCase.java b/lucene/src/test-framework/org/apache/lucene/index/ThreadedIndexingAndSearchingTestCase.java index 4c5d3a87766..425fe7b6568 100644 --- a/lucene/src/test-framework/org/apache/lucene/index/ThreadedIndexingAndSearchingTestCase.java +++ b/lucene/src/test-framework/org/apache/lucene/index/ThreadedIndexingAndSearchingTestCase.java @@ -55,7 +55,7 @@ import org.apache.lucene.util._TestUtil; // TODO // - mix in optimize, addIndexes -// - randomoly mix in non-congruent docs +// - randomly mix in non-congruent docs /** Utility class that spawns multiple indexing and * searching threads. */ diff --git a/lucene/src/test/org/apache/lucene/index/TestIndexWriterReader.java b/lucene/src/test/org/apache/lucene/index/TestIndexWriterReader.java index dffc9662afc..c012cdac5ff 100644 --- a/lucene/src/test/org/apache/lucene/index/TestIndexWriterReader.java +++ b/lucene/src/test/org/apache/lucene/index/TestIndexWriterReader.java @@ -857,7 +857,7 @@ public class TestIndexWriterReader extends LuceneTestCase { int sum = 0; while(System.currentTimeMillis() < endTime) { IndexReader r2 = IndexReader.openIfChanged(r); - if (r2 != r) { + if (r2 != null) { r.close(); r = r2; } @@ -1016,4 +1016,40 @@ public class TestIndexWriterReader extends LuceneTestCase { } } + public void testReopenAfterNoRealChange() throws Exception { + Directory d = newDirectory(); + IndexWriter w = new IndexWriter( + d, + newIndexWriterConfig(TEST_VERSION_CURRENT, new MockAnalyzer(random))); + w.setInfoStream(VERBOSE ? System.out : null); + + IndexReader r = w.getReader(); // start pooling readers + + IndexReader r2 = IndexReader.openIfChanged(r); + assertNull(r2); + + w.addDocument(new Document()); + IndexReader r3 = IndexReader.openIfChanged(r); + assertNotNull(r3); + assertTrue(r3.getVersion() != r.getVersion()); + assertTrue(r3.isCurrent()); + + // Deletes nothing in reality...: + w.deleteDocuments(new Term("foo", "bar")); + + // ... but IW marks this as not current: + assertFalse(r3.isCurrent()); + IndexReader r4 = IndexReader.openIfChanged(r3); + assertNull(r4); + + // Deletes nothing in reality...: + w.deleteDocuments(new Term("foo", "bar")); + IndexReader r5 = IndexReader.openIfChanged(r3, w, true); + assertNull(r5); + + r3.close(); + + w.close(); + d.close(); + } } diff --git a/lucene/src/test/org/apache/lucene/search/TestCachingSpanFilter.java b/lucene/src/test/org/apache/lucene/search/TestCachingSpanFilter.java index 702c2fa350c..eefeb301cf2 100644 --- a/lucene/src/test/org/apache/lucene/search/TestCachingSpanFilter.java +++ b/lucene/src/test/org/apache/lucene/search/TestCachingSpanFilter.java @@ -116,10 +116,10 @@ public class TestCachingSpanFilter extends LuceneTestCase { // make sure we get a cache hit when we reopen readers // that had no new deletions + // Deletes nothing: + writer.deleteDocuments(new Term("foo", "bar")); reader = refreshReader(reader); - assertTrue(reader != oldReader); - searcher.close(); - searcher = newSearcher(reader, false); + assertTrue(reader == oldReader); int missCount = filter.missCount; docs = searcher.search(constantScore, 1); assertEquals("[just filter] Should find a hit...", 1, docs.totalHits); diff --git a/lucene/src/test/org/apache/lucene/search/TestCachingWrapperFilter.java b/lucene/src/test/org/apache/lucene/search/TestCachingWrapperFilter.java index 48bf9915ef1..5bf9b7c08b3 100644 --- a/lucene/src/test/org/apache/lucene/search/TestCachingWrapperFilter.java +++ b/lucene/src/test/org/apache/lucene/search/TestCachingWrapperFilter.java @@ -235,10 +235,9 @@ public class TestCachingWrapperFilter extends LuceneTestCase { // make sure we get a cache hit when we reopen reader // that had no change to deletions + writer.deleteDocuments(new Term("foo", "bar")); reader = refreshReader(reader); - assertTrue(reader != oldReader); - searcher.close(); - searcher = newSearcher(reader, false); + assertTrue(reader == oldReader); int missCount = filter.missCount; docs = searcher.search(constantScore, 1); assertEquals("[just filter] Should find a hit...", 1, docs.totalHits);