Author: fortino
Date: Thu Jun 25 14:58:08 2020
New Revision: 1879190

URL: http://svn.apache.org/viewvc?rev=1879190&view=rev
Log:
OAK-9120: oak-search-elastic: integrate spellcheck with async iterator, remove 
sync iterator

Added:
    
jackrabbit/oak/trunk/oak-search-elastic/src/main/java/org/apache/jackrabbit/oak/plugins/index/elastic/query/ElasticRequestHandler.java
   (with props)
    
jackrabbit/oak/trunk/oak-search-elastic/src/main/java/org/apache/jackrabbit/oak/plugins/index/elastic/query/ElasticResponseHandler.java
   (with props)
    
jackrabbit/oak/trunk/oak-search-elastic/src/main/java/org/apache/jackrabbit/oak/plugins/index/elastic/query/ElasticSpellcheckIterator.java
   (with props)
Removed:
    
jackrabbit/oak/trunk/oak-search-elastic/src/main/java/org/apache/jackrabbit/oak/plugins/index/elastic/query/ElasticProcess.java
    
jackrabbit/oak/trunk/oak-search-elastic/src/main/java/org/apache/jackrabbit/oak/plugins/index/elastic/query/ElasticQueryProcess.java
    
jackrabbit/oak/trunk/oak-search-elastic/src/main/java/org/apache/jackrabbit/oak/plugins/index/elastic/query/ElasticResultRowIterator.java
    
jackrabbit/oak/trunk/oak-search-elastic/src/main/java/org/apache/jackrabbit/oak/plugins/index/elastic/query/ElasticSearcher.java
    
jackrabbit/oak/trunk/oak-search-elastic/src/main/java/org/apache/jackrabbit/oak/plugins/index/elastic/query/ElasticSearcherModel.java
    
jackrabbit/oak/trunk/oak-search-elastic/src/main/java/org/apache/jackrabbit/oak/plugins/index/elastic/query/ElasticSpellcheckProcess.java
    
jackrabbit/oak/trunk/oak-search-elastic/src/main/java/org/apache/jackrabbit/oak/plugins/index/elastic/query/async/ElasticRequestHandler.java
    
jackrabbit/oak/trunk/oak-search-elastic/src/main/java/org/apache/jackrabbit/oak/plugins/index/elastic/query/async/ElasticResponseHandler.java
    
jackrabbit/oak/trunk/oak-search-elastic/src/main/java/org/apache/jackrabbit/oak/plugins/index/elastic/query/facets/ElasticAggregationData.java
    
jackrabbit/oak/trunk/oak-search-elastic/src/main/java/org/apache/jackrabbit/oak/plugins/index/elastic/query/facets/ElasticFacetHelper.java
    
jackrabbit/oak/trunk/oak-search-elastic/src/main/java/org/apache/jackrabbit/oak/plugins/index/elastic/query/facets/ElasticFacets.java
    
jackrabbit/oak/trunk/oak-search-elastic/src/main/java/org/apache/jackrabbit/oak/plugins/index/elastic/query/facets/InsecureElasticFacets.java
    
jackrabbit/oak/trunk/oak-search-elastic/src/main/java/org/apache/jackrabbit/oak/plugins/index/elastic/query/facets/SecureElasticFacets.java
    
jackrabbit/oak/trunk/oak-search-elastic/src/main/java/org/apache/jackrabbit/oak/plugins/index/elastic/query/facets/StatisticalElasticFacets.java
    
jackrabbit/oak/trunk/oak-search-elastic/src/main/java/org/apache/jackrabbit/oak/plugins/index/elastic/util/ElasticAggregationBuilderUtil.java
    
jackrabbit/oak/trunk/oak-search-elastic/src/main/java/org/apache/jackrabbit/oak/plugins/index/elastic/util/ElasticConstants.java
    
jackrabbit/oak/trunk/oak-search-elastic/src/main/java/org/apache/jackrabbit/oak/plugins/index/elastic/util/ElasticQueryUtil.java
    
jackrabbit/oak/trunk/oak-search-elastic/src/main/java/org/apache/jackrabbit/oak/plugins/index/elastic/util/SearchSourceBuilderUtil.java
Modified:
    
jackrabbit/oak/trunk/oak-search-elastic/src/main/java/org/apache/jackrabbit/oak/plugins/index/elastic/ElasticIndexDefinition.java
    
jackrabbit/oak/trunk/oak-search-elastic/src/main/java/org/apache/jackrabbit/oak/plugins/index/elastic/index/ElasticIndexHelper.java
    
jackrabbit/oak/trunk/oak-search-elastic/src/main/java/org/apache/jackrabbit/oak/plugins/index/elastic/query/ElasticIndex.java
    
jackrabbit/oak/trunk/oak-search-elastic/src/main/java/org/apache/jackrabbit/oak/plugins/index/elastic/query/async/ElasticResultRowAsyncIterator.java
    
jackrabbit/oak/trunk/oak-search-elastic/src/main/java/org/apache/jackrabbit/oak/plugins/index/elastic/query/async/facets/ElasticFacetProvider.java
    
jackrabbit/oak/trunk/oak-search-elastic/src/main/java/org/apache/jackrabbit/oak/plugins/index/elastic/query/async/facets/ElasticSecureFacetAsyncProvider.java
    
jackrabbit/oak/trunk/oak-search-elastic/src/main/java/org/apache/jackrabbit/oak/plugins/index/elastic/query/async/facets/ElasticStatisticalFacetAsyncProvider.java
    
jackrabbit/oak/trunk/oak-search-elastic/src/test/java/org/apache/jackrabbit/oak/plugins/index/elastic/ElasticSpellcheckTest.java
    
jackrabbit/oak/trunk/oak-search-elastic/src/test/java/org/apache/jackrabbit/oak/plugins/index/elastic/index/ElasticIndexHelperTest.java

Modified: 
jackrabbit/oak/trunk/oak-search-elastic/src/main/java/org/apache/jackrabbit/oak/plugins/index/elastic/ElasticIndexDefinition.java
URL: 
http://svn.apache.org/viewvc/jackrabbit/oak/trunk/oak-search-elastic/src/main/java/org/apache/jackrabbit/oak/plugins/index/elastic/ElasticIndexDefinition.java?rev=1879190&r1=1879189&r2=1879190&view=diff
==============================================================================
--- 
jackrabbit/oak/trunk/oak-search-elastic/src/main/java/org/apache/jackrabbit/oak/plugins/index/elastic/ElasticIndexDefinition.java
 (original)
+++ 
jackrabbit/oak/trunk/oak-search-elastic/src/main/java/org/apache/jackrabbit/oak/plugins/index/elastic/ElasticIndexDefinition.java
 Thu Jun 25 14:58:08 2020
@@ -187,7 +187,7 @@ public class ElasticIndexDefinition exte
         }
 
         @Override
-        public ElasticIndexDefinition.Builder reindex() {
+        public Builder reindex() {
             super.reindex();
             return this;
         }

Modified: 
jackrabbit/oak/trunk/oak-search-elastic/src/main/java/org/apache/jackrabbit/oak/plugins/index/elastic/index/ElasticIndexHelper.java
URL: 
http://svn.apache.org/viewvc/jackrabbit/oak/trunk/oak-search-elastic/src/main/java/org/apache/jackrabbit/oak/plugins/index/elastic/index/ElasticIndexHelper.java?rev=1879190&r1=1879189&r2=1879190&view=diff
==============================================================================
--- 
jackrabbit/oak/trunk/oak-search-elastic/src/main/java/org/apache/jackrabbit/oak/plugins/index/elastic/index/ElasticIndexHelper.java
 (original)
+++ 
jackrabbit/oak/trunk/oak-search-elastic/src/main/java/org/apache/jackrabbit/oak/plugins/index/elastic/index/ElasticIndexHelper.java
 Thu Jun 25 14:58:08 2020
@@ -74,6 +74,13 @@ class ElasticIndexHelper {
                         settingsBuilder.field("preserve_original", 
indexDefinition.indexOriginalTerms());
                     }
                     settingsBuilder.endObject();
+                    if (indexDefinition.isSpellcheckEnabled()) {
+                        settingsBuilder.startObject("shingle")
+                                .field("type", "shingle")
+                                .field("min_shingle_size", 2)
+                                .field("max_shingle_size", 3)
+                                .endObject();
+                    }
                 }
                 settingsBuilder.endObject();
 
@@ -93,14 +100,18 @@ class ElasticIndexHelper {
                         settingsBuilder.field("tokenizer", "path_hierarchy");
                     }
                     settingsBuilder.endObject();
+                    if (indexDefinition.isSpellcheckEnabled()) {
+                        settingsBuilder.startObject("trigram")
+                                .field("type", "custom")
+                                .field("tokenizer", "standard")
+                                .array("filter", "lowercase", "shingle")
+                                .endObject();
+                    }
                 }
                 settingsBuilder.endObject();
             }
             settingsBuilder.endObject();
         }
-        if (indexDefinition.isSpellcheckEnabled()) {
-            createSpellcheckMapping(indexDefinition, settingsBuilder);
-        }
         settingsBuilder.endObject();
         return settingsBuilder;
     }
