You are viewing a plain text version of this content. The canonical link for it is here.
Posted to oak-commits@jackrabbit.apache.org by th...@apache.org on 2022/07/04 16:14:57 UTC

[jackrabbit-oak] branch OAK-9780 updated: OAK-9780 Prefetch Cursor

This is an automated email from the ASF dual-hosted git repository.

thomasm pushed a commit to branch OAK-9780
in repository https://gitbox.apache.org/repos/asf/jackrabbit-oak.git


The following commit(s) were added to refs/heads/OAK-9780 by this push:
     new 5f8a17634d OAK-9780 Prefetch Cursor
5f8a17634d is described below

commit 5f8a17634ddc488590d8d59fdf9873d9a5ce25ce
Author: Thomas Mueller <th...@apache.org>
AuthorDate: Mon Jul 4 18:14:47 2022 +0200

    OAK-9780 Prefetch Cursor
---
 .../jackrabbit/oak/plugins/index/Cursors.java      | 624 ---------------------
 .../plugins/index/aggregate/AggregateIndex.java    |   2 +-
 .../plugins/index/aggregate/AggregationCursor.java |   2 +-
 .../oak/plugins/index/cursor/AbstractCursor.java   |  38 ++
 .../oak/plugins/index/cursor/AncestorCursor.java   |  62 ++
 .../oak/plugins/index/cursor/ConcatCursor.java     | 120 ++++
 .../oak/plugins/index/cursor/Cursors.java          | 113 ++++
 .../plugins/index/cursor/IntersectionCursor.java   | 121 ++++
 .../oak/plugins/index/cursor/PathCursor.java       |  73 +++
 .../oak/plugins/index/cursor/PrefetchCursor.java   | 203 +++++++
 .../oak/plugins/index/cursor/TraversingCursor.java | 192 +++++++
 .../oak/plugins/index/diffindex/DiffIndex.java     |   2 +-
 .../oak/plugins/index/nodetype/NodeTypeIndex.java  |   3 +-
 .../plugins/index/property/PropertyIndexPlan.java  |   2 +-
 .../plugins/index/reference/ReferenceIndex.java    |   2 +-
 .../jackrabbit/oak/query/ast/SelectorImpl.java     |   2 +-
 .../oak/query/index/TraversingIndex.java           |   2 +-
 .../oak/plugins/index/cursor/CursorUtils.java      |  34 ++
 .../plugins/index/{ => cursor}/CursorsTest.java    |   2 +-
 .../plugins/index/cursor/PrefetchCursorTest.java   | 119 ++++
 .../oak/plugins/index/cursor/TestCursor.java       |  41 ++
 .../index/cursor/TestPrefetchNodeStore.java        |  42 ++
 .../oak/plugins/index/cursor/TestRow.java          |  45 ++
 .../plugins/index/nodetype/NodeTypeIndexTest.java  |   4 +-
 .../oak/security/CustomQueryIndexProviderTest.java |   2 +-
 25 files changed, 1215 insertions(+), 637 deletions(-)

