|
@@ -0,0 +1,241 @@
|
|
|
+/*
|
|
|
+ * Licensed to Elasticsearch under one or more contributor
|
|
|
+ * license agreements. See the NOTICE file distributed with
|
|
|
+ * this work for additional information regarding copyright
|
|
|
+ * ownership. Elasticsearch 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.elasticsearch.index.engine;
|
|
|
+
|
|
|
+import org.apache.lucene.codecs.PostingsFormat;
|
|
|
+import org.apache.lucene.codecs.lucene84.Lucene84Codec;
|
|
|
+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.search.Query;
|
|
|
+import org.apache.lucene.search.QueryCachingPolicy;
|
|
|
+import org.apache.lucene.search.suggest.document.Completion84PostingsFormat;
|
|
|
+import org.apache.lucene.search.suggest.document.SuggestField;
|
|
|
+import org.apache.lucene.store.Directory;
|
|
|
+import org.elasticsearch.ElasticsearchException;
|
|
|
+import org.elasticsearch.core.internal.io.IOUtils;
|
|
|
+import org.elasticsearch.search.suggest.completion.CompletionStats;
|
|
|
+import org.elasticsearch.test.ESTestCase;
|
|
|
+
|
|
|
+import java.io.IOException;
|
|
|
+import java.util.concurrent.BrokenBarrierException;
|
|
|
+import java.util.concurrent.CyclicBarrier;
|
|
|
+import java.util.concurrent.atomic.AtomicInteger;
|
|
|
+
|
|
|
+import static org.hamcrest.Matchers.equalTo;
|
|
|
+import static org.hamcrest.Matchers.greaterThan;
|
|
|
+
|
|
|
+public class CompletionStatsCacheTests extends ESTestCase {
|
|
|
+
|
|
|
+ public void testExceptionsAreNotCached() {
|
|
|
+ final AtomicInteger openCount = new AtomicInteger();
|
|
|
+ final CompletionStatsCache completionStatsCache = new CompletionStatsCache(() -> {
|
|
|
+ throw new ElasticsearchException("simulated " + openCount.incrementAndGet());
|
|
|
+ });
|
|
|
+
|
|
|
+ assertThat(expectThrows(ElasticsearchException.class, completionStatsCache::get).getMessage(), equalTo("simulated 1"));
|
|
|
+ assertThat(expectThrows(ElasticsearchException.class, completionStatsCache::get).getMessage(), equalTo("simulated 2"));
|
|
|
+ }
|
|
|
+
|
|
|
+ public void testCompletionStatsCache() throws IOException, InterruptedException {
|
|
|
+ final IndexWriterConfig indexWriterConfig = newIndexWriterConfig();
|
|
|
+ final PostingsFormat postingsFormat = new Completion84PostingsFormat();
|
|
|
+ indexWriterConfig.setCodec(new Lucene84Codec() {
|
|
|
+ @Override
|
|
|
+ public PostingsFormat getPostingsFormatForField(String field) {
|
|
|
+ return postingsFormat; // all fields are suggest fields
|
|
|
+ }
|
|
|
+ });
|
|
|
+
|
|
|
+ final QueryCachingPolicy queryCachingPolicy = new QueryCachingPolicy() {
|
|
|
+ @Override
|
|
|
+ public void onUse(Query query) {
|
|
|
+ }
|
|
|
+
|
|
|
+ @Override
|
|
|
+ public boolean shouldCache(Query query) {
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+ };
|
|
|
+
|
|
|
+ try (Directory directory = newDirectory();
|
|
|
+ IndexWriter indexWriter = new IndexWriter(directory, indexWriterConfig)) {
|
|
|
+
|
|
|
+ final Document document = new Document();
|
|
|
+ document.add(new SuggestField("suggest1", "val", 1));
|
|
|
+ document.add(new SuggestField("suggest2", "val", 1));
|
|
|
+ document.add(new SuggestField("suggest2", "anotherval", 1));
|
|
|
+ document.add(new SuggestField("otherfield", "val", 1));
|
|
|
+ document.add(new SuggestField("otherfield", "anotherval", 1));
|
|
|
+ document.add(new SuggestField("otherfield", "yetmoreval", 1));
|
|
|
+ indexWriter.addDocument(document);
|
|
|
+
|
|
|
+ final OpenCloseCounter openCloseCounter = new OpenCloseCounter();
|
|
|
+ final CompletionStatsCache completionStatsCache = new CompletionStatsCache(() -> {
|
|
|
+ openCloseCounter.countOpened();
|
|
|
+ try {
|
|
|
+ final DirectoryReader directoryReader = DirectoryReader.open(indexWriter);
|
|
|
+ return new Engine.Searcher("test", directoryReader, null, null, queryCachingPolicy, () -> {
|
|
|
+ openCloseCounter.countClosed();
|
|
|
+ IOUtils.close(directoryReader);
|
|
|
+ });
|
|
|
+ } catch (IOException e) {
|
|
|
+ throw new AssertionError(e);
|
|
|
+ }
|
|
|
+ });
|
|
|
+
|
|
|
+ final int threadCount = 6;
|
|
|
+ final TestHarness testHarness = new TestHarness(completionStatsCache, threadCount);
|
|
|
+ final Thread[] threads = new Thread[threadCount];
|
|
|
+ threads[0] = new Thread(() -> testHarness.getStats(0, "*"));
|
|
|
+ threads[1] = new Thread(() -> testHarness.getStats(1, "suggest1", "suggest2"));
|
|
|
+ threads[2] = new Thread(() -> testHarness.getStats(2, "sug*"));
|
|
|
+ threads[3] = new Thread(() -> testHarness.getStats(3, "no match*"));
|
|
|
+ threads[4] = new Thread(() -> testHarness.getStats(4));
|
|
|
+ threads[5] = new Thread(() -> testHarness.getStats(5, (String[]) null));
|
|
|
+
|
|
|
+ for (Thread thread : threads) {
|
|
|
+ thread.start();
|
|
|
+ }
|
|
|
+
|
|
|
+ testHarness.start();
|
|
|
+
|
|
|
+ for (Thread thread : threads) {
|
|
|
+ thread.join();
|
|
|
+ }
|
|
|
+
|
|
|
+ // 0: "*" should match all fields:
|
|
|
+ final long suggest1Size = testHarness.getResult(0).getFields().get("suggest1");
|
|
|
+ final long suggest2Size = testHarness.getResult(0).getFields().get("suggest2");
|
|
|
+ final long otherFieldSize = testHarness.getResult(0).getFields().get("otherfield");
|
|
|
+ final long totalSizeInBytes = testHarness.getResult(0).getSizeInBytes();
|
|
|
+ assertThat(suggest1Size, greaterThan(0L));
|
|
|
+ assertThat(suggest2Size, greaterThan(0L));
|
|
|
+ assertThat(otherFieldSize, greaterThan(0L));
|
|
|
+ assertThat(totalSizeInBytes, equalTo(suggest1Size + suggest2Size + otherFieldSize));
|
|
|
+
|
|
|
+ // 1: enumerating fields omits the other ones
|
|
|
+ assertThat(testHarness.getResult(1).getSizeInBytes(), equalTo(totalSizeInBytes));
|
|
|
+ assertThat(testHarness.getResult(1).getFields().get("suggest1"), equalTo(suggest1Size));
|
|
|
+ assertThat(testHarness.getResult(1).getFields().get("suggest2"), equalTo(suggest2Size));
|
|
|
+ assertFalse(testHarness.getResult(1).getFields().containsField("otherfield"));
|
|
|
+
|
|
|
+ // 2: wildcards also exclude some fields
|
|
|
+ assertThat(testHarness.getResult(2).getSizeInBytes(), equalTo(totalSizeInBytes));
|
|
|
+ assertThat(testHarness.getResult(2).getFields().get("suggest1"), equalTo(suggest1Size));
|
|
|
+ assertThat(testHarness.getResult(2).getFields().get("suggest2"), equalTo(suggest2Size));
|
|
|
+ assertFalse(testHarness.getResult(2).getFields().containsField("otherfield"));
|
|
|
+
|
|
|
+ // 3: non-matching wildcard returns empty set of fields
|
|
|
+ assertThat(testHarness.getResult(3).getSizeInBytes(), equalTo(totalSizeInBytes));
|
|
|
+ assertFalse(testHarness.getResult(3).getFields().containsField("suggest1"));
|
|
|
+ assertFalse(testHarness.getResult(3).getFields().containsField("suggest2"));
|
|
|
+ assertFalse(testHarness.getResult(3).getFields().containsField("otherfield"));
|
|
|
+
|
|
|
+ // 4: no fields means per-fields stats is null
|
|
|
+ assertThat(testHarness.getResult(4).getSizeInBytes(), equalTo(totalSizeInBytes));
|
|
|
+ assertNull(testHarness.getResult(4).getFields());
|
|
|
+
|
|
|
+ // 5: null fields means per-fields stats is null
|
|
|
+ assertThat(testHarness.getResult(5).getSizeInBytes(), equalTo(totalSizeInBytes));
|
|
|
+ assertNull(testHarness.getResult(5).getFields());
|
|
|
+
|
|
|
+ // the stats were only computed once
|
|
|
+ openCloseCounter.assertCount(1);
|
|
|
+
|
|
|
+ // the stats are not recomputed on a refresh
|
|
|
+ completionStatsCache.afterRefresh(true);
|
|
|
+ openCloseCounter.assertCount(1);
|
|
|
+
|
|
|
+ // but they are recomputed on the next get
|
|
|
+ completionStatsCache.get();
|
|
|
+ openCloseCounter.assertCount(2);
|
|
|
+
|
|
|
+ // and they do update
|
|
|
+ final Document document2 = new Document();
|
|
|
+ document2.add(new SuggestField("suggest1", "foo", 1));
|
|
|
+ document2.add(new SuggestField("suggest2", "bar", 1));
|
|
|
+ document2.add(new SuggestField("otherfield", "baz", 1));
|
|
|
+ indexWriter.addDocument(document2);
|
|
|
+ completionStatsCache.afterRefresh(true);
|
|
|
+ final CompletionStats updatedStats = completionStatsCache.get();
|
|
|
+ assertThat(updatedStats.getSizeInBytes(), greaterThan(totalSizeInBytes));
|
|
|
+ openCloseCounter.assertCount(3);
|
|
|
+
|
|
|
+ // beforeRefresh does not invalidate the cache
|
|
|
+ completionStatsCache.beforeRefresh();
|
|
|
+ completionStatsCache.get();
|
|
|
+ openCloseCounter.assertCount(3);
|
|
|
+
|
|
|
+ // afterRefresh does not invalidate the cache if no refresh took place
|
|
|
+ completionStatsCache.afterRefresh(false);
|
|
|
+ completionStatsCache.get();
|
|
|
+ openCloseCounter.assertCount(3);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ private static class OpenCloseCounter {
|
|
|
+ private final AtomicInteger openCount = new AtomicInteger();
|
|
|
+ private final AtomicInteger closeCount = new AtomicInteger();
|
|
|
+
|
|
|
+ void countOpened() {
|
|
|
+ openCount.incrementAndGet();
|
|
|
+ }
|
|
|
+
|
|
|
+ void countClosed() {
|
|
|
+ closeCount.incrementAndGet();
|
|
|
+ }
|
|
|
+
|
|
|
+ void assertCount(int expectedCount) {
|
|
|
+ assertThat(openCount.get(), equalTo(expectedCount));
|
|
|
+ assertThat(closeCount.get(), equalTo(expectedCount));
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ private static class TestHarness {
|
|
|
+ private final CompletionStatsCache completionStatsCache;
|
|
|
+ private final CyclicBarrier cyclicBarrier;
|
|
|
+ private final CompletionStats[] results;
|
|
|
+
|
|
|
+ TestHarness(CompletionStatsCache completionStatsCache, int resultCount) {
|
|
|
+ this.completionStatsCache = completionStatsCache;
|
|
|
+ results = new CompletionStats[resultCount];
|
|
|
+ cyclicBarrier = new CyclicBarrier(resultCount + 1);
|
|
|
+ }
|
|
|
+
|
|
|
+ void getStats(int threadIndex, String... fieldPatterns) {
|
|
|
+ start();
|
|
|
+ results[threadIndex] = completionStatsCache.get(fieldPatterns);
|
|
|
+ }
|
|
|
+
|
|
|
+ void start() {
|
|
|
+ try {
|
|
|
+ cyclicBarrier.await();
|
|
|
+ } catch (InterruptedException | BrokenBarrierException e) {
|
|
|
+ throw new AssertionError(e);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ CompletionStats getResult(int index) {
|
|
|
+ return results[index];
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+}
|