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 399f0eb255..d25fd3922a 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
@@ -389,8 +389,10 @@ public class SelectorImpl extends SourceImpl {
         for (ColumnImpl c : query.getColumns()) {
             if (c.getSelector().equals(this)) {
                 String columnName = c.getColumnName();
-                if (columnName.equals(QueryConstants.REP_EXCERPT) || columnName.equals(QueryConstants.OAK_SCORE_EXPLANATION)) {
+                if (columnName.equals(QueryConstants.OAK_SCORE_EXPLANATION)) {
                     f.restrictProperty(columnName, Operator.NOT_EQUAL, null);
+                } else if (columnName.startsWith(QueryConstants.REP_EXCERPT)) {
+                    f.restrictProperty(QueryConstants.REP_EXCERPT, Operator.EQUAL, PropertyValues.newString(columnName));
                 } else if (columnName.startsWith(QueryConstants.REP_FACET)) {
                     f.restrictProperty(QueryConstants.REP_FACET, Operator.EQUAL, PropertyValues.newString(columnName));
                 }
@@ -638,6 +640,7 @@ public class SelectorImpl extends SourceImpl {
             return PropertyValues.create(s);
         }
         boolean relative = !oakPropertyName.startsWith(QueryConstants.REP_FACET + "(")
+                && !oakPropertyName.startsWith(QueryConstants.REP_EXCERPT + "(")
                 && oakPropertyName.indexOf('/') >= 0;
         Tree t = currentTree();
         if (relative) {
@@ -673,8 +676,9 @@ public class SelectorImpl extends SourceImpl {
             result = PropertyValues.newString(local);
         } else if (oakPropertyName.equals(QueryConstants.JCR_SCORE)) {
             result = currentRow.getValue(QueryConstants.JCR_SCORE);
-        } else if (oakPropertyName.equals(QueryConstants.REP_EXCERPT)) {
-            result = currentRow.getValue(QueryConstants.REP_EXCERPT);
+        } else if (oakPropertyName.equals(QueryConstants.REP_EXCERPT)
+                || oakPropertyName.startsWith(QueryConstants.REP_EXCERPT + "(")) {
+            result = currentRow.getValue(oakPropertyName);
         } else if (oakPropertyName.equals(QueryConstants.OAK_SCORE_EXPLANATION)) {
             result = currentRow.getValue(QueryConstants.OAK_SCORE_EXPLANATION);
         } else if (oakPropertyName.equals(QueryConstants.REP_SPELLCHECK)) {
diff --git a/oak-lucene/src/main/java/org/apache/jackrabbit/oak/plugins/index/lucene/LuceneIndexConstants.java b/oak-lucene/src/main/java/org/apache/jackrabbit/oak/plugins/index/lucene/LuceneIndexConstants.java
index 603bfde233..0a5e913288 100644
--- a/oak-lucene/src/main/java/org/apache/jackrabbit/oak/plugins/index/lucene/LuceneIndexConstants.java
+++ b/oak-lucene/src/main/java/org/apache/jackrabbit/oak/plugins/index/lucene/LuceneIndexConstants.java
@@ -141,6 +141,8 @@ public interface LuceneIndexConstants {
 
     String PROP_USE_IN_EXCERPT = "useInExcerpt";
 
+    String EXCERPT_NODE_FIELD_NAME = ".";
+
     String PROP_NODE_SCOPE_INDEX = "nodeScopeIndex";
 
     String PROP_PROPERTY_INDEX = "propertyIndex";
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 a8074fd510..6f33c5fb73 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
@@ -33,12 +33,15 @@ import java.util.List;
 import java.util.Map;
 import java.util.Set;
 import java.util.concurrent.atomic.AtomicReference;
+import java.util.stream.Collectors;
 
+import com.google.common.base.Joiner;
 import com.google.common.collect.AbstractIterator;
 import com.google.common.collect.FluentIterable;
 import com.google.common.collect.Iterables;
 import com.google.common.collect.Iterators;
 import com.google.common.collect.Lists;
+import com.google.common.collect.Maps;
 import com.google.common.collect.Queues;
 import com.google.common.collect.Sets;
 import com.google.common.primitives.Chars;
@@ -138,11 +141,13 @@ import static org.apache.jackrabbit.oak.commons.PathUtils.denotesRoot;
 import static org.apache.jackrabbit.oak.commons.PathUtils.getParentPath;
 import static org.apache.jackrabbit.oak.plugins.index.lucene.FieldNames.ANALYZED_FIELD_PREFIX;
 import static org.apache.jackrabbit.oak.plugins.index.lucene.IndexDefinition.NATIVE_SORT_ORDER;
+import static org.apache.jackrabbit.oak.plugins.index.lucene.LuceneIndexConstants.EXCERPT_NODE_FIELD_NAME;
 import static org.apache.jackrabbit.oak.plugins.index.lucene.LuceneIndexConstants.VERSION;
 import static org.apache.jackrabbit.oak.plugins.index.lucene.TermFactory.newAncestorTerm;
 import static org.apache.jackrabbit.oak.plugins.index.lucene.TermFactory.newPathTerm;
 import static org.apache.jackrabbit.oak.plugins.memory.PropertyValues.newName;
 import static org.apache.jackrabbit.oak.spi.query.QueryConstants.JCR_PATH;
+import static org.apache.jackrabbit.oak.spi.query.QueryConstants.REP_EXCERPT;
 import static org.apache.jackrabbit.oak.spi.query.QueryIndex.AdvancedQueryIndex;
 import static org.apache.jackrabbit.oak.spi.query.QueryIndex.NativeQueryIndex;
 import static org.apache.lucene.search.BooleanClause.Occur.*;
@@ -350,7 +355,7 @@ public class LucenePropertyIndex implements AdvancedQueryIndex, QueryIndex, Nati
                 return endOfData();
             }
 
-            private LuceneResultRow convertToRow(ScoreDoc doc, IndexSearcher searcher, String excerpt,  Facets facets,
+            private LuceneResultRow convertToRow(ScoreDoc doc, IndexSearcher searcher, Map<String, String> excerpts,  Facets facets,
                                                  String explanation) throws IOException {
                 IndexReader reader = searcher.getIndexReader();
                 //TODO Look into usage of field cache for retrieving the path
@@ -381,7 +386,7 @@ public class LucenePropertyIndex implements AdvancedQueryIndex, QueryIndex, Nati
 
                     boolean shouldIncludeForHierarchy = shouldInclude(path, plan);
                     LOG.trace("Matched path {}; shouldIncludeForHierarchy: {}", path, shouldIncludeForHierarchy);
-                    return shouldIncludeForHierarchy? new LuceneResultRow(path, doc.score, excerpt, facets, explanation)
+                    return shouldIncludeForHierarchy? new LuceneResultRow(path, doc.score, excerpts, facets, explanation)
                             : null;
                 }
                 return null;
@@ -438,10 +443,16 @@ public class LucenePropertyIndex implements AdvancedQueryIndex, QueryIndex, Nati
                             Facets facets = FacetHelper.getFacets(searcher, query, docs, plan, indexNode.getDefinition().isSecureFacets());
                             PERF_LOGGER.end(f, -1, "facets retrieved");
 
-                            PropertyRestriction restriction = filter.getPropertyRestriction(QueryConstants.REP_EXCERPT);
-                            boolean addExcerpt = restriction != null && restriction.isNotNullRestriction();
+                            Set<String> excerptFields = Sets.newHashSet();
+                            for (PropertyRestriction pr : filter.getPropertyRestrictions()) {
+                                if (QueryConstants.REP_EXCERPT.equals(pr.propertyName)) {
+                                    String value = pr.first.getValue(Type.STRING);
+                                    excerptFields.add(value);
+                                }
+                            }
+                            boolean addExcerpt = excerptFields.size() > 0;
 
-                            restriction = filter.getPropertyRestriction(QueryConstants.OAK_SCORE_EXPLANATION);
+                            PropertyRestriction restriction = filter.getPropertyRestriction(QueryConstants.OAK_SCORE_EXPLANATION);
                             boolean addExplain = restriction != null && restriction.isNotNullRestriction();
 
                             Analyzer analyzer = indexNode.getDefinition().getAnalyzer();
@@ -456,9 +467,9 @@ public class LucenePropertyIndex implements AdvancedQueryIndex, QueryIndex, Nati
                             }
 
                             for (ScoreDoc doc : docs.scoreDocs) {
-                                String excerpt = null;
+                                Map<String, String> excerpts = null;
                                 if (addExcerpt) {
-                                    excerpt = getExcerpt(query, analyzer, searcher, doc, mergedFieldInfos);
+                                    excerpts = getExcerpt(query, excerptFields, analyzer, searcher, doc, mergedFieldInfos);
                                 }
 
                                 String explanation = null;
@@ -466,7 +477,7 @@ public class LucenePropertyIndex implements AdvancedQueryIndex, QueryIndex, Nati
                                     explanation = searcher.explain(query, doc.doc).toString();
                                 }
 
-                                LuceneResultRow row = convertToRow(doc, searcher, excerpt, facets, explanation);
+                                LuceneResultRow row = convertToRow(doc, searcher, excerpts, facets, explanation);
                                 if (row != null) {
                                     queue.add(row);
                                 }
@@ -662,9 +673,32 @@ public class LucenePropertyIndex implements AdvancedQueryIndex, QueryIndex, Nati
         return include;
     }
 
-    private String getExcerpt(Query query, Analyzer analyzer, IndexSearcher searcher, ScoreDoc doc,
-                              FieldInfos fieldInfos) throws IOException {
-        StringBuilder excerpt = new StringBuilder();
+    private Map<String, String> getExcerpt(Query query, Set<String> excerptFields,
+                              Analyzer analyzer, IndexSearcher searcher, ScoreDoc doc, FieldInfos fieldInfos)
+            throws IOException {
+        Set<String> excerptFieldNames = Sets.newHashSet();
+        Map<String, String> fieldNameToColumnNameMap = Maps.newHashMap();
+        Map<String, String> columnNameToExcerpts = Maps.newHashMap();
+        Set<String> nodeExcerptColumns = Sets.newHashSet();
+
+        excerptFields.forEach(columnName -> {
+            String fieldName;
+            if (REP_EXCERPT.equals(columnName)) {
+                fieldName = EXCERPT_NODE_FIELD_NAME;
+            } else {
+                fieldName = columnName.substring(REP_EXCERPT.length() + 1, columnName.length() - 1);
+            }
+
+            if (!EXCERPT_NODE_FIELD_NAME.equals(fieldName)) {
+                excerptFieldNames.add(fieldName);
+                fieldNameToColumnNameMap.put(fieldName, columnName);
+            } else {
+                nodeExcerptColumns.add(columnName);
+            }
+        });
+
+        final boolean requireNodeLevelExcerpt = nodeExcerptColumns.size() > 0;
+
         int docID = doc.doc;
         List<String> names = new LinkedList<String>();
 
@@ -676,6 +710,10 @@ public class LucenePropertyIndex implements AdvancedQueryIndex, QueryIndex, Nati
             }
         }
 
+        if (!requireNodeLevelExcerpt) {
+            names.retainAll(excerptFieldNames);
+        }
+
         if (names.size() > 0) {
             int[] maxPassages = new int[names.size()];
             for (int i = 0; i < maxPassages.length; i++) {
@@ -687,10 +725,10 @@ public class LucenePropertyIndex implements AdvancedQueryIndex, QueryIndex, Nati
                 for (Map.Entry<String, String[]> entry : stringMap.entrySet()) {
                     String value = Arrays.toString(entry.getValue());
                     if (value.contains("<b>")) {
-                        if (excerpt.length() > 0) {
-                            excerpt.append("...");
-                        }
-                        excerpt.append(value);
+                        String fieldName = entry.getKey();
+                        String columnName = fieldNameToColumnNameMap.get(fieldName);
+
+                        columnNameToExcerpts.put(columnName, value);
                     }
                 }
             } catch (Exception e) {
@@ -699,11 +737,11 @@ public class LucenePropertyIndex implements AdvancedQueryIndex, QueryIndex, Nati
         }
 
         // fallback if no excerpt could be retrieved using postings highlighter
-        if (excerpt.length() == 0) {
-
+        if (columnNameToExcerpts.size() == 0) {
             for (IndexableField field : searcher.getIndexReader().document(doc.doc).getFields()) {
                 String name = field.name();
                 // only full text or analyzed fields
+                // TODO: check how :fulltext gets stored
                 if (name.startsWith(FieldNames.FULLTEXT) || name.startsWith(FieldNames.ANALYZED_FIELD_PREFIX)) {
                     String text = field.stringValue();
                     TokenStream tokenStream = analyzer.tokenStream(name, text);
@@ -712,12 +750,21 @@ public class LucenePropertyIndex implements AdvancedQueryIndex, QueryIndex, Nati
                         TextFragment[] textFragments = highlighter.getBestTextFragments(tokenStream, text, true, 1);
                         if (textFragments != null && textFragments.length > 0) {
                             for (TextFragment fragment : textFragments) {
-                                if (excerpt.length() > 0) {
-                                    excerpt.append("...");
+                                String columnName = null;
+                                if (name.startsWith(FieldNames.ANALYZED_FIELD_PREFIX)) {
+                                    columnName = fieldNameToColumnNameMap.get(name.substring(FieldNames.ANALYZED_FIELD_PREFIX.length()));
                                 }
-                                excerpt.append(fragment.toString());
+                                if (columnName == null && requireNodeLevelExcerpt) {
+                                    columnName = name;
+                                }
+
+                                if (columnName != null) {
+                                    columnNameToExcerpts.put(columnName, fragment.toString());
+                                }
+                            }
+                            if (excerptFieldNames.size() == 0) {
+                                break;
                             }
-                            break;
                         }
                     } catch (InvalidTokenOffsetsException e) {
                         LOG.error("higlighting failed", e);
@@ -725,7 +772,18 @@ public class LucenePropertyIndex implements AdvancedQueryIndex, QueryIndex, Nati
                 }
             }
         }
-        return excerpt.toString();
+
+        if (requireNodeLevelExcerpt) {
+            String nodeExcerpt = Joiner.on("...").join(columnNameToExcerpts.values());
+
+            nodeExcerptColumns.forEach( nodeExcerptColumnName -> {
+                columnNameToExcerpts.put(nodeExcerptColumnName, nodeExcerpt);
+            });
+        }
+
+        columnNameToExcerpts.keySet().retainAll(excerptFields);
+
+        return columnNameToExcerpts;
     }
 
     @Override
@@ -1610,13 +1668,13 @@ public class LucenePropertyIndex implements AdvancedQueryIndex, QueryIndex, Nati
         final double score;
         final String suggestion;
         final boolean isVirutal;
-        final String excerpt;
+        final Map<String, String> excerpts;
         final String explanation;
         final Facets facets;
 
-        LuceneResultRow(String path, double score, String excerpt, Facets facets, String explanation) {
+        LuceneResultRow(String path, double score, Map<String, String> excerpts, Facets facets, String explanation) {
             this.explanation = explanation;
-            this.excerpt = excerpt;
+            this.excerpts = excerpts;
             this.facets = facets;
             this.isVirutal = false;
             this.path = path;
@@ -1629,7 +1687,7 @@ public class LucenePropertyIndex implements AdvancedQueryIndex, QueryIndex, Nati
             this.path = "/";
             this.score = weight;
             this.suggestion = suggestion;
-            this.excerpt = null;
+            this.excerpts = null;
             this.facets = null;
             this.explanation = null;
         }
@@ -1741,8 +1799,11 @@ public class LucenePropertyIndex implements AdvancedQueryIndex, QueryIndex, Nati
                     if (QueryConstants.OAK_SCORE_EXPLANATION.equals(columnName)) {
                         return PropertyValues.newString(currentRow.explanation);
                     }
-                    if (QueryConstants.REP_EXCERPT.equals(columnName)) {
-                        return PropertyValues.newString(currentRow.excerpt);
+                    if (columnName.startsWith(QueryConstants.REP_EXCERPT)) {
+                        String exceprt = currentRow.excerpts.get(columnName);
+                        if (exceprt != null) {
+                            return PropertyValues.newString(exceprt);
+                        }
                     }
                     if (columnName.startsWith(QueryConstants.REP_FACET)) {
                         String facetFieldName = FacetHelper.parseFacetField(columnName);
diff --git a/oak-lucene/src/test/java/org/apache/jackrabbit/oak/plugins/index/lucene/ExcerptTest.java b/oak-lucene/src/test/java/org/apache/jackrabbit/oak/plugins/index/lucene/ExcerptTest.java
new file mode 100644
index 0000000000..2f7dd7d4a7
--- /dev/null
+++ b/oak-lucene/src/test/java/org/apache/jackrabbit/oak/plugins/index/lucene/ExcerptTest.java
@@ -0,0 +1,315 @@
+/*
+ * 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.base.Joiner;
+import org.apache.jackrabbit.JcrConstants;
+import org.apache.jackrabbit.oak.InitialContent;
+import org.apache.jackrabbit.oak.Oak;
+import org.apache.jackrabbit.oak.api.Blob;
+import org.apache.jackrabbit.oak.api.ContentRepository;
+import org.apache.jackrabbit.oak.api.PropertyValue;
+import org.apache.jackrabbit.oak.api.Result;
+import org.apache.jackrabbit.oak.api.ResultRow;
+import org.apache.jackrabbit.oak.api.Tree;
+import org.apache.jackrabbit.oak.api.Type;
+import org.apache.jackrabbit.oak.plugins.index.nodetype.NodeTypeIndexProvider;
+import org.apache.jackrabbit.oak.plugins.index.property.PropertyIndexEditorProvider;
+import org.apache.jackrabbit.oak.plugins.memory.ArrayBasedBlob;
+import org.apache.jackrabbit.oak.plugins.memory.MemoryNodeStore;
+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.NodeStore;
+import org.junit.Before;
+import org.junit.Ignore;
+import org.junit.Test;
+
+import java.util.Iterator;
+import java.util.List;
+import java.util.stream.Collectors;
+
+import static com.google.common.collect.Lists.newArrayList;
+import static org.apache.jackrabbit.oak.api.QueryEngine.NO_BINDINGS;
+import static org.apache.jackrabbit.oak.api.Type.STRING;
+import static org.apache.jackrabbit.oak.plugins.index.IndexConstants.INDEX_DEFINITIONS_NAME;
+import static org.apache.jackrabbit.oak.plugins.index.IndexConstants.INDEX_DEFINITIONS_NODE_TYPE;
+import static org.apache.jackrabbit.oak.plugins.index.IndexConstants.REINDEX_PROPERTY_NAME;
+import static org.apache.jackrabbit.oak.plugins.index.IndexConstants.TYPE_PROPERTY_NAME;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertTrue;
+
+public class ExcerptTest extends AbstractQueryTest {
+
+    @Before
+    public void setup() throws Exception { //named so that it gets called after super.before :-/
+        Tree rootTree = root.getTree("/");
+
+        Tree def = rootTree.addChild(INDEX_DEFINITIONS_NAME).addChild("testExcerpt");
+        def.setProperty(JcrConstants.JCR_PRIMARYTYPE, INDEX_DEFINITIONS_NODE_TYPE, Type.NAME);
+        def.setProperty(TYPE_PROPERTY_NAME, LuceneIndexConstants.TYPE_LUCENE);
+        def.setProperty(REINDEX_PROPERTY_NAME, true);
+        def.setProperty(LuceneIndexConstants.EVALUATE_PATH_RESTRICTION, true);
+        def.setProperty(LuceneIndexConstants.COMPAT_MODE, IndexFormatVersion.V2.getVersion());
+
+        Tree properties = def.addChild(LuceneIndexConstants.INDEX_RULES)
+                .addChild("nt:base")
+                .addChild(LuceneIndexConstants.PROP_NODE);
+
+        Tree notIndexedProp = properties.addChild("baz");
+        notIndexedProp.setProperty(LuceneIndexConstants.PROP_NODE_SCOPE_INDEX, true);
+
+        Tree relativeProp = properties.addChild("relative-baz");
+        relativeProp.setProperty(LuceneIndexConstants.PROP_ANALYZED, true);
+        relativeProp.setProperty(LuceneIndexConstants.PROP_USE_IN_EXCERPT, true);
+        relativeProp.setProperty(LuceneIndexConstants.PROP_NAME, "relative/baz");
+
+        Tree allProps = properties.addChild("allProps");
+        allProps.setProperty(LuceneIndexConstants.PROP_ANALYZED, true);
+        allProps.setProperty(LuceneIndexConstants.PROP_NODE_SCOPE_INDEX, true);
+        allProps.setProperty(LuceneIndexConstants.PROP_USE_IN_EXCERPT, true);
+        allProps.setProperty(LuceneIndexConstants.PROP_NAME, LuceneIndexConstants.REGEX_ALL_PROPS);
+        allProps.setProperty(LuceneIndexConstants.PROP_IS_REGEX, true);
+
+        root.commit();
+    }
+
+    @Override
+    protected ContentRepository createRepository() {
+        LuceneIndexEditorProvider editorProvider = new LuceneIndexEditorProvider();
+        LuceneIndexProvider provider = new LuceneIndexProvider();
+
+        NodeStore nodeStore = new MemoryNodeStore();
+        return new Oak(nodeStore)
+                .with(new InitialContent())
+                .with(new OpenSecurityProvider())
+                .with((Observer) provider)
+                .with(editorProvider)
+                .with((QueryIndexProvider)provider)
+                .with(new PropertyIndexEditorProvider())
+                .with(new NodeTypeIndexProvider())
+                .createContentRepository();
+    }
+
+    @Test
+    public void getAllSelectedColumns() throws Exception {
+        Tree contentRoot = root.getTree("/").addChild("testRoot");
+        contentRoot.setProperty("foo", "is fox ifoxing");
+        contentRoot.setProperty("bar", "ifoxing fox");
+        contentRoot.setProperty("baz", "fox ifoxing");
+        root.commit();
+
+        List<String> columns = newArrayList("rep:excerpt", "rep:excerpt(.)", "rep:excerpt(foo)", "rep:excerpt(bar)");
+        String selectColumns = Joiner.on(",").join(
+                columns.stream().map(col -> "[" + col + "]").collect(Collectors.toList())
+        );
+        String query = "SELECT " + selectColumns + " FROM [nt:base] WHERE CONTAINS(*, 'fox')";
+        Result result = executeQuery(query, SQL2, NO_BINDINGS);
+        Iterator<? extends ResultRow> resultIter = result.getRows().iterator();
+        assertTrue(resultIter.hasNext());
+        ResultRow firstRow = resultIter.next();
+
+        PropertyValue excerptValue;
+        String excerpt;
+
+        for (String col : columns) {
+            excerptValue = firstRow.getValue(col);
+            assertNotNull(col + " not evaluated", excerptValue);
+            excerpt = excerptValue.getValue(STRING);
+            assertFalse(col + " didn't evaluate correctly - got '" + excerpt + "'",
+                    excerpt.contains("i<strong>fox</foxing>ing"));
+        }
+    }
+
+    @Test
+    public void nodeExcerpt() throws Exception {
+        Tree contentRoot = root.getTree("/").addChild("testRoot");
+        contentRoot.setProperty("foo", "is fox ifoxing");
+        contentRoot.setProperty("bar", "ifoxing fox");
+        root.commit();
+
+        String query = "SELECT [rep:excerpt],[rep:excerpt(.)] FROM [nt:base] WHERE CONTAINS(*, 'fox')";
+        Result result = executeQuery(query, SQL2, NO_BINDINGS);
+        Iterator<? extends ResultRow> resultIter = result.getRows().iterator();
+        assertTrue(resultIter.hasNext());
+        ResultRow firstRow = resultIter.next();
+
+        PropertyValue nodeExcerpt;
+        String excerpt1, excerpt2;
+
+
+        nodeExcerpt = firstRow.getValue("rep:excerpt");
+        assertNotNull("rep:excerpt not evaluated", nodeExcerpt);
+        excerpt1 = nodeExcerpt.getValue(STRING);
+        assertTrue("rep:excerpt didn't evaluate correctly - got '" + excerpt1 + "'",
+                "is <strong>fox</strong> ifoxing".equals(excerpt1) || "ifoxing <strong>fox</strong>".equals(excerpt1));
+
+        nodeExcerpt = firstRow.getValue("rep:excerpt(.)");
+        assertNotNull("rep:excerpt(.) not evaluated", nodeExcerpt);
+        excerpt2 = nodeExcerpt.getValue(STRING);
+        assertEquals("excerpt extracted via rep:excerpt not same as rep:excerpt(.)", excerpt1, excerpt2);
+    }
+
+    @Test
+    public void nonIndexedRequestedPropExcerpt() throws Exception {
+        Tree contentRoot = root.getTree("/").addChild("testRoot");
+        contentRoot.setProperty("foo", "fox");
+        contentRoot.setProperty("baz", "is fox ifoxing");
+        root.commit();
+
+        String query = "SELECT [rep:excerpt(baz)] FROM [nt:base] WHERE CONTAINS(*, 'fox')";
+        Result result = executeQuery(query, SQL2, NO_BINDINGS);
+        Iterator<? extends ResultRow> resultIter = result.getRows().iterator();
+        assertTrue(resultIter.hasNext());
+        ResultRow firstRow = resultIter.next();
+
+        PropertyValue nodeExcerpt = firstRow.getValue("rep:excerpt(baz)");
+        assertNull("rep:excerpt(baz) if requested explicitly must be indexed to be evaluated", nodeExcerpt);
+    }
+
+    @Test
+    public void propExcerpt() throws Exception {
+        Tree contentRoot = root.getTree("/").addChild("testRoot");
+        contentRoot.setProperty("foo", "is fox ifoxing");
+        root.commit();
+
+        String query = "SELECT [rep:excerpt(foo)] FROM [nt:base] WHERE CONTAINS(*, 'fox')";
+        Result result = executeQuery(query, SQL2, NO_BINDINGS);
+        Iterator<? extends ResultRow> resultIter = result.getRows().iterator();
+        assertTrue(resultIter.hasNext());
+        ResultRow firstRow = resultIter.next();
+
+        PropertyValue nodeExcerpt;
+        String excerpt;
+
+        nodeExcerpt = firstRow.getValue("rep:excerpt(foo)");
+        assertNotNull("rep:excerpt(foo) not evaluated", nodeExcerpt);
+        excerpt = nodeExcerpt.getValue(STRING);
+        assertTrue("rep:excerpt(foo) didn't evaluate correctly - got '" + excerpt + "'",
+                "is <strong>fox</strong> ifoxing".equals(excerpt));
+    }
+
+    @Ignore("OAK-4401")
+    @Test
+    public void indexedNonRequestedPropExcerpt() throws Exception {
+        Tree contentRoot = root.getTree("/").addChild("testRoot");
+        contentRoot.setProperty("foo", "is fox ifoxing");
+        root.commit();
+
+        String query = "SELECT [rep:excerpt] FROM [nt:base] WHERE CONTAINS(*, 'fox')";
+        Result result = executeQuery(query, SQL2, NO_BINDINGS);
+        Iterator<? extends ResultRow> resultIter = result.getRows().iterator();
+        assertTrue(resultIter.hasNext());
+        ResultRow firstRow = resultIter.next();
+
+        PropertyValue nodeExcerpt;
+        String excerpt;
+
+        nodeExcerpt = firstRow.getValue("rep:excerpt(foo)");
+        assertNotNull("rep:excerpt(foo) not evaluated", nodeExcerpt);
+        excerpt = nodeExcerpt.getValue(STRING);
+        assertTrue("rep:excerpt(foo) didn't evaluate correctly - got '" + excerpt + "'",
+                excerpt.contains("<strong>fox</strong>"));
+
+        // requires OAK-4401 to be resolved
+        assertTrue("rep:excerpt(foo) highlighting inside words - got '" + excerpt + "'",
+                !excerpt.contains("i<strong>fox</strong>ing"));
+    }
+
+    @Ignore("OAK-4401")
+    @Test
+    public void nonIndexedNonRequestedPropExcerpt() throws Exception {
+        Tree contentRoot = root.getTree("/").addChild("testRoot");
+        contentRoot.setProperty("foo", "fox");
+        contentRoot.setProperty("baz", "is fox ifoxing");
+        root.commit();
+
+        String query = "SELECT [rep:excerpt] FROM [nt:base] WHERE CONTAINS(*, 'fox')";
+        Result result = executeQuery(query, SQL2, NO_BINDINGS);
+        Iterator<? extends ResultRow> resultIter = result.getRows().iterator();
+        assertTrue(resultIter.hasNext());
+        ResultRow firstRow = resultIter.next();
+
+        PropertyValue nodeExcerpt;
+        String excerpt;
+
+        nodeExcerpt = firstRow.getValue("rep:excerpt(baz)");
+        assertNotNull("rep:excerpt(baz) not evaluated", nodeExcerpt);
+        excerpt = nodeExcerpt.getValue(STRING);
+        assertTrue("rep:excerpt(foo) didn't evaluate correctly - got '" + excerpt + "'",
+                excerpt.contains("<strong>fox</strong>"));
+
+        // requires OAK-4401 to be resolved
+        assertTrue("rep:excerpt(baz) highlighting inside words - got '" + excerpt + "'",
+                !excerpt.contains("i<strong>fox</strong>ing"));
+    }
+
+    @Test
+    public void relativePropExcerpt() throws Exception {
+        Tree contentRoot = root.getTree("/").addChild("testRoot");
+        contentRoot.addChild("relative").setProperty("baz", "is fox ifoxing");
+        root.commit();
+
+        String query = "SELECT [rep:excerpt(relative/baz)] FROM [nt:base] WHERE CONTAINS([relative/baz], 'fox')";
+        Result result = executeQuery(query, SQL2, NO_BINDINGS);
+        Iterator<? extends ResultRow> resultIter = result.getRows().iterator();
+        assertTrue(resultIter.hasNext());
+        ResultRow firstRow = resultIter.next();
+
+        PropertyValue nodeExcerpt;
+        String excerpt;
+
+        nodeExcerpt = firstRow.getValue("rep:excerpt(relative/baz)");
+        assertNotNull("rep:excerpt(relative/baz) not evaluated", nodeExcerpt);
+        excerpt = nodeExcerpt.getValue(STRING);
+        assertTrue("rep:excerpt(relative/baz) didn't evaluate correctly - got '" + excerpt + "'",
+                "is <strong>fox</strong> ifoxing".equals(excerpt));
+    }
+
+    @Test
+    public void binaryExcerpt() throws Exception {
+        Tree contentRoot = root.getTree("/").addChild("testRoot");
+
+        String binaryText = "is fox foxing as a fox cub";
+        Blob blob = new ArrayBasedBlob(binaryText.getBytes());
+        TestUtil.createFileNode(contentRoot, "binaryNode", blob, "text/plain");
+        root.commit();
+
+        String query = "SELECT [rep:excerpt] FROM [nt:base] WHERE CONTAINS(*, 'fox')";
+        Result result = executeQuery(query, SQL2, NO_BINDINGS);
+        Iterator<? extends ResultRow> resultIter = result.getRows().iterator();
+        assertTrue(resultIter.hasNext());
+        ResultRow firstRow = resultIter.next();
+
+        PropertyValue nodeExcerpt;
+        String excerpt;
+
+        nodeExcerpt = firstRow.getValue("rep:excerpt");
+        assertNotNull("rep:excerpt not evaluated", nodeExcerpt);
+        excerpt = nodeExcerpt.getValue(STRING);
+        String expected = binaryText.replaceAll(" fox ", " <strong>fox</strong> ");
+        assertTrue("rep:excerpt didn't evaluate correctly - got '" + excerpt + "'",
+                excerpt.contains(expected));
+    }
+}