diff --git a/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/index/Cursors.java b/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/index/Cursors.java
deleted file mode 100644
index e5125febd6..0000000000
--- a/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/index/Cursors.java
+++ /dev/null
@@ -1,624 +0,0 @@
-/*
- * 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;
-
-import java.util.ArrayList;
-import java.util.Deque;
-import java.util.HashMap;
-import java.util.HashSet;
-import java.util.Iterator;
-import java.util.List;
-import java.util.Set;
-import java.util.TreeSet;
-
-import org.apache.jackrabbit.oak.api.Result.SizePrecision;
-import org.apache.jackrabbit.oak.commons.PathUtils;
-import org.apache.jackrabbit.oak.plugins.memory.MemoryChildNodeEntry;
-import org.apache.jackrabbit.oak.query.FilterIterators;
-import org.apache.jackrabbit.oak.query.QueryImpl;
-import org.apache.jackrabbit.oak.query.index.IndexRowImpl;
-import org.apache.jackrabbit.oak.spi.query.Cursor;
-import org.apache.jackrabbit.oak.spi.query.Filter;
-import org.apache.jackrabbit.oak.spi.query.Filter.PathRestriction;
-import org.apache.jackrabbit.oak.spi.query.IndexRow;
-import org.apache.jackrabbit.oak.spi.query.QueryLimits;
-import org.apache.jackrabbit.oak.spi.state.ChildNodeEntry;
-import org.apache.jackrabbit.oak.spi.state.NodeState;
-import org.apache.jackrabbit.oak.spi.state.NodeStateUtils;
-import org.apache.jackrabbit.oak.spi.state.PrefetchNodeStore;
-import org.jetbrains.annotations.Nullable;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-
-import com.google.common.base.Function;
-import com.google.common.base.Predicate;
-import com.google.common.collect.Iterators;
-import com.google.common.collect.Queues;
-
-import static com.google.common.base.Preconditions.checkArgument;
-import static com.google.common.base.Preconditions.checkNotNull;
-import static org.apache.jackrabbit.oak.commons.PathUtils.isAbsolute;
-import static org.apache.jackrabbit.oak.spi.query.QueryConstants.REP_FACET;
-
-/**
- * This utility class provides factory methods to create commonly used types of
- * {@link Cursor}s.
- */
-public class Cursors {
-
-    private Cursors() {
-    }
-    
-    public static void checkMemoryLimit(long count, QueryLimits settings) {
-        FilterIterators.checkMemoryLimit(count, settings);
-    }    
-
-    public static void checkReadLimit(long count, QueryLimits settings) {
-        FilterIterators.checkReadLimit(count, settings);
-    }    
-
-    public static Cursor newIntersectionCursor(Cursor a, Cursor b, QueryLimits settings) {
-        return new IntersectionCursor(a, b, settings);
-    }
-
-    public static Cursor newConcatCursor(List<Cursor> cursors, QueryLimits settings) {
-        return new ConcatCursor(cursors, settings);
-    }
-
-    public static Cursor newPrefetchCursor(Cursor cursor, PrefetchNodeStore store, int prefetchCount,
-            NodeState rootState, List<String> prefetchRelative) {
-        return new PrefetchCursor(cursor, store, prefetchCount, rootState, prefetchRelative);
-    }
-
-    /**
-     * Creates a {@link Cursor} over paths.
-     *
-     * @param paths the paths to iterate over (must return distinct paths)
-     * @return the Cursor.
-     */
-    public static Cursor newPathCursor(Iterable<String> paths, QueryLimits settings) {
-        return new PathCursor(paths.iterator(), true, settings);
-    }
-
-    /**
-     * Creates a {@link Cursor} over paths, and make the result distinct.
-     * The iterator might return duplicate paths
-     * 
-     * @param paths the paths to iterate over (might contain duplicate entries)
-     * @return the Cursor.
-     */
-    public static Cursor newPathCursorDistinct(Iterable<String> paths, QueryLimits settings) {
-        return new PathCursor(paths.iterator(), true, settings);
-    }
-
-    /**
-     * Returns a traversing cursor based on the path restriction in the given
-     * {@link Filter}.
-     * 
-     * @param filter the filter.
-     * @param rootState the root {@link NodeState}.
-     * @return the {@link Cursor}.
-     */
-    public static Cursor newTraversingCursor(Filter filter,
-                                             NodeState rootState) {
-        return new TraversingCursor(filter, rootState);
-    }
-
-    /**
-     * Returns a cursor wrapper, which returns the ancestor rows at the given
-     * <code>level</code> of the wrapped cursor <code>c</code>. With
-     * <code>level</code> e.g. set to <code>1</code>, the returned cursor
-     * iterates over the parent rows of the passed cursor <code>c</code>. The
-     * returned cursor guarantees distinct rows.
-     *
-     * @param c the cursor to wrap.
-     * @param level the ancestor level. Must be {@code >= 1}.
-     * @return cursor over the ancestors of <code>c</code> at <code>level</code>.
-     */
-    public static Cursor newAncestorCursor(Cursor c, int level, QueryLimits settings) {
-        checkNotNull(c);
-        checkArgument(level >= 1);
-        return new AncestorCursor(c, level, settings);
-    }
-
-    /**
-     * A Cursor implementation where the remove method throws an
-     * UnsupportedOperationException.
-     */
-    public abstract static class AbstractCursor implements Cursor {
-        
-        @Override
-        public void remove() {
-            throw new UnsupportedOperationException();
-        }
-        
-        @Override
-        public long getSize(SizePrecision precision, long max) {
-            return -1;
-        }
-
-    }
-
-    /**
-     * This class allows to iterate over the parent nodes of the wrapped cursor.
-     */
-    private static class AncestorCursor extends PathCursor {
-
-        public AncestorCursor(Cursor cursor, int level, QueryLimits settings) {
-            super(transform(cursor, level), true, settings);
-        }
-
-        private static Iterator<String> transform(Cursor cursor, final int level) {
-            Iterator<String> unfiltered = Iterators.transform(cursor,
-                    new Function<IndexRow, String>() {
-                @Override
-                public String apply(@Nullable IndexRow input) {
-                    return input != null ? input.getPath() : null;
-                }
-            });
-            Iterator<String> filtered = Iterators.filter(unfiltered,
-                    new Predicate<String>() {
-                @Override
-                public boolean apply(@Nullable String input) {
-                    return input != null && PathUtils.getDepth(input) >= level;
-                }
-            });
-            return Iterators.transform(filtered, new Function<String, String>() {
-                @Override
-                public String apply(String input) {
-                    return PathUtils.getAncestorPath(input, level);
-                }
-            });
-        }
-    }
-
-    /**
-     * <code>PathCursor</code> implements a simple {@link Cursor} that iterates
-     * over a {@link String} based path {@link Iterable}.
-     */
-    public static class PathCursor extends AbstractCursor {
-
-        private final Iterator<String> iterator;
-
-        public PathCursor(Iterator<String> paths, boolean distinct, final QueryLimits settings) {
-            Iterator<String> it = paths;
-            if (distinct) {
-                it = Iterators.filter(it, new Predicate<String>() {
-                    
-                    private final HashSet<String> known = new HashSet<String>();
-
-                    @Override
-                    public boolean apply(@Nullable String input) {
-                        FilterIterators.checkMemoryLimit(known.size(), settings);
-                        // Set.add returns true for new entries
-                        return known.add(input);
-                    }
-                    
-                });
-            }
-            this.iterator = it;
-        }
-
-        @Override
-        public IndexRow next() {
-            // TODO support jcr:score and possibly rep:excerpt
-            String path = iterator.next();
-            return new IndexRowImpl(isAbsolute(path) ? path : "/" + path);
-        }
-        
-        @Override
-        public boolean hasNext() {
-            return iterator.hasNext();
-        }
-        
-    }
-
-    /**
-     * A cursor that reads all nodes in a given subtree.
-     */
-    private static class TraversingCursor extends AbstractCursor {
-
-        private static final Logger LOG = LoggerFactory.getLogger(TraversingCursor.class);
-
-        private final Filter filter;
-
-        private final Deque<Iterator<? extends ChildNodeEntry>> nodeIterators =
-                Queues.newArrayDeque();
-
-        private String parentPath;
-
-        private String currentPath;
-
-        private long readCount;
-
-        private boolean init;
-        
-        private boolean closed;
-        
-        private final QueryLimits settings;
-        
-        public TraversingCursor(Filter filter, NodeState rootState) {
-            this.filter = filter;
-            this.settings = filter.getQueryLimits();
-
-            String path = filter.getPath();
-            parentPath = null;
-            currentPath = "/";
-            NodeState parent = null;
-            NodeState node = rootState;
-            
-            if (filter.containsNativeConstraint()) {
-                // OAK-4313: if no other index was found,
-                // then, for native queries, we won't match anything
-                return;
-            }
-
-            if (filter.isAlwaysFalse()) {
-                // nothing can match this filter, leave nodes empty
-                return;
-            }
-
-            Filter.PropertyRestriction facetRestriction = filter.getPropertyRestriction(REP_FACET);
-            if (facetRestriction != null) {
-                // we don't evaluate facets by traversal
-                throw new IllegalArgumentException(facetRestriction + " can't be evaluated by traversal");
-            }
-
-            if (!path.equals("/")) {
-                for (String name : path.substring(1).split("/")) {
-                    parentPath = currentPath;
-                    currentPath = PathUtils.concat(parentPath, name);
-
-                    parent = node;
-                    node = parent.getChildNode(name);
-                }
-                if (!node.exists()) {
-                    // nothing can match this filter, leave nodes empty
-                    return;
-                }
-            }
-            Filter.PathRestriction restriction = filter.getPathRestriction();
-            switch (restriction) {
-            case NO_RESTRICTION:
-            case EXACT:
-            case ALL_CHILDREN:
-                nodeIterators.add(Iterators.singletonIterator(
-                        new MemoryChildNodeEntry(currentPath, node)));
-                parentPath = "";
-                break;
-            case PARENT:
-                if (parent != null) {
-                    nodeIterators.add(Iterators.singletonIterator(
-                            new MemoryChildNodeEntry(parentPath, parent)));
-                    parentPath = "";
-                }
-                break;
-            case DIRECT_CHILDREN:
-                nodeIterators.add(node.getChildNodeEntries().iterator());
-                parentPath = currentPath;
-                break;
-            default:
-                throw new IllegalArgumentException("Unknown restriction: " + restriction);
-            }
-        }
-
-        @Override
-        public IndexRow next() {
-            if (closed) {
-                throw new IllegalStateException("This cursor is closed");
-            }
-            if (!init) {
-                fetchNext();
-                init = true;
-            }
-            IndexRowImpl result = new IndexRowImpl(currentPath);
-            fetchNext();
-            return result;
-        }
-        
-        @Override 
-        public boolean hasNext() {
-            if (!closed && !init) {
-                fetchNext();
-                init = true;
-            }
-            return !closed;
-        }
-
-        private void fetchNext() {
-            while (!nodeIterators.isEmpty()) {
-                Iterator<? extends ChildNodeEntry> iterator = nodeIterators.getLast();
-                if (iterator.hasNext()) {
-                    ChildNodeEntry entry = iterator.next();
-
-                    readCount++;
-                    if (readCount % 1000 == 0) {
-                        FilterIterators.checkReadLimit(readCount, settings);
-                        String caller = IndexUtils.getCaller(this.settings.getIgnoredClassNamesInCallTrace());
-                        LOG.warn("Traversed {} nodes with filter {} called by {}; consider creating an index or changing the query" , 
-                                readCount, filter, caller);
-                    }
-
-                    NodeState node = entry.getNodeState();
-
-                    String name = entry.getName();
-                    if (NodeStateUtils.isHidden(name)) {
-                        continue;
-                    }
-                    currentPath = PathUtils.concat(parentPath, name);
-
-                    PathRestriction r = filter.getPathRestriction();
-                    if (r == PathRestriction.ALL_CHILDREN || 
-                            r == PathRestriction.NO_RESTRICTION) {
-                        nodeIterators.addLast(node.getChildNodeEntries().iterator());
-                        parentPath = currentPath;
-                    }
-                    return;
-                } else {
-                    nodeIterators.removeLast();
-                    parentPath = PathUtils.getParentPath(parentPath);
-                }
-            }
-            currentPath = null;
-            closed = true;
-        }
-
-    }
-    
-    /**
-     * A cursor that intersects two cursors.
-     */
-    private static class IntersectionCursor extends AbstractCursor {
-
-        private final HashMap<String, IndexRow> secondSet = new HashMap<String, IndexRow>();
-        private final HashSet<String> seen = new HashSet<String>();
-        private final Cursor first, second;
-        private final QueryLimits settings;
-        private boolean init;
-        private boolean closed;
-        private IndexRow current;
-        
-        IntersectionCursor(Cursor first, Cursor second, QueryLimits settings) {
-            this.first = first;
-            this.second = second;
-            this.settings = settings;
-        }
-        
-        @Override
-        public IndexRow next() {
-            if (closed) {
-                throw new IllegalStateException("This cursor is closed");
-            }
-            if (!init) {
-                fetchNext();
-                init = true;
-                if (closed) {
-                    throw new IllegalStateException("This cursor is closed");
-                }
-            }
-            IndexRow result = current;
-            // fetchNext();
-            init = false;
-            return result;
-        }
-
-        @Override
-        public boolean hasNext() {
-            if (!closed && !init) {
-                fetchNext();
-                init = true;
-            }
-            return !closed;
-        }
-        
-        private void fetchNext() {
-            while (true) {
-                if (!first.hasNext()) {
-                    closed = true;
-                    return;
-                }
-                IndexRow c = first.next();
-                String p = c.getPath();
-                if (seen.contains(p)) {
-                    continue;
-                }
-                if (secondSet.remove(p) != null) {
-                    current = c;
-                    markSeen(p);
-                    return;
-                }
-                while (second.hasNext()) {
-                    IndexRow s = second.next();
-                    String p2 = s.getPath();
-                    if (p.equals(p2)) {
-                        current = c;
-                        markSeen(p);
-                        return;
-                    }
-                    secondSet.put(p2, s);
-                    FilterIterators.checkMemoryLimit(secondSet.size(), settings);
-                }
-            }
-        }
-        
-        private void markSeen(String path) {
-            seen.add(path);
-            FilterIterators.checkMemoryLimit(seen.size(), settings);
-        }
-        
-        @Override
-        public long getSize(SizePrecision precision, long max) {
-            // this is the worst case
-            long a = first.getSize(precision, max);
-            long b = second.getSize(precision, max);
-            if (a < 0 || b < 0) {
-                return -1;
-            }
-            return QueryImpl.saturatedAdd(a, b);
-        }
-        
-    }
-
-    /**
-     * A cursor that combines multiple cursors into a single cursor.
-     */
-    private static class ConcatCursor extends AbstractCursor {
-
-        private final HashSet<String> seen = new HashSet<String>();
-        private final List<Cursor> cursors;
-        private final QueryLimits settings;
-        private boolean init;
-        private boolean closed;
-
-        private Cursor currentCursor;
-        private int cursorListIndex;
-        private IndexRow current;
-
-        ConcatCursor(List<Cursor> cursors, QueryLimits settings) {
-            this.cursors = cursors;
-            this.settings = settings;
-            nextCursor();
-        }
-        
-        private void nextCursor() {
-            if (cursorListIndex >= cursors.size()) {
-                init = true;
-                closed = true;
-            } else {
-                currentCursor = cursors.get(cursorListIndex++);
-            }
-        }
-
-        @Override
-        public IndexRow next() {
-            if (closed) {
-                throw new IllegalStateException("This cursor is closed");
-            }
-            if (!init) {
-                fetchNext();
-                init = true;
-            }
-            IndexRow result = current;
-            init = false;
-            return result;
-        }
-
-        @Override
-        public boolean hasNext() {
-            if (!closed && !init) {
-                fetchNext();
-                init = true;
-            }
-            return !closed;
-        }
-
-        private void fetchNext() {
-            while (true) {
-                while (!currentCursor.hasNext()) {
-                    nextCursor();
-                    if (closed) {
-                        return;
-                    }
-                }
-                IndexRow c = currentCursor.next();
-                String p = c.getPath();
-                if (seen.contains(p)) {
-                    continue;
-                }
-                current = c;
-                markSeen(p);
-                return;
-            }
-        }
-
-        private void markSeen(String path) {
-            seen.add(path);
-            FilterIterators.checkMemoryLimit(seen.size(), settings);
-        }
-        
-        @Override
-        public long getSize(SizePrecision precision, long max) {
-            // this is the worst case (duplicate entries are counted twice)
-            long total = 0;
-            for (Cursor c : cursors) {
-                long t = c.getSize(precision, max);
-                if (t < 0) {
-                    return -1;
-                }
-                total = QueryImpl.saturatedAdd(total, t);
-            }
-            return total;
-        }
-
-    }
-
-    /**
-     * A cursor that prefetches nodes from the node store
-     */
-    private static class PrefetchCursor extends AbstractCursor {
-
-        private final Cursor cursor;
-        private final PrefetchNodeStore store;
-        private final int prefetchCount;
-        private final NodeState rootState;
-        private Iterator<IndexRow> prefetched;
-        private final List<String> prefetchRelative;
-
-        PrefetchCursor(Cursor cursor, PrefetchNodeStore store, int prefetchCount, NodeState rootState, List<String> prefetchRelative) {
-            this.cursor = cursor;
-            this.store = store;
-            this.prefetchCount = prefetchCount;
-            this.rootState = rootState;
-            this.prefetched = Iterators.emptyIterator();
-            this.prefetchRelative = prefetchRelative;
-        }
-
-        @Override
-        public IndexRow next() {
-            if (!prefetched.hasNext()) {
-                ArrayList<IndexRow> rows = new ArrayList<>();
-                TreeSet<String> paths = new TreeSet<>();
-                for (int i = 0; i < prefetchCount && cursor.hasNext(); i++) {
-                    IndexRow row = cursor.next();
-                    rows.add(row);
-                    String p = row.getPath();
-                    prefetchRelative(paths, p);
-                    do {
-                        paths.add(p);
-                        p = PathUtils.getParentPath(p);
-                    } while (!PathUtils.denotesRoot(p));
-                }
-                store.prefetch(paths, rootState);
-                prefetched = rows.iterator();
-            }
-            return prefetched.next();
-        }
-
-        private void prefetchRelative(Set<String> target, String p) {
-            for (String r : prefetchRelative) {
-                String concat = PathUtils.concat(p, r);
-                target.add(concat);
-            }
-        }
-
-        @Override
-        public boolean hasNext() {
-            return prefetched.hasNext() || cursor.hasNext();
-        }
-    }
-
-}
diff --git a/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/index/aggregate/AggregateIndex.java b/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/index/aggregate/AggregateIndex.java
index d14a7b74be..a725cd68a4 100644
--- a/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/index/aggregate/AggregateIndex.java
+++ b/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/index/aggregate/AggregateIndex.java
@@ -27,9 +27,9 @@ 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.jackrabbit.oak.plugins.index.cursor.Cursors;
 import org.apache.jackrabbit.oak.query.index.FilterImpl;
 import org.apache.jackrabbit.oak.spi.query.Cursor;
