/* * ModeShape (http://www.modeshape.org) * * Licensed 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.modeshape.jcr.index.lucene; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertTrue; import java.io.File; import java.io.InputStream; import java.util.HashMap; import java.util.Map; import java.util.Random; import java.util.concurrent.atomic.AtomicLong; import javax.jcr.Node; import javax.jcr.PropertyType; import org.junit.Ignore; import org.junit.Test; import org.modeshape.common.FixFor; import org.modeshape.common.util.FileUtil; import org.modeshape.jcr.LocalIndexProviderTest; import org.modeshape.jcr.api.JcrTools; import org.modeshape.jcr.api.index.IndexDefinition; import org.modeshape.jcr.api.index.InvalidIndexDefinitionException; import org.modeshape.jcr.api.query.Query; /** * Unit test for {@link LuceneIndexProvider}. Since this is a local provider in term of repository locality, we want to run * at least the same tests as for {@link org.modeshape.jcr.index.local.LocalIndexProvider} * * @author Horia Chiorean (hchiorea@redhat.com) */ public class LuceneIndexProviderTest extends LocalIndexProviderTest { @Override public void beforeEach() throws Exception { super.beforeEach(); tools = new JcrTools(); } @Override protected InputStream repositoryConfiguration() { return resource("config/repo-config-persistent-lucene-provider-no-indexes.json"); } @Override protected String providerName() { return "lucene"; } @Test @FixFor( "MODE-2520" ) public void shouldUseMultiColumnIndex() throws Exception { registerNodeType("nt:testType"); Map<String, Integer> properties = new HashMap<>(); properties.put("stringProp", PropertyType.STRING); properties.put("longProp", PropertyType.LONG); properties.put("jcr:name", PropertyType.NAME); registerValueIndex("multiColIndex", "nt:testType", null, "*", properties); Node root = session().getRootNode(); Node node1 = root.addNode("node1", "nt:testType"); node1.setProperty("stringProp", "string1"); Node node2 = root.addNode("node2", "nt:testType"); node2.setProperty("longProp", 1); Node node3 = root.addNode("node3", "nt:testType"); node3.setProperty("stringProp", "string3"); node3.setProperty("longProp", 2); session.save(); Query query = jcrSql2Query("SELECT * FROM [nt:testType] WHERE stringProp LIKE 'string%'"); validateQuery().rowCount(2L).hasNodesAtPaths("/node1", "/node3").useIndex("multiColIndex").validate(query, query.execute()); query = jcrSql2Query("SELECT * FROM [nt:testType] WHERE longProp >= 1"); validateQuery().rowCount(2L).hasNodesAtPaths("/node2", "/node3").useIndex("multiColIndex").validate(query, query.execute()); query = jcrSql2Query("SELECT * FROM [nt:testType] WHERE longProp > 1 AND stringProp LIKE 'string%' "); validateQuery().rowCount(1L).hasNodesAtPaths("/node3").useIndex("multiColIndex").validate(query, query.execute()); query = jcrSql2Query("SELECT * FROM [nt:testType] WHERE NAME() LIKE 'node%' "); validateQuery().rowCount(3L).hasNodesAtPaths("/node1", "/node2", "/node3").useIndex("multiColIndex").validate(query, query.execute()); } @Test(expected = InvalidIndexDefinitionException.class) public void shouldNotAllowMultiColumnTextIndex() throws Exception { Map<String, Integer> properties = new HashMap<>(); properties.put("prop1", PropertyType.STRING); properties.put("prop2", PropertyType.STRING); registerIndex("multiColTextIndex", IndexDefinition.IndexKind.TEXT, providerName(), "nt:unstructured", null, "*", properties); } @Test public void shouldUseIndexForFTSOnStringProperty() throws Exception { registerNodeType("nt:testType"); registerTextIndex("textIndex", "nt:testType", null, "*", "FTSProp", PropertyType.STRING); Node root = session().getRootNode(); Node node1 = root.addNode("node", "nt:testType"); String propertyText = "the quick Brown fox jumps over to the dog in at the gate"; node1.setProperty("FTSProp", propertyText); session.save(); Query query = jcrSql2Query("select [jcr:path] from [nt:testType] as n where contains([nt:testType].*,'" + propertyText + "')"); validateQuery().rowCount(1L).hasNodesAtPaths("/node").useIndex("textIndex").validate(query, query.execute()); jcrSql2Query("select [jcr:path] from [nt:testType] as n where contains(FTSProp,'" + propertyText + "')"); validateQuery().rowCount(1L).hasNodesAtPaths("/node").useIndex("textIndex").validate(query, query.execute()); jcrSql2Query("select [jcr:path] from [nt:testType] as n where contains(FTSProp,'" + propertyText.toUpperCase() + "')"); validateQuery().rowCount(1L).hasNodesAtPaths("/node").useIndex("textIndex").validate(query, query.execute()); jcrSql2Query("select [jcr:path] from [nt:testType] as n where contains(FTSProp,'the quick Dog')"); validateQuery().rowCount(1L).hasNodesAtPaths("/node").useIndex("textIndex").validate(query, query.execute()); jcrSql2Query("select [jcr:path] from [nt:testType] as n where contains(n.*,'the quick Dog')"); validateQuery().rowCount(1L).hasNodesAtPaths("/node").useIndex("textIndex").validate(query, query.execute()); jcrSql2Query("select [jcr:path] from [nt:testType] as n where contains(FTSProp,'the quick jumps over gate')"); validateQuery().rowCount(1L).hasNodesAtPaths("/node").useIndex("textIndex").validate(query, query.execute()); jcrSql2Query("select [jcr:path] from [nt:testType] as n where contains(n.*,'the quick jumps over gate')"); validateQuery().rowCount(1L).hasNodesAtPaths("/node").useIndex("textIndex").validate(query, query.execute()); jcrSql2Query("select [jcr:path] from [nt:testType] as n where contains(FTSProp,'the gate')"); validateQuery().rowCount(1L).hasNodesAtPaths("/node").useIndex("textIndex").validate(query, query.execute()); jcrSql2Query("select [jcr:path] from [nt:testType] as n where contains(n.*,'the gate')"); validateQuery().rowCount(1L).hasNodesAtPaths("/node").useIndex("textIndex").validate(query, query.execute()); } @Test public void shouldUseIndexForFTSOnBinaryProperty() throws Exception { registerTextIndex("textIndex", "nt:resource", null, "*", "jcr:data", PropertyType.BINARY); tools.uploadFile(session, "/node", resourceStream("text-file.txt")); session.save(); Query query = jcrSql2Query("select [jcr:path] from [nt:resource] as n where contains(n.*,'the quick jumps')"); validateQuery().rowCount(1L).hasNodesAtPaths("/node/jcr:content").useIndex("textIndex").validate(query, query.execute()); } @FixFor("MODE-2565") @Override @Test public void shouldNotReindexOnStartup() throws Exception { super.shouldNotReindexOnStartup(); } @Test @FixFor("MODE-2683") public void shouldUseIndexWithUpperAndLowerOperands() throws Exception { registerValueIndex("descriptionIndex", "mix:title", "Index for the 'jcr:title' property on mix:title", "*", "jcr:title", PropertyType.STRING); // Add a node that uses this type ... Node root = session().getRootNode(); Node book1 = root.addNode("myFirstBook"); book1.addMixin("mix:title"); book1.setProperty("jcr:title", "The Title"); Node book2 = root.addNode("mySecondBook"); book2.addMixin("mix:title"); book2.setProperty("jcr:title", "A Different Title"); Node book3 = root.addNode("myThirdBook"); book3.addMixin("mix:title"); book3.setProperty("jcr:title", "UPPERCASE TITLE"); session.save(); // Compute a query plan that will NOT use this index, since the selector doesn't match the index's node type. // If we would use this index, the index doesn't know about non-mix:title nodes like the 'other' node ... Query query = jcrSql2Query("SELECT * FROM [mix:title] WHERE UPPER([jcr:title]) LIKE '%TITLE%'"); validateQuery().rowCount(3L).useIndex("descriptionIndex").validate(query, query.execute()); query = jcrSql2Query("SELECT * FROM [mix:title] WHERE LOWER([jcr:title]) LIKE '%title%'"); validateQuery().rowCount(3L).useIndex("descriptionIndex").validate(query, query.execute()); query = jcrSql2Query("SELECT * FROM [mix:title] WHERE [jcr:title] LIKE '%title%'"); validateQuery().rowCount(0L).useIndex("descriptionIndex").validate(query, query.execute()); query = jcrSql2Query("SELECT * FROM [mix:title] WHERE [jcr:title] LIKE '%Title%'"); validateQuery().rowCount(2L).useIndex("descriptionIndex").validate(query, query.execute()); query = jcrSql2Query("SELECT * FROM [mix:title] WHERE [jcr:title] LIKE '%TITLE%'"); validateQuery().rowCount(1L).useIndex("descriptionIndex").validate(query, query.execute()); } @Test @Ignore("perf test") public void testPerformanceOnStringFields() throws Exception { registerNodeType("nt:testType"); registerValueIndex("stringIndex", "nt:testType", null, "*", "stringProp", PropertyType.STRING); Node rootNode = session.getRootNode(); Node containerNode = rootNode.addNode("container", "nt:unstructured"); int numNodes = 1000; long start = System.currentTimeMillis(); for (int i = 0; i < numNodes; i++) { Node node = containerNode.addNode("node-" + i, "nt:testType"); node.setProperty("id", i); node.setProperty("stringProp", "str" + i); } session.save(); System.out.printf("Inserted %d in %d ms.\n", numNodes, System.currentTimeMillis() - start); Random random = new Random(); start = System.currentTimeMillis(); int numSearches = 1000; for (int i = 0; i < numSearches; i++) { int findId = random.nextInt(numNodes); String findStr = "str" + findId; String queryStr = "select test.[id] from [nt:testType] as test where test.stringProp = '" + findStr + "'"; Query query = jcrSql2Query(session, queryStr); validateQuery().useIndex("stringIndex").hasNodesAtPaths("/container/node-" + findId).validate(query, query.execute()); } System.out.printf("Searched %d nodes %d times in %d ms.\n", numNodes, numSearches, System.currentTimeMillis() - start); containerNode.remove(); session.save(); } @Override protected void assertStorageLocationUnchangedAfterRestart() throws Exception { // register the total size and last modified timestamp of the place where indexes are stored for the default provider.. File indexesDir1 = new File("target/persistent_repository/indexes/lucene_primary/default/ref1"); assertTrue(indexesDir1.exists() && indexesDir1.isDirectory() && indexesDir1.canRead()); long sizeDir1 = FileUtil.size(indexesDir1.getPath()); final AtomicLong lastModifiedDateDir1 = lastModifiedFileTime(indexesDir1, "_0.*"); File indexesDir2 = new File("target/persistent_repository/indexes/lucene_primary/default/ref2"); assertTrue(indexesDir2.exists() && indexesDir2.isDirectory() && indexesDir2.canRead()); long sizeDir2 = FileUtil.size(indexesDir1.getPath()); final AtomicLong lastModifiedDateDir2 = lastModifiedFileTime(indexesDir2, "_0.*"); startRepository(); printMessage("Repository restart complete"); // and now check that the storage folder is unchanged assertEquals(sizeDir1, FileUtil.size(indexesDir1.getPath())); assertEquals(lastModifiedDateDir1.get(), lastModifiedFileTime(indexesDir1, "_0.*").get()); assertEquals(sizeDir2, FileUtil.size(indexesDir2.getPath())); assertEquals(lastModifiedDateDir2.get(), lastModifiedFileTime(indexesDir2, "_0.*").get()); } }