From 50cd1d46a8d5ded4c15d5ce4606baa71eba72b56 Mon Sep 17 00:00:00 2001
From: Vikas Saurabh <vsaurabh@adobe.com>
Date: Sat, 19 Dec 2015 03:55:33 +0530
Subject: [PATCH] OAK-3576: Allow custom extension to augment indexed lucene
 documents and test case

---
 oak-lucene/pom.xml                                 |   1 +
 .../index/lucene/IndexAugmentorFactory.java        | 185 ++++++
 .../plugins/index/lucene/LuceneIndexEditor.java    |  51 +-
 .../index/lucene/LuceneIndexEditorContext.java     |  10 +-
 .../index/lucene/LuceneIndexEditorProvider.java    |  10 +-
 .../plugins/index/lucene/LuceneIndexProvider.java  |  11 +-
 .../index/lucene/LuceneIndexProviderService.java   |  33 +-
 .../plugins/index/lucene/LucenePropertyIndex.java  |  63 +-
 .../oak/plugins/index/lucene/package-info.java     |   4 +-
 .../lucene/spi/FulltextQueryTermsProvider.java     |  45 ++
 .../index/lucene/spi/IndexFieldProvider.java       |  47 ++
 .../oak/plugins/index/lucene/spi/package-info.java |  22 +
 .../index/lucene/IndexAugmentorFactoryTest.java    | 190 ++++++
 .../index/lucene/LuceneIndexAugmentTest.java       | 675 +++++++++++++++++++++
 14 files changed, 1312 insertions(+), 35 deletions(-)
 create mode 100644 oak-lucene/src/main/java/org/apache/jackrabbit/oak/plugins/index/lucene/IndexAugmentorFactory.java
 create mode 100644 oak-lucene/src/main/java/org/apache/jackrabbit/oak/plugins/index/lucene/spi/FulltextQueryTermsProvider.java
 create mode 100644 oak-lucene/src/main/java/org/apache/jackrabbit/oak/plugins/index/lucene/spi/IndexFieldProvider.java
 create mode 100644 oak-lucene/src/main/java/org/apache/jackrabbit/oak/plugins/index/lucene/spi/package-info.java
 create mode 100644 oak-lucene/src/test/java/org/apache/jackrabbit/oak/plugins/index/lucene/IndexAugmentorFactoryTest.java
 create mode 100644 oak-lucene/src/test/java/org/apache/jackrabbit/oak/plugins/index/lucene/LuceneIndexAugmentTest.java

diff --git a/oak-lucene/pom.xml b/oak-lucene/pom.xml
index 480bf1f..a3aa940 100644
--- a/oak-lucene/pom.xml
+++ b/oak-lucene/pom.xml
@@ -112,6 +112,7 @@
                 org.apache.jackrabbit.oak.plugins.index.lucene,
                 org.apache.jackrabbit.oak.plugins.index.lucene.util,
                 org.apache.jackrabbit.oak.plugins.index.lucene.score,
+                org.apache.jackrabbit.oak.plugins.index.lucene.spi,
             </Export-Package>
             <_exportcontents>
                 org.apache.lucene.*;version=${lucene.version}