-import org.apache.jackrabbit.oak.plugins.index.Cursors;
 import org.apache.jackrabbit.oak.spi.query.Filter;
 import org.apache.jackrabbit.oak.spi.state.NodeState;
 import org.slf4j.Logger;
diff --git a/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/index/aggregate/AggregationCursor.java b/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/index/aggregate/AggregationCursor.java
index 1862ed9cb0..87065a9d64 100644
--- a/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/index/aggregate/AggregationCursor.java
+++ b/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/index/aggregate/AggregationCursor.java
@@ -23,9 +23,9 @@ import java.util.Set;
 
 import org.apache.jackrabbit.oak.api.PropertyValue;
 import org.apache.jackrabbit.oak.api.Result.SizePrecision;
+import org.apache.jackrabbit.oak.plugins.index.cursor.AbstractCursor;
 import org.apache.jackrabbit.oak.spi.query.Cursor;
 import org.apache.jackrabbit.oak.spi.query.IndexRow;
-import org.apache.jackrabbit.oak.plugins.index.Cursors.AbstractCursor;
 import org.apache.jackrabbit.oak.spi.query.QueryIndex;
 import org.apache.jackrabbit.oak.spi.state.NodeState;
 
diff --git a/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/index/cursor/AbstractCursor.java b/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/index/cursor/AbstractCursor.java
new file mode 100644
index 0000000000..ed31f9de2c
--- /dev/null
+++ b/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/index/cursor/AbstractCursor.java
@@ -0,0 +1,38 @@
+/*
+ * 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.cursor;
+
+import org.apache.jackrabbit.oak.api.Result.SizePrecision;
+import org.apache.jackrabbit.oak.spi.query.Cursor;
+
+/**
+ * A Cursor implementation where the remove method throws an
+ * UnsupportedOperationException.
+ */
+public abstract class AbstractCursor implements Cursor {
+    
+    @Override
+    public void remove() {
+        throw new UnsupportedOperationException();
+    }
+    
+    @Override
+    public long getSize(SizePrecision precision, long max) {
+        return -1;
+    }
+
+}
\ No newline at end of file
diff --git a/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/index/cursor/AncestorCursor.java b/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/index/cursor/AncestorCursor.java
new file mode 100644
index 0000000000..1cee1ca256
--- /dev/null
+++ b/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/index/cursor/AncestorCursor.java
@@ -0,0 +1,62 @@
+/*
+ * 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.cursor;
+
+import java.util.Iterator;
+
+import org.apache.jackrabbit.oak.commons.PathUtils;
+import org.apache.jackrabbit.oak.spi.query.Cursor;
+import org.apache.jackrabbit.oak.spi.query.IndexRow;
+import org.apache.jackrabbit.oak.spi.query.QueryLimits;
+import org.jetbrains.annotations.Nullable;
+
+import com.google.common.base.Function;
+import com.google.common.base.Predicate;
+import com.google.common.collect.Iterators;
+
+/**
+ * This class allows to iterate over the parent nodes of the wrapped cursor.
+ */
+class AncestorCursor extends PathCursor {
+
+    public AncestorCursor(Cursor cursor, int level, QueryLimits settings) {
+        super(transform(cursor, level), true, settings);
+    }
+
+    private static Iterator<String> transform(Cursor cursor, final int level) {
+        Iterator<String> unfiltered = Iterators.transform(cursor,
+                new Function<IndexRow, String>() {
+            @Override
+            public String apply(@Nullable IndexRow input) {
+                return input != null ? input.getPath() : null;
+            }
+        });
+        Iterator<String> filtered = Iterators.filter(unfiltered,
+                new Predicate<String>() {
+            @Override
+            public boolean apply(@Nullable String input) {
+                return input != null && PathUtils.getDepth(input) >= level;
+            }
+        });
+        return Iterators.transform(filtered, new Function<String, String>() {
+            @Override
+            public String apply(String input) {
+                return PathUtils.getAncestorPath(input, level);
+            }
+        });
+    }
+}
\ No newline at end of file
diff --git a/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/index/cursor/ConcatCursor.java b/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/index/cursor/ConcatCursor.java
new file mode 100644
index 0000000000..c20494fde8
--- /dev/null
+++ b/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/index/cursor/ConcatCursor.java
@@ -0,0 +1,120 @@
+/*
+ * 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.cursor;
+
+import java.util.HashSet;
+import java.util.List;
+
+import org.apache.jackrabbit.oak.api.Result.SizePrecision;
+import org.apache.jackrabbit.oak.query.FilterIterators;
+import org.apache.jackrabbit.oak.query.QueryImpl;
+import org.apache.jackrabbit.oak.spi.query.Cursor;
+import org.apache.jackrabbit.oak.spi.query.IndexRow;
+import org.apache.jackrabbit.oak.spi.query.QueryLimits;
+
+/**
+ * A cursor that combines multiple cursors into a single cursor.
+ */
+class ConcatCursor extends AbstractCursor {
+
+    private final HashSet<String> seen = new HashSet<String>();
+    private final List<Cursor> cursors;
+    private final QueryLimits settings;
+    private boolean init;
+    private boolean closed;
+
+    private Cursor currentCursor;
+    private int cursorListIndex;
+    private IndexRow current;
+
+    ConcatCursor(List<Cursor> cursors, QueryLimits settings) {
+        this.cursors = cursors;
+        this.settings = settings;
+        nextCursor();
+    }
+    
+    private void nextCursor() {
+        if (cursorListIndex >= cursors.size()) {
+            init = true;
+            closed = true;
+        } else {
+            currentCursor = cursors.get(cursorListIndex++);
+        }
+    }
+
+    @Override
+    public IndexRow next() {
+        if (closed) {
+            throw new IllegalStateException("This cursor is closed");
+        }
+        if (!init) {
+            fetchNext();
+            init = true;
+        }
+        IndexRow result = current;
+        init = false;
+        return result;
+    }
+
+    @Override
+    public boolean hasNext() {
+        if (!closed && !init) {
+            fetchNext();
+            init = true;
+        }
+        return !closed;
+    }
+
+    private void fetchNext() {
+        while (true) {
+            while (!currentCursor.hasNext()) {
+                nextCursor();
+                if (closed) {
+                    return;
+                }
+            }
+            IndexRow c = currentCursor.next();
+            String p = c.getPath();
+            if (seen.contains(p)) {
+                continue;
+            }
+            current = c;
+            markSeen(p);
+            return;
+        }
+    }
+
+    private void markSeen(String path) {
+        seen.add(path);
+        FilterIterators.checkMemoryLimit(seen.size(), settings);
+    }
+    
+    @Override
+    public long getSize(SizePrecision precision, long max) {
+        // this is the worst case (duplicate entries are counted twice)
+        long total = 0;
+        for (Cursor c : cursors) {
+            long t = c.getSize(precision, max);
+            if (t < 0) {
+                return -1;
+            }
+            total = QueryImpl.saturatedAdd(total, t);
+        }
+        return total;
+    }
+
+}
\ No newline at end of file
diff --git a/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/index/cursor/Cursors.java b/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/index/cursor/Cursors.java
new file mode 100644
index 0000000000..62ad15a3fc
--- /dev/null
+++ b/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/index/cursor/Cursors.java
@@ -0,0 +1,113 @@
+/*
+ * 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.cursor;
+
+import java.util.List;
+
+import org.apache.jackrabbit.oak.query.FilterIterators;
+import org.apache.jackrabbit.oak.spi.query.Cursor;
+import org.apache.jackrabbit.oak.spi.query.Filter;
+import org.apache.jackrabbit.oak.spi.query.QueryLimits;
+import org.apache.jackrabbit.oak.spi.state.NodeState;
+import org.apache.jackrabbit.oak.spi.state.PrefetchNodeStore;
+
+import static com.google.common.base.Preconditions.checkArgument;
+import static com.google.common.base.Preconditions.checkNotNull;
+
+/**
+ * This utility class provides factory methods to create commonly used types of
+ * {@link Cursor}s.
+ */
+public class Cursors {
+
+    private Cursors() {
+    }
+    
+    public static void checkMemoryLimit(long count, QueryLimits settings) {
+        FilterIterators.checkMemoryLimit(count, settings);
+    }    
+
+    public static void checkReadLimit(long count, QueryLimits settings) {
+        FilterIterators.checkReadLimit(count, settings);
+    }    
+
+    public static Cursor newIntersectionCursor(Cursor a, Cursor b, QueryLimits settings) {
+        return new IntersectionCursor(a, b, settings);
+    }
+
+    public static Cursor newConcatCursor(List<Cursor> cursors, QueryLimits settings) {
+        return new ConcatCursor(cursors, settings);
+    }
+
+    public static Cursor newPrefetchCursor(Cursor cursor, PrefetchNodeStore store, int prefetchCount,
+            NodeState rootState, List<String> prefetchRelative) {
+        return new PrefetchCursor(cursor, store, prefetchCount, rootState, prefetchRelative);
+    }
+
+    /**
+     * Creates a {@link Cursor} over paths.
+     *
+     * @param paths the paths to iterate over (must return distinct paths)
+     * @return the Cursor.
+     */
+    public static Cursor newPathCursor(Iterable<String> paths, QueryLimits settings) {
+        return new PathCursor(paths.iterator(), true, settings);
+    }
+
+    /**
+     * Creates a {@link Cursor} over paths, and make the result distinct.
+     * The iterator might return duplicate paths
+     * 
+     * @param paths the paths to iterate over (might contain duplicate entries)
+     * @return the Cursor.
+     */
+    public static Cursor newPathCursorDistinct(Iterable<String> paths, QueryLimits settings) {
+        return new PathCursor(paths.iterator(), true, settings);
+    }
+
+    /**
+     * Returns a traversing cursor based on the path restriction in the given
+     * {@link Filter}.
+     * 
+     * @param filter the filter.
+     * @param rootState the root {@link NodeState}.
+     * @return the {@link Cursor}.
+     */
+    public static Cursor newTraversingCursor(Filter filter,
+                                             NodeState rootState) {
+        return new TraversingCursor(filter, rootState);
+    }
+
+    /**
+     * Returns a cursor wrapper, which returns the ancestor rows at the given
+     * <code>level</code> of the wrapped cursor <code>c</code>. With
+     * <code>level</code> e.g. set to <code>1</code>, the returned cursor
+     * iterates over the parent rows of the passed cursor <code>c</code>. The
+     * returned cursor guarantees distinct rows.
+     *
+     * @param c the cursor to wrap.
+     * @param level the ancestor level. Must be {@code >= 1}.
+     * @return cursor over the ancestors of <code>c</code> at <code>level</code>.
+     */
+    public static Cursor newAncestorCursor(Cursor c, int level, QueryLimits settings) {
+        checkNotNull(c);
+        checkArgument(level >= 1);
+        return new AncestorCursor(c, level, settings);
+    }
+
+
+}
diff --git a/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/index/cursor/IntersectionCursor.java b/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/index/cursor/IntersectionCursor.java
new file mode 100644
index 0000000000..41a2b373e5
--- /dev/null
+++ b/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/index/cursor/IntersectionCursor.java
@@ -0,0 +1,121 @@
+/*
+ * 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.cursor;
+
+import java.util.HashMap;
+import java.util.HashSet;
+
+import org.apache.jackrabbit.oak.api.Result.SizePrecision;
+import org.apache.jackrabbit.oak.query.FilterIterators;
+import org.apache.jackrabbit.oak.query.QueryImpl;
+import org.apache.jackrabbit.oak.spi.query.Cursor;
+import org.apache.jackrabbit.oak.spi.query.IndexRow;
+import org.apache.jackrabbit.oak.spi.query.QueryLimits;
+
+/**
+ * A cursor that intersects two cursors.
+ */
+class IntersectionCursor extends AbstractCursor {
+
+    private final HashMap<String, IndexRow> secondSet = new HashMap<String, IndexRow>();
+    private final HashSet<String> seen = new HashSet<String>();
+    private final Cursor first, second;
+    private final QueryLimits settings;
+    private boolean init;
+    private boolean closed;
+    private IndexRow current;
+    
+    IntersectionCursor(Cursor first, Cursor second, QueryLimits settings) {
+        this.first = first;
+        this.second = second;
+        this.settings = settings;
+    }
+    
+    @Override
+    public IndexRow next() {
+        if (closed) {
+            throw new IllegalStateException("This cursor is closed");
+        }
+        if (!init) {
+            fetchNext();
+            init = true;
+            if (closed) {
+                throw new IllegalStateException("This cursor is closed");
+            }
+        }
+        IndexRow result = current;
+        // fetchNext();
+        init = false;
+        return result;
+    }
+
+    @Override
+    public boolean hasNext() {
+        if (!closed && !init) {
+            fetchNext();
+            init = true;
+        }
+        return !closed;
+    }
+    
+    private void fetchNext() {
+        while (true) {
+            if (!first.hasNext()) {
+                closed = true;
+                return;
+            }
+            IndexRow c = first.next();
+            String p = c.getPath();
+            if (seen.contains(p)) {
+                continue;
+            }
+            if (secondSet.remove(p) != null) {
+                current = c;
+                markSeen(p);
+                return;
+            }
+            while (second.hasNext()) {
+                IndexRow s = second.next();
+                String p2 = s.getPath();
+                if (p.equals(p2)) {
+                    current = c;
+                    markSeen(p);
+                    return;
+                }
+                secondSet.put(p2, s);
+                FilterIterators.checkMemoryLimit(secondSet.size(), settings);
+            }
+        }
+    }
+    
+    private void markSeen(String path) {
+        seen.add(path);
+        FilterIterators.checkMemoryLimit(seen.size(), settings);
+    }
+    
+    @Override
+    public long getSize(SizePrecision precision, long max) {
+        // this is the worst case
+        long a = first.getSize(precision, max);
+        long b = second.getSize(precision, max);
+        if (a < 0 || b < 0) {
+            return -1;
+        }
+        return QueryImpl.saturatedAdd(a, b);
+    }
+    
+}
\ No newline at end of file
diff --git a/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/index/cursor/PathCursor.java b/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/index/cursor/PathCursor.java
new file mode 100644
index 0000000000..b071abb1ea
--- /dev/null
+++ b/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/index/cursor/PathCursor.java
@@ -0,0 +1,73 @@
+/*
+ * 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.cursor;
+
+import static org.apache.jackrabbit.oak.commons.PathUtils.isAbsolute;
+
+import java.util.HashSet;
+import java.util.Iterator;
+
+import org.apache.jackrabbit.oak.query.FilterIterators;
+import org.apache.jackrabbit.oak.query.index.IndexRowImpl;
+import org.apache.jackrabbit.oak.spi.query.Cursor;
+import org.apache.jackrabbit.oak.spi.query.IndexRow;
+import org.apache.jackrabbit.oak.spi.query.QueryLimits;
+import org.jetbrains.annotations.Nullable;
+
+import com.google.common.base.Predicate;
+import com.google.common.collect.Iterators;
+
+/**
+ * <code>PathCursor</code> implements a simple {@link Cursor} that iterates
+ * over a {@link String} based path {@link Iterable}.
+ */
+public class PathCursor extends AbstractCursor {
+
+    private final Iterator<String> iterator;
+
+    public PathCursor(Iterator<String> paths, boolean distinct, final QueryLimits settings) {
+        Iterator<String> it = paths;
+        if (distinct) {
+            it = Iterators.filter(it, new Predicate<String>() {
+                
+                private final HashSet<String> known = new HashSet<String>();
+
+                @Override
+                public boolean apply(@Nullable String input) {
+                    FilterIterators.checkMemoryLimit(known.size(), settings);
+                    // Set.add returns true for new entries
+                    return known.add(input);
+                }
+                
+            });
+        }
+        this.iterator = it;
+    }
+
+    @Override
+    public IndexRow next() {
+        // TODO support jcr:score and possibly rep:excerpt
+        String path = iterator.next();
+        return new IndexRowImpl(isAbsolute(path) ? path : "/" + path);
+    }
+    
+    @Override
+    public boolean hasNext() {
+        return iterator.hasNext();
+    }
+    
+}
\ No newline at end of file
diff --git a/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/index/cursor/PrefetchCursor.java b/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/index/cursor/PrefetchCursor.java
new file mode 100644
index 0000000000..2f5d77ef54
--- /dev/null
+++ b/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/index/cursor/PrefetchCursor.java
@@ -0,0 +1,203 @@
+/*
+ * 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.cursor;
+
+import java.util.ArrayList;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Set;
+import java.util.TreeSet;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+import java.util.regex.PatternSyntaxException;
+
+import org.apache.jackrabbit.oak.commons.PathUtils;
+import org.apache.jackrabbit.oak.spi.query.Cursor;
+import org.apache.jackrabbit.oak.spi.query.IndexRow;
+import org.apache.jackrabbit.oak.spi.state.NodeState;
+import org.apache.jackrabbit.oak.spi.state.PrefetchNodeStore;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import com.google.common.collect.Iterators;
+
+/**
+ * A cursor that is able to prefetch nodes from the node store.
+ */
+public class PrefetchCursor extends AbstractCursor {
+    
+    private static final Logger LOG = LoggerFactory.getLogger(PrefetchCursor.class);
+
+    // match ${...} -- unfortunately it needs a lot of escaping. 
+    // } inside {...} need to be escaped: ${...\}...} 
+    private static final Pattern FUNCTION = Pattern.compile("\\$\\{((\\}|[^}])*)\\}");
+
+    private final Cursor cursor;
+    private final PrefetchNodeStore store;
+    private final int prefetchCount;
+    private final NodeState rootState;
+    private Iterator<IndexRow> prefetched;
+    private final List<String> prefetchRelative;
+
+    PrefetchCursor(Cursor cursor, PrefetchNodeStore store, int prefetchCount, NodeState rootState, List<String> prefetchRelative) {
+        this.cursor = cursor;
+        this.store = store;
+        this.prefetchCount = prefetchCount;
+        this.rootState = rootState;
+        this.prefetched = Iterators.emptyIterator();
+        this.prefetchRelative = prefetchRelative;
+    }
+
+    @Override
+    public IndexRow next() {
+        if (!prefetched.hasNext()) {
+            ArrayList<IndexRow> rows = new ArrayList<>();
+            TreeSet<String> paths = new TreeSet<>();
+            for (int i = 0; i < prefetchCount && cursor.hasNext(); i++) {
+                IndexRow row = cursor.next();
+                rows.add(row);
+                if (row.isVirtualRow()) {
+                    continue;
+                }
+                String p = row.getPath();
+                if (!PathUtils.isAbsolute(p)) {
+                    LOG.warn("Unexpected relative path {}", p);
+                    continue;
+                }
+                prefetchRelative(paths, p);
+                do {
+                    paths.add(p);
+                    p = PathUtils.getParentPath(p);
+                } while (!PathUtils.denotesRoot(p));
+            }
+            store.prefetch(paths, rootState);
+            prefetched = rows.iterator();
+        }
+        return prefetched.next();
+    }
+
+    @Override
+    public boolean hasNext() {
+        return prefetched.hasNext() || cursor.hasNext();
+    }
+
+    private void prefetchRelative(Set<String> target, String p) {
+        try {
+            for (String r : prefetchRelative) {
+                String result = resolve(r, p);
+                if (result != null) {
+                    target.add(result);
+                }
+            }
+        } catch (IllegalArgumentException e) {
+            // ignore
+        }
+    }
+    
+    /**
+     * Try to compute an absolute path using a relative path specification.
+     * 
+     * @param relativePath a relative path that may include patterns, e.g. jcr:content/metadata,
+     *              or jcr:content/renditions/${regex(".+/(.*)\\..*")}
+     * @param path the input path, e.g. /content/images/abc
+     * @return null if not a match, or the output path, e.g.
+     *         /content/images/abc.png/jcr:content/renditions/abc
+     * @throws IllegalArgumentException if an error occurs
+     */
+    public static String resolve(String relativePath, String path) throws IllegalArgumentException {
+        String resolved = applyAndResolvePatterns(relativePath, path);
+        if (resolved == null) {
+            return null;
+        }
+        if (PathUtils.isAbsolute(resolved)) {
+            return resolved;
+        }
+        return PathUtils.concat(path, resolved);
+    }
+    
+    /**
+     * Compute the new relative path from a input path and a relative path (which
+     * may include patterns).
+     *
+     * @param relativePath the value that may include patterns, e.g.
+     *                     jcr:content/metadata, or
+     *                     jcr:content/renditions/${regex(".+/(.*)\\..*")}
+     * @param path the input path, e.g. /content/images/abc
+     * @return null if not a match, or the output (relative) path, e.g.
+     *         jcr:content/renditions/abc
+     * @throws IllegalArgumentException if an error occurs
+     */
+    public static String applyAndResolvePatterns(String relativePath, String path) 
+            throws IllegalArgumentException {
+        Matcher m = FUNCTION.matcher(relativePath);
+        // appendReplacement with StringBuilder is not supported in Java 8
+        StringBuffer buff = new StringBuffer();
+        // iterate over all the "${...}"
+        try {
+            while (m.find()) {
+                String f = m.group(1);
+                // un-escape: \} => }
+                f = f.replaceAll("\\\\}", "}");
+                // we only support regex("...") currently
+                if (f.startsWith("regex(\"") && f.endsWith("\")")) {
+                    String regexPattern = f.substring("regex(\"".length(), f.length() - 2);
+                    // un-escape: \" => "
+                    regexPattern = regexPattern.replaceAll("\\\"", "\"");
+                    String x = findMatch(path, regexPattern);
+                    if (x == null) {
+                        // not a match
+                        return null;
+                    }
+                    m.appendReplacement(buff, x);
+                } else {
+                    // don't know how to replace - don't
+                    // (we could also throw an exception)
+                    break;
+                }
+            }
+        } catch (IllegalStateException | IndexOutOfBoundsException e) {
+            LOG.warn("Can not replace {} match in {}", relativePath, path, e);
+            throw new IllegalArgumentException("Can not replace");
+        }            
+        m.appendTail(buff);
+        return buff.toString();
+    }
+
+    /**
+     * Find a pattern in a given path.
+     * 
+     * @param path the absolute path, e.g. "/content/images/hello.jpg"
+     * @param regexPattern the pattern, e.g. ".+/(.*).jpg"
+     * @return null if not found, or the matched group, e.g. "hello"
+     * @throws IllegalArgumentException if an error occurs
+     */
+    public static String findMatch(String path, String regexPattern) 
+            throws IllegalArgumentException {
+        try {
+            Pattern p = Pattern.compile(regexPattern);
+            Matcher m = p.matcher(path);
+            if (!m.find()) {
+                return null;
+            }
+            return m.group(1);
+        } catch (PatternSyntaxException | IllegalStateException | IndexOutOfBoundsException e) {
+            LOG.warn("Can not find match in {} pattern {}", path, regexPattern, e);
+            throw new IllegalArgumentException("Can not find match", e);
+        }
+    }
+
+}
diff --git a/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/index/cursor/TraversingCursor.java b/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/index/cursor/TraversingCursor.java
new file mode 100644
index 0000000000..0432a762aa
--- /dev/null
+++ b/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/index/cursor/TraversingCursor.java
@@ -0,0 +1,192 @@
+/*
+ * 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.cursor;
+
+import static org.apache.jackrabbit.oak.spi.query.QueryConstants.REP_FACET;
+
+import java.util.Deque;
+import java.util.Iterator;
+
+import org.apache.jackrabbit.oak.commons.PathUtils;
+import org.apache.jackrabbit.oak.plugins.index.IndexUtils;
+import org.apache.jackrabbit.oak.plugins.memory.MemoryChildNodeEntry;
+import org.apache.jackrabbit.oak.query.FilterIterators;
+import org.apache.jackrabbit.oak.query.index.IndexRowImpl;
+import org.apache.jackrabbit.oak.spi.query.Filter;
+import org.apache.jackrabbit.oak.spi.query.IndexRow;
+import org.apache.jackrabbit.oak.spi.query.QueryLimits;
+import org.apache.jackrabbit.oak.spi.query.Filter.PathRestriction;
+import org.apache.jackrabbit.oak.spi.state.ChildNodeEntry;
+import org.apache.jackrabbit.oak.spi.state.NodeState;
+import org.apache.jackrabbit.oak.spi.state.NodeStateUtils;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import com.google.common.collect.Iterators;
+import com.google.common.collect.Queues;
+
+/**
+ * A cursor that reads all nodes in a given subtree.
+ */
+class TraversingCursor extends AbstractCursor {
+
+    private static final Logger LOG = LoggerFactory.getLogger(TraversingCursor.class);
+
+    private final Filter filter;
+
+    private final Deque<Iterator<? extends ChildNodeEntry>> nodeIterators =
+            Queues.newArrayDeque();
+
+    private String parentPath;
+
+    private String currentPath;
+
+    private long readCount;
+
+    private boolean init;
+    
+    private boolean closed;
+    
+    private final QueryLimits settings;
+    
+    public TraversingCursor(Filter filter, NodeState rootState) {
+        this.filter = filter;
+        this.settings = filter.getQueryLimits();
+
+        String path = filter.getPath();
+        parentPath = null;
+        currentPath = "/";
+        NodeState parent = null;
+        NodeState node = rootState;
+        
+        if (filter.containsNativeConstraint()) {
+            // OAK-4313: if no other index was found,
+            // then, for native queries, we won't match anything
+            return;
+        }
+
+        if (filter.isAlwaysFalse()) {
+            // nothing can match this filter, leave nodes empty
+            return;
+        }
+
+        Filter.PropertyRestriction facetRestriction = filter.getPropertyRestriction(REP_FACET);
+        if (facetRestriction != null) {
+            // we don't evaluate facets by traversal
+            throw new IllegalArgumentException(facetRestriction + " can't be evaluated by traversal");
+        }
+
+        if (!path.equals("/")) {
+            for (String name : path.substring(1).split("/")) {
+                parentPath = currentPath;
+                currentPath = PathUtils.concat(parentPath, name);
+
+                parent = node;
+                node = parent.getChildNode(name);
+            }
+            if (!node.exists()) {
+                // nothing can match this filter, leave nodes empty
+                return;
+            }
+        }
+        Filter.PathRestriction restriction = filter.getPathRestriction();
+        switch (restriction) {
+        case NO_RESTRICTION:
+        case EXACT:
+        case ALL_CHILDREN:
+            nodeIterators.add(Iterators.singletonIterator(
+                    new MemoryChildNodeEntry(currentPath, node)));
+            parentPath = "";
+            break;
+        case PARENT:
+            if (parent != null) {
+                nodeIterators.add(Iterators.singletonIterator(
+                        new MemoryChildNodeEntry(parentPath, parent)));
+                parentPath = "";
+            }
+            break;
+        case DIRECT_CHILDREN:
+            nodeIterators.add(node.getChildNodeEntries().iterator());
+            parentPath = currentPath;
+            break;
+        default:
+            throw new IllegalArgumentException("Unknown restriction: " + restriction);
+        }
+    }
+
+    @Override
+    public IndexRow next() {
+        if (closed) {
+            throw new IllegalStateException("This cursor is closed");
+        }
+        if (!init) {
+            fetchNext();
+            init = true;
+        }
+        IndexRowImpl result = new IndexRowImpl(currentPath);
+        fetchNext();
+        return result;
+    }
+    
+    @Override 
+    public boolean hasNext() {
+        if (!closed && !init) {
+            fetchNext();
+            init = true;
+        }
+        return !closed;
+    }
+
+    private void fetchNext() {
+        while (!nodeIterators.isEmpty()) {
+            Iterator<? extends ChildNodeEntry> iterator = nodeIterators.getLast();
+            if (iterator.hasNext()) {
+                ChildNodeEntry entry = iterator.next();
+
+                readCount++;
+                if (readCount % 1000 == 0) {
+                    FilterIterators.checkReadLimit(readCount, settings);
+                    String caller = IndexUtils.getCaller(this.settings.getIgnoredClassNamesInCallTrace());
+                    LOG.warn("Traversed {} nodes with filter {} called by {}; consider creating an index or changing the query" , 
+                            readCount, filter, caller);
+                }
+
+                NodeState node = entry.getNodeState();
+
+                String name = entry.getName();
+                if (NodeStateUtils.isHidden(name)) {
+                    continue;
+                }
+                currentPath = PathUtils.concat(parentPath, name);
+
+                PathRestriction r = filter.getPathRestriction();
+                if (r == PathRestriction.ALL_CHILDREN || 
+                        r == PathRestriction.NO_RESTRICTION) {
+                    nodeIterators.addLast(node.getChildNodeEntries().iterator());
+                    parentPath = currentPath;
+                }
+                return;
+            } else {
+                nodeIterators.removeLast();
+                parentPath = PathUtils.getParentPath(parentPath);
+            }
+        }
+        currentPath = null;
+        closed = true;
+    }
+
+}
\ No newline at end of file
diff --git a/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/index/diffindex/DiffIndex.java b/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/index/diffindex/DiffIndex.java
index de271248fb..fb7bb9856a 100644
--- a/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/index/diffindex/DiffIndex.java
+++ b/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/index/diffindex/DiffIndex.java
@@ -16,8 +16,8 @@
  */
 package org.apache.jackrabbit.oak.plugins.index.diffindex;
 
