| /* |
| * 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.facet.taxonomy.directory; |
| |
| import java.io.IOException; |
| import java.util.Arrays; |
| import java.util.HashSet; |
| import java.util.Random; |
| import java.util.Set; |
| |
| import org.apache.lucene.analysis.MockAnalyzer; |
| import org.apache.lucene.facet.FacetTestCase; |
| import org.apache.lucene.facet.taxonomy.FacetLabel; |
| import org.apache.lucene.facet.taxonomy.TaxonomyReader.ChildrenIterator; |
| import org.apache.lucene.facet.taxonomy.TaxonomyReader; |
| import org.apache.lucene.facet.taxonomy.TaxonomyWriter; |
| import org.apache.lucene.index.IndexWriter; |
| import org.apache.lucene.index.IndexWriterConfig.OpenMode; |
| import org.apache.lucene.index.IndexWriterConfig; |
| import org.apache.lucene.index.LogByteSizeMergePolicy; |
| import org.apache.lucene.index.LogMergePolicy; |
| import org.apache.lucene.store.AlreadyClosedException; |
| import org.apache.lucene.store.Directory; |
| import org.apache.lucene.store.RAMDirectory; |
| import org.apache.lucene.util.IOUtils; |
| import org.junit.Test; |
| |
| public class TestDirectoryTaxonomyReader extends FacetTestCase { |
| |
| @Test |
| public void testCloseAfterIncRef() throws Exception { |
| Directory dir = newDirectory(); |
| DirectoryTaxonomyWriter ltw = new DirectoryTaxonomyWriter(dir); |
| ltw.addCategory(new FacetLabel("a")); |
| ltw.close(); |
| |
| DirectoryTaxonomyReader ltr = new DirectoryTaxonomyReader(dir); |
| ltr.incRef(); |
| ltr.close(); |
| |
| // should not fail as we incRef() before close |
| ltr.getSize(); |
| ltr.decRef(); |
| |
| dir.close(); |
| } |
| |
| @Test |
| public void testCloseTwice() throws Exception { |
| Directory dir = newDirectory(); |
| DirectoryTaxonomyWriter ltw = new DirectoryTaxonomyWriter(dir); |
| ltw.addCategory(new FacetLabel("a")); |
| ltw.close(); |
| |
| DirectoryTaxonomyReader ltr = new DirectoryTaxonomyReader(dir); |
| ltr.close(); |
| ltr.close(); // no exception should be thrown |
| |
| dir.close(); |
| } |
| |
| @Test |
| public void testOpenIfChangedResult() throws Exception { |
| Directory dir = null; |
| DirectoryTaxonomyWriter ltw = null; |
| DirectoryTaxonomyReader ltr = null; |
| |
| try { |
| dir = newDirectory(); |
| ltw = new DirectoryTaxonomyWriter(dir); |
| |
| ltw.addCategory(new FacetLabel("a")); |
| ltw.commit(); |
| |
| ltr = new DirectoryTaxonomyReader(dir); |
| assertNull("Nothing has changed", TaxonomyReader.openIfChanged(ltr)); |
| |
| ltw.addCategory(new FacetLabel("b")); |
| ltw.commit(); |
| |
| DirectoryTaxonomyReader newtr = TaxonomyReader.openIfChanged(ltr); |
| assertNotNull("changes were committed", newtr); |
| assertNull("Nothing has changed", TaxonomyReader.openIfChanged(newtr)); |
| newtr.close(); |
| } finally { |
| IOUtils.close(ltw, ltr, dir); |
| } |
| } |
| |
| @Test |
| public void testAlreadyClosed() throws Exception { |
| Directory dir = newDirectory(); |
| DirectoryTaxonomyWriter ltw = new DirectoryTaxonomyWriter(dir); |
| ltw.addCategory(new FacetLabel("a")); |
| ltw.close(); |
| |
| DirectoryTaxonomyReader ltr = new DirectoryTaxonomyReader(dir); |
| ltr.close(); |
| expectThrows(AlreadyClosedException.class, () -> { |
| ltr.getSize(); |
| }); |
| |
| dir.close(); |
| } |
| |
| /** |
| * recreating a taxonomy should work well with a freshly opened taxonomy reader |
| */ |
| @Test |
| public void testFreshReadRecreatedTaxonomy() throws Exception { |
| doTestReadRecreatedTaxonomy(random(), true); |
| } |
| |
| @Test |
| public void testOpenIfChangedReadRecreatedTaxonomy() throws Exception { |
| doTestReadRecreatedTaxonomy(random(), false); |
| } |
| |
| private void doTestReadRecreatedTaxonomy(Random random, boolean closeReader) throws Exception { |
| Directory dir = null; |
| TaxonomyWriter tw = null; |
| TaxonomyReader tr = null; |
| |
| // prepare a few categories |
| int n = 10; |
| FacetLabel[] cp = new FacetLabel[n]; |
| for (int i=0; i<n; i++) { |
| cp[i] = new FacetLabel("a", Integer.toString(i)); |
| } |
| |
| try { |
| dir = newDirectory(); |
| |
| tw = new DirectoryTaxonomyWriter(dir); |
| tw.addCategory(new FacetLabel("a")); |
| tw.close(); |
| |
| tr = new DirectoryTaxonomyReader(dir); |
| int baseNumCategories = tr.getSize(); |
| |
| for (int i=0; i<n; i++) { |
| int k = random.nextInt(n); |
| tw = new DirectoryTaxonomyWriter(dir, OpenMode.CREATE); |
| for (int j = 0; j <= k; j++) { |
| tw.addCategory(cp[j]); |
| } |
| tw.close(); |
| if (closeReader) { |
| tr.close(); |
| tr = new DirectoryTaxonomyReader(dir); |
| } else { |
| TaxonomyReader newtr = TaxonomyReader.openIfChanged(tr); |
| assertNotNull(newtr); |
| tr.close(); |
| tr = newtr; |
| } |
| assertEquals("Wrong #categories in taxonomy (i="+i+", k="+k+")", baseNumCategories + 1 + k, tr.getSize()); |
| } |
| } finally { |
| IOUtils.close(tr, tw, dir); |
| } |
| } |
| |
| @Test |
| public void testOpenIfChangedAndRefCount() throws Exception { |
| Directory dir = new RAMDirectory(); // no need for random directories here |
| |
| DirectoryTaxonomyWriter taxoWriter = new DirectoryTaxonomyWriter(dir); |
| taxoWriter.addCategory(new FacetLabel("a")); |
| taxoWriter.commit(); |
| |
| TaxonomyReader taxoReader = new DirectoryTaxonomyReader(dir); |
| assertEquals("wrong refCount", 1, taxoReader.getRefCount()); |
| |
| taxoReader.incRef(); |
| assertEquals("wrong refCount", 2, taxoReader.getRefCount()); |
| |
| taxoWriter.addCategory(new FacetLabel("a", "b")); |
| taxoWriter.commit(); |
| TaxonomyReader newtr = TaxonomyReader.openIfChanged(taxoReader); |
| assertNotNull(newtr); |
| taxoReader.close(); |
| taxoReader = newtr; |
| assertEquals("wrong refCount", 1, taxoReader.getRefCount()); |
| |
| taxoWriter.close(); |
| taxoReader.close(); |
| dir.close(); |
| } |
| |
| @Test |
| public void testOpenIfChangedManySegments() throws Exception { |
| // test openIfChanged() when the taxonomy contains many segments |
| Directory dir = newDirectory(); |
| |
| DirectoryTaxonomyWriter writer = new DirectoryTaxonomyWriter(dir) { |
| @Override |
| protected IndexWriterConfig createIndexWriterConfig(OpenMode openMode) { |
| IndexWriterConfig conf = super.createIndexWriterConfig(openMode); |
| LogMergePolicy lmp = (LogMergePolicy) conf.getMergePolicy(); |
| lmp.setMergeFactor(2); |
| return conf; |
| } |
| }; |
| TaxonomyReader reader = new DirectoryTaxonomyReader(writer); |
| |
| int numRounds = random().nextInt(10) + 10; |
| int numCategories = 1; // one for root |
| for (int i = 0; i < numRounds; i++) { |
| int numCats = random().nextInt(4) + 1; |
| for (int j = 0; j < numCats; j++) { |
| writer.addCategory(new FacetLabel(Integer.toString(i), Integer.toString(j))); |
| } |
| numCategories += numCats + 1 /* one for round-parent */; |
| TaxonomyReader newtr = TaxonomyReader.openIfChanged(reader); |
| assertNotNull(newtr); |
| reader.close(); |
| reader = newtr; |
| |
| // assert categories |
| assertEquals(numCategories, reader.getSize()); |
| int roundOrdinal = reader.getOrdinal(new FacetLabel(Integer.toString(i))); |
| int[] parents = reader.getParallelTaxonomyArrays().parents(); |
| assertEquals(0, parents[roundOrdinal]); // round's parent is root |
| for (int j = 0; j < numCats; j++) { |
| int ord = reader.getOrdinal(new FacetLabel(Integer.toString(i), Integer.toString(j))); |
| assertEquals(roundOrdinal, parents[ord]); // round's parent is root |
| } |
| } |
| |
| reader.close(); |
| writer.close(); |
| dir.close(); |
| } |
| |
| @Test |
| public void testOpenIfChangedMergedSegment() throws Exception { |
| // test openIfChanged() when all index segments were merged - used to be |
| // a bug in ParentArray, caught by testOpenIfChangedManySegments - only |
| // this test is not random |
| Directory dir = newDirectory(); |
| |
| // hold onto IW to forceMerge |
| // note how we don't close it, since DTW will close it. |
| final IndexWriter iw = new IndexWriter(dir, |
| new IndexWriterConfig(new MockAnalyzer(random())) |
| .setMergePolicy(new LogByteSizeMergePolicy())); |
| DirectoryTaxonomyWriter writer = new DirectoryTaxonomyWriter(dir) { |
| @Override |
| protected IndexWriter openIndexWriter(Directory directory, |
| IndexWriterConfig config) throws IOException { |
| return iw; |
| } |
| }; |
| |
| TaxonomyReader reader = new DirectoryTaxonomyReader(writer); |
| assertEquals(1, reader.getSize()); |
| assertEquals(1, reader.getParallelTaxonomyArrays().parents().length); |
| |
| // add category and call forceMerge -- this should flush IW and merge segments down to 1 |
| // in ParentArray.initFromReader, this used to fail assuming there are no parents. |
| writer.addCategory(new FacetLabel("1")); |
| iw.forceMerge(1); |
| |
| // now calling openIfChanged should trip on the bug |
| TaxonomyReader newtr = TaxonomyReader.openIfChanged(reader); |
| assertNotNull(newtr); |
| reader.close(); |
| reader = newtr; |
| assertEquals(2, reader.getSize()); |
| assertEquals(2, reader.getParallelTaxonomyArrays().parents().length); |
| |
| reader.close(); |
| writer.close(); |
| dir.close(); |
| } |
| |
| @Test |
| public void testOpenIfChangedNoChangesButSegmentMerges() throws Exception { |
| // test openIfChanged() when the taxonomy hasn't really changed, but segments |
| // were merged. The NRT reader will be reopened, and ParentArray used to assert |
| // that the new reader contains more ordinals than were given from the old |
| // TaxReader version |
| Directory dir = newDirectory(); |
| |
| // hold onto IW to forceMerge |
| // note how we don't close it, since DTW will close it. |
| final IndexWriter iw = new IndexWriter(dir, |
| new IndexWriterConfig(new MockAnalyzer(random())) |
| .setMergePolicy(new LogByteSizeMergePolicy())); |
| DirectoryTaxonomyWriter writer = new DirectoryTaxonomyWriter(dir) { |
| @Override |
| protected IndexWriter openIndexWriter(Directory directory, |
| IndexWriterConfig config) throws IOException { |
| return iw; |
| } |
| }; |
| |
| // add a category so that the following DTR open will cause a flush and |
| // a new segment will be created |
| writer.addCategory(new FacetLabel("a")); |
| |
| TaxonomyReader reader = new DirectoryTaxonomyReader(writer); |
| assertEquals(2, reader.getSize()); |
| assertEquals(2, reader.getParallelTaxonomyArrays().parents().length); |
| |
| // merge all the segments so that NRT reader thinks there's a change |
| iw.forceMerge(1); |
| |
| // now calling openIfChanged should trip on the wrong assert in ParetArray's ctor |
| TaxonomyReader newtr = TaxonomyReader.openIfChanged(reader); |
| assertNotNull(newtr); |
| reader.close(); |
| reader = newtr; |
| assertEquals(2, reader.getSize()); |
| assertEquals(2, reader.getParallelTaxonomyArrays().parents().length); |
| |
| reader.close(); |
| writer.close(); |
| dir.close(); |
| } |
| |
| @Test |
| public void testOpenIfChangedReuseAfterRecreate() throws Exception { |
| // tests that if the taxonomy is recreated, no data is reused from the previous taxonomy |
| Directory dir = newDirectory(); |
| DirectoryTaxonomyWriter writer = new DirectoryTaxonomyWriter(dir); |
| FacetLabel cp_a = new FacetLabel("a"); |
| writer.addCategory(cp_a); |
| writer.close(); |
| |
| DirectoryTaxonomyReader r1 = new DirectoryTaxonomyReader(dir); |
| // fill r1's caches |
| assertEquals(1, r1.getOrdinal(cp_a)); |
| assertEquals(cp_a, r1.getPath(1)); |
| |
| // now recreate, add a different category |
| writer = new DirectoryTaxonomyWriter(dir, OpenMode.CREATE); |
| FacetLabel cp_b = new FacetLabel("b"); |
| writer.addCategory(cp_b); |
| writer.close(); |
| |
| DirectoryTaxonomyReader r2 = TaxonomyReader.openIfChanged(r1); |
| assertNotNull(r2); |
| |
| // fill r2's caches |
| assertEquals(1, r2.getOrdinal(cp_b)); |
| assertEquals(cp_b, r2.getPath(1)); |
| |
| // check that r1 doesn't see cp_b |
| assertEquals(TaxonomyReader.INVALID_ORDINAL, r1.getOrdinal(cp_b)); |
| assertEquals(cp_a, r1.getPath(1)); |
| |
| // check that r2 doesn't see cp_a |
| assertEquals(TaxonomyReader.INVALID_ORDINAL, r2.getOrdinal(cp_a)); |
| assertEquals(cp_b, r2.getPath(1)); |
| |
| r2.close(); |
| r1.close(); |
| dir.close(); |
| } |
| |
| @Test |
| public void testOpenIfChangedReuse() throws Exception { |
| // test the reuse of data from the old DTR instance |
| for (boolean nrt : new boolean[] {false, true}) { |
| Directory dir = newDirectory(); |
| DirectoryTaxonomyWriter writer = new DirectoryTaxonomyWriter(dir); |
| |
| FacetLabel cp_a = new FacetLabel("a"); |
| writer.addCategory(cp_a); |
| if (!nrt) writer.commit(); |
| |
| DirectoryTaxonomyReader r1 = nrt ? new DirectoryTaxonomyReader(writer) : new DirectoryTaxonomyReader(dir); |
| // fill r1's caches |
| assertEquals(1, r1.getOrdinal(cp_a)); |
| assertEquals(cp_a, r1.getPath(1)); |
| |
| FacetLabel cp_b = new FacetLabel("b"); |
| writer.addCategory(cp_b); |
| if (!nrt) writer.commit(); |
| |
| DirectoryTaxonomyReader r2 = TaxonomyReader.openIfChanged(r1); |
| assertNotNull(r2); |
| |
| // add r2's categories to the caches |
| assertEquals(2, r2.getOrdinal(cp_b)); |
| assertEquals(cp_b, r2.getPath(2)); |
| |
| // check that r1 doesn't see cp_b |
| assertEquals(TaxonomyReader.INVALID_ORDINAL, r1.getOrdinal(cp_b)); |
| assertNull(r1.getPath(2)); |
| |
| r1.close(); |
| r2.close(); |
| writer.close(); |
| dir.close(); |
| } |
| } |
| |
| @Test |
| public void testOpenIfChangedReplaceTaxonomy() throws Exception { |
| // test openIfChanged when replaceTaxonomy is called, which is equivalent to recreate |
| // only can work with NRT as well |
| Directory src = newDirectory(); |
| DirectoryTaxonomyWriter w = new DirectoryTaxonomyWriter(src); |
| FacetLabel cp_b = new FacetLabel("b"); |
| w.addCategory(cp_b); |
| w.close(); |
| |
| for (boolean nrt : new boolean[] {false, true}) { |
| Directory dir = newDirectory(); |
| DirectoryTaxonomyWriter writer = new DirectoryTaxonomyWriter(dir); |
| |
| FacetLabel cp_a = new FacetLabel("a"); |
| writer.addCategory(cp_a); |
| if (!nrt) writer.commit(); |
| |
| DirectoryTaxonomyReader r1 = nrt ? new DirectoryTaxonomyReader(writer) : new DirectoryTaxonomyReader(dir); |
| // fill r1's caches |
| assertEquals(1, r1.getOrdinal(cp_a)); |
| assertEquals(cp_a, r1.getPath(1)); |
| |
| // now replace taxonomy |
| writer.replaceTaxonomy(src); |
| if (!nrt) writer.commit(); |
| |
| DirectoryTaxonomyReader r2 = TaxonomyReader.openIfChanged(r1); |
| assertNotNull(r2); |
| |
| // fill r2's caches |
| assertEquals(1, r2.getOrdinal(cp_b)); |
| assertEquals(cp_b, r2.getPath(1)); |
| |
| // check that r1 doesn't see cp_b |
| assertEquals(TaxonomyReader.INVALID_ORDINAL, r1.getOrdinal(cp_b)); |
| assertEquals(cp_a, r1.getPath(1)); |
| |
| // check that r2 doesn't see cp_a |
| assertEquals(TaxonomyReader.INVALID_ORDINAL, r2.getOrdinal(cp_a)); |
| assertEquals(cp_b, r2.getPath(1)); |
| |
| r2.close(); |
| r1.close(); |
| writer.close(); |
| dir.close(); |
| } |
| |
| src.close(); |
| } |
| |
| @Test |
| public void testGetChildren() throws Exception { |
| Directory dir = newDirectory(); |
| DirectoryTaxonomyWriter taxoWriter = new DirectoryTaxonomyWriter(dir); |
| int numCategories = atLeast(10); |
| int numA = 0, numB = 0; |
| Random random = random(); |
| // add the two categories for which we'll also add children (so asserts are simpler) |
| taxoWriter.addCategory(new FacetLabel("a")); |
| taxoWriter.addCategory(new FacetLabel("b")); |
| for (int i = 0; i < numCategories; i++) { |
| if (random.nextBoolean()) { |
| taxoWriter.addCategory(new FacetLabel("a", Integer.toString(i))); |
| ++numA; |
| } else { |
| taxoWriter.addCategory(new FacetLabel("b", Integer.toString(i))); |
| ++numB; |
| } |
| } |
| // add category with no children |
| taxoWriter.addCategory(new FacetLabel("c")); |
| taxoWriter.close(); |
| |
| DirectoryTaxonomyReader taxoReader = new DirectoryTaxonomyReader(dir); |
| |
| // non existing category |
| ChildrenIterator it = taxoReader.getChildren(taxoReader.getOrdinal(new FacetLabel("invalid"))); |
| assertEquals(TaxonomyReader.INVALID_ORDINAL, it.next()); |
| |
| // a category with no children |
| it = taxoReader.getChildren(taxoReader.getOrdinal(new FacetLabel("c"))); |
| assertEquals(TaxonomyReader.INVALID_ORDINAL, it.next()); |
| |
| // arbitrary negative ordinal |
| it = taxoReader.getChildren(-2); |
| assertEquals(TaxonomyReader.INVALID_ORDINAL, it.next()); |
| |
| // root's children |
| Set<String> roots = new HashSet<>(Arrays.asList("a", "b", "c")); |
| it = taxoReader.getChildren(TaxonomyReader.ROOT_ORDINAL); |
| while (!roots.isEmpty()) { |
| FacetLabel root = taxoReader.getPath(it.next()); |
| assertEquals(1, root.length); |
| assertTrue(roots.remove(root.components[0])); |
| } |
| assertEquals(TaxonomyReader.INVALID_ORDINAL, it.next()); |
| |
| for (int i = 0; i < 2; i++) { |
| FacetLabel cp = i == 0 ? new FacetLabel("a") : new FacetLabel("b"); |
| int ordinal = taxoReader.getOrdinal(cp); |
| it = taxoReader.getChildren(ordinal); |
| int numChildren = 0; |
| int child; |
| while ((child = it.next()) != TaxonomyReader.INVALID_ORDINAL) { |
| FacetLabel path = taxoReader.getPath(child); |
| assertEquals(2, path.length); |
| assertEquals(path.components[0], i == 0 ? "a" : "b"); |
| ++numChildren; |
| } |
| int expected = i == 0 ? numA : numB; |
| assertEquals("invalid num children", expected, numChildren); |
| } |
| taxoReader.close(); |
| |
| dir.close(); |
| } |
| |
| public void testAccountable() throws Exception { |
| Directory dir = newDirectory(); |
| DirectoryTaxonomyWriter taxoWriter = new DirectoryTaxonomyWriter(dir); |
| int numCategories = atLeast(10); |
| int numA = 0, numB = 0; |
| Random random = random(); |
| // add the two categories for which we'll also add children (so asserts are simpler) |
| taxoWriter.addCategory(new FacetLabel("a")); |
| taxoWriter.addCategory(new FacetLabel("b")); |
| for (int i = 0; i < numCategories; i++) { |
| if (random.nextBoolean()) { |
| taxoWriter.addCategory(new FacetLabel("a", Integer.toString(i))); |
| ++numA; |
| } else { |
| taxoWriter.addCategory(new FacetLabel("b", Integer.toString(i))); |
| ++numB; |
| } |
| } |
| // add category with no children |
| taxoWriter.addCategory(new FacetLabel("c")); |
| taxoWriter.close(); |
| |
| DirectoryTaxonomyReader taxoReader = new DirectoryTaxonomyReader(dir); |
| assertTrue(taxoReader.ramBytesUsed() > 0); |
| assertTrue(taxoReader.getChildResources().size() > 0); |
| taxoReader.close(); |
| dir.close(); |
| } |
| } |