diff --git a/oak-lucene/src/main/java/org/apache/jackrabbit/oak/plugins/index/lucene/IndexAugmentorFactory.java b/oak-lucene/src/main/java/org/apache/jackrabbit/oak/plugins/index/lucene/IndexAugmentorFactory.java
new file mode 100644
index 0000000..bdeecba
--- /dev/null
+++ b/oak-lucene/src/main/java/org/apache/jackrabbit/oak/plugins/index/lucene/IndexAugmentorFactory.java
@@ -0,0 +1,185 @@
+/*
+ * 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.lucene;
+
+import com.google.common.collect.LinkedListMultimap;
+import com.google.common.collect.ListMultimap;
+import com.google.common.collect.Lists;
+import com.google.common.collect.Maps;
+import org.apache.jackrabbit.oak.plugins.index.lucene.spi.FulltextQueryTermsProvider;
+import org.apache.jackrabbit.oak.plugins.index.lucene.spi.IndexFieldProvider;
+import org.apache.jackrabbit.oak.spi.state.NodeState;
+import org.apache.jackrabbit.oak.spi.whiteboard.Tracker;
+import org.apache.jackrabbit.oak.spi.whiteboard.Whiteboard;
+import org.apache.jackrabbit.oak.util.PerfLogger;
+import org.apache.lucene.analysis.Analyzer;
+import org.apache.lucene.document.Field;
+import org.apache.lucene.search.BooleanClause;
+import org.apache.lucene.search.BooleanQuery;
+import org.apache.lucene.search.Query;
+import org.slf4j.LoggerFactory;
+
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+public class IndexAugmentorFactory {
+
+    private static final PerfLogger PERFLOG = new PerfLogger(
+            LoggerFactory.getLogger(IndexAugmentorFactory.class.getName() + ".perf"));
+
+    private final Tracker<IndexFieldProvider> indexFieldProviderTracker;
+    private final Tracker<FulltextQueryTermsProvider> fulltextQueryTermsProviderTracker;
+
+    private Map<String, CompositeIndexFieldProvider> indexFieldProviderMap = Maps.newConcurrentMap();
+    private Map<String, CompositeFulltextQueryTermsProvider> fulltextQueryTermsProviderMap = Maps.newConcurrentMap();
+
+    public IndexAugmentorFactory(Whiteboard whiteboard) {
+        indexFieldProviderTracker = whiteboard.track(IndexFieldProvider.class);
+        fulltextQueryTermsProviderTracker = whiteboard.track(FulltextQueryTermsProvider.class);
+    }
+
+    public IndexFieldProvider getIndexFieldProvider(String nodeType) {
+        return indexFieldProviderMap.get(nodeType);
+    }
+
+    public FulltextQueryTermsProvider getFulltextQueryTermsProvider(String nodeType) {
+        return fulltextQueryTermsProviderMap.get(nodeType);
+    }
+
+    public void refreshServices() {
+        refreshIndexFieldProviderServices();
+        refreshFulltextQueryTermsProviderServices();
+    }
+
+    private void refreshIndexFieldProviderServices() {
+        ListMultimap<String, IndexFieldProvider> indexFieldProviderListMultimap =
+                LinkedListMultimap.create();
+        for (IndexFieldProvider provider : indexFieldProviderTracker.getServices()) {
+            Set<String> supportedNodeTypes = provider.getSupportedTypes();
+            for (String nodeType : supportedNodeTypes) {
+                indexFieldProviderListMultimap.put(nodeType, provider);
+            }
+        }
+
+        indexFieldProviderMap.clear();
+        for (String nodeType : indexFieldProviderListMultimap.keySet()) {
+            List<IndexFieldProvider> providers = indexFieldProviderListMultimap.get(nodeType);
+            CompositeIndexFieldProvider compositeIndexFieldProvider =
+                    new CompositeIndexFieldProvider(nodeType, providers);
+            indexFieldProviderMap.put(nodeType, compositeIndexFieldProvider);
+        }
+    }
+
+    private void refreshFulltextQueryTermsProviderServices() {
+        ListMultimap<String, FulltextQueryTermsProvider> fulltextQueryTermsProviderLinkedListMultimap =
+                LinkedListMultimap.create();
+        for (FulltextQueryTermsProvider provider : fulltextQueryTermsProviderTracker.getServices()) {
+            Set<String> supportedNodeTypes = provider.getSupportedTypes();
+            for (String nodeType : supportedNodeTypes) {
+                fulltextQueryTermsProviderLinkedListMultimap.put(nodeType, provider);
+            }
+        }
+
+        fulltextQueryTermsProviderMap.clear();
+        for (String nodeType : fulltextQueryTermsProviderLinkedListMultimap.keySet()) {
+            List<FulltextQueryTermsProvider> providers = fulltextQueryTermsProviderLinkedListMultimap.get(nodeType);
+            CompositeFulltextQueryTermsProvider compositeFulltextQueryTermsProvider =
+                    new CompositeFulltextQueryTermsProvider(nodeType, providers);
+            fulltextQueryTermsProviderMap.put(nodeType, compositeFulltextQueryTermsProvider);
+        }
+    }
+
+    class CompositeIndexFieldProvider implements IndexFieldProvider {
+
+        private final String nodeType;
+        private final List<IndexFieldProvider> providers;
+
+        CompositeIndexFieldProvider(String nodeType, List<IndexFieldProvider> providers) {
+            this.nodeType = nodeType;
+            this.providers = providers;
+        }
+
+        @Override
+        public List<Field> getAugmentedFields(final String path,
+                                              final NodeState document, final NodeState indexDefinition) {
+            List<Field> fields = Lists.newArrayList();
+            for (IndexFieldProvider indexFieldProvider : providers) {
+                final long start = PERFLOG.start();
+                Iterable<Field> providedFields = indexFieldProvider.getAugmentedFields(path, document, indexDefinition);
+                PERFLOG.end(start, 1, "indexFieldProvider: {}, path: {}, doc: {}, indexDef: {}",
+                        indexFieldProvider, path, document, indexDefinition);
+                if (providedFields != null) {
+                    for (Field f : providedFields) {
+                        fields.add(f);
+                    }
+                }
+            }
+            return fields;
+        }
+
+        @Override
+        public Set<String> getSupportedTypes() {
+            return Collections.singleton(nodeType);
+        }
+    }
+
+    class CompositeFulltextQueryTermsProvider implements FulltextQueryTermsProvider {
+
+        private final String nodeType;
+        private final List<FulltextQueryTermsProvider> providers;
+
+        CompositeFulltextQueryTermsProvider(String nodeType, List<FulltextQueryTermsProvider> providers) {
+            this.nodeType = nodeType;
+            this.providers = providers;
+        }
+
+        @Override
+        public Query getQueryTerm(final String text, final Analyzer analyzer) {
+            List<Query> subQueries = Lists.newArrayList();
+            for (FulltextQueryTermsProvider fulltextQueryTermsProvider : providers) {
+                final long start = PERFLOG.start();
+                Query subQuery = fulltextQueryTermsProvider.getQueryTerm(text, analyzer);
+                PERFLOG.end(start, 1, "fulltextQueryTermsProvider: {}, text: {}", fulltextQueryTermsProvider, text);
+                if (subQuery != null) {
+                    subQueries.add(subQuery);
+                }
+            }
+
+            Query ret;
+            if (subQueries.size() == 0) {
+                ret = null;
+            } else if (subQueries.size() == 1) {
+                ret = subQueries.get(0);
+            } else {
+                BooleanQuery query = new BooleanQuery();
+                for ( Query subQuery : subQueries ) {
+                    query.add(subQuery, BooleanClause.Occur.SHOULD);
+                }
+                ret = query;
+            }
+
+            return ret;
+        }
+
+        @Override
+        public Set<String> getSupportedTypes() {
+            return Collections.singleton(nodeType);
+        }
+    }
+}
diff --git a/oak-lucene/src/main/java/org/apache/jackrabbit/oak/plugins/index/lucene/LuceneIndexEditor.java b/oak-lucene/src/main/java/org/apache/jackrabbit/oak/plugins/index/lucene/LuceneIndexEditor.java
index 1e247af..6a8dfdb 100644
--- a/oak-lucene/src/main/java/org/apache/jackrabbit/oak/plugins/index/lucene/LuceneIndexEditor.java
+++ b/oak-lucene/src/main/java/org/apache/jackrabbit/oak/plugins/index/lucene/LuceneIndexEditor.java
@@ -57,6 +57,7 @@ import org.apache.jackrabbit.oak.plugins.index.PathFilter;
 import org.apache.jackrabbit.oak.plugins.index.fulltext.ExtractedText;
 import org.apache.jackrabbit.oak.plugins.index.fulltext.ExtractedText.ExtractionResult;
 import org.apache.jackrabbit.oak.plugins.index.lucene.Aggregate.Matcher;
+import org.apache.jackrabbit.oak.plugins.index.lucene.spi.IndexFieldProvider;
 import org.apache.jackrabbit.oak.plugins.memory.EmptyNodeState;
 import org.apache.jackrabbit.oak.plugins.tree.TreeFactory;
 import org.apache.jackrabbit.oak.spi.commit.Editor;
@@ -133,12 +134,13 @@ public class LuceneIndexEditor implements IndexEditor, Aggregate.AggregateRoot {
     LuceneIndexEditor(NodeState root, NodeBuilder definition,
                         IndexUpdateCallback updateCallback,
                         @Nullable IndexCopier indexCopier,
-                        ExtractedTextCache extractedTextCache) throws CommitFailedException {
+                        ExtractedTextCache extractedTextCache,
+                      IndexAugmentorFactory augmentorFactory) throws CommitFailedException {
         this.parent = null;
         this.name = null;
         this.path = "/";
         this.context = new LuceneIndexEditorContext(root, definition,
-                updateCallback, indexCopier, extractedTextCache);
+                updateCallback, indexCopier, extractedTextCache, augmentorFactory);
         this.root = root;
         this.isDeleted = false;
         this.matcherState = MatcherState.NONE;
@@ -355,6 +357,8 @@ public class LuceneIndexEditor implements IndexEditor, Aggregate.AggregateRoot {
         dirty |= indexNullCheckEnabledProps(path, fields, state);
         dirty |= indexNotNullCheckEnabledProps(path, fields, state);
 
+        dirty |= augmentCustomFields(path, fields, state);
+
         // Check if a node having a single property was modified/deleted
         if (!dirty) {
             dirty = indexIfSinglePropertyRemoved();
@@ -484,14 +488,14 @@ public class LuceneIndexEditor implements IndexEditor, Aggregate.AggregateRoot {
                                   String pname,
                                   PropertyDefinition pd) {
         boolean includeTypeForFullText = indexingRule.includePropertyType(property.getType().tag());
+
+        boolean dirty = false;
         if (Type.BINARY.tag() == property.getType().tag()
                 && includeTypeForFullText) {
             fields.addAll(newBinary(property, state, null, path + "@" + pname));
-            return true;
-        }  else {
-            boolean dirty = false;
-
-            if (pd.propertyIndex && pd.includePropertyType(property.getType().tag())){
+            dirty = true;
+        } else {
+            if (pd.propertyIndex && pd.includePropertyType(property.getType().tag())) {
                 dirty |= addTypedFields(fields, property, pname);
             }
 
@@ -517,8 +521,9 @@ public class LuceneIndexEditor implements IndexEditor, Aggregate.AggregateRoot {
                     dirty = true;
                 }
             }
-            return dirty;
         }
+
+        return dirty;
     }
 
     private String constructAnalyzedPropertyName(String pname) {
@@ -655,6 +660,33 @@ public class LuceneIndexEditor implements IndexEditor, Aggregate.AggregateRoot {
         return fields;
     }
 
+    private boolean augmentCustomFields(final String path, final List<Field> fields,
+                                        final NodeState document) {
+        boolean dirty = false;
+
+        IndexDefinition defn = getDefinition();
+
+        if (defn.getVersion().isAtLeast(IndexFormatVersion.V2)){
+            IndexAugmentorFactory augmentorFactory = context.getAugmentorFactory();
+            if (augmentorFactory != null) {
+                IndexFieldProvider provider = augmentorFactory.getIndexFieldProvider(indexingRule.getNodeTypeName());
+                if (provider != null) {
+                    Iterable<Field> augmentedFields =
+                            provider.getAugmentedFields(path, document, defn.getDefinitionNodeState());
+
+                    if (augmentedFields != null) {
+                        for (Field field : augmentedFields) {
+                            fields.add(field);
+                            dirty = true;
+                        }
+                    }
+                }
+            }
+        }
+
+        return dirty;
+    }
+
     //~-------------------------------------------------------< NullCheck Support >
 
     private boolean indexNotNullCheckEnabledProps(String path, List<Field> fields, NodeState state) {
@@ -793,7 +825,6 @@ public class LuceneIndexEditor implements IndexEditor, Aggregate.AggregateRoot {
         });
         return dirtyFlag.get();
     }
-
     /**
      * Create the fulltext field from the aggregated nodes. If result is for aggregate for a relative node
      * include then
@@ -831,7 +862,7 @@ public class LuceneIndexEditor implements IndexEditor, Aggregate.AggregateRoot {
             //Check if any explicit property defn is defined via relative path
             // and is marked to exclude this property from being indexed
             PropertyDefinition pdForRootNode = indexingRule.getConfig(propertyPath);
-            if (pdForRootNode != null && !pdForRootNode.index) {
+            if (pdForRootNode != null && !pdForRootNode.index ) {
                 continue;
             }
 
diff --git a/oak-lucene/src/main/java/org/apache/jackrabbit/oak/plugins/index/lucene/LuceneIndexEditorContext.java b/oak-lucene/src/main/java/org/apache/jackrabbit/oak/plugins/index/lucene/LuceneIndexEditorContext.java
index 5ef5f01..b8fcd36 100644
--- a/oak-lucene/src/main/java/org/apache/jackrabbit/oak/plugins/index/lucene/LuceneIndexEditorContext.java
+++ b/oak-lucene/src/main/java/org/apache/jackrabbit/oak/plugins/index/lucene/LuceneIndexEditorContext.java
@@ -143,19 +143,23 @@ public class LuceneIndexEditorContext {
     private final TextExtractionStats textExtractionStats = new TextExtractionStats();
 
     private final ExtractedTextCache extractedTextCache;
+
+    private final IndexAugmentorFactory augmentorFactory;
     /**
      * The media types supported by the parser used.
      */
     private Set<MediaType> supportedMediaTypes;
 
     LuceneIndexEditorContext(NodeState root, NodeBuilder definition, IndexUpdateCallback updateCallback,
-                             @Nullable IndexCopier indexCopier, ExtractedTextCache extractedTextCache) {
+                             @Nullable IndexCopier indexCopier, ExtractedTextCache extractedTextCache,
+                             IndexAugmentorFactory augmentorFactory) {
         this.definitionBuilder = definition;
         this.indexCopier = indexCopier;
         this.definition = new IndexDefinition(root, definition);
         this.indexedNodes = 0;
         this.updateCallback = updateCallback;
         this.extractedTextCache = extractedTextCache;
+        this.augmentorFactory = augmentorFactory;
         if (this.definition.isOfOldFormat()){
             IndexDefinition.updateDefinition(definition);
         }
@@ -340,6 +344,10 @@ public class LuceneIndexEditorContext {
         return extractedTextCache;
     }
 
+    IndexAugmentorFactory getAugmentorFactory() {
+        return augmentorFactory;
+    }
+
     public boolean isReindex() {
         return reindex;
     }
diff --git a/oak-lucene/src/main/java/org/apache/jackrabbit/oak/plugins/index/lucene/LuceneIndexEditorProvider.java b/oak-lucene/src/main/java/org/apache/jackrabbit/oak/plugins/index/lucene/LuceneIndexEditorProvider.java
index 227187b..24a8c2c 100644
--- a/oak-lucene/src/main/java/org/apache/jackrabbit/oak/plugins/index/lucene/LuceneIndexEditorProvider.java
+++ b/oak-lucene/src/main/java/org/apache/jackrabbit/oak/plugins/index/lucene/LuceneIndexEditorProvider.java
@@ -39,6 +39,7 @@ import static org.apache.jackrabbit.oak.plugins.index.lucene.LuceneIndexConstant
 public class LuceneIndexEditorProvider implements IndexEditorProvider {
     private final IndexCopier indexCopier;
     private final ExtractedTextCache extractedTextCache;
+    private final IndexAugmentorFactory augmentorFactory;
 
     public LuceneIndexEditorProvider() {
         this(null);
@@ -51,8 +52,15 @@ public class LuceneIndexEditorProvider implements IndexEditorProvider {
 
     public LuceneIndexEditorProvider(@Nullable IndexCopier indexCopier,
                                      ExtractedTextCache extractedTextCache) {
+        this(indexCopier, extractedTextCache, null);
+    }
+
+    public LuceneIndexEditorProvider(@Nullable IndexCopier indexCopier,
+                                     ExtractedTextCache extractedTextCache,
+                                     IndexAugmentorFactory augmentorFactory) {
         this.indexCopier = indexCopier;
         this.extractedTextCache = extractedTextCache;
+        this.augmentorFactory = augmentorFactory;
     }
 
     @Override
@@ -61,7 +69,7 @@ public class LuceneIndexEditorProvider implements IndexEditorProvider {
             @Nonnull IndexUpdateCallback callback)
             throws CommitFailedException {
         if (TYPE_LUCENE.equals(type)) {
-            return new LuceneIndexEditor(root, definition, callback, indexCopier, extractedTextCache);
+            return new LuceneIndexEditor(root, definition, callback, indexCopier, extractedTextCache, augmentorFactory);
         }
         return null;
     }
diff --git a/oak-lucene/src/main/java/org/apache/jackrabbit/oak/plugins/index/lucene/LuceneIndexProvider.java b/oak-lucene/src/main/java/org/apache/jackrabbit/oak/plugins/index/lucene/LuceneIndexProvider.java
index 8a16511..438900c 100644
--- a/oak-lucene/src/main/java/org/apache/jackrabbit/oak/plugins/index/lucene/LuceneIndexProvider.java
+++ b/oak-lucene/src/main/java/org/apache/jackrabbit/oak/plugins/index/lucene/LuceneIndexProvider.java
@@ -45,21 +45,24 @@ public class LuceneIndexProvider implements QueryIndexProvider, Observer, Closea
 
     ScorerProviderFactory scorerFactory;
 
+    IndexAugmentorFactory augmentorFactory;
+
     public LuceneIndexProvider() {
         this(new IndexTracker());
     }
 
     public LuceneIndexProvider(IndexCopier indexCopier) {
-        this(new IndexTracker(indexCopier), ScorerProviderFactory.DEFAULT);
+        this(new IndexTracker(indexCopier));
     }
 
     public LuceneIndexProvider(IndexTracker tracker) {
-        this(tracker, ScorerProviderFactory.DEFAULT);
+        this(tracker, ScorerProviderFactory.DEFAULT, null);
     }
 
-    public LuceneIndexProvider(IndexTracker tracker, ScorerProviderFactory scorerFactory) {
+    public LuceneIndexProvider(IndexTracker tracker, ScorerProviderFactory scorerFactory, IndexAugmentorFactory augmentorFactory) {
         this.tracker = tracker;
         this.scorerFactory = scorerFactory;
+        this.augmentorFactory = augmentorFactory;
     }
 
     public void close() {
@@ -85,7 +88,7 @@ public class LuceneIndexProvider implements QueryIndexProvider, Observer, Closea
     }
 
     protected LucenePropertyIndex newLucenePropertyIndex() {
-        return new LucenePropertyIndex(tracker, scorerFactory);
+        return new LucenePropertyIndex(tracker, scorerFactory, augmentorFactory);
     }
 
     /**
diff --git a/oak-lucene/src/main/java/org/apache/jackrabbit/oak/plugins/index/lucene/LuceneIndexProviderService.java b/oak-lucene/src/main/java/org/apache/jackrabbit/oak/plugins/index/lucene/LuceneIndexProviderService.java
index 673da5f..c29981c 100644
--- a/oak-lucene/src/main/java/org/apache/jackrabbit/oak/plugins/index/lucene/LuceneIndexProviderService.java
+++ b/oak-lucene/src/main/java/org/apache/jackrabbit/oak/plugins/index/lucene/LuceneIndexProviderService.java
@@ -45,6 +45,7 @@ import org.apache.felix.scr.annotations.Reference;
 import org.apache.felix.scr.annotations.ReferenceCardinality;
 import org.apache.felix.scr.annotations.ReferencePolicy;
 import org.apache.felix.scr.annotations.ReferencePolicyOption;
+import org.apache.felix.scr.annotations.References;
 import org.apache.jackrabbit.oak.api.jmx.CacheStatsMBean;
 import org.apache.jackrabbit.oak.cache.CacheStats;
 import org.apache.jackrabbit.oak.commons.PropertiesUtil;
@@ -52,6 +53,8 @@ import org.apache.jackrabbit.oak.osgi.OsgiWhiteboard;
 import org.apache.jackrabbit.oak.plugins.index.IndexEditorProvider;
 import org.apache.jackrabbit.oak.plugins.index.aggregate.NodeAggregator;
 import org.apache.jackrabbit.oak.plugins.index.fulltext.PreExtractedTextProvider;
+import org.apache.jackrabbit.oak.plugins.index.lucene.spi.FulltextQueryTermsProvider;
+import org.apache.jackrabbit.oak.plugins.index.lucene.spi.IndexFieldProvider;
 import org.apache.jackrabbit.oak.spi.commit.BackgroundObserver;
 import org.apache.jackrabbit.oak.plugins.index.lucene.score.ScorerProviderFactory;
 import org.apache.jackrabbit.oak.spi.commit.BackgroundObserverMBean;
@@ -74,6 +77,20 @@ import static org.apache.jackrabbit.oak.spi.whiteboard.WhiteboardUtils.registerM
 
 @SuppressWarnings("UnusedDeclaration")
 @Component(metatype = true, label = "Apache Jackrabbit Oak LuceneIndexProvider")
+@References({
+        @Reference(name = "IndexFieldProvider",
+                policy = ReferencePolicy.DYNAMIC,
+                cardinality = ReferenceCardinality.OPTIONAL_MULTIPLE,
+                referenceInterface = IndexFieldProvider.class,
+                bind = "indexFieldProviderServiceUpdated",
+                unbind = "indexFieldProviderServiceUpdated"),
+        @Reference(name = "FulltextQueryTermsProvider",
+                policy = ReferencePolicy.DYNAMIC,
+                cardinality = ReferenceCardinality.OPTIONAL_MULTIPLE,
+                referenceInterface = FulltextQueryTermsProvider.class,
+                bind = "indexFulltextQueryTermsProviderServiceUpdated",
+                unbind = "indexFulltextQueryTermsProviderServiceUpdated")
+})
 public class LuceneIndexProviderService {
     public static final String REPOSITORY_HOME = "repository.home";
 
@@ -177,6 +194,8 @@ public class LuceneIndexProviderService {
     @Reference
     ScorerProviderFactory scorerFactory;
 
+    private IndexAugmentorFactory augmentorFactory;
+
     @Reference(policy = ReferencePolicy.DYNAMIC,
             cardinality = ReferenceCardinality.OPTIONAL_MULTIPLE,
             policyOption = ReferencePolicyOption.GREEDY
@@ -207,7 +226,8 @@ public class LuceneIndexProviderService {
         whiteboard = new OsgiWhiteboard(bundleContext);
         threadPoolSize = PropertiesUtil.toInteger(config.get(PROP_THREAD_POOL_SIZE), PROP_THREAD_POOL_SIZE_DEFAULT);
         initializeExtractedTextCache(bundleContext, config);
-        indexProvider = new LuceneIndexProvider(createTracker(bundleContext, config), scorerFactory);
+        augmentorFactory = new IndexAugmentorFactory(whiteboard);
+        indexProvider = new LuceneIndexProvider(createTracker(bundleContext, config), scorerFactory, augmentorFactory);
         initializeLogging(config);
         initialize();
 
@@ -288,10 +308,10 @@ public class LuceneIndexProviderService {
         LuceneIndexEditorProvider editorProvider;
         if (enableCopyOnWrite){
             initializeIndexCopier(bundleContext, config);
-            editorProvider = new LuceneIndexEditorProvider(indexCopier, extractedTextCache);
+            editorProvider = new LuceneIndexEditorProvider(indexCopier, extractedTextCache, augmentorFactory);
             log.info("Enabling CopyOnWrite support. Index files would be copied under {}", indexDir.getAbsolutePath());
         } else {
-            editorProvider = new LuceneIndexEditorProvider(null, extractedTextCache);
+            editorProvider = new LuceneIndexEditorProvider(null, extractedTextCache, augmentorFactory);
         }
         regs.add(bundleContext.registerService(IndexEditorProvider.class.getName(), editorProvider, null));
         oakRegs.add(registerMBean(whiteboard,
@@ -471,4 +491,11 @@ public class LuceneIndexProviderService {
         registerExtractedTextProvider(null);
     }
 
+    private void indexFieldProviderServiceUpdated(IndexFieldProvider indexFieldProvider) {
+        augmentorFactory.refreshServices();
+    }
+
+    private void indexFulltextQueryTermsProviderServiceUpdated(FulltextQueryTermsProvider fulltextQueryTermsProvider) {
+        augmentorFactory.refreshServices();
+    }
 }
diff --git a/oak-lucene/src/main/java/org/apache/jackrabbit/oak/plugins/index/lucene/LucenePropertyIndex.java b/oak-lucene/src/main/java/org/apache/jackrabbit/oak/plugins/index/lucene/LucenePropertyIndex.java
index 253f946..40b6223 100644
--- a/oak-lucene/src/main/java/org/apache/jackrabbit/oak/plugins/index/lucene/LucenePropertyIndex.java
+++ b/oak-lucene/src/main/java/org/apache/jackrabbit/oak/plugins/index/lucene/LucenePropertyIndex.java
@@ -47,6 +47,7 @@ import org.apache.jackrabbit.oak.plugins.index.aggregate.NodeAggregator;
 import org.apache.jackrabbit.oak.plugins.index.lucene.IndexDefinition.IndexingRule;
 import org.apache.jackrabbit.oak.plugins.index.lucene.IndexPlanner.PlanResult;
 import org.apache.jackrabbit.oak.plugins.index.lucene.score.ScorerProviderFactory;
+import org.apache.jackrabbit.oak.plugins.index.lucene.spi.FulltextQueryTermsProvider;
 import org.apache.jackrabbit.oak.plugins.index.lucene.util.MoreLikeThisHelper;
 import org.apache.jackrabbit.oak.plugins.index.lucene.util.SpellcheckHelper;
 import org.apache.jackrabbit.oak.plugins.index.lucene.util.SuggestHelper;
@@ -207,14 +208,20 @@ public class LucenePropertyIndex implements AdvancedQueryIndex, QueryIndex, Nati
     private final Highlighter highlighter = new Highlighter(new SimpleHTMLFormatter("<strong>", "</strong>"),
             new SimpleHTMLEncoder(), null);
 
+    private final IndexAugmentorFactory augmentorFactory;
+
     public LucenePropertyIndex(IndexTracker tracker) {
-        this.tracker = tracker;
-        this.scorerProviderFactory = ScorerProviderFactory.DEFAULT;
+        this(tracker, ScorerProviderFactory.DEFAULT);
     }
 
     public LucenePropertyIndex(IndexTracker tracker, ScorerProviderFactory factory) {
+        this(tracker, factory, null);
+    }
+
+    public LucenePropertyIndex(IndexTracker tracker, ScorerProviderFactory factory, IndexAugmentorFactory augmentorFactory) {
         this.tracker = tracker;
         this.scorerProviderFactory = factory;
+        this.augmentorFactory = augmentorFactory;
     }
 
     @Override
@@ -274,7 +281,7 @@ public class LucenePropertyIndex implements AdvancedQueryIndex, QueryIndex, Nati
                     .append("(")
                     .append(path)
                     .append(") ");
-            sb.append(getLuceneRequest(plan, null));
+            sb.append(getLuceneRequest(plan, augmentorFactory, null));
             if (plan.getSortOrder() != null && !plan.getSortOrder().isEmpty()) {
                 sb.append(" ordering:").append(plan.getSortOrder());
             }
@@ -365,7 +372,7 @@ public class LucenePropertyIndex implements AdvancedQueryIndex, QueryIndex, Nati
                 checkState(indexNode != null);
                 try {
                     IndexSearcher searcher = indexNode.getSearcher();
-                    LuceneRequestFacade luceneRequestFacade = getLuceneRequest(plan, searcher.getIndexReader());
+                    LuceneRequestFacade luceneRequestFacade = getLuceneRequest(plan, augmentorFactory, searcher.getIndexReader());
                     if (luceneRequestFacade.getLuceneRequest() instanceof Query) {
                         Query query = (Query) luceneRequestFacade.getLuceneRequest();
 
@@ -547,7 +554,7 @@ public class LucenePropertyIndex implements AdvancedQueryIndex, QueryIndex, Nati
                 checkState(indexNode != null);
                 try {
                     IndexSearcher searcher = indexNode.getSearcher();
-                    LuceneRequestFacade luceneRequestFacade = getLuceneRequest(plan, searcher.getIndexReader());
+                    LuceneRequestFacade luceneRequestFacade = getLuceneRequest(plan, augmentorFactory, searcher.getIndexReader());
                     if (luceneRequestFacade.getLuceneRequest() instanceof Query) {
                         Query query = (Query) luceneRequestFacade.getLuceneRequest();
                         TotalHitCountCollector collector = new TotalHitCountCollector();
@@ -682,7 +689,8 @@ public class LucenePropertyIndex implements AdvancedQueryIndex, QueryIndex, Nati
      * @param reader the Lucene reader
      * @return the Lucene query
      */
-    private static LuceneRequestFacade getLuceneRequest(IndexPlan plan, IndexReader reader) {
+    private static LuceneRequestFacade getLuceneRequest(IndexPlan plan, IndexAugmentorFactory augmentorFactory, IndexReader reader) {
+        FulltextQueryTermsProvider augmentor = getIndexAgumentor(plan, augmentorFactory);
         List<Query> qs = new ArrayList<Query>();
         Filter filter = plan.getFilter();
         FullTextExpression ft = filter.getFullTextConstraint();
@@ -694,7 +702,7 @@ public class LucenePropertyIndex implements AdvancedQueryIndex, QueryIndex, Nati
             // when using the LowCostLuceneIndexProvider
             // which is used for testing
         } else {
-            qs.add(getFullTextQuery(plan, ft, analyzer));
+            qs.add(getFullTextQuery(plan, ft, analyzer, augmentor));
         }
 
 
@@ -848,6 +856,16 @@ public class LucenePropertyIndex implements AdvancedQueryIndex, QueryIndex, Nati
         }
         return null;
     }
+    private static FulltextQueryTermsProvider getIndexAgumentor(IndexPlan plan, IndexAugmentorFactory augmentorFactory) {
+        PlanResult planResult = getPlanResult(plan);
+        IndexDefinition defn = planResult.indexDefinition;
+
+        if (augmentorFactory != null && defn.getVersion().isAtLeast(IndexFormatVersion.V2)){
+            return augmentorFactory.getFulltextQueryTermsProvider(getPlanResult(plan).indexingRule.getNodeTypeName());
+        }
+
+        return null;
+    }
 
     private static void addNonFullTextConstraints(List<Query> qs,
                                                   IndexPlan plan, IndexReader reader) {
@@ -1194,7 +1212,7 @@ public class LucenePropertyIndex implements AdvancedQueryIndex, QueryIndex, Nati
     }
 
     static Query getFullTextQuery(final IndexPlan plan, FullTextExpression ft,
-                                  final Analyzer analyzer) {
+                                  final Analyzer analyzer, final FulltextQueryTermsProvider augmentor) {
         final PlanResult pr = getPlanResult(plan);
         // a reference to the query, so it can be set in the visitor
         // (a "non-local return")
@@ -1211,7 +1229,7 @@ public class LucenePropertyIndex implements AdvancedQueryIndex, QueryIndex, Nati
             public boolean visit(FullTextOr or) {
                 BooleanQuery q = new BooleanQuery();
                 for (FullTextExpression e : or.list) {
-                    Query x = getFullTextQuery(plan, e, analyzer);
+                    Query x = getFullTextQuery(plan, e, analyzer, augmentor);
                     q.add(x, SHOULD);
                 }
                 result.set(q);
@@ -1222,7 +1240,7 @@ public class LucenePropertyIndex implements AdvancedQueryIndex, QueryIndex, Nati
             public boolean visit(FullTextAnd and) {
                 BooleanQuery q = new BooleanQuery();
                 for (FullTextExpression e : and.list) {
-                    Query x = getFullTextQuery(plan, e, analyzer);
+                    Query x = getFullTextQuery(plan, e, analyzer, augmentor);
                     /* Only unwrap the clause if MUST_NOT(x) */
                     boolean hasMustNot = false;
                     if (x instanceof BooleanQuery) {
@@ -1249,7 +1267,7 @@ public class LucenePropertyIndex implements AdvancedQueryIndex, QueryIndex, Nati
 
             private boolean visitTerm(String propertyName, String text, String boost, boolean not) {
                 String p = getLuceneFieldName(propertyName, pr);
-                Query q = tokenToQuery(text, p, pr.indexingRule, analyzer);
+                Query q = tokenToQuery(text, p, pr.indexingRule, analyzer, augmentor);
                 if (q == null) {
                     return false;
                 }
@@ -1295,7 +1313,8 @@ public class LucenePropertyIndex implements AdvancedQueryIndex, QueryIndex, Nati
         return p;
     }
 
-    private static Query tokenToQuery(String text, String fieldName, IndexingRule indexingRule, Analyzer analyzer) {
+    private static Query tokenToQuery(String text, String fieldName, IndexingRule indexingRule, Analyzer analyzer, FulltextQueryTermsProvider augmentor) {
+        Query ret;
         //Expand the query on fulltext field
         if (FieldNames.FULLTEXT.equals(fieldName) &&
                 !indexingRule.getNodeScopeAnalyzedProps().isEmpty()) {
@@ -1309,9 +1328,25 @@ public class LucenePropertyIndex implements AdvancedQueryIndex, QueryIndex, Nati
             //Add the query for actual fulltext field also. That query would
             //not be boosted
             in.add(tokenToQuery(text, fieldName, analyzer), BooleanClause.Occur.SHOULD);
-            return in;
+            ret = in;
+        } else {
+            ret = tokenToQuery(text, fieldName, analyzer);
         }
-        return tokenToQuery(text, fieldName, analyzer);
+
+        //Augment query terms if available (as a 'SHOULD' clause)
+        if (augmentor != null && FieldNames.FULLTEXT.equals(fieldName)) {
+            Query subQuery = augmentor.getQueryTerm(text, analyzer);
+            if (subQuery != null) {
+                BooleanQuery query = new BooleanQuery();
+
+                query.add(ret, BooleanClause.Occur.SHOULD);
+                query.add(subQuery, BooleanClause.Occur.SHOULD);
+
+                ret = query;
+            }
+        }
+
+        return ret;
     }
 
     static Query tokenToQuery(String text, String fieldName, Analyzer analyzer) {
diff --git a/oak-lucene/src/main/java/org/apache/jackrabbit/oak/plugins/index/lucene/package-info.java b/oak-lucene/src/main/java/org/apache/jackrabbit/oak/plugins/index/lucene/package-info.java
index 75043ca..03e1e2d 100644
--- a/oak-lucene/src/main/java/org/apache/jackrabbit/oak/plugins/index/lucene/package-info.java
+++ b/oak-lucene/src/main/java/org/apache/jackrabbit/oak/plugins/index/lucene/package-info.java
@@ -14,9 +14,9 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-@Version("2.9.0")
+@Version("3.0.0")
 @Export(optional = "provide:=true")
 package org.apache.jackrabbit.oak.plugins.index.lucene;
 
 import aQute.bnd.annotation.Version;
-import aQute.bnd.annotation.Export;
\ No newline at end of file
+import aQute.bnd.annotation.Export;
diff --git a/oak-lucene/src/main/java/org/apache/jackrabbit/oak/plugins/index/lucene/spi/FulltextQueryTermsProvider.java b/oak-lucene/src/main/java/org/apache/jackrabbit/oak/plugins/index/lucene/spi/FulltextQueryTermsProvider.java
new file mode 100644
index 0000000..be320d7
--- /dev/null
+++ b/oak-lucene/src/main/java/org/apache/jackrabbit/oak/plugins/index/lucene/spi/FulltextQueryTermsProvider.java
@@ -0,0 +1,45 @@
+/*
+ * 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.lucene.spi;
+
+import org.apache.lucene.analysis.Analyzer;
+import org.apache.lucene.search.Query;
+
+import java.util.Set;
+
+/**
+ * Implementations of this interface would get callbacks while forming lucene full text queries.
+ */
+public interface FulltextQueryTermsProvider {
+    /**
+     * This method would get called while forming full text clause for full text clause not constrained on a particular
+     * field.
+     * @param text full text term
+     * @param analyzer {@link Analyzer} being used while forming the query. Can be used to analyze text consistently.
+     * @return {@link Query} object to be OR'ed with query being prepared. {@code null}, if nothing is to be added.
+     */
+    Query getQueryTerm(final String text, final Analyzer analyzer);
+
+    /**
+     * This method is used to find which node types are supported by the implementation. Based, on the index
+     * definition being used to query the document, only those implementations would get callback to
+     * {@link FulltextQueryTermsProvider#getQueryTerm} which declare a matching node type. Note, node types are
+     * exact matches and do not support inheritance.
+     * @return {@link Set} of types supported by the implementation
+     */
+    Set<String> getSupportedTypes();
+}
diff --git a/oak-lucene/src/main/java/org/apache/jackrabbit/oak/plugins/index/lucene/spi/IndexFieldProvider.java b/oak-lucene/src/main/java/org/apache/jackrabbit/oak/plugins/index/lucene/spi/IndexFieldProvider.java
new file mode 100644
index 0000000..87991e1
--- /dev/null
+++ b/oak-lucene/src/main/java/org/apache/jackrabbit/oak/plugins/index/lucene/spi/IndexFieldProvider.java
@@ -0,0 +1,47 @@
+/*
+ * 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.lucene.spi;
+
+import org.apache.jackrabbit.oak.spi.state.NodeState;
+import org.apache.lucene.document.Field;
+
+import java.util.Set;
+
+/**
+ * Implementations of this interface would get callbacks while indexing documents. It's the responsibility
+ * of the implementation to exit as early as possible if it doesn't care about the document being indexed.
+ */
+public interface IndexFieldProvider {
+    /**
+     * This method would get called while indexing a document.
+     *
+     * @param path path of the document being indexed
+     * @param document {@link NodeState} of the document being indexed
+     * @param indexDefinition {@link NodeState} of index definition
+     * @return {@link Iterable} of fields that are to be added to {@link org.apache.lucene.document.Document} being prepared
+     */
+    Iterable<Field> getAugmentedFields(final String path, final NodeState document, final NodeState indexDefinition);
+
+    /**
+     * This method is used to find which node types are supported by the implementation. Based, on the index
+     * definition being used to index the document, only those implementations would get callback to
+     * {@link IndexFieldProvider#getAugmentedFields} which declare a matching node type. Note, node types are
+     * exact matches and do not support inheritance.
+     * @return {@link Set} of types supported by the implementation
+     */
+    Set<String> getSupportedTypes();
+}
diff --git a/oak-lucene/src/main/java/org/apache/jackrabbit/oak/plugins/index/lucene/spi/package-info.java b/oak-lucene/src/main/java/org/apache/jackrabbit/oak/plugins/index/lucene/spi/package-info.java
new file mode 100644
index 0000000..57ba4b1
--- /dev/null
+++ b/oak-lucene/src/main/java/org/apache/jackrabbit/oak/plugins/index/lucene/spi/package-info.java
@@ -0,0 +1,22 @@
+/*
+ * 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.
+ */
+@Version("1.0.0")
+@Export(optional = "provide:=true")
+package org.apache.jackrabbit.oak.plugins.index.lucene.spi;
+
+import aQute.bnd.annotation.Export;
+import aQute.bnd.annotation.Version;
diff --git a/oak-lucene/src/test/java/org/apache/jackrabbit/oak/plugins/index/lucene/IndexAugmentorFactoryTest.java b/oak-lucene/src/test/java/org/apache/jackrabbit/oak/plugins/index/lucene/IndexAugmentorFactoryTest.java
new file mode 100644
index 0000000..1d58e91
--- /dev/null
+++ b/oak-lucene/src/test/java/org/apache/jackrabbit/oak/plugins/index/lucene/IndexAugmentorFactoryTest.java
@@ -0,0 +1,190 @@
+/*
+ * 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.lucene;
+
+import com.google.common.collect.Iterables;
+import com.google.common.collect.Lists;
+import com.google.common.collect.Sets;
+import org.apache.jackrabbit.oak.plugins.index.lucene.spi.FulltextQueryTermsProvider;
+import org.apache.jackrabbit.oak.plugins.index.lucene.spi.IndexFieldProvider;
+import org.apache.jackrabbit.oak.spi.state.NodeState;
+import org.apache.jackrabbit.oak.spi.whiteboard.DefaultWhiteboard;
+import org.apache.jackrabbit.oak.spi.whiteboard.Whiteboard;
+import org.apache.lucene.analysis.Analyzer;
+import org.apache.lucene.document.Field;
+import org.apache.lucene.document.StringField;
+import org.apache.lucene.index.Term;
+import org.apache.lucene.search.BooleanClause;
+import org.apache.lucene.search.BooleanQuery;
+import org.apache.lucene.search.Query;
+import org.apache.lucene.search.TermQuery;
+import org.hamcrest.CoreMatchers;
+import org.junit.Before;
+import org.junit.Test;
+
+import java.util.List;
+import java.util.Set;
+
+import static org.apache.lucene.search.BooleanClause.Occur.SHOULD;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertThat;
+
+public class IndexAugmentorFactoryTest {
+    private IndexAugmentorFactory indexAugmentorFactory;
+    private Whiteboard whiteboard = new DefaultWhiteboard();
+
+    @Before
+    public void initializeFactory() {
+        indexAugmentorFactory = new IndexAugmentorFactory(whiteboard);
+    }
+
+    @Test
+    public void compositeIndexProvider()
+    {
+        final String typeA = "type:A";
+        final String typeB = "type:B";
+        final String typeC = "type:C";
+        final String typeD = "type:D";
+
+        new IdentifiableIndexFiledProvider("1", Sets.newHashSet(typeA, typeB));
+        new IdentifiableIndexFiledProvider("2", Sets.newHashSet(typeC));
+        new IdentifiableIndexFiledProvider("3", Sets.newHashSet(typeA, typeB));
+
+        indexAugmentorFactory.refreshServices();
+
+        validateComposedFields(typeA, "1", "3");
+        validateComposedFields(typeC, "2");
+        validateComposedFields(typeD);
+    }
+
+    @Test
+    public void compositeQueryTermsProvider()
+    {
+        final String typeA = "type:A";
+        final String typeB = "type:B";
+        final String typeC = "type:C";
+        final String typeD = "type:D";
+        final String typeE = "type:E";
+
+        new IdentifiableQueryTermsProvider("1", Sets.newHashSet(typeA, typeB));
+        new IdentifiableQueryTermsProvider("2", Sets.newHashSet(typeC));
+        new IdentifiableQueryTermsProvider("3", Sets.newHashSet(typeA, typeB));
+        new IdentifiableQueryTermsProvider(null, Sets.newHashSet(typeE));
+
+        indexAugmentorFactory.refreshServices();
+
+        validateComposedQueryTerms(typeA, "1", "3");
+        validateComposedQueryTerms(typeC, "2");
+        validateComposedQueryTerms(typeD);
+        validateComposedQueryTerms(typeE);
+    }
+
+    void validateComposedFields(String type, String ... expected) {
+        IndexFieldProvider compositeIndexProvider = indexAugmentorFactory.getIndexFieldProvider(type);
+        if (compositeIndexProvider == null) {
+            assertEquals("No index field provider found for " + type + ".", 0, expected.length);
+        } else {
+            Iterable<Field> fields = compositeIndexProvider.getAugmentedFields(null, null, null);
+            Set<String> ids = Sets.newHashSet();
+            for (Field f : fields) {
+                ids.add(f.stringValue());
+            }
+
+            assertEquals(expected.length, Iterables.size(ids));
+            assertThat(ids, CoreMatchers.hasItems(expected));
+        }
+    }
+
+    void validateComposedQueryTerms(String type, String ... expected) {
+        FulltextQueryTermsProvider compositeQueryTermsProvider = indexAugmentorFactory.getFulltextQueryTermsProvider(type);
+        if (compositeQueryTermsProvider == null) {
+            assertEquals("No query terms provider found for " + type + ".", 0, expected.length);
+        } else {
+            Query q = compositeQueryTermsProvider.getQueryTerm(null, null);
+            if (q == null) {
+                assertEquals("No query terms generated for " + type + ".", 0, expected.length);
+            } else {
+                Set<String> ids = Sets.newHashSet();
+                if (q instanceof BooleanQuery) {
+                    BooleanQuery query = (BooleanQuery) q;
+                    List<BooleanClause> clauses = query.clauses();
+                    for (BooleanClause clause : clauses) {
+                        assertEquals(SHOULD, clause.getOccur());
+
+                        Query subQuery = clause.getQuery();
+                        String subQueryStr = subQuery.toString();
+                        ids.add(subQueryStr.substring(0, subQueryStr.indexOf(":1")));
+                    }
+                } else {
+                    Query subQuery = q;
+                    String subQueryStr = subQuery.toString();
+                    ids.add(subQueryStr.substring(0, subQueryStr.indexOf(":1")));
+                }
+
+                assertEquals(expected.length, Iterables.size(ids));
+                assertThat(ids, CoreMatchers.hasItems(expected));
+            }
+        }
+    }
+
+    class IdentifiableIndexFiledProvider implements IndexFieldProvider {
+        private final Field id;
+        private final Set<String> nodeTypes;
+
+        IdentifiableIndexFiledProvider(String id, Set<String> nodeTypes) {
+            this.id = new StringField("id", id, Field.Store.NO);
+            this.nodeTypes = nodeTypes;
+
+            whiteboard.register(IndexFieldProvider.class, this, null);
+        }
+
+        @Override
+        public Iterable<Field> getAugmentedFields(String path, NodeState document, NodeState indexDefinition) {
+            return Lists.newArrayList(id);
+        }
+
+        @Override
+        public Set<String> getSupportedTypes() {
+            return nodeTypes;
+        }
+    }
+
+    class IdentifiableQueryTermsProvider implements FulltextQueryTermsProvider {
+        private final Query id;
+        private final Set<String> nodeTypes;
+
+        IdentifiableQueryTermsProvider(String id, Set<String> nodeTypes) {
+            this.id = (id == null)?null:new TermQuery(new Term(id, "1"));
+            this.nodeTypes = nodeTypes;
+
+            whiteboard.register(FulltextQueryTermsProvider.class, this, null);
+        }
+
+        @Override
+        public Query getQueryTerm(String text, Analyzer analyzer) {
+            return id;
+        }
+
+        @Override
+        public Set<String> getSupportedTypes() {
+            return nodeTypes;
+        }
+    }
+}
diff --git a/oak-lucene/src/test/java/org/apache/jackrabbit/oak/plugins/index/lucene/LuceneIndexAugmentTest.java b/oak-lucene/src/test/java/org/apache/jackrabbit/oak/plugins/index/lucene/LuceneIndexAugmentTest.java
new file mode 100644
index 0000000..27f1c43
--- /dev/null
+++ b/oak-lucene/src/test/java/org/apache/jackrabbit/oak/plugins/index/lucene/LuceneIndexAugmentTest.java
@@ -0,0 +1,675 @@
+/*
+ * 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.lucene;
+
+import com.google.common.collect.Lists;
+import org.apache.commons.io.IOUtils;
+import org.apache.jackrabbit.JcrConstants;
+import org.apache.jackrabbit.oak.Oak;
+import org.apache.jackrabbit.oak.api.ContentRepository;
+import org.apache.jackrabbit.oak.api.Tree;
+import org.apache.jackrabbit.oak.api.Type;
+import org.apache.jackrabbit.oak.plugins.index.IndexConstants;
+import org.apache.jackrabbit.oak.plugins.index.lucene.score.ScorerProviderFactory;
+import org.apache.jackrabbit.oak.plugins.index.lucene.spi.FulltextQueryTermsProvider;
+import org.apache.jackrabbit.oak.plugins.index.lucene.spi.IndexFieldProvider;
+import org.apache.jackrabbit.oak.plugins.nodetype.write.NodeTypeRegistry;
+import org.apache.jackrabbit.oak.query.AbstractQueryTest;
+import org.apache.jackrabbit.oak.spi.commit.Observer;
+import org.apache.jackrabbit.oak.spi.query.QueryIndexProvider;
+import org.apache.jackrabbit.oak.spi.security.OpenSecurityProvider;
+import org.apache.jackrabbit.oak.spi.state.NodeState;
+import org.apache.jackrabbit.oak.spi.whiteboard.DefaultWhiteboard;
+import org.apache.jackrabbit.oak.spi.whiteboard.Whiteboard;
+import org.apache.lucene.analysis.Analyzer;
+import org.apache.lucene.document.Document;
+import org.apache.lucene.document.Field;
+import org.apache.lucene.document.StringField;
+import org.apache.lucene.index.Term;
+import org.apache.lucene.search.IndexSearcher;
+import org.apache.lucene.search.Query;
+import org.apache.lucene.search.ScoreDoc;
+import org.apache.lucene.search.TermQuery;
+import org.apache.lucene.search.TopDocs;
+import org.junit.Test;
+
+import java.util.Collections;
+import java.util.List;
+import java.util.Set;
+import java.util.concurrent.atomic.AtomicInteger;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+
+public class LuceneIndexAugmentTest extends AbstractQueryTest {
+    private final SimpleIndexAugmentorFactory factory = new SimpleIndexAugmentorFactory();
+
+    private IndexTracker tracker = new IndexTracker();
+
+    private IndexNode indexNode;
+
+    @Override
+    protected void createTestIndexNode() throws Exception {
+        setTraversalEnabled(false);
+    }
+
+    @Override
+    protected ContentRepository createRepository() {
+        LuceneIndexEditorProvider editorProvider = new LuceneIndexEditorProvider(null,
+                new ExtractedTextCache(0, 0),
+                factory);
+        LuceneIndexProvider provider = new LuceneIndexProvider(tracker,
+                ScorerProviderFactory.DEFAULT,
+                factory);
+        return new Oak()
+                .with(new OpenSecurityProvider())
+                .with((QueryIndexProvider) provider)
+                .with((Observer) provider)
+                .with(editorProvider)
+                .createContentRepository();
+    }
+
+    //OAK-3576
+    @Test public void queryHook() throws Exception {
+        //setup repo and index
+        NodeTypeRegistry.register(root, IOUtils.toInputStream(TestUtil.TEST_NODE_TYPE), "test nodeType");
+        Tree props = createIndex(TestUtil.NT_TEST);
+        TestUtil.enableForFullText(props, "foo");
+        root.commit();
+
+        //setup query augmentor
+        final String testSearchText = "search this text";
+        final String realSearchText = "bar";
+        factory.fulltextQueryTermsProvider = new FulltextQueryTermsProvider() {
+            @Override
+            public Query getQueryTerm(String text, Analyzer analyzer) {
+                assertEquals("Full text term passed to provider isn't same as the one passed in query",
+                        testSearchText, text);
+                return new TermQuery(new Term(":fulltext", realSearchText));
+            }
+
+            @Override
+            public Set<String> getSupportedTypes() {
+                return Collections.singleton(TestUtil.NT_TEST);
+            }
+        };
+
+        //add content
+        Tree test = root.getTree("/").addChild("test");
+        Tree node = createNodeWithType(test, "item", TestUtil.NT_TEST);
+        node.setProperty("foo", realSearchText);
+        root.commit();
+
+        //query (testSearchText doesn't have 'bar'... our augment would search for :fulltext:bar
+        String query = "select [jcr:path] from [oak:TestNode] where CONTAINS(*, '" + testSearchText + "')";
+        List<String> paths = executeQuery(query, SQL2);
+        assertEquals("Augmented query wasn't used to search", 1, paths.size());
+        assertEquals("/test/item", paths.get(0));
+    }
+
+    //OAK-3576
+    @Test public void indexHookCallbackFrequency() throws Exception {
+        //setup repo and index
+        NodeTypeRegistry.register(root, IOUtils.toInputStream(TestUtil.TEST_NODE_TYPE), "test nodeType");
+        Tree props = createIndex(TestUtil.NT_TEST);
+        TestUtil.enablePropertyIndex(props, "foo1", false);
+        TestUtil.enablePropertyIndex(props, "foo2", false);
+        TestUtil.enablePropertyIndex(props, "subChild/foo3", false);
+        root.commit();
+
+        //setup index augmentor
+        final AtomicInteger counter = new AtomicInteger(0);
+        factory.indexFieldProvider = new IndexFieldProvider() {
+            @Override
+            public Iterable<Field> getAugmentedFields(String path, NodeState document, NodeState indexDefinition) {
+                counter.incrementAndGet();
+                return null;
+            }
+
+            @Override
+            public Set<String> getSupportedTypes() {
+                return Collections.singleton(TestUtil.NT_TEST);
+            }
+        };
+
+        //add content
+        counter.set(0);
+        Tree test = root.getTree("/").addChild("test");
+        Tree node = createNodeWithType(test, "item", TestUtil.NT_TEST);
+        node.setProperty("foo1", "bar1");
+        node.setProperty("foo2", "bar2");
+        Tree subChild = node.addChild("subChild");
+        subChild.setProperty("foo3", "bar3");
+        root.commit();
+        assertEquals("Number of callbacks should be same as number of changed properties", 1, counter.get());
+
+        //change sub-property
+        counter.set(0);
+        subChild = root.getTree("/test/item/subChild");
+        subChild.setProperty("foo3", "bar4");
+        root.commit();
+        assertEquals("Sub child property change should make call backs for all indexed properties", 1, counter.get());
+    }
+
+    //OAK-3576
+    @Test public void indexHookCallbackAndStorage() throws Exception {
+        final String propName = "subChild/foo";
+
+        //setup repo and index
+        NodeTypeRegistry.register(root, IOUtils.toInputStream(TestUtil.TEST_NODE_TYPE), "test nodeType");
+        Tree props = createIndex(TestUtil.NT_TEST);
+        TestUtil.enableForFullText(props, propName);
+        root.commit();
+
+        //setup index augmentor
+        factory.indexFieldProvider = new IndexFieldProvider() {
+            @Override
+            public Iterable<Field> getAugmentedFields(String path, NodeState document, NodeState indexDefinition) {
+                assertEquals("/test/item", path);
+                assertEquals(TestUtil.NT_TEST, document.getName(JcrConstants.JCR_PRIMARYTYPE));
+                assertEquals(IndexConstants.INDEX_DEFINITIONS_NODE_TYPE,
+                        indexDefinition.getName(JcrConstants.JCR_PRIMARYTYPE));
+                return Lists.<Field>newArrayList(new StringField("barbar", "1", Field.Store.NO));
+            }
+
+            @Override
+            public Set<String> getSupportedTypes() {
+                return Collections.singleton(TestUtil.NT_TEST);
+            }
+        };
+
+        //add content
+        Tree test = root.getTree("/").addChild("test");
+        Tree node = createNodeWithType(test, "item", TestUtil.NT_TEST).addChild("subChild");
+        node.setProperty("foo", "bar");
+        root.commit();
+
+        //Check document that made to the index
+        IndexSearcher searcher = getSearcher();
+        TopDocs docs = searcher.search(new TermQuery(new Term("barbar", "1")), 10);
+        ScoreDoc[] scoreDocs = docs.scoreDocs;
+        assertEquals("Number of results should be 1", 1, scoreDocs.length);
+        Document doc = searcher.doc(scoreDocs[0].doc);
+        String path = doc.get(":path");
+        assertEquals("/test/item", path);
+    }
+
+    //OAK-3576
+    @Test
+    public void nullBehavior() throws Exception {
+        //setup repo and index
+        NodeTypeRegistry.register(root, IOUtils.toInputStream(TestUtil.TEST_NODE_TYPE), "test nodeType");
+        Tree props = createIndex(TestUtil.NT_TEST);
+        TestUtil.enableForFullText(props, "foo");
+        root.commit();
+
+        Tree rootTree = root.getTree("/").addChild("test");
+
+        //Note: augmentor behavior is test elsewhere... we are just checking if default works
+
+        int testIndex = 1;
+        //both query and index augmentors are null (no exception expected)
+        checkSimpleBehavior(rootTree, testIndex++);
+
+        //Set a very sad query augmentor
+        factory.fulltextQueryTermsProvider = new FulltextQueryTermsProvider() {
+            @Override
+            public Query getQueryTerm(String text, Analyzer analyzer) {
+                return null;
+            }
+
+            @Override
+            public Set<String> getSupportedTypes() {
+                return null;
+            }
+        };
+        checkSimpleBehavior(rootTree, testIndex++);
+
+        //Set query augmentor... with null query
+        factory.fulltextQueryTermsProvider = new FulltextQueryTermsProvider() {
+            @Override
+            public Query getQueryTerm(String text, Analyzer analyzer) {
+                return null;
+            }
+
+            @Override
+            public Set<String> getSupportedTypes() {
+                return Collections.singleton(TestUtil.NT_TEST);
+            }
+        };
+        checkSimpleBehavior(rootTree, testIndex++);
+
+        //Set query augmentor... with some query
+        factory.fulltextQueryTermsProvider = new FulltextQueryTermsProvider() {
+            @Override
+            public Query getQueryTerm(String text, Analyzer analyzer) {
+                return new TermQuery(new Term("bar", "baz"));
+            }
+
+            @Override
+            public Set<String> getSupportedTypes() {
+                return Collections.singleton(TestUtil.NT_TEST);
+            }
+        };
+        checkSimpleBehavior(rootTree, testIndex++);
+
+        factory.fulltextQueryTermsProvider = null;
+
+        //Set a very sad index augmentor
+        factory.indexFieldProvider = new IndexFieldProvider() {
+            @Override
+            public Iterable<Field> getAugmentedFields(String path, NodeState document, NodeState indexDefinition) {
+                return null;
+            }
+
+            @Override
+            public Set<String> getSupportedTypes() {
+                return null;
+            }
+        };
+        checkSimpleBehavior(rootTree, testIndex++);
+
+        //Set index augmentor... with null fields
+        factory.indexFieldProvider = new IndexFieldProvider() {
+            @Override
+            public Iterable<Field> getAugmentedFields(String path, NodeState document, NodeState indexDefinition) {
+                return null;
+            }
+
+            @Override
+            public Set<String> getSupportedTypes() {
+                return Collections.singleton(TestUtil.NT_TEST);
+            }
+        };
+        checkSimpleBehavior(rootTree, testIndex++);
+
+        //Set index augmentor... with some fields
+        factory.fulltextQueryTermsProvider = null;
+        factory.indexFieldProvider = new IndexFieldProvider() {
+            @Override
+            public Iterable<Field> getAugmentedFields(String path, NodeState document, NodeState indexDefinition) {
+                List<Field> fields = Lists.newArrayList();
+                fields.add(new StringField("bar", "baz", Field.Store.NO));
+                return fields;
+            }
+
+            @Override
+            public Set<String> getSupportedTypes() {
+                return Collections.singleton(TestUtil.NT_TEST);
+            }
+        };
+        checkSimpleBehavior(rootTree, testIndex++);
+    }
+
+    //OAK-3576
+    @Test
+    public void skipDefaultOnlyUsingAugmentors() throws Exception {
+        //setup repo and index
+        NodeTypeRegistry.register(root, IOUtils.toInputStream(TestUtil.TEST_NODE_TYPE), "test nodeType");
+        Tree props = createIndex(TestUtil.NT_TEST);
+        Tree prop = props.addChild("foo1");
+        prop.setProperty(LuceneIndexConstants.PROP_INDEX, true);
+        prop = props.addChild("foo2");
+        prop.setProperty(LuceneIndexConstants.PROP_NAME, "subChild/foo2");
+        prop.setProperty(LuceneIndexConstants.PROP_INDEX, true);
+        root.commit();
+
+        //setup augmentors
+        final AtomicInteger indexingCounter = new AtomicInteger(0);
+        factory.indexFieldProvider = new IndexFieldProvider() {
+            @Override
+            public Iterable<Field> getAugmentedFields(String path, NodeState document, NodeState indexDefinition) {
+                indexingCounter.incrementAndGet();
+                return null;
+            }
+
+            @Override
+            public Set<String> getSupportedTypes() {
+                return Collections.singleton(TestUtil.NT_TEST);
+            }
+        };
+        final AtomicInteger queryingCounter = new AtomicInteger(0);
+        factory.fulltextQueryTermsProvider = new FulltextQueryTermsProvider() {
+            @Override
+            public Query getQueryTerm(String text, Analyzer analyzer) {
+                queryingCounter.set(1);
+                return null;
+            }
+
+            @Override
+            public Set<String> getSupportedTypes() {
+                return Collections.singleton(TestUtil.NT_TEST);
+            }
+        };
+
+        //add content
+        Tree node1 = createNodeWithType(root.getTree("/"), "node1", TestUtil.NT_TEST);
+        node1.setProperty("foo1", "bar1");
+        node1.addChild("subChild").setProperty("foo2", "bar2");
+        root.commit();
+
+        //indexing assertions
+        assertEquals("Indexing augment should get called once", 1, indexingCounter.get());
+        assertEquals("No docs should get indexed (augmentor hasn't added any field)",
+                0, getSearcher().getIndexReader().numDocs());
+
+        String query = "EXPLAIN SELECT [jcr:path] from [" + TestUtil.NT_TEST + "] WHERE [foo1]='bar1'";
+        List<String> paths = executeQuery(query, SQL2);
+        assertTrue("skipDefaultIndexing shouldn't decide query plan (" + paths.get(0) + ")",
+                paths.get(0).contains("/* no-index "));
+
+        query = "EXPLAIN SELECT [jcr:path] from [" + TestUtil.NT_TEST + "] WHERE [subChild/foo2]='bar2'";
+         paths = executeQuery(query, SQL2);
+        assertTrue("skipDefaultIndexing shouldn't decide query plan (" + paths.get(0) + ")",
+                paths.get(0).contains("/* no-index "));
+    }
+
+    //OAK-3576
+    @Test
+    public void propertyIndexUsingAugmentors() throws Exception {
+        //setup repo and index
+        NodeTypeRegistry.register(root, IOUtils.toInputStream(TestUtil.TEST_NODE_TYPE), "test nodeType");
+        Tree props = createIndex(TestUtil.NT_TEST);
+        TestUtil.enablePropertyIndex(props, "foo1", false);
+        TestUtil.enablePropertyIndex(props, "subChild/foo2", false);
+        root.commit();
+
+        //setup augmentors
+        final AtomicInteger indexingCounter = new AtomicInteger(0);
+        factory.indexFieldProvider = new IndexFieldProvider() {
+            @Override
+            public Iterable<Field> getAugmentedFields(String path, NodeState document, NodeState indexDefinition) {
+                indexingCounter.incrementAndGet();
+                return null;
+            }
+
+            @Override
+            public Set<String> getSupportedTypes() {
+                return Collections.singleton(TestUtil.NT_TEST);
+            }
+        };
+        final AtomicInteger queryingCounter = new AtomicInteger(0);
+        factory.fulltextQueryTermsProvider = new FulltextQueryTermsProvider() {
+            @Override
+            public Query getQueryTerm(String text, Analyzer analyzer) {
+                queryingCounter.set(1);
+                return null;
+            }
+
+            @Override
+            public Set<String> getSupportedTypes() {
+                return Collections.singleton(TestUtil.NT_TEST);
+            }
+        };
+
+        //add content
+        Tree node1 = createNodeWithType(root.getTree("/"), "node1", TestUtil.NT_TEST);
+        node1.setProperty("foo1", "bar1");
+        node1.addChild("subChild").setProperty("foo2", "bar2");
+        root.commit();
+
+        //indexing assertions
+        assertEquals("Indexing augment should get called once", 1, indexingCounter.get());
+
+        String query = "SELECT [jcr:path] from [" + TestUtil.NT_TEST + "] WHERE [foo1]='bar1'";
+        executeQuery(query, SQL2);
+        assertEquals("Query augmentor should not get called for property constraints", 0, queryingCounter.get());
+        query = "EXPLAIN " + query;
+        List<String> paths = executeQuery(query, SQL2, false);
+        assertTrue("property index should have made the index selected (" + paths.get(0) + ")",
+                paths.get(0).contains("/* lucene:test-index("));
+
+        query = "SELECT [jcr:path] from [" + TestUtil.NT_TEST + "] WHERE [subChild/foo2]='bar2'";
+        executeQuery(query, SQL2);
+        assertEquals("Query augmentor should not get called for property constraints", 0, queryingCounter.get());
+        query = "EXPLAIN " + query;
+        paths = executeQuery(query, SQL2);
+        assertTrue("property index should have made the index selected (" + paths.get(0) + ")",
+                paths.get(0).contains("/* lucene:test-index("));
+    }
+
+    //OAK-3576
+    @Test
+    public void fulltextIndexUsingAugmentors() throws Exception {
+        //setup repo and index
+        NodeTypeRegistry.register(root, IOUtils.toInputStream(TestUtil.TEST_NODE_TYPE), "test nodeType");
+        Tree props = createIndex(TestUtil.NT_TEST);
+        TestUtil.enableForFullText(props, "foo1");
+        TestUtil.enableForFullText(props, "subChild/foo2");
+        root.commit();
+
+        //setup augmentors
+        final AtomicInteger indexingCounter = new AtomicInteger(0);
+        factory.indexFieldProvider = new IndexFieldProvider() {
+            @Override
+            public Iterable<Field> getAugmentedFields(String path, NodeState document, NodeState indexDefinition) {
+                indexingCounter.incrementAndGet();
+                return null;
+            }
+
+            @Override
+            public Set<String> getSupportedTypes() {
+                return Collections.singleton(TestUtil.NT_TEST);
+            }
+        };
+        final AtomicInteger queryingCounter = new AtomicInteger(0);
+        factory.fulltextQueryTermsProvider = new FulltextQueryTermsProvider() {
+            @Override
+            public Query getQueryTerm(String text, Analyzer analyzer) {
+                queryingCounter.set(1);
+                return null;
+            }
+
+            @Override
+            public Set<String> getSupportedTypes() {
+                return Collections.singleton(TestUtil.NT_TEST);
+            }
+        };
+
+        //add content
+        Tree node1 = createNodeWithType(root.getTree("/"), "node1", TestUtil.NT_TEST);
+        node1.setProperty("foo1", "bar1");
+        node1.addChild("subChild").setProperty("foo2", "bar2");
+        root.commit();
+
+        //indexing assertions
+        assertEquals("Indexing augment should get called once", 1, indexingCounter.get());
+
+        String query = "SELECT [jcr:path] from [" + TestUtil.NT_TEST + "] WHERE CONTAINS(*, 'bar1')";
+        executeQuery(query, SQL2);
+        assertEquals("Query augmentor should get called for full text constraints", 1, queryingCounter.get());
+        queryingCounter.set(0);
+        query = "EXPLAIN " + query;
+        List<String> paths = executeQuery(query, SQL2, false);
+        assertEquals("Query augmentor should get called for full text constraints", 1, queryingCounter.get());
+        assertTrue("property index should have made the index selected (" + paths.get(0) + ")",
+                paths.get(0).contains("/* lucene:test-index("));
+
+        queryingCounter.set(0);
+        query = "SELECT [jcr:path] from [" + TestUtil.NT_TEST + "] WHERE CONTAINS(*, 'bar2')";
+        executeQuery(query, SQL2);
+        assertEquals("Query augmentor should get called for full text constraints", 1, queryingCounter.get());
+        queryingCounter.set(0);
+        query = "EXPLAIN " + query;
+        paths = executeQuery(query, SQL2, false);
+        assertEquals("Query augmentor should get called for full text constraints", 1, queryingCounter.get());
+        assertTrue("property index should have made the index selected (" + paths.get(0) + ")",
+                paths.get(0).contains("/* lucene:test-index("));
+    }
+
+    @Test
+    public void indexAugmentorMismatchedNodeType() throws Exception {
+        //setup repo and index
+        NodeTypeRegistry.register(root, IOUtils.toInputStream(TestUtil.TEST_NODE_TYPE), "test nodeType");
+        Tree props = createIndex(TestUtil.NT_TEST);
+        TestUtil.enableForFullText(props, "foo1");
+        root.commit();
+
+        //setup augmentors
+        final AtomicInteger indexingCounter1 = new AtomicInteger(0);
+        final AtomicInteger indexingCounter2 = new AtomicInteger(0);
+        factory.registerIndexFieldProvider(new IndexFieldProvider() {
+            @Override
+            public Iterable<Field> getAugmentedFields(String path, NodeState document, NodeState indexDefinition) {
+                indexingCounter1.incrementAndGet();
+                return null;
+            }
+
+            @Override
+            public Set<String> getSupportedTypes() {
+                return Collections.singleton(JcrConstants.NT_BASE);
+            }
+        });
+        factory.registerIndexFieldProvider(new IndexFieldProvider() {
+            @Override
+            public Iterable<Field> getAugmentedFields(String path, NodeState document, NodeState indexDefinition) {
+                indexingCounter2.incrementAndGet();
+                return null;
+            }
+
+            @Override
+            public Set<String> getSupportedTypes() {
+                return Collections.singleton(TestUtil.NT_TEST);
+            }
+        });
+        factory.useSuperBehavior = true;
+
+        //add content
+        createNodeWithType(root.getTree("/"), "node1", TestUtil.NT_TEST).setProperty("foo1", "bar1");
+        root.commit();
+
+        assertEquals("Mismatching node type should not let index augmentor called", 0, indexingCounter1.get());
+        assertEquals("Matching node type should get augmentor called", 1, indexingCounter2.get());
+    }
+
+    @Test
+    public void queryAugmentorMismatchedNodeType() throws Exception {
+        //setup repo and index
+        NodeTypeRegistry.register(root, IOUtils.toInputStream(TestUtil.TEST_NODE_TYPE), "test nodeType");
+        Tree props = createIndex(TestUtil.NT_TEST);
+        TestUtil.enableForFullText(props, "foo1", false);
+        root.commit();
+
+        //setup augmentors
+        final AtomicInteger indexingCounter1 = new AtomicInteger(0);
+        final AtomicInteger indexingCounter2 = new AtomicInteger(0);
+        factory.registerQueryTermsProvider(new FulltextQueryTermsProvider() {
+            @Override
+            public Query getQueryTerm(String text, Analyzer analyzer) {
+                indexingCounter1.set(1);
+                return null;
+            }
+
+            @Override
+            public Set<String> getSupportedTypes() {
+                return Collections.singleton(JcrConstants.NT_BASE);
+            }
+        });
+        factory.registerQueryTermsProvider(new FulltextQueryTermsProvider() {
+            @Override
+            public Query getQueryTerm(String text, Analyzer analyzer) {
+                indexingCounter2.set(1);
+                return null;
+            }
+
+            @Override
+            public Set<String> getSupportedTypes() {
+                return Collections.singleton(TestUtil.NT_TEST);
+            }
+        });
+        factory.useSuperBehavior = true;
+
+
+        executeQuery("SELECT [jcr:path] FROM [" + TestUtil.NT_TEST + "] WHERE CONTAINS(*, 'test')", SQL2, false);
+
+        assertEquals("Mismatching node type should not let index augmentor called", 0, indexingCounter1.get());
+        assertEquals("Matching node type should get augmentor called", 1, indexingCounter2.get());
+    }
+
+    private static Tree createNodeWithType(Tree t, String nodeName, String typeName){
+        t = t.addChild(nodeName);
+        t.setProperty(JcrConstants.JCR_PRIMARYTYPE, typeName, Type.NAME);
+        return t;
+    }
+
+    private Tree createIndex(String nodeType) throws Exception {
+        Tree rootTree = root.getTree("/");
+        return createIndex(rootTree, nodeType);
+    }
+
+    private Tree createIndex(Tree root, String nodeType) throws Exception {
+        Tree index = createTestIndexNode(root, LuceneIndexConstants.TYPE_LUCENE);
+        return TestUtil.newRulePropTree(index, nodeType);
+    }
+
+    private static class SimpleIndexAugmentorFactory extends IndexAugmentorFactory {
+        IndexFieldProvider indexFieldProvider = null;
+        FulltextQueryTermsProvider fulltextQueryTermsProvider = null;
+        private boolean useSuperBehavior = false;
+        private final Whiteboard whiteboard;
+
+        SimpleIndexAugmentorFactory() {
+            this(new DefaultWhiteboard());
+        }
+
+        SimpleIndexAugmentorFactory(Whiteboard whiteboard) {
+            super(whiteboard);
+            this.whiteboard = whiteboard;
+        }
+
+        void registerIndexFieldProvider(IndexFieldProvider provider) {
+            whiteboard.register(IndexFieldProvider.class, provider, null);
+            refreshServices();
+        }
+
+        void registerQueryTermsProvider(FulltextQueryTermsProvider provider) {
+            whiteboard.register(FulltextQueryTermsProvider.class, provider, null);
+            refreshServices();
+        }
+
+        @Override
+        public IndexFieldProvider getIndexFieldProvider(String nodeType) {
+            return useSuperBehavior?super.getIndexFieldProvider(nodeType):indexFieldProvider;
+        }
+
+        @Override
+        public FulltextQueryTermsProvider getFulltextQueryTermsProvider(String nodeType) {
+            return useSuperBehavior?super.getFulltextQueryTermsProvider(nodeType):fulltextQueryTermsProvider;
+        }
+    }
+
+    private IndexSearcher getSearcher(){
+        if(indexNode == null){
+            indexNode = tracker.acquireIndexNode("/oak:index/" + TEST_INDEX_NAME);
+        }
+        return indexNode.getSearcher();
+    }
+
+    private void checkSimpleBehavior(Tree rootTree, int testIndex) throws Exception {
+        createNodeWithType(rootTree, "node" + testIndex, TestUtil.NT_TEST)
+                .setProperty("foo", "bar" + testIndex);
+        root.commit();
+
+        String query = "SELECT [jcr:path] from [" + TestUtil.NT_TEST + "] WHERE contains(*, 'bar" + testIndex + "')";
+        List<String> paths = executeQuery(query, SQL2);
+        assertEquals(1, paths.size());
+        assertEquals("/test/node" + testIndex, paths.get(0));
+    }
+}
-- 
2.6.2