+import org.apache.jackrabbit.oak.plugins.index.cursor.Cursors;
 import org.apache.jackrabbit.oak.spi.query.Cursor;
-import org.apache.jackrabbit.oak.plugins.index.Cursors;
 import org.apache.jackrabbit.oak.spi.query.Filter;
 import org.apache.jackrabbit.oak.spi.query.QueryIndex;
 import org.apache.jackrabbit.oak.spi.state.NodeState;
diff --git a/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/index/nodetype/NodeTypeIndex.java b/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/index/nodetype/NodeTypeIndex.java
index 1997f0a6f2..672158d620 100644
--- a/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/index/nodetype/NodeTypeIndex.java
+++ b/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/index/nodetype/NodeTypeIndex.java
@@ -19,12 +19,11 @@
 package org.apache.jackrabbit.oak.plugins.index.nodetype;
 
 import org.apache.jackrabbit.oak.plugins.index.IndexConstants;
-
+import org.apache.jackrabbit.oak.plugins.index.cursor.Cursors;
 import org.apache.jackrabbit.JcrConstants;
 import org.apache.jackrabbit.oak.spi.mount.MountInfoProvider;
 import org.apache.jackrabbit.oak.spi.query.Cursor;
 import org.apache.jackrabbit.oak.api.Type;
