blob: 63e7bc6ae5af6fee6cf32cd110c8790ab278641b [file] [log] [blame]
/*
* 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.apache.lucene.search;
import java.io.IOException;
import java.lang.reflect.Field;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.ConcurrentModificationException;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicLong;
import java.util.concurrent.atomic.AtomicReference;
import com.carrotsearch.randomizedtesting.generators.RandomPicks;
import org.apache.lucene.document.Document;
import org.apache.lucene.document.Field.Store;
import org.apache.lucene.document.LongPoint;
import org.apache.lucene.document.NumericDocValuesField;
import org.apache.lucene.document.SortedNumericDocValuesField;
import org.apache.lucene.document.StringField;
import org.apache.lucene.document.TextField;
import org.apache.lucene.index.DirectoryReader;
import org.apache.lucene.index.DocValues;
import org.apache.lucene.index.FilterDirectoryReader;
import org.apache.lucene.index.FilterLeafReader;
import org.apache.lucene.index.IndexReader;
import org.apache.lucene.index.IndexWriter;
import org.apache.lucene.index.IndexWriterConfig;
import org.apache.lucene.index.LeafReader;
import org.apache.lucene.index.LeafReaderContext;
import org.apache.lucene.index.NoMergePolicy;
import org.apache.lucene.index.RandomIndexWriter;
import org.apache.lucene.index.SerialMergeScheduler;
import org.apache.lucene.index.Term;
import org.apache.lucene.search.BooleanClause.Occur;
import org.apache.lucene.store.Directory;
import org.apache.lucene.util.Accountable;
import org.apache.lucene.util.Constants;
import org.apache.lucene.util.IOUtils;
import org.apache.lucene.util.LuceneTestCase;
import org.apache.lucene.util.RamUsageTester;
import org.apache.lucene.util.TestUtil;
import static org.apache.lucene.util.RamUsageEstimator.HASHTABLE_RAM_BYTES_PER_ENTRY;
import static org.apache.lucene.util.RamUsageEstimator.LINKED_HASHTABLE_RAM_BYTES_PER_ENTRY;
import static org.apache.lucene.util.RamUsageEstimator.QUERY_DEFAULT_RAM_BYTES_USED;
public class TestLRUQueryCache extends LuceneTestCase {
private static final QueryCachingPolicy ALWAYS_CACHE = new QueryCachingPolicy() {
@Override
public void onUse(Query query) {}
@Override
public boolean shouldCache(Query query) throws IOException {
return true;
}
};
private static final QueryCachingPolicy NEVER_CACHE = new QueryCachingPolicy() {
@Override
public void onUse(Query query) {}
@Override
public boolean shouldCache(Query query) throws IOException {
return false;
}
};
public void testConcurrency() throws Throwable {
final LRUQueryCache queryCache = new LRUQueryCache(1 + random().nextInt(20), 1 + random().nextInt(10000), context -> random().nextBoolean(), Float.POSITIVE_INFINITY);
Directory dir = newDirectory();
final RandomIndexWriter w = new RandomIndexWriter(random(), dir);
final SearcherFactory searcherFactory = new SearcherFactory() {
@Override
public IndexSearcher newSearcher(IndexReader reader, IndexReader previous) throws IOException {
IndexSearcher searcher = new IndexSearcher(reader);
searcher.setQueryCachingPolicy(MAYBE_CACHE_POLICY);
searcher.setQueryCache(queryCache);
return searcher;
}
};
final boolean applyDeletes = random().nextBoolean();
final SearcherManager mgr = new SearcherManager(w.w, applyDeletes, false, searcherFactory);
final AtomicBoolean indexing = new AtomicBoolean(true);
final AtomicReference<Throwable> error = new AtomicReference<>();
final int numDocs = atLeast(1000);
Thread[] threads = new Thread[3];
threads[0] = new Thread() {
public void run() {
Document doc = new Document();
StringField f = new StringField("color", "", Store.NO);
doc.add(f);
for (int i = 0; indexing.get() && i < numDocs; ++i) {
f.setStringValue(RandomPicks.randomFrom(random(), new String[] {"blue", "red", "yellow"}));
try {
w.addDocument(doc);
if ((i & 63) == 0) {
mgr.maybeRefresh();
if (rarely()) {
queryCache.clear();
}
if (rarely()) {
final String color = RandomPicks.randomFrom(random(), new String[] {"blue", "red", "yellow"});
w.deleteDocuments(new Term("color", color));
}
}
} catch (Throwable t) {
error.compareAndSet(null, t);
break;
}
}
indexing.set(false);
}
};
for (int i = 1; i < threads.length; ++i) {
threads[i] = new Thread() {
@Override
public void run() {
while (indexing.get()) {
try {
final IndexSearcher searcher = mgr.acquire();
try {
final String value = RandomPicks.randomFrom(random(), new String[] {"blue", "red", "yellow", "green"});
final Query q = new TermQuery(new Term("color", value));
TotalHitCountCollector collector = new TotalHitCountCollector();
searcher.search(q, collector); // will use the cache
final int totalHits1 = collector.getTotalHits();
TotalHitCountCollector collector2 = new TotalHitCountCollector();
searcher.search(q, new FilterCollector(collector2) {
public ScoreMode scoreMode() {
return ScoreMode.COMPLETE; // will not use the cache because of scores
}
});
final long totalHits2 = collector2.getTotalHits();
assertEquals(totalHits2, totalHits1);
} finally {
mgr.release(searcher);
}
} catch (Throwable t) {
error.compareAndSet(null, t);
}
}
}
};
}
for (Thread thread : threads) {
thread.start();
}
for (Thread thread : threads) {
thread.join();
}
try {
if (error.get() != null) {
throw error.get();
}
queryCache.assertConsistent();
} finally {
mgr.close();
w.close();
dir.close();
queryCache.assertConsistent();
}
}
public void testLRUEviction() throws Exception {
Directory dir = newDirectory();
final RandomIndexWriter w = new RandomIndexWriter(random(), dir);
Document doc = new Document();
StringField f = new StringField("color", "blue", Store.NO);
doc.add(f);
w.addDocument(doc);
f.setStringValue("red");
w.addDocument(doc);
f.setStringValue("green");
w.addDocument(doc);
final DirectoryReader reader = w.getReader();
final IndexSearcher searcher = newSearcher(reader);
final LRUQueryCache queryCache = new LRUQueryCache(2, 100000, context -> true, Float.POSITIVE_INFINITY);
final Query blue = new TermQuery(new Term("color", "blue"));
final Query red = new TermQuery(new Term("color", "red"));
final Query green = new TermQuery(new Term("color", "green"));
assertEquals(Collections.emptyList(), queryCache.cachedQueries());
searcher.setQueryCache(queryCache);
// the filter is not cached on any segment: no changes
searcher.setQueryCachingPolicy(NEVER_CACHE);
searcher.search(new ConstantScoreQuery(green), 1);
assertEquals(Collections.emptyList(), queryCache.cachedQueries());
searcher.setQueryCachingPolicy(ALWAYS_CACHE);
searcher.search(new ConstantScoreQuery(red), 1);
assertEquals(Collections.singletonList(red), queryCache.cachedQueries());
searcher.search(new ConstantScoreQuery(green), 1);
assertEquals(Arrays.asList(red, green), queryCache.cachedQueries());
searcher.search(new ConstantScoreQuery(red), 1);
assertEquals(Arrays.asList(green, red), queryCache.cachedQueries());
searcher.search(new ConstantScoreQuery(blue), 1);
assertEquals(Arrays.asList(red, blue), queryCache.cachedQueries());
searcher.search(new ConstantScoreQuery(blue), 1);
assertEquals(Arrays.asList(red, blue), queryCache.cachedQueries());
searcher.search(new ConstantScoreQuery(green), 1);
assertEquals(Arrays.asList(blue, green), queryCache.cachedQueries());
searcher.setQueryCachingPolicy(NEVER_CACHE);
searcher.search(new ConstantScoreQuery(red), 1);
assertEquals(Arrays.asList(blue, green), queryCache.cachedQueries());
reader.close();
w.close();
dir.close();
}
public void testClearFilter() throws IOException {
Directory dir = newDirectory();
final RandomIndexWriter w = new RandomIndexWriter(random(), dir);
Document doc = new Document();
StringField f = new StringField("color", "", Store.NO);
doc.add(f);
final int numDocs = atLeast(10);
for (int i = 0; i < numDocs; ++i) {
f.setStringValue(random().nextBoolean() ? "red" : "blue");
w.addDocument(doc);
}
final DirectoryReader reader = w.getReader();
final IndexSearcher searcher = newSearcher(reader);
final Query query1 = new TermQuery(new Term("color", "blue"));
// different instance yet equal
final Query query2 = new TermQuery(new Term("color", "blue"));
final LRUQueryCache queryCache = new LRUQueryCache(Integer.MAX_VALUE, Long.MAX_VALUE, context -> true, 1);
searcher.setQueryCache(queryCache);
searcher.setQueryCachingPolicy(ALWAYS_CACHE);
searcher.search(new BoostQuery(new ConstantScoreQuery(query1), random().nextFloat()), 1);
assertEquals(1, queryCache.cachedQueries().size());
queryCache.clearQuery(query2);
assertTrue(queryCache.cachedQueries().isEmpty());
queryCache.assertConsistent();
reader.close();
w.close();
dir.close();
}
// This test makes sure that by making the same assumptions as LRUQueryCache, RAMUsageTester
// computes the same memory usage.
public void testRamBytesUsedAgreesWithRamUsageTester() throws IOException {
assumeFalse("LUCENE-7595: RamUsageTester does not work exact in Java 9 (estimations for maps and lists)", Constants.JRE_IS_MINIMUM_JAVA9);
final LRUQueryCache queryCache = new LRUQueryCache(1 + random().nextInt(5), 1 + random().nextInt(10000), context -> random().nextBoolean(), Float.POSITIVE_INFINITY);
// an accumulator that only sums up memory usage of referenced filters and doc id sets
final RamUsageTester.Accumulator acc = new RamUsageTester.Accumulator() {
@Override
public long accumulateObject(Object o, long shallowSize, Map<Field,Object> fieldValues, Collection<Object> queue) {
if (o instanceof DocIdSet) {
return ((DocIdSet) o).ramBytesUsed();
}
if (o instanceof Query) {
return QUERY_DEFAULT_RAM_BYTES_USED;
}
if (o instanceof IndexReader || o.getClass().getSimpleName().equals("SegmentCoreReaders")) {
// do not take readers or core cache keys into account
return 0;
}
if (o instanceof Map) {
Map<?,?> map = (Map<?,?>) o;
queue.addAll(map.keySet());
queue.addAll(map.values());
final long sizePerEntry = o instanceof LinkedHashMap
? LINKED_HASHTABLE_RAM_BYTES_PER_ENTRY
: HASHTABLE_RAM_BYTES_PER_ENTRY;
return sizePerEntry * map.size();
}
// follow links to other objects, but ignore their memory usage
super.accumulateObject(o, shallowSize, fieldValues, queue);
return 0;
}
@Override
public long accumulateArray(Object array, long shallowSize, List<Object> values, Collection<Object> queue) {
// follow links to other objects, but ignore their memory usage
super.accumulateArray(array, shallowSize, values, queue);
return 0;
}
};
Directory dir = newDirectory();
// serial merges so that segments do not get closed while we are measuring ram usage
// with RamUsageTester
IndexWriterConfig iwc = newIndexWriterConfig().setMergeScheduler(new SerialMergeScheduler());
final RandomIndexWriter w = new RandomIndexWriter(random(), dir, iwc);
final List<String> colors = Arrays.asList("blue", "red", "green", "yellow");
Document doc = new Document();
StringField f = new StringField("color", "", Store.NO);
doc.add(f);
final int iters = atLeast(5);
for (int iter = 0; iter < iters; ++iter) {
final int numDocs = atLeast(10);
for (int i = 0; i < numDocs; ++i) {
f.setStringValue(RandomPicks.randomFrom(random(), colors));
w.addDocument(doc);
}
try (final DirectoryReader reader = w.getReader()) {
final IndexSearcher searcher = newSearcher(reader);
searcher.setQueryCache(queryCache);
searcher.setQueryCachingPolicy(MAYBE_CACHE_POLICY);
for (int i = 0; i < 3; ++i) {
final Query query = new TermQuery(new Term("color", RandomPicks.randomFrom(random(), colors)));
searcher.search(new ConstantScoreQuery(query), 1);
}
}
queryCache.assertConsistent();
assertEquals(RamUsageTester.sizeOf(queryCache, acc), queryCache.ramBytesUsed());
}
w.close();
dir.close();
}
/** A query that doesn't match anything */
private static class DummyQuery extends Query {
private static int COUNTER = 0;
private final int id;
DummyQuery() {
id = COUNTER++;
}
@Override
public Weight createWeight(IndexSearcher searcher, ScoreMode scoreMode, float boost) throws IOException {
return new ConstantScoreWeight(this, boost) {
@Override
public Scorer scorer(LeafReaderContext context) throws IOException {
return null;
}
@Override
public boolean isCacheable(LeafReaderContext ctx) {
return true;
}
};
}
@Override
public void visit(QueryVisitor visitor) {
}
@Override
public boolean equals(Object other) {
return sameClassAs(other) &&
id == ((DummyQuery) other).id;
}
@Override
public int hashCode() {
return id;
}
@Override
public String toString(String field) {
return "DummyQuery";
}
}
// Test what happens when the cache contains only filters and doc id sets
// that require very little memory. In that case most of the memory is taken
// by the cache itself, not cache entries, and we want to make sure that
// memory usage is not grossly underestimated.
public void testRamBytesUsedConstantEntryOverhead() throws IOException {
assumeFalse("LUCENE-7595: RamUsageTester does not work exact in Java 9 (estimations for maps and lists)", Constants.JRE_IS_MINIMUM_JAVA9);
final LRUQueryCache queryCache = new LRUQueryCache(1000000, 10000000, context -> true, Float.POSITIVE_INFINITY);
final RamUsageTester.Accumulator acc = new RamUsageTester.Accumulator() {
@Override
public long accumulateObject(Object o, long shallowSize, Map<Field,Object> fieldValues, Collection<Object> queue) {
if (o instanceof DocIdSet) {
return ((DocIdSet) o).ramBytesUsed();
}
if (o instanceof Query) {
return QUERY_DEFAULT_RAM_BYTES_USED;
}
if (o.getClass().getSimpleName().equals("SegmentCoreReaders")) {
// do not follow references to core cache keys
return 0;
}
return super.accumulateObject(o, shallowSize, fieldValues, queue);
}
};
Directory dir = newDirectory();
final RandomIndexWriter w = new RandomIndexWriter(random(), dir);
Document doc = new Document();
final int numDocs = atLeast(100);
for (int i = 0; i < numDocs; ++i) {
w.addDocument(doc);
}
final DirectoryReader reader = w.getReader();
final IndexSearcher searcher = new IndexSearcher(reader);
searcher.setQueryCache(queryCache);
searcher.setQueryCachingPolicy(ALWAYS_CACHE);
final int numQueries = atLeast(1000);
for (int i = 0; i < numQueries; ++i) {
final Query query = new DummyQuery();
searcher.search(new ConstantScoreQuery(query), 1);
}
assertTrue(queryCache.getCacheCount() > 0);
final long actualRamBytesUsed = RamUsageTester.sizeOf(queryCache, acc);
final long expectedRamBytesUsed = queryCache.ramBytesUsed();
// error < 30%
assertEquals(actualRamBytesUsed, expectedRamBytesUsed, 30 * actualRamBytesUsed / 100);
reader.close();
w.close();
dir.close();
}
/** DummyQuery with Accountable, pretending to be a memory-eating query */
private class AccountableDummyQuery extends DummyQuery implements Accountable {
@Override
public long ramBytesUsed() {
return 10 * QUERY_DEFAULT_RAM_BYTES_USED;
}
}
public void testCachingAccountableQuery() throws IOException {
final LRUQueryCache queryCache =
new LRUQueryCache(1000000, 10000000, context -> true, Float.POSITIVE_INFINITY);
Directory dir = newDirectory();
final RandomIndexWriter w = new RandomIndexWriter(random(), dir);
Document doc = new Document();
final int numDocs = atLeast(100);
for (int i = 0; i < numDocs; ++i) {
w.addDocument(doc);
}
final DirectoryReader reader = w.getReader();
final IndexSearcher searcher = new IndexSearcher(reader);
searcher.setQueryCache(queryCache);
searcher.setQueryCachingPolicy(ALWAYS_CACHE);
final int numQueries = random().nextInt(100) + 100;
for (int i = 0; i < numQueries; ++i) {
final Query query = new AccountableDummyQuery();
searcher.count(query);
}
long queryRamBytesUsed =
numQueries * (10 * QUERY_DEFAULT_RAM_BYTES_USED + LINKED_HASHTABLE_RAM_BYTES_PER_ENTRY);
// allow 10% error for other ram bytes used estimation inside query cache
assertEquals(queryRamBytesUsed, queryCache.ramBytesUsed(), 10 * queryRamBytesUsed / 100);
reader.close();
w.close();
dir.close();
}
public void testOnUse() throws IOException {
final LRUQueryCache queryCache = new LRUQueryCache(1 + random().nextInt(5), 1 + random().nextInt(1000), context -> random().nextBoolean(), Float.POSITIVE_INFINITY);
Directory dir = newDirectory();
final RandomIndexWriter w = new RandomIndexWriter(random(), dir);
Document doc = new Document();
StringField f = new StringField("color", "", Store.NO);
doc.add(f);
final int numDocs = atLeast(10);
for (int i = 0; i < numDocs; ++i) {
f.setStringValue(RandomPicks.randomFrom(random(), Arrays.asList("red", "blue", "green", "yellow")));
w.addDocument(doc);
if (random().nextBoolean()) {
w.getReader().close();
}
}
final DirectoryReader reader = w.getReader();
final IndexSearcher searcher = new IndexSearcher(reader);
final Map<Query, Integer> actualCounts = new HashMap<>();
final Map<Query, Integer> expectedCounts = new HashMap<>();
final QueryCachingPolicy countingPolicy = new QueryCachingPolicy() {
@Override
public boolean shouldCache(Query query) throws IOException {
return random().nextBoolean();
}
@Override
public void onUse(Query query) {
expectedCounts.put(query, 1 + expectedCounts.getOrDefault(query, 0));
}
};
Query[] queries = new Query[10 + random().nextInt(10)];
for (int i = 0; i < queries.length; ++i) {
queries[i] = new BoostQuery(new TermQuery(new Term("color", RandomPicks.randomFrom(random(), Arrays.asList("red", "blue", "green", "yellow")))), random().nextFloat());
}
searcher.setQueryCache(queryCache);
searcher.setQueryCachingPolicy(countingPolicy);
for (int i = 0; i < 20; ++i) {
final int idx = random().nextInt(queries.length);
searcher.search(new ConstantScoreQuery(queries[idx]), 1);
Query cacheKey = queries[idx];
while (cacheKey instanceof BoostQuery) {
cacheKey = ((BoostQuery) cacheKey).getQuery();
}
actualCounts.put(cacheKey, 1 + actualCounts.getOrDefault(cacheKey, 0));
}
assertEquals(actualCounts, expectedCounts);
reader.close();
w.close();
dir.close();
}
public void testStats() throws IOException {
final LRUQueryCache queryCache = new LRUQueryCache(1, 10000000, context -> true, 1);
Directory dir = newDirectory();
final RandomIndexWriter w = new RandomIndexWriter(random(), dir);
final List<String> colors = Arrays.asList("blue", "red", "green", "yellow");
Document doc = new Document();
StringField f = new StringField("color", "", Store.NO);
doc.add(f);
for (int i = 0; i < 10; ++i) {
f.setStringValue(RandomPicks.randomFrom(random(), colors));
w.addDocument(doc);
if (random().nextBoolean()) {
w.getReader().close();
}
}
final DirectoryReader reader = w.getReader();
final int segmentCount = reader.leaves().size();
final IndexSearcher searcher = new IndexSearcher(reader);
final Query query = new TermQuery(new Term("color", "red"));
final Query query2 = new TermQuery(new Term("color", "blue"));
searcher.setQueryCache(queryCache);
// first pass, lookups without caching that all miss
searcher.setQueryCachingPolicy(NEVER_CACHE);
for (int i = 0; i < 10; ++i) {
searcher.search(new ConstantScoreQuery(query), 1);
}
assertEquals(10 * segmentCount, queryCache.getTotalCount());
assertEquals(0, queryCache.getHitCount());
assertEquals(10 * segmentCount, queryCache.getMissCount());
assertEquals(0, queryCache.getCacheCount());
assertEquals(0, queryCache.getEvictionCount());
assertEquals(0, queryCache.getCacheSize());
// second pass, lookups + caching, only the first one is a miss
searcher.setQueryCachingPolicy(ALWAYS_CACHE);
for (int i = 0; i < 10; ++i) {
searcher.search(new ConstantScoreQuery(query), 1);
}
assertEquals(20 * segmentCount, queryCache.getTotalCount());
assertEquals(9 * segmentCount, queryCache.getHitCount());
assertEquals(11 * segmentCount, queryCache.getMissCount());
assertEquals(1 * segmentCount, queryCache.getCacheCount());
assertEquals(0, queryCache.getEvictionCount());
assertEquals(1 * segmentCount, queryCache.getCacheSize());
// third pass lookups without caching, we only have hits
searcher.setQueryCachingPolicy(NEVER_CACHE);
for (int i = 0; i < 10; ++i) {
searcher.search(new ConstantScoreQuery(query), 1);
}
assertEquals(30 * segmentCount, queryCache.getTotalCount());
assertEquals(19 * segmentCount, queryCache.getHitCount());
assertEquals(11 * segmentCount, queryCache.getMissCount());
assertEquals(1 * segmentCount, queryCache.getCacheCount());
assertEquals(0, queryCache.getEvictionCount());
assertEquals(1 * segmentCount, queryCache.getCacheSize());
// fourth pass with a different filter which will trigger evictions since the size is 1
searcher.setQueryCachingPolicy(ALWAYS_CACHE);
for (int i = 0; i < 10; ++i) {
searcher.search(new ConstantScoreQuery(query2), 1);
}
assertEquals(40 * segmentCount, queryCache.getTotalCount());
assertEquals(28 * segmentCount, queryCache.getHitCount());
assertEquals(12 * segmentCount, queryCache.getMissCount());
assertEquals(2 * segmentCount, queryCache.getCacheCount());
assertEquals(1 * segmentCount, queryCache.getEvictionCount());
assertEquals(1 * segmentCount, queryCache.getCacheSize());
// now close, causing evictions due to the closing of segment cores
reader.close();
w.close();
assertEquals(40 * segmentCount, queryCache.getTotalCount());
assertEquals(28 * segmentCount, queryCache.getHitCount());
assertEquals(12 * segmentCount, queryCache.getMissCount());
assertEquals(2 * segmentCount, queryCache.getCacheCount());
assertEquals(2 * segmentCount, queryCache.getEvictionCount());
assertEquals(0, queryCache.getCacheSize());
dir.close();
}
public void testFineGrainedStats() throws IOException {
Directory dir1 = newDirectory();
final RandomIndexWriter w1 = new RandomIndexWriter(random(), dir1);
Directory dir2 = newDirectory();
final RandomIndexWriter w2 = new RandomIndexWriter(random(), dir2);
final List<String> colors = Arrays.asList("blue", "red", "green", "yellow");
Document doc = new Document();
StringField f = new StringField("color", "", Store.NO);
doc.add(f);
for (RandomIndexWriter w : Arrays.asList(w1, w2)) {
for (int i = 0; i < 10; ++i) {
f.setStringValue(RandomPicks.randomFrom(random(), colors));
w.addDocument(doc);
if (random().nextBoolean()) {
w.getReader().close();
}
}
}
final DirectoryReader reader1 = w1.getReader();
final int segmentCount1 = reader1.leaves().size();
final IndexSearcher searcher1 = new IndexSearcher(reader1);
final DirectoryReader reader2 = w2.getReader();
final int segmentCount2 = reader2.leaves().size();
final IndexSearcher searcher2 = new IndexSearcher(reader2);
final Map<IndexReader.CacheKey, Integer> indexId = new HashMap<>();
for (LeafReaderContext ctx : reader1.leaves()) {
indexId.put(ctx.reader().getCoreCacheHelper().getKey(), 1);
}
for (LeafReaderContext ctx : reader2.leaves()) {
indexId.put(ctx.reader().getCoreCacheHelper().getKey(), 2);
}
final AtomicLong hitCount1 = new AtomicLong();
final AtomicLong hitCount2 = new AtomicLong();
final AtomicLong missCount1 = new AtomicLong();
final AtomicLong missCount2 = new AtomicLong();
final AtomicLong ramBytesUsage = new AtomicLong();
final AtomicLong cacheSize = new AtomicLong();
final LRUQueryCache queryCache = new LRUQueryCache(2, 10000000, context -> true, 1) {
@Override
protected void onHit(Object readerCoreKey, Query query) {
super.onHit(readerCoreKey, query);
switch(indexId.get(readerCoreKey).intValue()) {
case 1:
hitCount1.incrementAndGet();
break;
case 2:
hitCount2.incrementAndGet();
break;
default:
throw new AssertionError();
}
}
@Override
protected void onMiss(Object readerCoreKey, Query query) {
super.onMiss(readerCoreKey, query);
switch(indexId.get(readerCoreKey).intValue()) {
case 1:
missCount1.incrementAndGet();
break;
case 2:
missCount2.incrementAndGet();
break;
default:
throw new AssertionError();
}
}
@Override
protected void onQueryCache(Query query, long ramBytesUsed) {
super.onQueryCache(query, ramBytesUsed);
assertNotNull("cached query is null", query);
ramBytesUsage.addAndGet(ramBytesUsed);
}
@Override
protected void onQueryEviction(Query query, long ramBytesUsed) {
super.onQueryEviction(query, ramBytesUsed);
assertNotNull("evicted query is null", query);
ramBytesUsage.addAndGet(-ramBytesUsed);
}
@Override
protected void onDocIdSetCache(Object readerCoreKey, long ramBytesUsed) {
super.onDocIdSetCache(readerCoreKey, ramBytesUsed);
ramBytesUsage.addAndGet(ramBytesUsed);
cacheSize.incrementAndGet();
}
@Override
protected void onDocIdSetEviction(Object readerCoreKey, int numEntries, long sumRamBytesUsed) {
super.onDocIdSetEviction(readerCoreKey, numEntries, sumRamBytesUsed);
ramBytesUsage.addAndGet(-sumRamBytesUsed);
cacheSize.addAndGet(-numEntries);
}
@Override
protected void onClear() {
super.onClear();
ramBytesUsage.set(0);
cacheSize.set(0);
}
};
final Query query = new TermQuery(new Term("color", "red"));
final Query query2 = new TermQuery(new Term("color", "blue"));
final Query query3 = new TermQuery(new Term("color", "green"));
for (IndexSearcher searcher : Arrays.asList(searcher1, searcher2)) {
searcher.setQueryCache(queryCache);
searcher.setQueryCachingPolicy(ALWAYS_CACHE);
}
// search on searcher1
for (int i = 0; i < 10; ++i) {
searcher1.search(new ConstantScoreQuery(query), 1);
}
assertEquals(9 * segmentCount1, hitCount1.longValue());
assertEquals(0, hitCount2.longValue());
assertEquals(segmentCount1, missCount1.longValue());
assertEquals(0, missCount2.longValue());
// then on searcher2
for (int i = 0; i < 20; ++i) {
searcher2.search(new ConstantScoreQuery(query2), 1);
}
assertEquals(9 * segmentCount1, hitCount1.longValue());
assertEquals(19 * segmentCount2, hitCount2.longValue());
assertEquals(segmentCount1, missCount1.longValue());
assertEquals(segmentCount2, missCount2.longValue());
// now on searcher1 again to trigger evictions
for (int i = 0; i < 30; ++i) {
searcher1.search(new ConstantScoreQuery(query3), 1);
}
assertEquals(segmentCount1, queryCache.getEvictionCount());
assertEquals(38 * segmentCount1, hitCount1.longValue());
assertEquals(19 * segmentCount2, hitCount2.longValue());
assertEquals(2 * segmentCount1, missCount1.longValue());
assertEquals(segmentCount2, missCount2.longValue());
// check that the recomputed stats are the same as those reported by the cache
assertEquals(queryCache.ramBytesUsed(), (segmentCount1 + segmentCount2) * HASHTABLE_RAM_BYTES_PER_ENTRY + ramBytesUsage.longValue());
assertEquals(queryCache.getCacheSize(), cacheSize.longValue());
reader1.close();
reader2.close();
w1.close();
w2.close();
assertEquals(queryCache.ramBytesUsed(), ramBytesUsage.longValue());
assertEquals(0, cacheSize.longValue());
queryCache.clear();
assertEquals(0, ramBytesUsage.longValue());
assertEquals(0, cacheSize.longValue());
dir1.close();
dir2.close();
}
public void testUseRewrittenQueryAsCacheKey() throws IOException {
final Query expectedCacheKey = new TermQuery(new Term("foo", "bar"));
final BooleanQuery.Builder query = new BooleanQuery.Builder();
query.add(new BoostQuery(expectedCacheKey, 42f), Occur.MUST);
final LRUQueryCache queryCache = new LRUQueryCache(1000000, 10000000, context -> random().nextBoolean(), Float.POSITIVE_INFINITY);
Directory dir = newDirectory();
final RandomIndexWriter w = new RandomIndexWriter(random(), dir);
Document doc = new Document();
doc.add(new StringField("foo", "bar", Store.YES));
w.addDocument(doc);
w.commit();
final IndexReader reader = w.getReader();
final IndexSearcher searcher = newSearcher(reader);
w.close();
final QueryCachingPolicy policy = new QueryCachingPolicy() {
@Override
public boolean shouldCache(Query query) throws IOException {
assertEquals(expectedCacheKey, query);
return true;
}
@Override
public void onUse(Query query) {
assertEquals(expectedCacheKey, query);
}
};
searcher.setQueryCache(queryCache);
searcher.setQueryCachingPolicy(policy);
searcher.search(query.build(), new TotalHitCountCollector());
reader.close();
dir.close();
}
public void testBooleanQueryCachesSubClauses() throws IOException {
Directory dir = newDirectory();
final RandomIndexWriter w = new RandomIndexWriter(random(), dir);
Document doc = new Document();
doc.add(new StringField("foo", "bar", Store.YES));
doc.add(new StringField("foo", "quux", Store.YES));
w.addDocument(doc);
w.commit();
final IndexReader reader = w.getReader();
final IndexSearcher searcher = newSearcher(reader);
w.close();
final LRUQueryCache queryCache = new LRUQueryCache(1000000, 10000000, context -> true, Float.POSITIVE_INFINITY);
searcher.setQueryCache(queryCache);
searcher.setQueryCachingPolicy(ALWAYS_CACHE);
BooleanQuery.Builder bq = new BooleanQuery.Builder();
TermQuery must = new TermQuery(new Term("foo", "bar"));
TermQuery filter = new TermQuery(new Term("foo", "quux"));
TermQuery mustNot = new TermQuery(new Term("foo", "foo"));
bq.add(must, Occur.MUST);
bq.add(filter, Occur.FILTER);
bq.add(mustNot, Occur.MUST_NOT);
// same bq but with FILTER instead of MUST
BooleanQuery.Builder bq2 = new BooleanQuery.Builder();
bq2.add(must, Occur.FILTER);
bq2.add(filter, Occur.FILTER);
bq2.add(mustNot, Occur.MUST_NOT);
assertEquals(Collections.emptySet(), new HashSet<>(queryCache.cachedQueries()));
searcher.search(bq.build(), 1);
assertEquals(new HashSet<>(Arrays.asList(filter, mustNot)), new HashSet<>(queryCache.cachedQueries()));
queryCache.clear();
assertEquals(Collections.emptySet(), new HashSet<>(queryCache.cachedQueries()));
searcher.search(new ConstantScoreQuery(bq.build()), 1);
assertEquals(new HashSet<>(Arrays.asList(bq2.build(), must, filter, mustNot)), new HashSet<>(queryCache.cachedQueries()));
reader.close();
dir.close();
}
private static Term randomTerm() {
final String term = RandomPicks.randomFrom(random(), Arrays.asList("foo", "bar", "baz"));
return new Term("foo", term);
}
private static Query buildRandomQuery(int level) {
if (level == 10) {
// at most 10 levels
return new MatchAllDocsQuery();
}
switch (random().nextInt(6)) {
case 0:
return new TermQuery(randomTerm());
case 1:
BooleanQuery.Builder bq = new BooleanQuery.Builder();
final int numClauses = TestUtil.nextInt(random(), 1, 3);
int numShould = 0;
for (int i = 0; i < numClauses; ++i) {
final Occur occur = RandomPicks.randomFrom(random(), Occur.values());
bq.add(buildRandomQuery(level + 1), occur);
if (occur == Occur.SHOULD) {
numShould++;
}
}
bq.setMinimumNumberShouldMatch(TestUtil.nextInt(random(), 0, numShould));
return bq.build();
case 2:
Term t1 = randomTerm();
Term t2 = randomTerm();
PhraseQuery pq = new PhraseQuery(random().nextInt(2), t1.field(), t1.bytes(), t2.bytes());
return pq;
case 3:
return new MatchAllDocsQuery();
case 4:
return new ConstantScoreQuery(buildRandomQuery(level + 1));
case 5:
List<Query> disjuncts = new ArrayList<>();
final int numQueries = TestUtil.nextInt(random(), 1, 3);
for (int i = 0; i < numQueries; ++i) {
disjuncts.add(buildRandomQuery(level + 1));
}
return new DisjunctionMaxQuery(disjuncts, random().nextFloat());
default:
throw new AssertionError();
}
}
public void testRandom() throws IOException {
Directory dir = newDirectory();
final RandomIndexWriter w = new RandomIndexWriter(random(), dir);
Document doc = new Document();
TextField f = new TextField("foo", "foo", Store.NO);
doc.add(f);
w.addDocument(doc);
IndexReader reader = w.getReader();
final int maxSize;
final long maxRamBytesUsed;
final int iters;
if (TEST_NIGHTLY) {
maxSize = TestUtil.nextInt(random(), 1, 10000);
maxRamBytesUsed = TestUtil.nextLong(random(), 1, 5000000);
iters = atLeast(20000);
} else {
maxSize = TestUtil.nextInt(random(), 1, 1000);
maxRamBytesUsed = TestUtil.nextLong(random(), 1, 500000);
iters = atLeast(2000);
}
final LRUQueryCache queryCache = new LRUQueryCache(maxSize, maxRamBytesUsed, context -> random().nextBoolean(), Float.POSITIVE_INFINITY);
IndexSearcher uncachedSearcher = null;
IndexSearcher cachedSearcher = null;
for (int i = 0; i < iters; ++i) {
if (i == 0 || random().nextInt(100) == 1) {
reader.close();
f.setStringValue(RandomPicks.randomFrom(random(), Arrays.asList("foo", "bar", "bar baz")));
w.addDocument(doc);
if (random().nextBoolean()) {
w.deleteDocuments(buildRandomQuery(0));
}
reader = w.getReader();
uncachedSearcher = newSearcher(reader);
uncachedSearcher.setQueryCache(null);
cachedSearcher = newSearcher(reader);
cachedSearcher.setQueryCache(queryCache);
cachedSearcher.setQueryCachingPolicy(ALWAYS_CACHE);
}
final Query q = buildRandomQuery(0);
assertEquals(uncachedSearcher.count(q), cachedSearcher.count(q));
if (rarely()) {
queryCache.assertConsistent();
}
}
queryCache.assertConsistent();
w.close();
reader.close();
dir.close();
queryCache.assertConsistent();
}
private static class BadQuery extends Query {
int[] i = new int[] {42}; // an array so that clone keeps the reference
@Override
public Weight createWeight(IndexSearcher searcher, ScoreMode scoreMode, float boost) throws IOException {
return new ConstantScoreWeight(this, boost) {
@Override
public Scorer scorer(LeafReaderContext context) throws IOException {
return null;
}
@Override
public boolean isCacheable(LeafReaderContext ctx) {
return true;
}
};
}
@Override
public void visit(QueryVisitor visitor) {
}
@Override
public String toString(String field) {
return "BadQuery";
}
@Override
public int hashCode() {
return classHash() ^ i[0];
}
@Override
public boolean equals(Object other) {
return sameClassAs(other) &&
i[0] == ((BadQuery) other).i[0];
}
}
public void testDetectMutatedQueries() throws IOException {
LuceneTestCase.assumeFalse("LUCENE-7604: For some unknown reason the non-constant BadQuery#hashCode() does not trigger ConcurrentModificationException on Java 9 b150",
Constants.JRE_IS_MINIMUM_JAVA9);
Directory dir = newDirectory();
final RandomIndexWriter w = new RandomIndexWriter(random(), dir);
w.addDocument(new Document());
IndexReader reader = w.getReader();
// size of 1 so that 2nd query evicts from the cache
final LRUQueryCache queryCache = new LRUQueryCache(1, 10000, context -> true, Float.POSITIVE_INFINITY);
final IndexSearcher searcher = newSearcher(reader);
searcher.setQueryCache(queryCache);
searcher.setQueryCachingPolicy(ALWAYS_CACHE);
BadQuery query = new BadQuery();
searcher.count(query);
query.i[0] += 1; // change the hashCode!
try {
// trigger an eviction
searcher.search(new MatchAllDocsQuery(), new TotalHitCountCollector());
fail();
} catch (ConcurrentModificationException e) {
// expected
} catch (RuntimeException e) {
// expected: wrapped when executor is in use
Throwable cause = e.getCause();
assertTrue(cause instanceof ExecutionException);
assertTrue(cause.getCause() instanceof ConcurrentModificationException);
}
IOUtils.close(w, reader, dir);
}
public void testRefuseToCacheTooLargeEntries() throws IOException {
Directory dir = newDirectory();
final RandomIndexWriter w = new RandomIndexWriter(random(), dir);
for (int i = 0; i < 100; ++i) {
w.addDocument(new Document());
}
IndexReader reader = w.getReader();
// size of 1 byte
final LRUQueryCache queryCache = new LRUQueryCache(1, 1, context -> random().nextBoolean(), Float.POSITIVE_INFINITY);
final IndexSearcher searcher = newSearcher(reader);
searcher.setQueryCache(queryCache);
searcher.setQueryCachingPolicy(ALWAYS_CACHE);
searcher.count(new MatchAllDocsQuery());
assertEquals(0, queryCache.getCacheCount());
assertEquals(0, queryCache.getEvictionCount());
reader.close();
w.close();
dir.close();
}
/**
* Tests CachingWrapperWeight.scorer() propagation of {@link QueryCachingPolicy#onUse(Query)} when the first segment
* is skipped.
*
* #f:foo #f:bar causes all frequencies to increment
* #f:bar #f:foo does not increment the frequency for f:foo
*/
public void testOnUseWithRandomFirstSegmentSkipping() throws IOException {
try (final Directory directory = newDirectory()) {
try (final RandomIndexWriter indexWriter = new RandomIndexWriter(random(), directory, newIndexWriterConfig().setMergePolicy(NoMergePolicy.INSTANCE))) {
Document doc = new Document();
doc.add(new StringField("f", "bar", Store.NO));
indexWriter.addDocument(doc);
if (random().nextBoolean()) {
indexWriter.getReader().close();
}
doc = new Document();
doc.add(new StringField("f", "foo", Store.NO));
doc.add(new StringField("f", "bar", Store.NO));
indexWriter.addDocument(doc);
indexWriter.commit();
}
try (final IndexReader indexReader = DirectoryReader.open(directory)) {
final FrequencyCountingPolicy policy = new FrequencyCountingPolicy();
final IndexSearcher indexSearcher = new IndexSearcher(indexReader);
indexSearcher.setQueryCache(new LRUQueryCache(100, 10240, context -> random().nextBoolean(), Float.POSITIVE_INFINITY));
indexSearcher.setQueryCachingPolicy(policy);
final Query foo = new TermQuery(new Term("f", "foo"));
final Query bar = new TermQuery(new Term("f", "bar"));
final BooleanQuery.Builder query = new BooleanQuery.Builder();
if (random().nextBoolean()) {
query.add(foo, Occur.FILTER);
query.add(bar, Occur.FILTER);
} else {
query.add(bar, Occur.FILTER);
query.add(foo, Occur.FILTER);
}
indexSearcher.count(query.build());
assertEquals(1, policy.frequency(query.build()));
assertEquals(1, policy.frequency(foo));
assertEquals(1, policy.frequency(bar));
}
}
}
private static class FrequencyCountingPolicy implements QueryCachingPolicy {
private final Map<Query,AtomicInteger> counts = new HashMap<>();
public int frequency(final Query query) {
AtomicInteger count;
synchronized (counts) {
count = counts.get(query);
}
return count != null ? count.get() : 0;
}
@Override
public void onUse(final Query query) {
AtomicInteger count;
synchronized (counts) {
count = counts.get(query);
if (count == null) {
count = new AtomicInteger();
counts.put(query, count);
}
}
count.incrementAndGet();
}
@Override
public boolean shouldCache(Query query) throws IOException {
return true;
}
}
private static class WeightWrapper extends FilterWeight {
private final AtomicBoolean scorerCalled;
private final AtomicBoolean bulkScorerCalled;
protected WeightWrapper(Weight in, AtomicBoolean scorerCalled, AtomicBoolean bulkScorerCalled) {
super(in);
this.scorerCalled = scorerCalled;
this.bulkScorerCalled = bulkScorerCalled;
}
@Override
public Scorer scorer(LeafReaderContext context) throws IOException {
scorerCalled.set(true);
return in.scorer(context);
}
@Override
public BulkScorer bulkScorer(LeafReaderContext context) throws IOException {
bulkScorerCalled.set(true);
return in.bulkScorer(context);
}
}
public void testPropagateBulkScorer() throws IOException {
Directory dir = newDirectory();
RandomIndexWriter w = new RandomIndexWriter(random(), dir);
w.addDocument(new Document());
IndexReader reader = w.getReader();
w.close();
IndexSearcher searcher = newSearcher(reader);
LeafReaderContext leaf = searcher.getIndexReader().leaves().get(0);
AtomicBoolean scorerCalled = new AtomicBoolean();
AtomicBoolean bulkScorerCalled = new AtomicBoolean();
LRUQueryCache cache = new LRUQueryCache(1, Long.MAX_VALUE, context -> true, Float.POSITIVE_INFINITY);
// test that the bulk scorer is propagated when a scorer should not be cached
Weight weight = searcher.createWeight(new MatchAllDocsQuery(), ScoreMode.COMPLETE_NO_SCORES, 1);
weight = new WeightWrapper(weight, scorerCalled, bulkScorerCalled);
weight = cache.doCache(weight, NEVER_CACHE);
weight.bulkScorer(leaf);
assertEquals(true, bulkScorerCalled.get());
assertEquals(false, scorerCalled.get());
assertEquals(0, cache.getCacheCount());
searcher.getIndexReader().close();
dir.close();
}
public void testEvictEmptySegmentCache() throws IOException {
Directory dir = newDirectory();
final RandomIndexWriter w = new RandomIndexWriter(random(), dir);
w.addDocument(new Document());
final DirectoryReader reader = w.getReader();
final IndexSearcher searcher = newSearcher(reader);
final LRUQueryCache queryCache = new LRUQueryCache(2, 100000, context -> true, Float.POSITIVE_INFINITY) {
@Override
protected void onDocIdSetEviction(Object readerCoreKey, int numEntries, long sumRamBytesUsed) {
super.onDocIdSetEviction(readerCoreKey, numEntries, sumRamBytesUsed);
assertTrue(numEntries > 0);
}
};
searcher.setQueryCache(queryCache);
searcher.setQueryCachingPolicy(ALWAYS_CACHE);
Query query = new DummyQuery();
searcher.count(query);
assertEquals(Collections.singletonList(query), queryCache.cachedQueries());
queryCache.clearQuery(query);
reader.close(); // make sure this does not trigger eviction of segment caches with no entries
w.close();
dir.close();
}
public void testMinSegmentSizePredicate() throws IOException {
Directory dir = newDirectory();
IndexWriterConfig iwc = newIndexWriterConfig().setMergePolicy(NoMergePolicy.INSTANCE);
RandomIndexWriter w = new RandomIndexWriter(random(), dir, iwc);
w.addDocument(new Document());
DirectoryReader reader = w.getReader();
IndexSearcher searcher = newSearcher(reader);
searcher.setQueryCachingPolicy(ALWAYS_CACHE);
LRUQueryCache cache = new LRUQueryCache(2, 10000, new LRUQueryCache.MinSegmentSizePredicate(2, 0f), Float.POSITIVE_INFINITY);
searcher.setQueryCache(cache);
searcher.count(new DummyQuery());
assertEquals(0, cache.getCacheCount());
cache = new LRUQueryCache(2, 10000, new LRUQueryCache.MinSegmentSizePredicate(1, 0f), Float.POSITIVE_INFINITY);
searcher.setQueryCache(cache);
searcher.count(new DummyQuery());
assertEquals(1, cache.getCacheCount());
cache = new LRUQueryCache(2, 10000, new LRUQueryCache.MinSegmentSizePredicate(0, .6f), Float.POSITIVE_INFINITY);
searcher.setQueryCache(cache);
searcher.count(new DummyQuery());
assertEquals(1, cache.getCacheCount());
w.addDocument(new Document());
reader.close();
reader = w.getReader();
searcher = newSearcher(reader);
searcher.setQueryCachingPolicy(ALWAYS_CACHE);
cache = new LRUQueryCache(2, 10000, new LRUQueryCache.MinSegmentSizePredicate(0, .6f), Float.POSITIVE_INFINITY);
searcher.setQueryCache(cache);
searcher.count(new DummyQuery());
assertEquals(0, cache.getCacheCount());
reader.close();
w.close();
dir.close();
}
// a reader whose sole purpose is to not be cacheable
private static class DummyDirectoryReader extends FilterDirectoryReader {
public DummyDirectoryReader(DirectoryReader in) throws IOException {
super(in, new SubReaderWrapper() {
@Override
public LeafReader wrap(LeafReader reader) {
return new FilterLeafReader(reader) {
@Override
public CacheHelper getCoreCacheHelper() {
return null;
}
@Override
public CacheHelper getReaderCacheHelper() {
return null;
}
};
}
});
}
@Override
protected DirectoryReader doWrapDirectoryReader(DirectoryReader in) throws IOException {
return new DummyDirectoryReader(in);
}
@Override
public CacheHelper getReaderCacheHelper() {
return null;
}
}
public void testReaderNotSuitedForCaching() throws IOException {
Directory dir = newDirectory();
IndexWriterConfig iwc = newIndexWriterConfig().setMergePolicy(NoMergePolicy.INSTANCE);
RandomIndexWriter w = new RandomIndexWriter(random(), dir, iwc);
w.addDocument(new Document());
DirectoryReader reader = new DummyDirectoryReader(w.getReader());
IndexSearcher searcher = newSearcher(reader);
searcher.setQueryCachingPolicy(ALWAYS_CACHE);
// don't cache if the reader does not expose a cache helper
assertNull(reader.leaves().get(0).reader().getCoreCacheHelper());
LRUQueryCache cache = new LRUQueryCache(2, 10000, context -> true, Float.POSITIVE_INFINITY);
searcher.setQueryCache(cache);
assertEquals(0, searcher.count(new DummyQuery()));
assertEquals(0, cache.getCacheCount());
reader.close();
w.close();
dir.close();
}
// A query that returns null from Weight.getCacheHelper
private static class NoCacheQuery extends Query {
@Override
public Weight createWeight(IndexSearcher searcher, ScoreMode scoreMode, float boost) throws IOException {
return new Weight(this) {
@Override
public void extractTerms(Set<Term> terms) {
}
@Override
public Explanation explain(LeafReaderContext context, int doc) throws IOException {
return null;
}
@Override
public Scorer scorer(LeafReaderContext context) throws IOException {
return null;
}
@Override
public boolean isCacheable(LeafReaderContext ctx) {
return false;
}
};
}
@Override
public void visit(QueryVisitor visitor) {
}
@Override
public String toString(String field) {
return "NoCacheQuery";
}
@Override
public boolean equals(Object obj) {
return sameClassAs(obj);
}
@Override
public int hashCode() {
return 0;
}
}
public void testQueryNotSuitedForCaching() throws IOException {
Directory dir = newDirectory();
IndexWriterConfig iwc = newIndexWriterConfig().setMergePolicy(NoMergePolicy.INSTANCE);
RandomIndexWriter w = new RandomIndexWriter(random(), dir, iwc);
w.addDocument(new Document());
DirectoryReader reader = w.getReader();
IndexSearcher searcher = newSearcher(reader);
searcher.setQueryCachingPolicy(ALWAYS_CACHE);
LRUQueryCache cache = new LRUQueryCache(2, 10000, context -> true, Float.POSITIVE_INFINITY);
searcher.setQueryCache(cache);
assertEquals(0, searcher.count(new NoCacheQuery()));
assertEquals(0, cache.getCacheCount());
// BooleanQuery wrapping an uncacheable query should also not be cached
BooleanQuery bq = new BooleanQuery.Builder()
.add(new NoCacheQuery(), Occur.MUST)
.add(new TermQuery(new Term("field", "term")), Occur.MUST).build();
assertEquals(0, searcher.count(bq));
assertEquals(0, cache.getCacheCount());
reader.close();
w.close();
dir.close();
}
private static class DummyQuery2 extends Query {
private final AtomicBoolean scorerCreated;
DummyQuery2(AtomicBoolean scorerCreated) {
this.scorerCreated = scorerCreated;
}
@Override
public Weight createWeight(IndexSearcher searcher, ScoreMode scoreMode, float boost) throws IOException {
return new ConstantScoreWeight(this, boost) {
@Override
public Scorer scorer(LeafReaderContext context) throws IOException {
return scorerSupplier(context).get(Long.MAX_VALUE);
}
@Override
public boolean isCacheable(LeafReaderContext ctx) {
return true;
}
@Override
public ScorerSupplier scorerSupplier(LeafReaderContext context) throws IOException {
final Weight weight = this;
return new ScorerSupplier() {
@Override
public Scorer get(long leadCost) throws IOException {
scorerCreated.set(true);
return new ConstantScoreScorer(weight, boost, scoreMode, DocIdSetIterator.all(1));
}
@Override
public long cost() {
return 1;
}
};
}
};
}
@Override
public void visit(QueryVisitor visitor) {
}
@Override
public boolean equals(Object other) {
return sameClassAs(other);
}
@Override
public int hashCode() {
return 0;
}
@Override
public String toString(String field) {
return "DummyQuery2";
}
}
public void testPropagatesScorerSupplier() throws IOException {
Directory dir = newDirectory();
IndexWriterConfig iwc = newIndexWriterConfig().setMergePolicy(NoMergePolicy.INSTANCE);
RandomIndexWriter w = new RandomIndexWriter(random(), dir, iwc);
w.addDocument(new Document());
DirectoryReader reader = w.getReader();
IndexSearcher searcher = newSearcher(reader);
searcher.setQueryCachingPolicy(NEVER_CACHE);
LRUQueryCache cache = new LRUQueryCache(1, 1000);
searcher.setQueryCache(cache);
AtomicBoolean scorerCreated = new AtomicBoolean(false);
Query query = new DummyQuery2(scorerCreated);
Weight weight = searcher.createWeight(searcher.rewrite(query), ScoreMode.COMPLETE_NO_SCORES, 1);
ScorerSupplier supplier = weight.scorerSupplier(searcher.getIndexReader().leaves().get(0));
assertFalse(scorerCreated.get());
supplier.get(random().nextLong() & 0x7FFFFFFFFFFFFFFFL);
assertTrue(scorerCreated.get());
reader.close();
w.close();
dir.close();
}
static class DVCacheQuery extends Query {
final String field;
AtomicInteger scorerCreatedCount = new AtomicInteger(0);
DVCacheQuery(String field) {
this.field = field;
}
@Override
public String toString(String field) {
return "DVCacheQuery";
}
@Override
public boolean equals(Object obj) {
return sameClassAs(obj);
}
@Override
public int hashCode() {
return 0;
}
@Override
public Weight createWeight(IndexSearcher searcher, ScoreMode scoreMode, float boost) throws IOException {
return new ConstantScoreWeight(this, 1) {
@Override
public Scorer scorer(LeafReaderContext context) throws IOException {
scorerCreatedCount.incrementAndGet();
return new ConstantScoreScorer(this, 1, scoreMode, DocIdSetIterator.all(context.reader().maxDoc()));
}
@Override
public boolean isCacheable(LeafReaderContext ctx) {
return DocValues.isCacheable(ctx, field);
}
};
}
@Override
public void visit(QueryVisitor visitor) {
}
}
public void testDocValuesUpdatesDontBreakCache() throws IOException {
Directory dir = newDirectory();
IndexWriterConfig iwc = newIndexWriterConfig().setMergePolicy(NoMergePolicy.INSTANCE);
IndexWriter w = new IndexWriter(dir, iwc);
w.addDocument(new Document());
w.commit();
DirectoryReader reader = DirectoryReader.open(w);
// IMPORTANT:
// Don't use newSearcher(), because that will sometimes use an ExecutorService, and
// we need to be single threaded to ensure that LRUQueryCache doesn't skip the cache
// due to thread contention
IndexSearcher searcher = new AssertingIndexSearcher(random(), reader);
searcher.setQueryCachingPolicy(ALWAYS_CACHE);
LRUQueryCache cache = new LRUQueryCache(1, 10000, context -> true, Float.POSITIVE_INFINITY);
searcher.setQueryCache(cache);
DVCacheQuery query = new DVCacheQuery("field");
assertEquals(1, searcher.count(query));
assertEquals(1, query.scorerCreatedCount.get());
assertEquals(1, searcher.count(query));
assertEquals(1, query.scorerCreatedCount.get()); // should be cached
Document doc = new Document();
doc.add(new NumericDocValuesField("field", 1));
doc.add(newTextField("text", "text", Store.NO));
w.addDocument(doc);
reader.close();
reader = DirectoryReader.open(w);
searcher = new AssertingIndexSearcher(random(), reader); // no newSearcher(reader) - see comment above
searcher.setQueryCachingPolicy(ALWAYS_CACHE);
searcher.setQueryCache(cache);
assertEquals(2, searcher.count(query));
assertEquals(2, query.scorerCreatedCount.get()); // first segment cached
reader.close();
reader = DirectoryReader.open(w);
searcher = new AssertingIndexSearcher(random(), reader); // no newSearcher(reader) - see comment above
searcher.setQueryCachingPolicy(ALWAYS_CACHE);
searcher.setQueryCache(cache);
assertEquals(2, searcher.count(query));
assertEquals(2, query.scorerCreatedCount.get()); // both segments cached
w.updateNumericDocValue(new Term("text", "text"), "field", 2l);
reader.close();
reader = DirectoryReader.open(w);
searcher = new AssertingIndexSearcher(random(), reader); // no newSearcher(reader) - see comment above
searcher.setQueryCachingPolicy(ALWAYS_CACHE);
searcher.setQueryCache(cache);
assertEquals(2, searcher.count(query));
assertEquals(3, query.scorerCreatedCount.get()); // second segment no longer cached due to DV update
assertEquals(2, searcher.count(query));
assertEquals(4, query.scorerCreatedCount.get()); // still no caching
reader.close();
w.close();
dir.close();
}
public void testQueryCacheSoftUpdate() throws IOException {
Directory dir = newDirectory();
IndexWriterConfig iwc = newIndexWriterConfig().setSoftDeletesField("soft_delete");
IndexWriter w = new IndexWriter(dir, iwc);
LRUQueryCache queryCache = new LRUQueryCache(10, 1000 * 1000, ctx -> true, Float.POSITIVE_INFINITY);
IndexSearcher.setDefaultQueryCache(queryCache);
IndexSearcher.setDefaultQueryCachingPolicy(ALWAYS_CACHE);
SearcherManager sm = new SearcherManager(w, new SearcherFactory());
Document doc = new Document();
doc.add(new StringField("id", "1", org.apache.lucene.document.Field.Store.YES));
w.addDocument(doc);
doc = new Document();
doc.add(new StringField("id", "2", org.apache.lucene.document.Field.Store.YES));
w.addDocument(doc);
sm.maybeRefreshBlocking();
IndexSearcher searcher = sm.acquire();
Query query = new BooleanQuery.Builder().add(new TermQuery(new Term("id", "1")), BooleanClause.Occur.FILTER).build();
assertEquals(1, searcher.count(query));
assertEquals(1, queryCache.getCacheSize());
assertEquals(0, queryCache.getEvictionCount());
boolean softDelete = true;
if (softDelete) {
Document tombstone = new Document();
tombstone.add(new NumericDocValuesField("soft_delete", 1));
w.softUpdateDocument(new Term("id", "1"), tombstone, new NumericDocValuesField("soft_delete", 1));
w.softUpdateDocument(new Term("id", "2"), tombstone, new NumericDocValuesField("soft_delete", 1));
} else {
w.deleteDocuments(new Term("id", "1"));
w.deleteDocuments(new Term("id", "2"));
}
sm.maybeRefreshBlocking();
// All docs in the first segment are deleted - we should drop it with the default merge policy.
sm.release(searcher);
assertEquals(0, queryCache.getCacheSize());
assertEquals(1, queryCache.getEvictionCount());
sm.close();
w.close();
dir.close();
}
public void testBulkScorerLocking() throws Exception {
Directory dir = newDirectory();
IndexWriterConfig iwc = newIndexWriterConfig()
.setMergePolicy(NoMergePolicy.INSTANCE)
// the test framework sometimes sets crazy low values, prevent this since we are indexing many docs
.setMaxBufferedDocs(-1);
IndexWriter w = new IndexWriter(dir, iwc);
final int numDocs = atLeast(10);
Document emptyDoc = new Document();
for (int d = 0; d < numDocs; ++d) {
for (int i = random().nextInt(5000); i >= 0; --i) {
w.addDocument(emptyDoc);
}
Document doc = new Document();
for (String value : Arrays.asList("foo", "bar", "baz")) {
if (random().nextBoolean()) {
doc.add(new StringField("field", value, Store.NO));
}
}
}
for (int i = TestUtil.nextInt(random(), 3000, 5000); i >= 0; --i) {
w.addDocument(emptyDoc);
}
if (random().nextBoolean()) {
w.forceMerge(1);
}
DirectoryReader reader = DirectoryReader.open(w);
DirectoryReader noCacheReader = new DummyDirectoryReader(reader);
LRUQueryCache cache = new LRUQueryCache(1, 100000, context -> true, Float.POSITIVE_INFINITY);
IndexSearcher searcher = new AssertingIndexSearcher(random(), reader);
searcher.setQueryCache(cache);
searcher.setQueryCachingPolicy(ALWAYS_CACHE);
Query query = new ConstantScoreQuery(new BooleanQuery.Builder()
.add(new BoostQuery(new TermQuery(new Term("field", "foo")), 3), Occur.SHOULD)
.add(new BoostQuery(new TermQuery(new Term("field", "bar")), 3), Occur.SHOULD)
.add(new BoostQuery(new TermQuery(new Term("field", "baz")), 3), Occur.SHOULD)
.build());
searcher.search(query, 1);
IndexSearcher noCacheHelperSearcher = new AssertingIndexSearcher(random(), noCacheReader);
noCacheHelperSearcher.setQueryCache(cache);
noCacheHelperSearcher.setQueryCachingPolicy(ALWAYS_CACHE);
noCacheHelperSearcher.search(query, 1);
Thread t = new Thread(() -> {
try {
noCacheReader.close();
w.close();
dir.close();
}
catch (Exception e) {
throw new RuntimeException(e);
}
});
t.start();
t.join();
}
public void testSkipCachingForRangeQuery() throws IOException {
Directory dir = newDirectory();
final RandomIndexWriter w = new RandomIndexWriter(random(), dir);
Document doc1 = new Document();
doc1.add(new StringField("name", "tom", Store.YES));
doc1.add(new LongPoint("age", 15));
doc1.add(new SortedNumericDocValuesField("age", 15));
Document doc2 = new Document();
doc2.add(new StringField("name", "alice", Store.YES));
doc2.add(new LongPoint("age", 20));
doc2.add(new SortedNumericDocValuesField("age", 20));
w.addDocuments(Arrays.asList(doc1, doc2));
final IndexReader reader = w.getReader();
final IndexSearcher searcher = newSearcher(reader);
searcher.setQueryCachingPolicy(ALWAYS_CACHE);
w.close();
// lead cost is 1, cost of subQuery1 is 1, cost of subQuery2 is 2
BooleanQuery.Builder bq = new BooleanQuery.Builder();
TermQuery subQuery1 = new TermQuery(new Term("name", "tom"));
IndexOrDocValuesQuery subQuery2 = new IndexOrDocValuesQuery(
LongPoint.newRangeQuery("age", 10, 30),
SortedNumericDocValuesField.newSlowRangeQuery("age", 10, 30));
BooleanQuery query = bq.add(subQuery1, Occur.FILTER).add(subQuery2, Occur.FILTER).build();
Set<Query> cacheSet = new HashSet<>();
// only term query is cached
final LRUQueryCache partCache = new LRUQueryCache(1000000, 10000000, context -> true, 1);
searcher.setQueryCache(partCache);
searcher.search(query, 1);
cacheSet.add(subQuery1);
assertEquals(cacheSet, new HashSet<>(partCache.cachedQueries()));
// both queries are cached
final LRUQueryCache allCache = new LRUQueryCache(1000000, 10000000, context -> true, Float.POSITIVE_INFINITY);
searcher.setQueryCache(allCache);
searcher.search(query, 1);
cacheSet.add(subQuery2);
assertEquals(cacheSet, new HashSet<>(allCache.cachedQueries()));
reader.close();
dir.close();
}
public void testSkipCachingForTermQuery() throws IOException {
Directory dir = newDirectory();
final RandomIndexWriter w = new RandomIndexWriter(random(), dir);
Document doc1 = new Document();
doc1.add(new StringField("name", "tom", Store.YES));
doc1.add(new StringField("hobby", "movie", Store.YES));
Document doc2 = new Document();
doc2.add(new StringField("name", "alice", Store.YES));
doc2.add(new StringField("hobby", "book", Store.YES));
Document doc3 = new Document();
doc3.add(new StringField("name", "alice", Store.YES));
doc3.add(new StringField("hobby", "movie", Store.YES));
w.addDocuments(Arrays.asList(doc1, doc2, doc3));
final IndexReader reader = w.getReader();
final IndexSearcher searcher = newSearcher(reader);
final UsageTrackingQueryCachingPolicy policy = new UsageTrackingQueryCachingPolicy();
searcher.setQueryCachingPolicy(policy);
w.close();
// lead cost is 2, cost of subQuery1 is 3, cost of subQuery2 is 2
BooleanQuery.Builder inner = new BooleanQuery.Builder();
TermQuery innerSubQuery1 = new TermQuery(new Term("hobby", "book"));
TermQuery innerSubQuery2 = new TermQuery(new Term("hobby", "movie"));
BooleanQuery subQuery1 = inner.add(innerSubQuery1, Occur.SHOULD).add(innerSubQuery2, Occur.SHOULD).build();
BooleanQuery.Builder bq = new BooleanQuery.Builder();
TermQuery subQuery2 = new TermQuery(new Term("name", "alice"));
BooleanQuery query = bq.add(new ConstantScoreQuery(subQuery1), Occur.FILTER).add(subQuery2, Occur.FILTER).build();
Set<Query> cacheSet = new HashSet<>();
// both queries are not cached
final LRUQueryCache partCache = new LRUQueryCache(1000000, 10000000, context -> true, 1);
searcher.setQueryCache(partCache);
searcher.search(query, 1);
assertEquals(cacheSet, new HashSet<>(partCache.cachedQueries()));
// only subQuery1 is cached
final LRUQueryCache allCache = new LRUQueryCache(1000000, 10000000, context -> true, Float.POSITIVE_INFINITY);
searcher.setQueryCache(allCache);
searcher.search(query, 1);
cacheSet.add(subQuery1);
assertEquals(cacheSet, new HashSet<>(allCache.cachedQueries()));
reader.close();
dir.close();
}
}