blob: c3409b8c0202522fbbe1f0ed7202f74d7b746a07 [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.jackrabbit.oak.plugins.index.elastic;
import org.apache.commons.io.FileUtils;
import org.apache.jackrabbit.JcrConstants;
import org.apache.jackrabbit.commons.jackrabbit.authorization.AccessControlUtils;
import org.apache.jackrabbit.oak.Oak;
import org.apache.jackrabbit.oak.api.Type;
import org.apache.jackrabbit.oak.jcr.Jcr;
import org.apache.jackrabbit.oak.plugins.index.elastic.index.ElasticIndexEditorProvider;
import org.apache.jackrabbit.oak.plugins.index.elastic.query.ElasticIndexProvider;
import org.apache.jackrabbit.oak.plugins.index.elastic.util.ElasticIndexDefinitionBuilder;
import org.apache.jackrabbit.oak.plugins.index.search.ExtractedTextCache;
import org.apache.jackrabbit.oak.plugins.index.search.util.IndexDefinitionBuilder;
import org.apache.jackrabbit.oak.plugins.memory.MemoryNodeStore;
import org.apache.jackrabbit.oak.spi.commit.Observer;
import org.apache.jackrabbit.oak.spi.query.QueryIndexProvider;
import org.apache.jackrabbit.oak.spi.state.NodeStore;
import org.apache.jackrabbit.oak.stats.StatisticsProvider;
import org.junit.After;
import org.junit.Before;
import org.junit.ClassRule;
import org.junit.Test;
import javax.jcr.GuestCredentials;
import javax.jcr.Node;
import javax.jcr.Repository;
import javax.jcr.RepositoryException;
import javax.jcr.Session;
import javax.jcr.SimpleCredentials;
import javax.jcr.query.Query;
import javax.jcr.query.QueryManager;
import javax.jcr.query.QueryResult;
import javax.jcr.query.Row;
import javax.jcr.query.RowIterator;
import javax.jcr.security.Privilege;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import java.util.UUID;
import static org.apache.jackrabbit.commons.JcrUtils.getOrCreateByPath;
import static org.apache.jackrabbit.oak.InitialContentHelper.INITIAL_CONTENT;
import static org.apache.jackrabbit.oak.plugins.index.IndexConstants.INDEX_DEFINITIONS_NAME;
import static org.apache.jackrabbit.oak.plugins.index.elastic.ElasticIndexDefinition.BULK_FLUSH_INTERVAL_MS_DEFAULT;
import static org.apache.jackrabbit.oak.plugins.index.search.FulltextIndexConstants.PROP_ANALYZED;
import static org.apache.jackrabbit.oak.plugins.index.search.FulltextIndexConstants.PROP_USE_IN_SPELLCHECK;
import static org.junit.Assert.assertEquals;
public class ElasticSpellcheckTest {
private Session adminSession;
private Session anonymousSession;
private QueryManager qe;
private Node indexNode;
// Set this connection string as
// <scheme>://<hostname>:<port>?key_id=<>,key_secret=<>
// key_id and key_secret are optional in case the ES server
// needs authentication
// Do not set this if docker is running and you want to run the tests on docker instead.
private static final String elasticConnectionString = System.getProperty("elasticConnectionString");
@ClassRule
public static final ElasticConnectionRule elasticRule = new ElasticConnectionRule(elasticConnectionString);
/*
Close the ES connection after every test method execution
*/
@After
public void cleanup() throws IOException {
anonymousSession.logout();
adminSession.logout();
elasticRule.closeElasticConnection();
}
@Before
public void setup() throws Exception {
createRepository();
final String indexName = createIndex();
indexNode = adminSession.getRootNode().getNode(INDEX_DEFINITIONS_NAME).getNode(indexName);
}
private void createRepository() throws RepositoryException {
ElasticConnection connection = elasticRule.useDocker() ? elasticRule.getElasticConnectionForDocker() :
elasticRule.getElasticConnectionFromString();
ElasticIndexEditorProvider editorProvider = new ElasticIndexEditorProvider(connection,
new ExtractedTextCache(10 * FileUtils.ONE_MB, 100));
ElasticIndexProvider indexProvider = new ElasticIndexProvider(connection,
new ElasticMetricHandler(StatisticsProvider.NOOP));
NodeStore nodeStore = new MemoryNodeStore(INITIAL_CONTENT);
Oak oak = new Oak(nodeStore)
.with(editorProvider)
.with((Observer) indexProvider)
.with((QueryIndexProvider) indexProvider);
Jcr jcr = new Jcr(oak);
Repository repository = jcr.createRepository();
adminSession = repository.login(new SimpleCredentials("admin", "admin".toCharArray()), null);
// we'd always query anonymously
anonymousSession = repository.login(new GuestCredentials(), null);
anonymousSession.refresh(true);
anonymousSession.save();
qe = anonymousSession.getWorkspace().getQueryManager();
}
private class IndexSkeleton {
IndexDefinitionBuilder indexDefinitionBuilder;
IndexDefinitionBuilder.IndexRule indexRule;
void initialize() {
initialize(JcrConstants.NT_BASE);
}
void initialize(String nodeType) {
indexDefinitionBuilder = new ElasticIndexDefinitionBuilder();
indexRule = indexDefinitionBuilder.indexRule(nodeType);
}
String build() throws RepositoryException {
final String indexName = UUID.randomUUID().toString();
indexDefinitionBuilder.build(adminSession.getRootNode().getNode(INDEX_DEFINITIONS_NAME).addNode(indexName));
return indexName;
}
}
private String createIndex() throws RepositoryException {
IndexSkeleton indexSkeleton = new IndexSkeleton();
indexSkeleton.initialize();
indexSkeleton.indexDefinitionBuilder.noAsync();
indexSkeleton.indexRule.property("cons").propertyIndex();
indexSkeleton.indexRule.property("foo").propertyIndex();
indexSkeleton.indexRule.property("foo").getBuilderTree().setProperty(PROP_USE_IN_SPELLCHECK, true, Type.BOOLEAN);
indexSkeleton.indexRule.property("foo").getBuilderTree().setProperty(PROP_ANALYZED, true, Type.BOOLEAN);
return indexSkeleton.build();
}
@Test
public void testSpellcheckSingleWord() throws Exception {
QueryManager qm = adminSession.getWorkspace().getQueryManager();
Node par = allow(getOrCreateByPath("/parent", "oak:Unstructured", adminSession));
Node n1 = par.addNode("node1");
n1.setProperty("foo", "descent");
Node n2 = n1.addNode("node2");
n2.setProperty("foo", "decent");
adminSession.save();
String sql = "SELECT [rep:spellcheck()] FROM nt:base WHERE SPELLCHECK('desent')";
Query q = qm.createQuery(sql, Query.SQL);
assertEventually(() -> {
try {
assertEquals("[decent, descent]", getResult(q.execute()).toString());
} catch (RepositoryException e) {
throw new RuntimeException(e);
}
});
}
@Test
public void testSpellcheckSingleWordWithDescendantNode() throws Exception {
QueryManager qm = adminSession.getWorkspace().getQueryManager();
Node par = allow(getOrCreateByPath("/parent", "oak:Unstructured", adminSession));
Node n1 = par.addNode("node1");
n1.setProperty("foo", "descent");
Node n2 = n1.addNode("node2");
n2.setProperty("foo", "decent");
adminSession.save();
String sql = "SELECT [rep:spellcheck()] FROM nt:base WHERE SPELLCHECK('desent') and isDescendantNode('/parent/node1')";
Query q = qm.createQuery(sql, Query.SQL);
assertEventually(() -> {
try {
assertEquals("[decent]", getResult(q.execute()).toString());
} catch (RepositoryException e) {
throw new RuntimeException(e);
}
});
}
@Test
public void testSpellcheckMultipleWords() throws Exception {
adminSession.save();
QueryManager qm = adminSession.getWorkspace().getQueryManager();
Node par = allow(getOrCreateByPath("/parent", "oak:Unstructured", adminSession));
Node n1 = par.addNode("node1");
n1.setProperty("foo", "it is always a good idea to go visiting ontario");
Node n2 = par.addNode("node2");
n2.setProperty("foo", "ontario is a nice place to live in");
Node n3 = par.addNode("node3");
n2.setProperty("foo", "I flied to ontario for voting for the major polls");
Node n4 = par.addNode("node4");
n2.setProperty("foo", "I will go voting in ontario, I always voted since I've been allowed to");
adminSession.save();
String sql = "SELECT [rep:spellcheck()] FROM nt:base WHERE SPELLCHECK('votin in ontari')";
Query q = qm.createQuery(sql, Query.SQL);
assertEventually(() -> {
try {
assertEquals("[voting in ontario]", getResult(q.execute()).toString());
} catch (RepositoryException e) {
throw new RuntimeException(e);
}
});
}
private Node allow(Node node) throws RepositoryException {
AccessControlUtils.allow(node, "anonymous", Privilege.JCR_READ);
return node;
}
private static List<String> getResult(QueryResult result) throws RepositoryException {
List<String> results = new ArrayList<>();
RowIterator it = result.getRows();
while (it.hasNext()) {
Row row = it.nextRow();
results.add(row.getValue("rep:spellcheck()").getString());
}
return results;
}
private static void assertEventually(Runnable r) {
ElasticTestUtils.assertEventually(r, BULK_FLUSH_INTERVAL_MS_DEFAULT * 3);
}
}