-import org.apache.jackrabbit.oak.plugins.index.Cursors;
 import org.apache.jackrabbit.oak.spi.query.Filter;
 import org.apache.jackrabbit.oak.spi.query.QueryIndex;
 import org.apache.jackrabbit.oak.spi.query.Filter.PropertyRestriction;
diff --git a/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/index/property/PropertyIndexPlan.java b/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/index/property/PropertyIndexPlan.java
index 10664bd3a5..a00347b88e 100644
--- a/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/index/property/PropertyIndexPlan.java
+++ b/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/index/property/PropertyIndexPlan.java
@@ -28,9 +28,9 @@ import java.util.List;
 import java.util.Set;
 
 import org.apache.jackrabbit.oak.commons.PathUtils;
-import org.apache.jackrabbit.oak.plugins.index.Cursors;
 import org.apache.jackrabbit.oak.plugins.index.IndexConstants;
 import org.apache.jackrabbit.oak.plugins.index.IndexUtils;
+import org.apache.jackrabbit.oak.plugins.index.cursor.Cursors;
 import org.apache.jackrabbit.oak.plugins.index.property.strategy.IndexStoreStrategy;
 import org.apache.jackrabbit.oak.spi.filter.PathFilter;
 import org.apache.jackrabbit.oak.spi.mount.MountInfoProvider;