@@ -132,25 +143,23 @@ class ElasticIndexHelper {
     }
 
     private static void mapIndexRules(ElasticIndexDefinition indexDefinition, 
XContentBuilder mappingBuilder) throws IOException {
-        // we need to check if in the defined rules there are properties with 
the same name and different types
-        final List<Map.Entry<String, List<PropertyDefinition>>> 
multiTypesFields = indexDefinition.getPropertiesByName()
-                .entrySet()
-                .stream()
-                .filter(e -> e.getValue().size() > 1)
-                .filter(e -> 
e.getValue().stream().map(PropertyDefinition::getType).distinct().count() > 1)
-                .collect(Collectors.toList());
-
-        if (!multiTypesFields.isEmpty()) {
-            String fields = 
multiTypesFields.stream().map(Map.Entry::getKey).collect(Collectors.joining(", 
", "[", "]"));
-            throw new IllegalStateException(indexDefinition.getIndexPath() + " 
has properties with the same name and " +
-                    "different types " + fields);
-        }
+        checkIndexRules(indexDefinition);
 
         for (Map.Entry<String, List<PropertyDefinition>> entry : 
indexDefinition.getPropertiesByName().entrySet()) {
             final String name = entry.getKey();
             final List<PropertyDefinition> propertyDefinitions = 
entry.getValue();
 
-            Type<?> type = Type.fromTag(propertyDefinitions.get(0).getType(), 
false);
+            Type<?> type = null;
+            boolean useInSpellCheck = false;
+            for (PropertyDefinition pd: propertyDefinitions) {
+                type = Type.fromTag(pd.getType(), false);
+                if (pd.useInSpellcheck) {
+                    useInSpellCheck = true;
+                    break;
+                }
+
+            }
+
             mappingBuilder.startObject(name);
             {
                 // 
https://www.elastic.co/guide/en/elasticsearch/reference/current/mapping-types.html
@@ -174,7 +183,7 @@ class ElasticIndexHelper {
                             mappingBuilder.startObject("keyword")
                                     .field("type", "keyword")
                                     .endObject();
-                            if (indexDefinition.isSpellcheckEnabled()) {
+                            if (useInSpellCheck) {
                                 mappingBuilder.startObject("trigram")
                                         .field("type", 
"text").field("analyzer", "trigram")
                                         .endObject();
@@ -191,33 +200,19 @@ class ElasticIndexHelper {
         }
     }
 
-    private static void createSpellcheckMapping(ElasticIndexDefinition 
indexDefinition, XContentBuilder settingsBuilder) throws IOException {
-        settingsBuilder.startObject("index");
-        {
-            settingsBuilder.startObject("analysis");
-            {
-                settingsBuilder.startObject("analyzer");
-                {
-                    settingsBuilder.startObject("trigram")
-                            .field("type", "custom")
-                            .field("tokenizer", "standard")
-                            .array("filter", "lowercase", "shingle")
-                            .endObject();
-                }
-                settingsBuilder.endObject();
+    // we need to check if in the defined rules there are properties with the 
same name and different types
+    private static void checkIndexRules(ElasticIndexDefinition 
indexDefinition) {
+        final List<Map.Entry<String, List<PropertyDefinition>>> 
multiTypesFields = indexDefinition.getPropertiesByName()
+                .entrySet()
+                .stream()
+                .filter(e -> e.getValue().size() > 1)
+                .filter(e -> 
e.getValue().stream().map(PropertyDefinition::getType).distinct().count() > 1)
+                .collect(Collectors.toList());
 
-                settingsBuilder.startObject("filter");
-                {
-                    settingsBuilder.startObject("shingle")
-                            .field("type", "shingle")
-                            .field("min_shingle_size", 2)
-                            .field("max_shingle_size", 3)
-                            .endObject();
-                }
-                settingsBuilder.endObject();
-            }
-            settingsBuilder.endObject();
+        if (!multiTypesFields.isEmpty()) {
+            String fields = 
multiTypesFields.stream().map(Map.Entry::getKey).collect(Collectors.joining(", 
", "[", "]"));
+            throw new IllegalStateException(indexDefinition.getIndexPath() + " 
has properties with the same name and " +
+                    "different types " + fields);
         }
-        settingsBuilder.endObject();
     }
 }

Modified: 
jackrabbit/oak/trunk/oak-search-elastic/src/main/java/org/apache/jackrabbit/oak/plugins/index/elastic/query/ElasticIndex.java
URL: 
http://svn.apache.org/viewvc/jackrabbit/oak/trunk/oak-search-elastic/src/main/java/org/apache/jackrabbit/oak/plugins/index/elastic/query/ElasticIndex.java?rev=1879190&r1=1879189&r2=1879190&view=diff
==============================================================================
--- 
jackrabbit/oak/trunk/oak-search-elastic/src/main/java/org/apache/jackrabbit/oak/plugins/index/elastic/query/ElasticIndex.java
 (original)
+++ 
jackrabbit/oak/trunk/oak-search-elastic/src/main/java/org/apache/jackrabbit/oak/plugins/index/elastic/query/ElasticIndex.java
 Thu Jun 25 14:58:08 2020
@@ -18,7 +18,7 @@ package org.apache.jackrabbit.oak.plugin
 
 import org.apache.jackrabbit.oak.commons.PathUtils;
 import org.apache.jackrabbit.oak.plugins.index.elastic.ElasticConnection;
-import 
org.apache.jackrabbit.oak.plugins.index.elastic.query.async.ElasticRequestHandler;
+import 
org.apache.jackrabbit.oak.plugins.index.elastic.query.async.ElasticResultRowAsyncIterator;
 import org.apache.jackrabbit.oak.plugins.index.search.IndexNode;
 import org.apache.jackrabbit.oak.plugins.index.search.SizeEstimator;
 import org.apache.jackrabbit.oak.plugins.index.search.spi.query.FulltextIndex;
@@ -28,12 +28,12 @@ import org.apache.jackrabbit.oak.spi.que
 import org.apache.jackrabbit.oak.spi.query.Filter;
 import org.apache.jackrabbit.oak.spi.state.NodeState;
 import org.elasticsearch.common.Strings;
-import org.elasticsearch.index.query.QueryBuilder;
 import org.jetbrains.annotations.NotNull;
 
 import java.util.Iterator;
 import java.util.Map;
 import java.util.WeakHashMap;
+import java.util.function.BiFunction;
 import java.util.function.Predicate;
 
 import static 
org.apache.jackrabbit.oak.plugins.index.IndexConstants.TYPE_PROPERTY_NAME;
@@ -44,6 +44,9 @@ class ElasticIndex extends FulltextIndex
             state -> 
TYPE_ELASTICSEARCH.equals(state.getString(TYPE_PROPERTY_NAME));
     private static final Map<String, LMSEstimator> ESTIMATORS = new 
WeakHashMap<>();
 
+    // no concept of rewound in ES (even if it might be doing it internally, 
we can't do much about it
+    private static final IteratorRewoundStateProvider 
REWOUND_STATE_PROVIDER_NOOP = () -> 0;
+
     // higher than some threshold below which the query should rather be 
answered by something else if possible
     private static final double MIN_COST = 100.1;
 
@@ -92,44 +95,38 @@ class ElasticIndex extends FulltextIndex
 
     @Override
     protected String getFulltextRequestString(IndexPlan plan, IndexNode 
indexNode) {
-        return Strings.toString(new ElasticRequestHandler(plan, 
getPlanResult(plan)).build());
+        return Strings.toString(new ElasticRequestHandler(plan, 
getPlanResult(plan)).baseQuery());
     }
 
     @Override
     public Cursor query(IndexPlan plan, NodeState rootState) {
         final Filter filter = plan.getFilter();
+        final FulltextIndexPlanner.PlanResult planResult = getPlanResult(plan);
 
-        // TODO: sorting
+        final ElasticRequestHandler requestHandler = new 
ElasticRequestHandler(plan, planResult);
+        final ElasticResponseHandler responseHandler = new 
ElasticResponseHandler(planResult, filter);
 
-        final FulltextIndexPlanner.PlanResult pr = getPlanResult(plan);
+        final Iterator<FulltextResultRow> itr;
+        if (requestHandler.requiresSpellCheck()) {
+            itr = new ElasticSpellcheckIterator(acquireIndexNode(plan), 
requestHandler, responseHandler);
+        } else {
+            // this function is called for each extracted row. Passing 
FulltextIndex::shouldInclude means that for each
+            // row we evaluate getPathRestriction(plan) & 
plan.getFilter().getPathRestriction(). Providing a partial
+            // function (https://en.wikipedia.org/wiki/Partial_function) we 
can evaluate them once and still use a predicate as before
+            BiFunction<String, Filter.PathRestriction, Predicate<String>> 
partialShouldInclude = (path, pathRestriction) -> docPath ->
+                    shouldInclude(path, pathRestriction, docPath);
+
+            itr = new ElasticResultRowAsyncIterator(
+                    acquireIndexNode(plan),
+                    requestHandler,
+                    responseHandler,
+                    plan,
+                    partialShouldInclude.apply(getPathRestriction(plan), 
filter.getPathRestriction()),
+                    getEstimator(plan.getPlanName())
+            );
 
-        // this function is called for each extracted row. Passing 
FulltextIndex::shouldInclude means that for each
-        // row we evaluate getPathRestriction(plan) & 
plan.getFilter().getPathRestriction(). Providing a partial
-        // function (https://en.wikipedia.org/wiki/Partial_function) we can 
evaluate them once and still use a predicate as before
-//        BiFunction<String, Filter.PathRestriction, Predicate<String>> 
partialShouldInclude = (path, pathRestriction) -> docPath ->
-//                shouldInclude(path, pathRestriction, docPath);
-//
-//        Iterator<FulltextResultRow> itr = new ElasticResultRowAsyncIterator(
-//                acquireIndexNode(plan),
-//                plan,
-//                pr,
-//                partialShouldInclude.apply(getPathRestriction(plan), 
plan.getFilter().getPathRestriction()),
-//                getEstimator(plan.getPlanName())
-//        );
-
-        Iterator<FulltextResultRow> itr = new ElasticResultRowIterator(filter, 
pr, plan,
-                acquireIndexNode(plan), FulltextIndex::shouldInclude, 
getEstimator(plan.getPlanName()));
-
-        /*
-        TODO: sync (nrt too??)
-        if (pr.hasPropertyIndexResult() || 
pr.evaluateSyncNodeTypeRestriction()) {
-            itr = mergePropertyIndexResult(plan, rootState, itr);
         }
-        */
-
-        // no concept of rewound in ES (even if it might be doing it 
internally, we can't do much about it
-        IteratorRewoundStateProvider rewoundStateProvider = () -> 0;
-        return new FulltextPathCursor(itr, rewoundStateProvider, plan, 
filter.getQueryLimits(), getSizeEstimator(plan));
+        return new FulltextPathCursor(itr, REWOUND_STATE_PROVIDER_NOOP, plan, 
filter.getQueryLimits(), getSizeEstimator(plan));
     }
 
     private LMSEstimator getEstimator(String path) {

Added: 
jackrabbit/oak/trunk/oak-search-elastic/src/main/java/org/apache/jackrabbit/oak/plugins/index/elastic/query/ElasticRequestHandler.java
URL: 
http://svn.apache.org/viewvc/jackrabbit/oak/trunk/oak-search-elastic/src/main/java/org/apache/jackrabbit/oak/plugins/index/elastic/query/ElasticRequestHandler.java?rev=1879190&view=auto
==============================================================================
--- 
jackrabbit/oak/trunk/oak-search-elastic/src/main/java/org/apache/jackrabbit/oak/plugins/index/elastic/query/ElasticRequestHandler.java
 (added)
+++ 
jackrabbit/oak/trunk/oak-search-elastic/src/main/java/org/apache/jackrabbit/oak/plugins/index/elastic/query/ElasticRequestHandler.java
 Thu Jun 25 14:58:08 2020
@@ -0,0 +1,549 @@
+/*
+ * 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.query;
+
+import org.apache.jackrabbit.oak.api.Type;
+import org.apache.jackrabbit.oak.commons.PathUtils;
+import org.apache.jackrabbit.oak.plugins.index.elastic.ElasticIndexDefinition;
+import 
org.apache.jackrabbit.oak.plugins.index.elastic.query.async.facets.ElasticFacetProvider;
+import org.apache.jackrabbit.oak.plugins.index.search.FieldNames;
+import org.apache.jackrabbit.oak.plugins.index.search.IndexDefinition;
+import org.apache.jackrabbit.oak.plugins.index.search.PropertyDefinition;
+import org.apache.jackrabbit.oak.plugins.index.search.spi.query.FulltextIndex;
+import 
org.apache.jackrabbit.oak.plugins.index.search.spi.query.FulltextIndexPlanner;
+import 
org.apache.jackrabbit.oak.plugins.index.search.spi.query.FulltextIndexPlanner.PlanResult;
+import org.apache.jackrabbit.oak.spi.query.Filter;
+import org.apache.jackrabbit.oak.spi.query.QueryConstants;
+import org.apache.jackrabbit.oak.spi.query.QueryIndex.IndexPlan;
+import org.apache.jackrabbit.oak.spi.query.fulltext.FullTextAnd;
+import org.apache.jackrabbit.oak.spi.query.fulltext.FullTextContains;
+import org.apache.jackrabbit.oak.spi.query.fulltext.FullTextExpression;
+import org.apache.jackrabbit.oak.spi.query.fulltext.FullTextOr;
+import org.apache.jackrabbit.oak.spi.query.fulltext.FullTextTerm;
+import org.apache.jackrabbit.oak.spi.query.fulltext.FullTextVisitor;
+import org.apache.lucene.search.WildcardQuery;
+import org.elasticsearch.index.query.BoolQueryBuilder;
+import org.elasticsearch.index.query.MatchPhraseQueryBuilder;
+import org.elasticsearch.index.query.MultiMatchQueryBuilder;
+import org.elasticsearch.index.query.Operator;
+import org.elasticsearch.index.query.QueryBuilder;
+import org.elasticsearch.index.query.QueryBuilders;
+import org.elasticsearch.index.search.MatchQuery;
+import org.elasticsearch.search.aggregations.AggregationBuilders;
+import 
org.elasticsearch.search.aggregations.bucket.terms.TermsAggregationBuilder;
+import org.elasticsearch.search.suggest.SuggestBuilders;
+import org.elasticsearch.search.suggest.phrase.DirectCandidateGeneratorBuilder;
+import org.elasticsearch.search.suggest.phrase.PhraseSuggestionBuilder;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+import javax.jcr.PropertyType;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.atomic.AtomicReference;
+import java.util.function.BiPredicate;
+import java.util.stream.Stream;
+import java.util.stream.StreamSupport;
+
+import static org.apache.jackrabbit.JcrConstants.JCR_MIXINTYPES;
+import static org.apache.jackrabbit.JcrConstants.JCR_PRIMARYTYPE;
+import static org.apache.jackrabbit.oak.commons.PathUtils.denotesRoot;
+import static org.apache.jackrabbit.oak.commons.PathUtils.getParentPath;
+import static 
org.apache.jackrabbit.oak.plugins.index.elastic.util.TermQueryBuilderFactory.newAncestorQuery;
+import static 
org.apache.jackrabbit.oak.plugins.index.elastic.util.TermQueryBuilderFactory.newDepthQuery;
+import static 
org.apache.jackrabbit.oak.plugins.index.elastic.util.TermQueryBuilderFactory.newMixinTypeQuery;
+import static 
org.apache.jackrabbit.oak.plugins.index.elastic.util.TermQueryBuilderFactory.newNodeTypeQuery;
+import static 
org.apache.jackrabbit.oak.plugins.index.elastic.util.TermQueryBuilderFactory.newNotNullPropQuery;
+import static 
org.apache.jackrabbit.oak.plugins.index.elastic.util.TermQueryBuilderFactory.newNullPropQuery;
+import static 
org.apache.jackrabbit.oak.plugins.index.elastic.util.TermQueryBuilderFactory.newPathQuery;
+import static 
org.apache.jackrabbit.oak.plugins.index.elastic.util.TermQueryBuilderFactory.newPrefixPathQuery;
+import static 
org.apache.jackrabbit.oak.plugins.index.elastic.util.TermQueryBuilderFactory.newPrefixQuery;
+import static 
org.apache.jackrabbit.oak.plugins.index.elastic.util.TermQueryBuilderFactory.newPropertyRestrictionQuery;
+import static 
org.apache.jackrabbit.oak.plugins.index.elastic.util.TermQueryBuilderFactory.newWildcardPathQuery;
+import static 
org.apache.jackrabbit.oak.plugins.index.elastic.util.TermQueryBuilderFactory.newWildcardQuery;
+import static org.apache.jackrabbit.oak.spi.query.QueryConstants.JCR_PATH;
+import static org.apache.jackrabbit.util.ISO8601.parse;
+import static org.elasticsearch.index.query.QueryBuilders.boolQuery;
+import static org.elasticsearch.index.query.QueryBuilders.matchAllQuery;
+import static org.elasticsearch.index.query.QueryBuilders.matchQuery;
+import static org.elasticsearch.index.query.QueryBuilders.queryStringQuery;
+import static org.elasticsearch.index.query.QueryBuilders.termQuery;
+
+/**
+ * Class to map query plans into Elastic request objects.
+ */
+public class ElasticRequestHandler {
+
+    private final static String SPELLCHECK_PREFIX = "spellcheck?term=";
+    private static final String ES_TRIGRAM_SUFFIX = ".trigram";
+
+    private final IndexPlan indexPlan;
+    private final Filter filter;
+    private final PlanResult planResult;
+    private final ElasticIndexDefinition elasticIndexDefinition;
+    private final String propertyRestrictionQuery;
+
+    ElasticRequestHandler(@NotNull IndexPlan indexPlan, @NotNull 
FulltextIndexPlanner.PlanResult planResult) {
+        this.indexPlan = indexPlan;
+        this.filter = indexPlan.getFilter();
+        this.planResult = planResult;
+        this.elasticIndexDefinition = (ElasticIndexDefinition) 
planResult.indexDefinition;
+
+        //Check if native function is supported
+        Filter.PropertyRestriction pr = null;
+        if (elasticIndexDefinition.hasFunctionDefined()) {
+            pr = 
filter.getPropertyRestriction(elasticIndexDefinition.getFunctionName());
+        }
+
+        this.propertyRestrictionQuery = pr != null ? 
String.valueOf(pr.first.getValue(pr.first.getType())) : null;
+    }
+
+    public BoolQueryBuilder baseQuery() {
+        final BoolQueryBuilder boolQuery = boolQuery();
+
+        FullTextExpression ft = filter.getFullTextConstraint();
+
+        if (ft != null) {
+            boolQuery.must(fullTextQuery(ft, planResult));
+        }
+
+        if (propertyRestrictionQuery != null) {
+            boolQuery.must(queryStringQuery(propertyRestrictionQuery));
+        } else if (planResult.evaluateNonFullTextConstraints()) {
+            for (QueryBuilder constraint: nonFullTextConstraints(indexPlan, 
planResult)) {
+                boolQuery.filter(constraint);
+            }
+        }
+
+        // TODO: sort with no other restriction
+
+        if (!boolQuery.hasClauses()) {
+            // TODO: what happens here in planning mode (specially, apparently 
for things like rep:similar)
+            //For purely nodeType based queries all the documents would have to
+            //be returned (if the index definition has a single rule)
+            if (planResult.evaluateNodeTypeRestriction()) {
+                boolQuery.must(matchAllQuery());
+            }
+        }
+
+        return boolQuery;
+    }
+
+    public String getPropertyRestrictionQuery() {
+        return propertyRestrictionQuery;
+    }
+
+    public boolean requiresSpellCheck() {
+        return propertyRestrictionQuery != null && 
propertyRestrictionQuery.startsWith(SPELLCHECK_PREFIX);
+    }
+
+    public ElasticFacetProvider getAsyncFacetProvider(ElasticResponseHandler 
responseHandler) {
+        return requiresFacets() ?
+                ElasticFacetProvider.getProvider(
+                        
planResult.indexDefinition.getSecureFacetConfiguration(),
+                        this, responseHandler,
+                        filter::isAccessible
+                ) : null;
+    }
+
+    private boolean requiresFacets() {
+        return filter.getPropertyRestrictions()
+                .stream()
+                .anyMatch(pr -> 
QueryConstants.REP_FACET.equals(pr.propertyName));
+    }
+
+    public Stream<TermsAggregationBuilder> aggregations() {
+        return facetFields()
+                .map(facetProp ->
+                        AggregationBuilders.terms(facetProp)
+                                
.field(elasticIndexDefinition.getElasticKeyword(facetProp))
+                                
.size(elasticIndexDefinition.getNumberOfTopFacets())
+                );
+    }
+
+    public Stream<String> facetFields() {
+        return filter.getPropertyRestrictions()
+                .stream()
+                .filter(pr -> QueryConstants.REP_FACET.equals(pr.propertyName))
+                .map(pr -> 
FulltextIndex.parseFacetField(pr.first.getValue(Type.STRING)));
+    }
+
+    public Stream<String> spellCheckFields() {
+        return StreamSupport
+                .stream(planResult.indexingRule.getProperties().spliterator(), 
false)
+                .filter(pd -> pd.useInSpellcheck)
+                .map(pd -> pd.name);
+    }
+
+    public PhraseSuggestionBuilder suggestQuery(String field, String 
spellCheckQuery) {
+        BoolQueryBuilder query = boolQuery()
+                .must(new MatchPhraseQueryBuilder(field, "{{suggestion}}"));
+
+        nonFullTextConstraints(indexPlan, planResult).forEach(query::must);
+
+        PhraseSuggestionBuilder.CandidateGenerator candidateGeneratorBuilder =
+                new 
DirectCandidateGeneratorBuilder(getTrigramField(field)).suggestMode("missing");
+        return SuggestBuilders
+                .phraseSuggestion(getTrigramField(field))
+                .size(10)
+                .addCandidateGenerator(candidateGeneratorBuilder)
+                .text(spellCheckQuery)
+                .collateQuery(query.toString());
+    }
+
+    public BoolQueryBuilder suggestMatchQuery(String suggestion, String[] 
fields) {
+        BoolQueryBuilder query = boolQuery()
+                .must(new MultiMatchQueryBuilder(suggestion, fields)
+                        .operator(Operator.AND).fuzzyTranspositions(false)
+                        .autoGenerateSynonymsPhraseQuery(false)
+                        .type(MatchQuery.Type.PHRASE));
+
+        nonFullTextConstraints(indexPlan, planResult).forEach(query::must);
+
+        return query;
+    }
+
+    private String getTrigramField(String field) {
+        return field + ES_TRIGRAM_SUFFIX;
+    }
+
+    private QueryBuilder fullTextQuery(FullTextExpression ft, final PlanResult 
pr) {
+        // a reference to the query, so it can be set in the visitor
+        // (a "non-local return")
+        final AtomicReference<QueryBuilder> result = new AtomicReference<>();
+        ft.accept(new FullTextVisitor() {
+
+            @Override
+            public boolean visit(FullTextContains contains) {
+                visitTerm(contains.getPropertyName(), contains.getRawText(), 
null, contains.isNot());
+                return true;
+            }
+
+            @Override
+            public boolean visit(FullTextOr or) {
+                BoolQueryBuilder q = boolQuery();
+                for (FullTextExpression e : or.list) {
+                    q.should(fullTextQuery(e, pr));
+                }
+                result.set(q);
+                return true;
+            }
+
+            @Override
+            public boolean visit(FullTextAnd and) {
+                BoolQueryBuilder q = boolQuery();
+                for (FullTextExpression e : and.list) {
+                    QueryBuilder x = fullTextQuery(e, pr);
+                    // TODO: see OAK-2434 and see if ES also can't work 
without unwrapping
+                    /* Only unwrap the clause if MUST_NOT(x) */
+                    boolean hasMustNot = false;
+                    if (x instanceof BoolQueryBuilder) {
+                        BoolQueryBuilder bq = (BoolQueryBuilder) x;
+                        if (bq.mustNot().size() == 1
+                                // no other clauses
+                                && bq.should().isEmpty() && 
bq.must().isEmpty() && bq.filter().isEmpty()) {
+                            hasMustNot = true;
+                            q.mustNot(bq.mustNot().get(0));
+                        }
+                    }
+
+                    if (!hasMustNot) {
+                        q.must(x);
+                    }
+                }
+                result.set(q);
+                return true;
+            }
+
+            @Override
+            public boolean visit(FullTextTerm term) {
+                return visitTerm(term.getPropertyName(), term.getText(), 
term.getBoost(), term.isNot());
+            }
+
+            private boolean visitTerm(String propertyName, String text, String 
boost, boolean not) {
+                String p = getLuceneFieldName(propertyName, pr);
+                QueryBuilder q = tokenToQuery(text, p, pr);
+                if (boost != null) {
+                    q.boost(Float.parseFloat(boost));
+                }
+                if (not) {
+                    BoolQueryBuilder bq = boolQuery().mustNot(q);
+                    result.set(bq);
+                } else {
+                    result.set(q);
+                }
+                return true;
+            }
+        });
+        return result.get();
+    }
+
+    private List<QueryBuilder> nonFullTextConstraints(IndexPlan plan, 
PlanResult planResult) {
+        final BiPredicate<Iterable<String>, String> any = (iterable, value) ->
+                StreamSupport.stream(iterable.spliterator(), 
false).anyMatch(value::equals);
+
+        final List<QueryBuilder> queries = new ArrayList<>();
+
+        Filter filter = plan.getFilter();
+        if (!filter.matchesAllTypes()) {
+            queries.add(nodeTypeConstraints(planResult.indexingRule, filter));
+        }
+
+        String path = FulltextIndex.getPathRestriction(plan);
+        switch (filter.getPathRestriction()) {
+            case ALL_CHILDREN:
+                if (!"/".equals(path)) {
+                    queries.add(newAncestorQuery(path));
+                }
+                break;
+            case DIRECT_CHILDREN:
+                BoolQueryBuilder bq = boolQuery()
+                    .must(newAncestorQuery(path))
+                    .must(newDepthQuery(path, planResult));
+                queries.add(bq);
+                break;
+            case EXACT:
+                // For transformed paths, we can only add path restriction if 
absolute path to property can be
+                // deduced
+                if (planResult.isPathTransformed()) {
+                    String parentPathSegment = 
planResult.getParentPathSegment();
+                    if (!any.test(PathUtils.elements(parentPathSegment), "*")) 
{
+                        queries.add(newPathQuery(path + parentPathSegment));
+                    }
+                } else {
+                    queries.add(newPathQuery(path));
+                }
+                break;
+            case PARENT:
+                if (denotesRoot(path)) {
+                    // there's no parent of the root node
+                    // we add a path that can not possibly occur because there
+                    // is no way to say "match no documents" in Lucene
+                    queries.add(newPathQuery("///"));
+                } else {
+                    // For transformed paths, we can only add path restriction 
if absolute path to property can be
+                    // deduced
+                    if (planResult.isPathTransformed()) {
+                        String parentPathSegment = 
planResult.getParentPathSegment();
+                        if (!any.test(PathUtils.elements(parentPathSegment), 
"*")) {
+                            queries.add(newPathQuery(getParentPath(path) + 
parentPathSegment));
+                        }
+                    } else {
+                        queries.add(newPathQuery(getParentPath(path)));
+                    }
+                }
+                break;
+            case NO_RESTRICTION:
+                break;
+        }
+
+        for (Filter.PropertyRestriction pr : filter.getPropertyRestrictions()) 
{
+            String name = pr.propertyName;
+
+            if (QueryConstants.REP_EXCERPT.equals(name) || 
QueryConstants.OAK_SCORE_EXPLANATION.equals(name)
+                    || QueryConstants.REP_FACET.equals(name)) {
+                continue;
+            }
+
+            if (QueryConstants.RESTRICTION_LOCAL_NAME.equals(name)) {
+                if (planResult.evaluateNodeNameRestriction()) {
+                    QueryBuilder q = nodeName(pr);
+                    if (q != null) {
+                        queries.add(q);
+                    }
+                }
+                continue;
+            }
+
+            if (pr.first != null && pr.first.equals(pr.last) && 
pr.firstIncluding && pr.lastIncluding) {
+                String first = pr.first.getValue(Type.STRING);
+                first = first.replace("\\", "");
+                if (JCR_PATH.equals(name)) {
+                    queries.add(newPathQuery(first));
+                    continue;
+                } else if ("*".equals(name)) {
+                    //TODO Revisit reference constraint. For performant impl
+                    //references need to be indexed in a different manner
+                    queries.add(referenceConstraint(first));
+                    continue;
+                }
+            }
+
+            PropertyDefinition pd = planResult.getPropDefn(pr);
+            if (pd == null) {
+                continue;
+            }
+
+            QueryBuilder q = createQuery(planResult.getPropertyName(pr), pr, 
pd);
+            if (q != null) {
+                queries.add(q);
+            }
+        }
+        return queries;
+    }
+
+    private static QueryBuilder 
nodeTypeConstraints(IndexDefinition.IndexingRule defn, Filter filter) {
+        final BoolQueryBuilder bq = boolQuery();
+        PropertyDefinition primaryType = defn.getConfig(JCR_PRIMARYTYPE);
+        //TODO OAK-2198 Add proper nodeType query support
+
+        if (primaryType != null && primaryType.propertyIndex) {
+            for (String type : filter.getPrimaryTypes()) {
+                bq.should(newNodeTypeQuery(type));
+            }
+        }
+
+        PropertyDefinition mixinType = defn.getConfig(JCR_MIXINTYPES);
+        if (mixinType != null && mixinType.propertyIndex) {
+            for (String type : filter.getMixinTypes()) {
+                bq.should(newMixinTypeQuery(type));
+            }
+        }
+
+        return bq;
+    }
+
+    private static QueryBuilder nodeName(Filter.PropertyRestriction pr) {
+        String first = pr.first != null ? pr.first.getValue(Type.STRING) : 
null;
+        if (pr.first != null && pr.first.equals(pr.last) && pr.firstIncluding
+                && pr.lastIncluding) {
+            // [property]=[value]
+            return termQuery(FieldNames.NODE_NAME, first);
+        }
+
+        if (pr.isLike) {
+            return like(FieldNames.NODE_NAME, first);
+        }
+
+        throw new IllegalStateException("For nodeName queries only EQUALS and 
LIKE are supported " + pr);
+    }
+
+    private static QueryBuilder like(String name, String first) {
+        first = first.replace('%', WildcardQuery.WILDCARD_STRING);
+        first = first.replace('_', WildcardQuery.WILDCARD_CHAR);
+
+        int indexOfWS = first.indexOf(WildcardQuery.WILDCARD_STRING);
+        int indexOfWC = first.indexOf(WildcardQuery.WILDCARD_CHAR);
+        int len = first.length();
+
+        if (indexOfWS == len || indexOfWC == len) {
+            // remove trailing "*" for prefix query
+            first = first.substring(0, first.length() - 1);
+            if (JCR_PATH.equals(name)) {
+                return newPrefixPathQuery(first);
+            } else {
+                return newPrefixQuery(name, first);
+            }
+        } else {
+            if (JCR_PATH.equals(name)) {
+                return newWildcardPathQuery(first);
+            } else {
+                return newWildcardQuery(name, first);
+            }
+        }
+    }
+
+    private static QueryBuilder referenceConstraint(String uuid) {
+        // TODO: this seems very bad as a query - do we really want to support 
it. In fact, is it even used?
+        // reference query
+        return QueryBuilders.multiMatchQuery(uuid);
+    }
+
+    private static QueryBuilder tokenToQuery(String text, String fieldName, 
PlanResult pr) {
+        QueryBuilder ret;
+        IndexDefinition.IndexingRule indexingRule = pr.indexingRule;
+        //Expand the query on fulltext field
+        if (FieldNames.FULLTEXT.equals(fieldName) &&
+                !indexingRule.getNodeScopeAnalyzedProps().isEmpty()) {
+            BoolQueryBuilder in = boolQuery();
+            for (PropertyDefinition pd : 
indexingRule.getNodeScopeAnalyzedProps()) {
+                QueryBuilder q = matchQuery(pd.name, text).boost(pd.boost);
+                in.should(q);
+            }
+
+            //Add the query for actual fulltext field also. That query would 
not be boosted
+            // TODO: do we need this if all the analyzed fields are queried?
+            ret = in.should(matchQuery(fieldName, text));
+        } else {
+            ret = matchQuery(fieldName, text);
+        }
+
+        return ret;
+    }
+
+    private QueryBuilder createQuery(String propertyName, 
Filter.PropertyRestriction pr,
+                                     PropertyDefinition defn) {
+        int propType = FulltextIndex.determinePropertyType(defn, pr);
+
+        if (pr.isNullRestriction()) {
+            return newNullPropQuery(defn.name);
+        }
+
+        //If notNullCheckEnabled explicitly enabled use the simple TermQuery
+        //otherwise later fallback to range query
+        if (pr.isNotNullRestriction() && defn.notNullCheckEnabled) {
+            return newNotNullPropQuery(defn.name);
+        }
+
+        final String field = 
elasticIndexDefinition.getElasticKeyword(propertyName);
+
+        QueryBuilder in;
+        switch (propType) {
+            case PropertyType.DATE: {
+                in = newPropertyRestrictionQuery(field, pr, value -> 
parse(value.getValue(Type.DATE)).getTime());
+                break;
+            }
+            case PropertyType.DOUBLE: {
+                in = newPropertyRestrictionQuery(field, pr, value -> 
value.getValue(Type.DOUBLE));
+                break;
+            }
+            case PropertyType.LONG: {
+                in = newPropertyRestrictionQuery(field, pr, value -> 
value.getValue(Type.LONG));
+                break;
+            }
+            default: {
+                if (pr.isLike) {
+                    return like(propertyName, pr.first.getValue(Type.STRING));
+                }
+
+                //TODO Confirm that all other types can be treated as string
+                in = newPropertyRestrictionQuery(field, pr, value -> 
value.getValue(Type.STRING));
+            }
+        }
+
+        if (in != null) {
+            return in;
+        }
+
+        throw new IllegalStateException("PropertyRestriction not handled " + 
pr + " for index " + defn);
+    }
+
+    private static String getLuceneFieldName(@Nullable String p, PlanResult 
pr) {
+        if (p == null) {
+            return FieldNames.FULLTEXT;
+        }
+
+        if (pr.isPathTransformed()) {
+            p = PathUtils.getName(p);
+        }
+
+        if ("*".equals(p)) {
+            p = FieldNames.FULLTEXT;
+        }
+        return p;
+    }
+}

Propchange: 
jackrabbit/oak/trunk/oak-search-elastic/src/main/java/org/apache/jackrabbit/oak/plugins/index/elastic/query/ElasticRequestHandler.java
------------------------------------------------------------------------------
    svn:eol-style = native

Added: 
jackrabbit/oak/trunk/oak-search-elastic/src/main/java/org/apache/jackrabbit/oak/plugins/index/elastic/query/ElasticResponseHandler.java
URL: 
http://svn.apache.org/viewvc/jackrabbit/oak/trunk/oak-search-elastic/src/main/java/org/apache/jackrabbit/oak/plugins/index/elastic/query/ElasticResponseHandler.java?rev=1879190&view=auto
==============================================================================
--- 
jackrabbit/oak/trunk/oak-search-elastic/src/main/java/org/apache/jackrabbit/oak/plugins/index/elastic/query/ElasticResponseHandler.java
 (added)
+++ 
jackrabbit/oak/trunk/oak-search-elastic/src/main/java/org/apache/jackrabbit/oak/plugins/index/elastic/query/ElasticResponseHandler.java
 Thu Jun 25 14:58:08 2020
@@ -0,0 +1,68 @@
+/*
+ * 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.query;
+
+import org.apache.jackrabbit.oak.plugins.index.search.FieldNames;
+import 
org.apache.jackrabbit.oak.plugins.index.search.spi.query.FulltextIndexPlanner;
+import 
org.apache.jackrabbit.oak.plugins.index.search.spi.query.FulltextIndexPlanner.PlanResult;
+import org.apache.jackrabbit.oak.spi.query.Filter;
+import org.elasticsearch.search.SearchHit;
+import org.jetbrains.annotations.NotNull;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.util.Map;
+
+/**
+ * Class to process Elastic response objects.
+ */
+public class ElasticResponseHandler {
+
+    private static final Logger LOG = 
LoggerFactory.getLogger(ElasticResponseHandler.class);
+
+    private final PlanResult planResult;
+    private final Filter filter;
+
+    ElasticResponseHandler(@NotNull FulltextIndexPlanner.PlanResult 
planResult, @NotNull Filter filter) {
+        this.planResult = planResult;
+        this.filter = filter;
+    }
+
+    public String getPath(SearchHit hit) {
+        final Map<String, Object> sourceMap = hit.getSourceAsMap();
+        String path = (String) sourceMap.get(FieldNames.PATH);
+
+        if ("".equals(path)) {
+            path = "/";
+        }
+        if (planResult.isPathTransformed()) {
+            String originalPath = path;
+            path = planResult.transformPath(path);
+
+            if (path == null) {
+                LOG.trace("Ignoring path {} : Transformation returned null", 
originalPath);
+                return null;
+            }
+        }
+
+        return path;
+    }
+
+    public boolean isAccessible(String path) {
+        return filter.isAccessible(path);
+    }
+}

Propchange: 
jackrabbit/oak/trunk/oak-search-elastic/src/main/java/org/apache/jackrabbit/oak/plugins/index/elastic/query/ElasticResponseHandler.java
------------------------------------------------------------------------------
    svn:eol-style = native

Added: 
jackrabbit/oak/trunk/oak-search-elastic/src/main/java/org/apache/jackrabbit/oak/plugins/index/elastic/query/ElasticSpellcheckIterator.java
URL: 
http://svn.apache.org/viewvc/jackrabbit/oak/trunk/oak-search-elastic/src/main/java/org/apache/jackrabbit/oak/plugins/index/elastic/query/ElasticSpellcheckIterator.java?rev=1879190&view=auto
==============================================================================
--- 
jackrabbit/oak/trunk/oak-search-elastic/src/main/java/org/apache/jackrabbit/oak/plugins/index/elastic/query/ElasticSpellcheckIterator.java
 (added)
+++ 
jackrabbit/oak/trunk/oak-search-elastic/src/main/java/org/apache/jackrabbit/oak/plugins/index/elastic/query/ElasticSpellcheckIterator.java
 Thu Jun 25 14:58:08 2020
@@ -0,0 +1,135 @@
+package org.apache.jackrabbit.oak.plugins.index.elastic.query;
+
+import org.apache.jackrabbit.oak.plugins.index.search.FieldNames;
+import 
org.apache.jackrabbit.oak.plugins.index.search.spi.query.FulltextIndex.FulltextResultRow;
+import org.elasticsearch.action.search.MultiSearchRequest;
+import org.elasticsearch.action.search.MultiSearchResponse;
+import org.elasticsearch.action.search.SearchRequest;
+import org.elasticsearch.action.search.SearchResponse;
+import org.elasticsearch.client.RequestOptions;
+import org.elasticsearch.search.SearchHit;
+import org.elasticsearch.search.builder.SearchSourceBuilder;
+import org.elasticsearch.search.suggest.SuggestBuilder;
+import org.elasticsearch.search.suggest.phrase.PhraseSuggestion;
+import org.jetbrains.annotations.NotNull;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.IOException;
+import java.util.ArrayDeque;
+import java.util.ArrayList;
+import java.util.Iterator;
+import java.util.stream.Stream;
+import java.util.stream.StreamSupport;
+
+/**
+ * This class is in charge to extract spell checked suggestions for a given 
query.
+ *
+ * It requires 2 calls to Elastic:
+ * <ul>
+ *     <li>get all the possible spellchecked suggestions</li>
+ *     <li>multi search query to get a sample of 100 results for each 
suggestion for ACL check</li>
+ * </ul>
+ */
+class ElasticSpellcheckIterator implements Iterator<FulltextResultRow> {
+
+    private static final Logger LOG = 
LoggerFactory.getLogger(ElasticSpellcheckIterator.class);
+    protected final static String SPELLCHECK_PREFIX = "spellcheck?term=";
+
+    private final ElasticIndexNode indexNode;
+    private final ElasticRequestHandler requestHandler;
+    private final ElasticResponseHandler responseHandler;
+    private final String[] spellCheckFields;
+    private final String spellCheckQuery;
+
+    private Iterator<FulltextResultRow> internalIterator;
+    private boolean loaded = false;
+
+    ElasticSpellcheckIterator(@NotNull ElasticIndexNode indexNode,
+                              @NotNull ElasticRequestHandler requestHandler,
+                              @NotNull ElasticResponseHandler responseHandler) 
{
+        this.indexNode = indexNode;
+        this.requestHandler = requestHandler;
+        this.responseHandler = responseHandler;
+        this.spellCheckFields = 
requestHandler.spellCheckFields().toArray(String[]::new);
+        this.spellCheckQuery = 
requestHandler.getPropertyRestrictionQuery().replace(SPELLCHECK_PREFIX, "");
+    }
+
+    @Override
+    public boolean hasNext() {
+        if (!loaded) {
+            loadSuggestions();
+            loaded = true;
+        }
+        return internalIterator != null && internalIterator.hasNext();
+    }
+
+    @Override
+    public FulltextResultRow next() {
+        return internalIterator.next();
+    }
+
+    private void loadSuggestions() {
+        try {
+            final ArrayDeque<String> suggestionTexts = new ArrayDeque<>();
+            final MultiSearchRequest multiSearch = suggestions()
+                    .map(s -> {
+                        String text = s.getText().string();
+                        suggestionTexts.offer(text);
+                        return requestHandler.suggestMatchQuery(text, 
spellCheckFields);
+                    })
+                    .map(query -> SearchSourceBuilder.searchSource()
+                            .query(query)
+                            .size(100)
+                            .fetchSource(FieldNames.PATH, null))
+                    .map(searchSource -> new 
SearchRequest(indexNode.getDefinition().getRemoteIndexAlias())
+                            .source(searchSource))
+                    .reduce(new MultiSearchRequest(), MultiSearchRequest::add, 
(ms, ms2) -> ms);
+
+            ArrayList<FulltextResultRow> results = new ArrayList<>();
+            MultiSearchResponse res = 
indexNode.getConnection().getClient().msearch(multiSearch, 
RequestOptions.DEFAULT);
+            for (MultiSearchResponse.Item response : res.getResponses()) {
+                boolean hasResults = false;
+                for (SearchHit doc : response.getResponse().getHits()) {
+                    if 
(responseHandler.isAccessible(responseHandler.getPath(doc))) {
+                        hasResults = true;
+                        break;
+                    }
+                }
+                String suggestion = suggestionTexts.poll();
+                if (hasResults) {
+                    results.add(new FulltextResultRow(suggestion));
+                }
+            }
+
+            this.internalIterator = results.iterator();
+
+        } catch (IOException e) {
+            LOG.error("Error processing suggestions for " + spellCheckQuery, 
e);
+        }
+
+    }
+
+    private Stream<PhraseSuggestion.Entry.Option> suggestions() throws 
IOException {
+        final SuggestBuilder suggestBuilder = new SuggestBuilder();
+        for (int i = 0; i < spellCheckFields.length; i++) {
+            suggestBuilder.addSuggestion("oak:suggestion-" + i,
+                    requestHandler.suggestQuery(spellCheckFields[i], 
spellCheckQuery));
+        }
+
+        final SearchSourceBuilder searchSourceBuilder = 
SearchSourceBuilder.searchSource()
+                .suggest(suggestBuilder);
+
+        final SearchRequest searchRequest = new 
SearchRequest(indexNode.getDefinition().getRemoteIndexAlias())
+                .source(searchSourceBuilder);
+
+        SearchResponse searchResponse = 
indexNode.getConnection().getClient().search(searchRequest, 
RequestOptions.DEFAULT);
+
+        return StreamSupport
+                .stream(searchResponse.getSuggest().spliterator(), false)
+                .map(s -> (PhraseSuggestion) s)
+                .flatMap(ps -> ps.getEntries().stream())
+                .flatMap(ps -> ps.getOptions().stream())
+                .sorted((o1, o2) -> Float.compare(o2.getScore(), 
o1.getScore()));
+    }
+}

Propchange: 
jackrabbit/oak/trunk/oak-search-elastic/src/main/java/org/apache/jackrabbit/oak/plugins/index/elastic/query/ElasticSpellcheckIterator.java
------------------------------------------------------------------------------
    svn:eol-style = native

Modified: 
jackrabbit/oak/trunk/oak-search-elastic/src/main/java/org/apache/jackrabbit/oak/plugins/index/elastic/query/async/ElasticResultRowAsyncIterator.java
URL: 
http://svn.apache.org/viewvc/jackrabbit/oak/trunk/oak-search-elastic/src/main/java/org/apache/jackrabbit/oak/plugins/index/elastic/query/async/ElasticResultRowAsyncIterator.java?rev=1879190&r1=1879189&r2=1879190&view=diff
==============================================================================
--- 
jackrabbit/oak/trunk/oak-search-elastic/src/main/java/org/apache/jackrabbit/oak/plugins/index/elastic/query/async/ElasticResultRowAsyncIterator.java
 (original)
+++ 
jackrabbit/oak/trunk/oak-search-elastic/src/main/java/org/apache/jackrabbit/oak/plugins/index/elastic/query/async/ElasticResultRowAsyncIterator.java
 Thu Jun 25 14:58:08 2020
@@ -17,11 +17,11 @@
 package org.apache.jackrabbit.oak.plugins.index.elastic.query.async;
 
 import org.apache.jackrabbit.oak.plugins.index.elastic.query.ElasticIndexNode;
+import 
org.apache.jackrabbit.oak.plugins.index.elastic.query.ElasticRequestHandler;
+import 
org.apache.jackrabbit.oak.plugins.index.elastic.query.ElasticResponseHandler;
 import 
org.apache.jackrabbit.oak.plugins.index.elastic.query.async.facets.ElasticFacetProvider;
 import org.apache.jackrabbit.oak.plugins.index.search.FieldNames;
 import 
org.apache.jackrabbit.oak.plugins.index.search.spi.query.FulltextIndex.FulltextResultRow;
-import 
org.apache.jackrabbit.oak.plugins.index.search.spi.query.FulltextIndexPlanner;
-import 
org.apache.jackrabbit.oak.plugins.index.search.spi.query.FulltextIndexPlanner.PlanResult;
 import org.apache.jackrabbit.oak.plugins.index.search.util.LMSEstimator;
 import org.apache.jackrabbit.oak.spi.query.QueryIndex;
 import org.apache.jackrabbit.oak.spi.query.QueryIndex.IndexPlan;
@@ -68,7 +68,6 @@ public class ElasticResultRowAsyncIterat
 
     private final ElasticIndexNode indexNode;
     private final IndexPlan indexPlan;
-    private final PlanResult planResult;
     private final Predicate<String> rowInclusionPredicate;
     private final LMSEstimator estimator;
 
@@ -80,19 +79,19 @@ public class ElasticResultRowAsyncIterat
     private FulltextResultRow nextRow;
 
     public ElasticResultRowAsyncIterator(@NotNull ElasticIndexNode indexNode,
+                                         @NotNull ElasticRequestHandler 
elasticRequestHandler,
+                                         @NotNull ElasticResponseHandler 
elasticResponseHandler,
                                          @NotNull QueryIndex.IndexPlan 
indexPlan,
-                                         @NotNull 
FulltextIndexPlanner.PlanResult planResult,
                                          Predicate<String> 
rowInclusionPredicate,
                                          LMSEstimator estimator) {
         this.indexNode = indexNode;
+        this.elasticRequestHandler = elasticRequestHandler;
+        this.elasticResponseHandler = elasticResponseHandler;
         this.indexPlan = indexPlan;
-        this.planResult = planResult;
         this.rowInclusionPredicate = rowInclusionPredicate;
         this.estimator = estimator;
 
-        this.elasticRequestHandler = new ElasticRequestHandler(indexPlan, 
planResult);
-        this.elasticResponseHandler = new ElasticResponseHandler(planResult);
-        this.elasticFacetProvider = initFacetProvider();
+        this.elasticFacetProvider = 
elasticRequestHandler.getAsyncFacetProvider(elasticResponseHandler);
         this.elasticQueryScanner = initScanner();
     }
 
@@ -107,15 +106,17 @@ public class ElasticResultRowAsyncIterat
         } catch (InterruptedException e) {
             throw new IllegalStateException("Error reading next result from 
Elastic", e);
         }
-        if (POISON_PILL.path.equals(nextRow.path)) {
-            nextRow = null;
-        }
-        return nextRow != null;
+        return !POISON_PILL.path.equals(nextRow.path);
     }
 
     @Override
     public FulltextResultRow next() {
-        return nextRow;
+        if (nextRow == null) { // next is called without hasNext
+            if (!hasNext()) {
+                return null;
+            }
+        }
+        return nextRow != null && POISON_PILL.path.equals(nextRow.path) ? null 
: nextRow;
     }
 
     @Override
@@ -143,15 +144,6 @@ public class ElasticResultRowAsyncIterat
         }
     }
 
-    private ElasticFacetProvider initFacetProvider() {
-        return elasticRequestHandler.requiresFacets() ?
-                ElasticFacetProvider.getProvider(
-                        
planResult.indexDefinition.getSecureFacetConfiguration(),
-                        elasticRequestHandler, elasticResponseHandler,
-                        indexPlan.getFilter()::isAccessible
-                ) : null;
-    }
-
     private ElasticQueryScanner initScanner() {
         List<ElasticResponseListener> listeners = new ArrayList<>();
         // TODO: we could avoid to register this listener when the client is 
interested in facets only. It would save space and time
@@ -194,7 +186,7 @@ public class ElasticResultRowAsyncIterat
 
         ElasticQueryScanner(ElasticRequestHandler requestHandler,
                             List<ElasticResponseListener> listeners) {
-            this.query = requestHandler.build();
+            this.query = requestHandler.baseQuery();
 
             final Set<String> sourceFieldsSet = new HashSet<>();
             final AtomicBoolean needsAggregations = new AtomicBoolean(false);
@@ -242,6 +234,9 @@ public class ElasticResultRowAsyncIterat
         /**
          * Handle the response action notifying the registered listeners. 
Depending on the listeners' configuration
          * it could keep loading chunks or wait for a {@code #scan} call to 
resume scanning.
+         *
+         * Some code in this method relies on structure that are not thread 
safe. We need to make sure
+         * these data structures are modified before releasing the semaphore.
          */
         @Override
         public void onResponse(SearchResponse searchResponse) {
@@ -303,7 +298,7 @@ public class ElasticResultRowAsyncIterat
         /**
          * Triggers a scan of a new chunk of the result set, if needed.
          */
-        public void scan() {
+        private void scan() {
             if (semaphore.tryAcquire() && anyDataLeft.get()) {
                 final SearchSourceBuilder searchSourceBuilder = 
SearchSourceBuilder.searchSource()
                         .query(query)

Modified: 
jackrabbit/oak/trunk/oak-search-elastic/src/main/java/org/apache/jackrabbit/oak/plugins/index/elastic/query/async/facets/ElasticFacetProvider.java
URL: 
http://svn.apache.org/viewvc/jackrabbit/oak/trunk/oak-search-elastic/src/main/java/org/apache/jackrabbit/oak/plugins/index/elastic/query/async/facets/ElasticFacetProvider.java?rev=1879190&r1=1879189&r2=1879190&view=diff
==============================================================================
--- 
jackrabbit/oak/trunk/oak-search-elastic/src/main/java/org/apache/jackrabbit/oak/plugins/index/elastic/query/async/facets/ElasticFacetProvider.java
 (original)
+++ 
jackrabbit/oak/trunk/oak-search-elastic/src/main/java/org/apache/jackrabbit/oak/plugins/index/elastic/query/async/facets/ElasticFacetProvider.java
 Thu Jun 25 14:58:08 2020
@@ -16,8 +16,8 @@
  */
 package org.apache.jackrabbit.oak.plugins.index.elastic.query.async.facets;
 
-import 
org.apache.jackrabbit.oak.plugins.index.elastic.query.async.ElasticRequestHandler;
-import 
org.apache.jackrabbit.oak.plugins.index.elastic.query.async.ElasticResponseHandler;
+import 
org.apache.jackrabbit.oak.plugins.index.elastic.query.ElasticRequestHandler;
+import 
org.apache.jackrabbit.oak.plugins.index.elastic.query.ElasticResponseHandler;
 import 
org.apache.jackrabbit.oak.plugins.index.elastic.query.async.ElasticResponseListener;
 import 
org.apache.jackrabbit.oak.plugins.index.search.IndexDefinition.SecureFacetConfiguration;
 import org.apache.jackrabbit.oak.plugins.index.search.spi.query.FulltextIndex;

Modified: 
jackrabbit/oak/trunk/oak-search-elastic/src/main/java/org/apache/jackrabbit/oak/plugins/index/elastic/query/async/facets/ElasticSecureFacetAsyncProvider.java
URL: 
http://svn.apache.org/viewvc/jackrabbit/oak/trunk/oak-search-elastic/src/main/java/org/apache/jackrabbit/oak/plugins/index/elastic/query/async/facets/ElasticSecureFacetAsyncProvider.java?rev=1879190&r1=1879189&r2=1879190&view=diff
==============================================================================
--- 
jackrabbit/oak/trunk/oak-search-elastic/src/main/java/org/apache/jackrabbit/oak/plugins/index/elastic/query/async/facets/ElasticSecureFacetAsyncProvider.java
 (original)
+++ 
jackrabbit/oak/trunk/oak-search-elastic/src/main/java/org/apache/jackrabbit/oak/plugins/index/elastic/query/async/facets/ElasticSecureFacetAsyncProvider.java
 Thu Jun 25 14:58:08 2020
@@ -16,8 +16,8 @@
  */
 package org.apache.jackrabbit.oak.plugins.index.elastic.query.async.facets;
 
-import 
org.apache.jackrabbit.oak.plugins.index.elastic.query.async.ElasticRequestHandler;
-import 
org.apache.jackrabbit.oak.plugins.index.elastic.query.async.ElasticResponseHandler;
+import 
org.apache.jackrabbit.oak.plugins.index.elastic.query.ElasticRequestHandler;
+import 
org.apache.jackrabbit.oak.plugins.index.elastic.query.ElasticResponseHandler;
 import 
org.apache.jackrabbit.oak.plugins.index.elastic.query.async.ElasticResponseListener;
 import org.apache.jackrabbit.oak.plugins.index.search.spi.query.FulltextIndex;
 import org.elasticsearch.search.SearchHit;
@@ -123,7 +123,7 @@ class ElasticSecureFacetAsyncProvider im
         } catch (InterruptedException e) {
             throw new IllegalStateException("Error while waiting for facets", 
e);
         }
-        LOG.trace("Reading facets for {} from {}", columnName, facetsMap);
-        return facets.get(FulltextIndex.parseFacetField(columnName));
+        LOG.trace("Reading facets for {} from {}", columnName, facets);
+        return facets != null ? 
facets.get(FulltextIndex.parseFacetField(columnName)) : null;
     }
 }

Modified: 
jackrabbit/oak/trunk/oak-search-elastic/src/main/java/org/apache/jackrabbit/oak/plugins/index/elastic/query/async/facets/ElasticStatisticalFacetAsyncProvider.java
URL: 
http://svn.apache.org/viewvc/jackrabbit/oak/trunk/oak-search-elastic/src/main/java/org/apache/jackrabbit/oak/plugins/index/elastic/query/async/facets/ElasticStatisticalFacetAsyncProvider.java?rev=1879190&r1=1879189&r2=1879190&view=diff
==============================================================================
--- 
jackrabbit/oak/trunk/oak-search-elastic/src/main/java/org/apache/jackrabbit/oak/plugins/index/elastic/query/async/facets/ElasticStatisticalFacetAsyncProvider.java
 (original)
+++ 
jackrabbit/oak/trunk/oak-search-elastic/src/main/java/org/apache/jackrabbit/oak/plugins/index/elastic/query/async/facets/ElasticStatisticalFacetAsyncProvider.java
 Thu Jun 25 14:58:08 2020
@@ -16,8 +16,8 @@
  */
 package org.apache.jackrabbit.oak.plugins.index.elastic.query.async.facets;
 
-import 
org.apache.jackrabbit.oak.plugins.index.elastic.query.async.ElasticRequestHandler;
-import 
org.apache.jackrabbit.oak.plugins.index.elastic.query.async.ElasticResponseHandler;
+import 
org.apache.jackrabbit.oak.plugins.index.elastic.query.ElasticRequestHandler;
+import 
org.apache.jackrabbit.oak.plugins.index.elastic.query.ElasticResponseHandler;
 import 
org.apache.jackrabbit.oak.plugins.index.elastic.query.async.ElasticResponseListener;
 import org.apache.jackrabbit.oak.plugins.index.search.spi.query.FulltextIndex;
 import org.elasticsearch.search.SearchHit;

Modified: 
jackrabbit/oak/trunk/oak-search-elastic/src/test/java/org/apache/jackrabbit/oak/plugins/index/elastic/ElasticSpellcheckTest.java
URL: 
http://svn.apache.org/viewvc/jackrabbit/oak/trunk/oak-search-elastic/src/test/java/org/apache/jackrabbit/oak/plugins/index/elastic/ElasticSpellcheckTest.java?rev=1879190&r1=1879189&r2=1879190&view=diff
==============================================================================
--- 
jackrabbit/oak/trunk/oak-search-elastic/src/test/java/org/apache/jackrabbit/oak/plugins/index/elastic/ElasticSpellcheckTest.java
 (original)
+++ 
jackrabbit/oak/trunk/oak-search-elastic/src/test/java/org/apache/jackrabbit/oak/plugins/index/elastic/ElasticSpellcheckTest.java
 Thu Jun 25 14:58:08 2020
@@ -16,9 +16,6 @@
  */
 package org.apache.jackrabbit.oak.plugins.index.elastic;
 
-import com.github.dockerjava.api.DockerClient;
-import com.google.common.collect.Lists;
-import com.google.common.io.Closer;
 import org.apache.commons.io.FileUtils;
 import org.apache.jackrabbit.JcrConstants;
 import 
org.apache.jackrabbit.commons.jackrabbit.authorization.AccessControlUtils;
@@ -32,17 +29,10 @@ import org.apache.jackrabbit.oak.plugins
 import 
org.apache.jackrabbit.oak.plugins.index.search.util.IndexDefinitionBuilder;
 import org.apache.jackrabbit.oak.plugins.memory.MemoryNodeStore;
 import org.apache.jackrabbit.oak.spi.state.NodeStore;
-import org.elasticsearch.Version;
 import org.junit.After;
 import org.junit.Before;
-import org.junit.BeforeClass;
 import org.junit.ClassRule;
-import org.junit.Rule;
 import org.junit.Test;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-import org.testcontainers.DockerClientFactory;
-import org.testcontainers.elasticsearch.ElasticsearchContainer;
 
 import javax.jcr.GuestCredentials;
 import javax.jcr.Node;
@@ -57,6 +47,7 @@ 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;
 
@@ -67,16 +58,13 @@ import static org.apache.jackrabbit.oak.
 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;
-import static org.junit.Assume.assumeNotNull;
 
 public class ElasticSpellcheckTest {
 
-    private static final Logger LOG = 
LoggerFactory.getLogger(ElasticSpellcheckTest.class);
     private Session adminSession;
     private Session anonymousSession;
     private QueryManager qe;
     private Node indexNode;
-    private static final String TEST_INDEX = "testIndex";
 
     // Set this connection string as
     // <scheme>://<hostname>:<port>?key_id=<>,key_secret=<>
@@ -164,7 +152,6 @@ public class ElasticSpellcheckTest {
 
     @Test
     public void testSpellcheckSingleWord() throws Exception {
-        //Session session = superuser;
         QueryManager qm = adminSession.getWorkspace().getQueryManager();
         Node par = allow(getOrCreateByPath("/parent", "oak:Unstructured", 
adminSession));
         Node n1 = par.addNode("node1");
@@ -177,7 +164,7 @@ public class ElasticSpellcheckTest {
         Query q = qm.createQuery(sql, Query.SQL);
         assertEventually(() -> {
             try {
-                assertEquals("[decent, descent]", getResult(q.execute(), 
"rep:spellcheck()").toString());
+                assertEquals("[decent, descent]", 
getResult(q.execute()).toString());
             } catch (RepositoryException e) {
                 throw new RuntimeException(e);
             }
@@ -186,7 +173,6 @@ public class ElasticSpellcheckTest {
 
     @Test
     public void testSpellcheckSingleWordWithDescendantNode() throws Exception {
-        //Session session = superuser;
         QueryManager qm = adminSession.getWorkspace().getQueryManager();
         Node par = allow(getOrCreateByPath("/parent", "oak:Unstructured", 
adminSession));
         Node n1 = par.addNode("node1");
@@ -199,7 +185,7 @@ public class ElasticSpellcheckTest {
         Query q = qm.createQuery(sql, Query.SQL);
         assertEventually(() -> {
             try {
-                assertEquals("[decent]", getResult(q.execute(), 
"rep:spellcheck()").toString());
+                assertEquals("[decent]", getResult(q.execute()).toString());
             } catch (RepositoryException e) {
                 throw new RuntimeException(e);
             }
@@ -226,31 +212,24 @@ public class ElasticSpellcheckTest {
 
         assertEventually(() -> {
             try {
-                assertEquals("[voting in ontario]", getResult(q.execute(), 
"rep:spellcheck()").toString());
+                assertEquals("[voting in ontario]", 
getResult(q.execute()).toString());
             } catch (RepositoryException e) {
                 throw new RuntimeException(e);
             }
         });
     }
 
-    private Node deny(Node node) throws RepositoryException {
-        AccessControlUtils.deny(node, "anonymous", Privilege.JCR_ALL);
-        return node;
-    }
-
     private Node allow(Node node) throws RepositoryException {
         AccessControlUtils.allow(node, "anonymous", Privilege.JCR_READ);
         return node;
     }
 
-    static List<String> getResult(QueryResult result, String propertyName) 
throws RepositoryException {
-        List<String> results = Lists.newArrayList();
-        RowIterator it = null;
-
-        it = result.getRows();
+    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(propertyName).getString());
+            results.add(row.getValue("rep:spellcheck()").getString());
         }
         return results;
     }

Modified: 
jackrabbit/oak/trunk/oak-search-elastic/src/test/java/org/apache/jackrabbit/oak/plugins/index/elastic/index/ElasticIndexHelperTest.java
URL: 
http://svn.apache.org/viewvc/jackrabbit/oak/trunk/oak-search-elastic/src/test/java/org/apache/jackrabbit/oak/plugins/index/elastic/index/ElasticIndexHelperTest.java?rev=1879190&r1=1879189&r2=1879190&view=diff
==============================================================================
--- 
jackrabbit/oak/trunk/oak-search-elastic/src/test/java/org/apache/jackrabbit/oak/plugins/index/elastic/index/ElasticIndexHelperTest.java
 (original)
+++ 
jackrabbit/oak/trunk/oak-search-elastic/src/test/java/org/apache/jackrabbit/oak/plugins/index/elastic/index/ElasticIndexHelperTest.java
 Thu Jun 25 14:58:08 2020
@@ -29,6 +29,7 @@ import java.io.IOException;
 import java.util.Map;
 
 import static org.hamcrest.CoreMatchers.is;
+import static org.hamcrest.CoreMatchers.notNullValue;
 import static org.hamcrest.CoreMatchers.nullValue;
 import static org.hamcrest.MatcherAssert.assertThat;
 
@@ -88,6 +89,9 @@ public class ElasticIndexHelperTest {
 
         
assertThat(request.settings().get("analysis.filter.oak_word_delimiter_graph_filter.preserve_original"),
 is("false"));
 
+        assertThat(request.settings().get("analysis.filter.shingle.type"), 
nullValue());
+        
assertThat(request.settings().get("analysis.filter.analyzer.trigram.type"), 
nullValue());
+
         ObjectMapper mapper = new ObjectMapper();
         Map<String, Object> jsonMappings = 
mapper.readValue(request.mappings().streamInput(), Map.class);
         Map fooMapping = (Map) ((Map) 
jsonMappings.get("properties")).get("foo");
@@ -114,4 +118,31 @@ public class ElasticIndexHelperTest {
         
assertThat(request.settings().get("analysis.filter.oak_word_delimiter_graph_filter.preserve_original"),
 is("true"));
     }
 
+    @Test
+    public void spellCheck() throws IOException {
+        IndexDefinitionBuilder builder = new ElasticIndexDefinitionBuilder();
+        IndexDefinitionBuilder.IndexRule indexRule = builder.indexRule("type");
+        indexRule.property("foo").type("String").analyzed().useInSpellcheck();
+        indexRule.property("bar").type("String").analyzed();
+
+        NodeState nodeState = builder.build();
+
+        ElasticIndexDefinition definition =
+                new ElasticIndexDefinition(nodeState, nodeState, "path", 
"prefix");
+
+        CreateIndexRequest request = 
ElasticIndexHelper.createIndexRequest(definition);
+
+        assertThat(request.settings().get("analysis.filter.shingle.type"), 
is("shingle"));
+        assertThat(request.settings().get("analysis.analyzer.trigram.type"), 
is("custom"));
+
+        ObjectMapper mapper = new ObjectMapper();
+        Map<String, Object> jsonMappings = 
mapper.readValue(request.mappings().streamInput(), Map.class);
+        Map fooMapping = (Map) ((Map) 
jsonMappings.get("properties")).get("foo");
+        Map fooFields = (Map) fooMapping.get("fields");
+        assertThat(fooFields.get("trigram"), notNullValue());
+        Map barMapping = (Map) ((Map) 
jsonMappings.get("properties")).get("bar");
+        Map barFields = (Map) barMapping.get("fields");
+        assertThat(barFields.get("trigram"), nullValue());
+    }
+
 }


Reply via email to