diff --git a/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/index/reference/ReferenceIndex.java b/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/index/reference/ReferenceIndex.java
index f00b19955f..864f524df1 100644
--- a/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/index/reference/ReferenceIndex.java
+++ b/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/index/reference/ReferenceIndex.java
@@ -25,10 +25,10 @@ import static org.apache.jackrabbit.oak.api.Type.STRING;
 import static org.apache.jackrabbit.oak.commons.PathUtils.getName;
 import static org.apache.jackrabbit.oak.commons.PathUtils.getParentPath;
 import static org.apache.jackrabbit.oak.plugins.index.IndexConstants.INDEX_DEFINITIONS_NAME;
+import static org.apache.jackrabbit.oak.plugins.index.cursor.Cursors.newPathCursor;
 import static org.apache.jackrabbit.oak.plugins.index.reference.NodeReferenceConstants.NAME;
 import static org.apache.jackrabbit.oak.plugins.index.reference.NodeReferenceConstants.REF_NAME;
 import static org.apache.jackrabbit.oak.plugins.index.reference.NodeReferenceConstants.WEAK_REF_NAME;
-import static org.apache.jackrabbit.oak.plugins.index.Cursors.newPathCursor;
 
 import java.util.ArrayList;
 import java.util.List;
diff --git a/oak-core/src/main/java/org/apache/jackrabbit/oak/query/ast/SelectorImpl.java b/oak-core/src/main/java/org/apache/jackrabbit/oak/query/ast/SelectorImpl.java
index db1daaa6ee..8610e770c4 100644
--- a/oak-core/src/main/java/org/apache/jackrabbit/oak/query/ast/SelectorImpl.java
+++ b/oak-core/src/main/java/org/apache/jackrabbit/oak/query/ast/SelectorImpl.java
@@ -49,8 +49,8 @@ import org.apache.jackrabbit.oak.query.index.FilterImpl;
 import org.apache.jackrabbit.oak.query.plan.ExecutionPlan;
 import org.apache.jackrabbit.oak.query.plan.SelectorExecutionPlan;
 import org.apache.jackrabbit.oak.spi.query.Cursor;
-import org.apache.jackrabbit.oak.plugins.index.Cursors;
 import org.apache.jackrabbit.oak.plugins.index.IndexConstants;
+import org.apache.jackrabbit.oak.plugins.index.cursor.Cursors;
 import org.apache.jackrabbit.oak.spi.query.IndexRow;
 import org.apache.jackrabbit.oak.plugins.memory.PropertyValues;
 import org.apache.jackrabbit.oak.spi.query.QueryConstants;
diff --git a/oak-core/src/main/java/org/apache/jackrabbit/oak/query/index/TraversingIndex.java b/oak-core/src/main/java/org/apache/jackrabbit/oak/query/index/TraversingIndex.java
index cad4f4d847..b61ace1eb1 100644
--- a/oak-core/src/main/java/org/apache/jackrabbit/oak/query/index/TraversingIndex.java
+++ b/oak-core/src/main/java/org/apache/jackrabbit/oak/query/index/TraversingIndex.java
@@ -19,8 +19,8 @@
 package org.apache.jackrabbit.oak.query.index;
 
 import org.apache.jackrabbit.oak.commons.PathUtils;
-import org.apache.jackrabbit.oak.plugins.index.Cursors;
 import org.apache.jackrabbit.oak.plugins.index.counter.jmx.NodeCounter;
+import org.apache.jackrabbit.oak.plugins.index.cursor.Cursors;
 import org.apache.jackrabbit.oak.query.ast.JoinConditionImpl;
 import org.apache.jackrabbit.oak.spi.query.Cursor;
 import org.apache.jackrabbit.oak.spi.query.Filter;
diff --git a/oak-core/src/test/java/org/apache/jackrabbit/oak/plugins/index/cursor/CursorUtils.java b/oak-core/src/test/java/org/apache/jackrabbit/oak/plugins/index/cursor/CursorUtils.java
new file mode 100644
index 0000000000..870a670ddb
--- /dev/null
+++ b/oak-core/src/test/java/org/apache/jackrabbit/oak/plugins/index/cursor/CursorUtils.java
@@ -0,0 +1,34 @@
+/*
+ * 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.cursor;
+
+import org.apache.jackrabbit.oak.spi.query.Cursor;
+
+public class CursorUtils {
+
+    public static String toString(Cursor cursor) {
+        StringBuilder buff = new StringBuilder();
+        while (cursor.hasNext()) {
+            if (buff.length() > 0) {
+                buff.append(", ");
+            }
+            buff.append(cursor.next().getPath());
+        }
+        return buff.toString();
+    }
+
+}
diff --git a/oak-core/src/test/java/org/apache/jackrabbit/oak/plugins/index/CursorsTest.java b/oak-core/src/test/java/org/apache/jackrabbit/oak/plugins/index/cursor/CursorsTest.java
similarity index 98%
rename from oak-core/src/test/java/org/apache/jackrabbit/oak/plugins/index/CursorsTest.java
rename to oak-core/src/test/java/org/apache/jackrabbit/oak/plugins/index/cursor/CursorsTest.java
index 99d7264134..f46e30df62 100644
--- a/oak-core/src/test/java/org/apache/jackrabbit/oak/plugins/index/CursorsTest.java
+++ b/oak-core/src/test/java/org/apache/jackrabbit/oak/plugins/index/cursor/CursorsTest.java
@@ -14,7 +14,7 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-package org.apache.jackrabbit.oak.plugins.index;
+package org.apache.jackrabbit.oak.plugins.index.cursor;
 
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertFalse;
diff --git a/oak-core/src/test/java/org/apache/jackrabbit/oak/plugins/index/cursor/PrefetchCursorTest.java b/oak-core/src/test/java/org/apache/jackrabbit/oak/plugins/index/cursor/PrefetchCursorTest.java
new file mode 100644
index 0000000000..1b427a7659
--- /dev/null
+++ b/oak-core/src/test/java/org/apache/jackrabbit/oak/plugins/index/cursor/PrefetchCursorTest.java
@@ -0,0 +1,119 @@
+/*
+ * 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.cursor;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNull;
+
+import java.util.Arrays;
+import java.util.List;
+
+import org.apache.jackrabbit.oak.spi.state.NodeState;
+import org.junit.Test;
+
+public class PrefetchCursorTest {
+    
+    @Test
+    public void findMatch() {
+        // find the file name, but only if it's a jpg
+        assertEquals("hello", PrefetchCursor.findMatch(
+                "/content/images/hello.jpg", 
+                ".+/(.*)\\.jpg"));
+        // it will find "null" it it's not a jpg
+        assertNull(PrefetchCursor.findMatch(
+                "/content/images/hello.tiff", 
+                ".+/(.*)\\.jpg"));
+    }
+
+    @Test(expected = IllegalArgumentException.class)
+    public void findMatchException() {
+        PrefetchCursor.findMatch(
+                "/content/images/hello.tiff", 
+                "***");
+    }
+    
+    @Test
+    public void applyAndResolvePatterns() {
+        assertEquals("jcr:content/metadata", 
+                PrefetchCursor.applyAndResolvePatterns(
+                "jcr:content/metadata", 
+                "/content/images/abc"));
+        assertEquals("jcr:content/renditions/abc", 
+                PrefetchCursor.applyAndResolvePatterns(
+                "jcr:content/renditions/${regex(\".+/(.*)\\.jpg\")}", 
+                "/content/images/abc.jpg"));
+        assertNull(
+                PrefetchCursor.applyAndResolvePatterns(
+                "jcr:content/renditions/${regex(\".+/(.*)\\.jpg\")}", 
+                "/content/images/abc.tiff"));
+        assertEquals("jcr:content/renditions/abc", 
+                PrefetchCursor.applyAndResolvePatterns(
+                "jcr:content/renditions/${regex(\"\\{(.*)\\\\}\")}", 
+                "/content/images/{abc}/def"));
+    }
+
+    @Test(expected = IllegalArgumentException.class)
+    public void applyAndResolvePatternsException() {
+        PrefetchCursor.applyAndResolvePatterns(
+                "jcr:content/${regex(\"***\")}",
+                "/content/images/abc");
+    }
+
+    @Test
+    public void resolve() {
+        assertEquals("/content/images/abc/jcr:content/metadata", 
+                PrefetchCursor.resolve(
+                "jcr:content/metadata", 
+                "/content/images/abc"));
+        assertEquals("/content/images/abc.jpg/jcr:content/renditions/abc.xml", 
+                PrefetchCursor.resolve(
+                "jcr:content/renditions/${regex(\".+/(.*)\\.jpg\")}.xml", 
+                "/content/images/abc.jpg"));
+        assertNull(
+                PrefetchCursor.resolve(
+                "jcr:content/renditions/${regex(\".+/(.*)\\.jpg\")}.xml", 
+                "/content/images/abc.tiff"));
+        assertEquals("/fileTypes/jpg", 
+                PrefetchCursor.resolve(
+                "/fileTypes/${regex(\".+/.*\\.(.*)\")}", 
+                "/content/images/abc.jpg"));
+    }
+    
+    @Test
+    public void cursor() {
+        List<String> paths = Arrays.asList(
+                "/test/a.jpg", 
+                "/test/b.tiff", 
+                "/test/c.pdf");
+        TestCursor cursor = new TestCursor(paths.iterator());
+        TestPrefetchNodeStore ns = new TestPrefetchNodeStore();
+        NodeState rootState = null;
+        List<String> prefetchRelative = Arrays.asList(
+                "jcr:content/metadata",
+                "jcr:content/renditions/${regex(\".+/(.*)\\.jpg\")}.xml",
+                "/fileTypes/${regex(\".+/.*\\.(.*)\")}");
+        PrefetchCursor pc = new PrefetchCursor(cursor, ns, 10, rootState, prefetchRelative);
+        assertEquals("/test/a.jpg, /test/b.tiff, /test/c.pdf", 
+                CursorUtils.toString(pc));
+        assertEquals("[/fileTypes/jpg, /fileTypes/pdf, /fileTypes/tiff, " + 
+                "/test, /test/a.jpg, /test/a.jpg/jcr:content/metadata, " + 
+                "/test/a.jpg/jcr:content/renditions/a.xml, " + 
+                "/test/b.tiff, /test/b.tiff/jcr:content/metadata, " + 
+                "/test/c.pdf, /test/c.pdf/jcr:content/metadata]", ns.toString());
+    }
+    
+}
diff --git a/oak-core/src/test/java/org/apache/jackrabbit/oak/plugins/index/cursor/TestCursor.java b/oak-core/src/test/java/org/apache/jackrabbit/oak/plugins/index/cursor/TestCursor.java
new file mode 100644
index 0000000000..26462cc224
--- /dev/null
+++ b/oak-core/src/test/java/org/apache/jackrabbit/oak/plugins/index/cursor/TestCursor.java
@@ -0,0 +1,41 @@
+/*
+ * 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.cursor;
+
+import java.util.Iterator;
+
+import org.apache.jackrabbit.oak.spi.query.IndexRow;
+
+public class TestCursor extends AbstractCursor {
+
+    private final Iterator<String> it;
+    
+    TestCursor(Iterator<String> it) {
+        this.it = it;
+    }
+    
+    @Override
+    public IndexRow next() {
+        return new TestRow(it.next());
+    }
+
+    @Override
+    public boolean hasNext() {
+        return it.hasNext();
+    }
+    
+}
\ No newline at end of file
diff --git a/oak-core/src/test/java/org/apache/jackrabbit/oak/plugins/index/cursor/TestPrefetchNodeStore.java b/oak-core/src/test/java/org/apache/jackrabbit/oak/plugins/index/cursor/TestPrefetchNodeStore.java
new file mode 100644
index 0000000000..0ca500311a
--- /dev/null
+++ b/oak-core/src/test/java/org/apache/jackrabbit/oak/plugins/index/cursor/TestPrefetchNodeStore.java
@@ -0,0 +1,42 @@
+/*
+ * 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.cursor;
+
+import java.util.Collection;
+import java.util.TreeSet;
+
+import org.apache.jackrabbit.oak.spi.state.NodeState;
+import org.apache.jackrabbit.oak.spi.state.PrefetchNodeStore;
+
+public class TestPrefetchNodeStore implements PrefetchNodeStore {
+    
+    private final TreeSet<String> prefetched = new TreeSet<String>();
+
+    @Override
+    public void prefetch(Collection<String> paths, NodeState rootState) {
+        prefetched.addAll(paths);
+    }
+    
+    public void reset() {
+        prefetched.clear();
+    }
+    
+    public String toString() {
+        return prefetched.toString();
+    }
+
+}
diff --git a/oak-core/src/test/java/org/apache/jackrabbit/oak/plugins/index/cursor/TestRow.java b/oak-core/src/test/java/org/apache/jackrabbit/oak/plugins/index/cursor/TestRow.java
new file mode 100644
index 0000000000..fe6d74623e
--- /dev/null
+++ b/oak-core/src/test/java/org/apache/jackrabbit/oak/plugins/index/cursor/TestRow.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.cursor;
+
+import org.apache.jackrabbit.oak.api.PropertyValue;
+import org.apache.jackrabbit.oak.spi.query.IndexRow;
+
+public class TestRow implements IndexRow {
+    
+    private final String path;
+
+    TestRow(String path) {
+        this.path = path;
+    }
+    
+    @Override
+    public String getPath() {
+        return path;
+    }
+
+    @Override
+    public PropertyValue getValue(String arg0) {
+        return null;
+    }
+
+    @Override
+    public boolean isVirtualRow() {
+        return false;
+    }
+    
+}
\ No newline at end of file
diff --git a/oak-core/src/test/java/org/apache/jackrabbit/oak/plugins/index/nodetype/NodeTypeIndexTest.java b/oak-core/src/test/java/org/apache/jackrabbit/oak/plugins/index/nodetype/NodeTypeIndexTest.java
index 8b23b8611e..9903cc5c00 100644
--- a/oak-core/src/test/java/org/apache/jackrabbit/oak/plugins/index/nodetype/NodeTypeIndexTest.java
+++ b/oak-core/src/test/java/org/apache/jackrabbit/oak/plugins/index/nodetype/NodeTypeIndexTest.java
@@ -28,6 +28,7 @@ import org.apache.jackrabbit.JcrConstants;
 import org.apache.jackrabbit.oak.api.Blob;
 import org.apache.jackrabbit.oak.api.Type;
 import org.apache.jackrabbit.oak.plugins.index.IndexUpdateProvider;
+import org.apache.jackrabbit.oak.plugins.index.cursor.PathCursor;
 import org.apache.jackrabbit.oak.plugins.index.property.PropertyIndexEditorProvider;
 import org.apache.jackrabbit.oak.plugins.memory.MemoryNodeStore;
 import org.apache.jackrabbit.oak.query.NodeStateNodeTypeInfoProvider;
@@ -40,7 +41,6 @@ import org.apache.jackrabbit.oak.spi.commit.CommitInfo;
 import org.apache.jackrabbit.oak.spi.commit.EditorHook;
 import org.apache.jackrabbit.oak.spi.mount.Mounts;
 import org.apache.jackrabbit.oak.spi.query.Cursor;
-import org.apache.jackrabbit.oak.plugins.index.Cursors;
 import org.apache.jackrabbit.oak.spi.state.NodeBuilder;
 import org.apache.jackrabbit.oak.spi.state.NodeState;
 import org.apache.jackrabbit.oak.spi.state.NodeStore;
@@ -102,7 +102,7 @@ public class NodeTypeIndexTest {
     private static void checkCursor(Cursor cursor, String... matches) {
         // make sure the index is actually used
         // and does not traverse
-        assertEquals(Cursors.class.getName() + "$PathCursor",
+        assertEquals(PathCursor.class.getName(),
                 cursor.getClass().getName());
         Set<String> expected = Sets.newHashSet();
         expected.addAll(Arrays.asList(matches));
diff --git a/oak-core/src/test/java/org/apache/jackrabbit/oak/security/CustomQueryIndexProviderTest.java b/oak-core/src/test/java/org/apache/jackrabbit/oak/security/CustomQueryIndexProviderTest.java
index 450b24f3b8..d01b4bae46 100644
--- a/oak-core/src/test/java/org/apache/jackrabbit/oak/security/CustomQueryIndexProviderTest.java
+++ b/oak-core/src/test/java/org/apache/jackrabbit/oak/security/CustomQueryIndexProviderTest.java
@@ -25,9 +25,9 @@ import java.util.List;
 import org.apache.jackrabbit.oak.AbstractSecurityTest;
 import org.apache.jackrabbit.oak.Oak;
 import org.apache.jackrabbit.oak.api.CommitFailedException;
-import org.apache.jackrabbit.oak.plugins.index.Cursors;
 import org.apache.jackrabbit.oak.plugins.index.IndexEditorProvider;
 import org.apache.jackrabbit.oak.plugins.index.IndexUpdateCallback;
+import org.apache.jackrabbit.oak.plugins.index.cursor.Cursors;
 import org.apache.jackrabbit.oak.plugins.index.property.PropertyIndexEditorProvider;
 import org.apache.jackrabbit.oak.plugins.index.property.PropertyIndexLookup;
 import org.apache.jackrabbit.oak.query.QueryEngineSettings;