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 mk...@apache.org on 2020/08/12 13:46:45 UTC

svn commit: r1880807 [2/4] - in /jackrabbit/oak/trunk: oak-jcr/src/test/java/org/apache/jackrabbit/oak/query/ oak-lucene/src/test/java/org/apache/jackrabbit/oak/plugins/index/lucene/ oak-search-elastic/ oak-search-elastic/src/test/java/org/apache/jackr...

Added: jackrabbit/oak/trunk/oak-search-elastic/src/test/java/org/apache/jackrabbit/oak/plugins/index/elastic/index/ElasticDocumentMakerLargeStringPropertiesLogTest.java
URL: http://svn.apache.org/viewvc/jackrabbit/oak/trunk/oak-search-elastic/src/test/java/org/apache/jackrabbit/oak/plugins/index/elastic/index/ElasticDocumentMakerLargeStringPropertiesLogTest.java?rev=1880807&view=auto
==============================================================================
--- jackrabbit/oak/trunk/oak-search-elastic/src/test/java/org/apache/jackrabbit/oak/plugins/index/elastic/index/ElasticDocumentMakerLargeStringPropertiesLogTest.java (added)
+++ jackrabbit/oak/trunk/oak-search-elastic/src/test/java/org/apache/jackrabbit/oak/plugins/index/elastic/index/ElasticDocumentMakerLargeStringPropertiesLogTest.java Wed Aug 12 13:46:45 2020
@@ -0,0 +1,191 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.jackrabbit.oak.plugins.index.elastic.index;
+
+import ch.qos.logback.classic.LoggerContext;
+import ch.qos.logback.classic.spi.ILoggingEvent;
+import ch.qos.logback.core.read.ListAppender;
+import org.apache.jackrabbit.oak.api.Type;
+import org.apache.jackrabbit.oak.commons.junit.TemporarySystemProperty;
+import org.apache.jackrabbit.oak.plugins.index.elastic.ElasticIndexDefinition;
+import org.apache.jackrabbit.oak.plugins.index.elastic.util.ElasticIndexDefinitionBuilder;
+import org.apache.jackrabbit.oak.plugins.index.search.IndexDefinition;
+import org.apache.jackrabbit.oak.plugins.index.search.spi.editor.FulltextDocumentMaker;
+import org.apache.jackrabbit.oak.spi.state.NodeBuilder;
+import org.apache.jackrabbit.oak.spi.state.NodeState;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.slf4j.LoggerFactory;
+
+import java.io.IOException;
+import java.security.InvalidParameterException;
+
+import static java.util.Arrays.asList;
+import static org.apache.jackrabbit.oak.InitialContentHelper.INITIAL_CONTENT;
+import static org.apache.jackrabbit.oak.plugins.memory.EmptyNodeState.EMPTY_NODE;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertTrue;
+
+public class ElasticDocumentMakerLargeStringPropertiesLogTest {
+
+    ListAppender<ILoggingEvent> listAppender = null;
+    private final String nodeImplLogger = ElasticDocumentMaker.class.getName();
+    private final String warnMessage = "String length: {} for property: {} at Node: {} is greater than configured value {}";
+    private String customStringPropertyThresholdLimit = "9";
+    private String smallStringProperty = "1234567";
+    private String largeStringPropertyAsPerCustomThreshold = "1234567890";
+
+    @Rule
+    public TemporarySystemProperty temporarySystemProperty = new TemporarySystemProperty();
+
+    @Before
+    public void loggingAppenderStart() {
+        LoggerContext context = (LoggerContext) LoggerFactory.getILoggerFactory();
+        listAppender = new ListAppender<>();
+        listAppender.start();
+        context.getLogger(nodeImplLogger).addAppender(listAppender);
+    }
+
+    @After
+    public void loggingAppenderStop() {
+        listAppender.stop();
+    }
+
+    private void setThresholdLimit(String threshold) {
+        System.setProperty(FulltextDocumentMaker.WARN_LOG_STRING_SIZE_THRESHOLD_KEY, threshold);
+    }
+
+    private ElasticDocumentMaker addPropertyAccordingToType(NodeBuilder test, Type type, String... str) throws IOException {
+        NodeState root = INITIAL_CONTENT;
+        ElasticIndexDefinitionBuilder builder = new ElasticIndexDefinitionBuilder();
+        builder.indexRule("nt:base")
+                .property("foo")
+                .propertyIndex()
+                .analyzed()
+                .valueExcludedPrefixes("/jobs");
+
+        IndexDefinition defn = ElasticIndexDefinition.newBuilder(root, builder.build(), "/foo").build();
+        ElasticDocumentMaker docMaker = new ElasticDocumentMaker(null, defn,
+                defn.getApplicableIndexingRule("nt:base"), "/x");
+
+        if (Type.STRINGS == type) {
+            test.setProperty("foo", asList(str), Type.STRINGS);
+        } else if (Type.STRING == type && str.length == 1) {
+            test.setProperty("foo", str[0]);
+        } else {
+            throw new InvalidParameterException();
+        }
+        return docMaker;
+    }
+
+    @Test
+    public void testDefaultThreshold() throws IOException {
+        NodeBuilder test = EMPTY_NODE.builder();
+        ElasticDocumentMaker docMaker = addPropertyAccordingToType(test, Type.STRING, largeStringPropertyAsPerCustomThreshold);
+        assertNotNull(docMaker.makeDocument(test.getNodeState()));
+        assertFalse(isWarnMessagePresent(listAppender));
+    }
+
+    @Test
+    public void testNoLoggingOnAddingSmallStringWithCustomThreshold() throws IOException {
+        setThresholdLimit(customStringPropertyThresholdLimit);
+        NodeBuilder test = EMPTY_NODE.builder();
+        ElasticDocumentMaker docMaker = addPropertyAccordingToType(test, Type.STRING, smallStringProperty);
+        assertNotNull(docMaker.makeDocument(test.getNodeState()));
+        assertFalse(isWarnMessagePresent(listAppender));
+    }
+
+    @Test
+    public void testNoLoggingOnAddingSmallStringArrayWithCustomThreshold() throws IOException {
+        setThresholdLimit(customStringPropertyThresholdLimit);
+        NodeBuilder test = EMPTY_NODE.builder();
+        ElasticDocumentMaker docMaker = addPropertyAccordingToType(test, Type.STRINGS, smallStringProperty, smallStringProperty);
+        assertNotNull(docMaker.makeDocument(test.getNodeState()));
+        assertFalse(isWarnMessagePresent(listAppender));
+    }
+
+    @Test
+    public void testNoLoggingOnAddingSmallStringArrayWithoutCustomThreshold() throws IOException {
+        NodeBuilder test = EMPTY_NODE.builder();
+        ElasticDocumentMaker docMaker = addPropertyAccordingToType(test, Type.STRINGS, smallStringProperty, smallStringProperty);
+        assertNotNull(docMaker.makeDocument(test.getNodeState()));
+        assertFalse(isWarnMessagePresent(listAppender));
+    }
+
+    @Test
+    public void testLoggingOnAddingLargeStringWithCustomThreshold() throws IOException {
+        setThresholdLimit(customStringPropertyThresholdLimit);
+        NodeBuilder test = EMPTY_NODE.builder();
+        ElasticDocumentMaker docMaker = addPropertyAccordingToType(test, Type.STRING, largeStringPropertyAsPerCustomThreshold);
+        assertNotNull(docMaker.makeDocument(test.getNodeState()));
+        assertTrue(isWarnMessagePresent(listAppender));
+    }
+
+    @Test
+    public void testLoggingOnAddingLargeStringWithoutCustomThreshold() throws IOException {
+        //  setThresholdLimit(null);
+        NodeBuilder test = EMPTY_NODE.builder();
+        ElasticDocumentMaker docMaker = addPropertyAccordingToType(test, Type.STRING, smallStringProperty);
+        assertNotNull(docMaker.makeDocument(test.getNodeState()));
+        assertFalse(isWarnMessagePresent(listAppender));
+    }
+
+    @Test
+    public void testLoggingOnAddingLargeStringArrayOneLargePropertyWithoutCustomThreshold() throws IOException {
+        NodeBuilder test = EMPTY_NODE.builder();
+        ElasticDocumentMaker docMaker = addPropertyAccordingToType(test, Type.STRINGS, largeStringPropertyAsPerCustomThreshold, smallStringProperty);
+        assertNotNull(docMaker.makeDocument(test.getNodeState()));
+        assertFalse(isWarnMessagePresent(listAppender));
+    }
+
+    @Test
+    public void testLoggingOnAddingLargeStringArrayOneLargePropertyWithCustomThreshold() throws IOException {
+        setThresholdLimit(customStringPropertyThresholdLimit);
+        NodeBuilder test = EMPTY_NODE.builder();
+        ElasticDocumentMaker docMaker = addPropertyAccordingToType(test, Type.STRINGS, largeStringPropertyAsPerCustomThreshold, smallStringProperty);
+        assertNotNull(docMaker.makeDocument(test.getNodeState()));
+        assertTrue(isWarnMessagePresent(listAppender));
+        assertEquals(2, listAppender.list.size());
+    }
+
+    @Test
+    public void testLoggingOnAddingLargeStringArrayTwoLargePropertiesWithCustomThreshold() throws IOException {
+        setThresholdLimit(customStringPropertyThresholdLimit);
+        NodeBuilder test = EMPTY_NODE.builder();
+        ElasticDocumentMaker docMaker = addPropertyAccordingToType(test, Type.STRINGS, largeStringPropertyAsPerCustomThreshold, largeStringPropertyAsPerCustomThreshold);
+        assertNotNull(docMaker.makeDocument(test.getNodeState()));
+        assertTrue(isWarnMessagePresent(listAppender));
+        // number of logs equal twice the number of large properties once for fulltext indexing
+        // and once for property indexing.
+        assertEquals(4, listAppender.list.size());
+    }
+
+    private boolean isWarnMessagePresent(ListAppender<ILoggingEvent> listAppender) {
+        for (ILoggingEvent loggingEvent : listAppender.list) {
+            if (loggingEvent.getMessage().contains(warnMessage)) {
+                return true;
+            }
+        }
+        return false;
+    }
+
+}
\ No newline at end of file

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

Modified: jackrabbit/oak/trunk/oak-search/pom.xml
URL: http://svn.apache.org/viewvc/jackrabbit/oak/trunk/oak-search/pom.xml?rev=1880807&r1=1880806&r2=1880807&view=diff
==============================================================================
--- jackrabbit/oak/trunk/oak-search/pom.xml (original)
+++ jackrabbit/oak/trunk/oak-search/pom.xml Wed Aug 12 13:46:45 2020
@@ -182,6 +182,11 @@
             <type>test-jar</type>
             <scope>test</scope>
         </dependency>
+        <dependency>
+            <groupId>ch.qos.logback</groupId>
+            <artifactId>logback-classic</artifactId>
+            <scope>test</scope>
+        </dependency>
 
     </dependencies>
 </project>

Added: jackrabbit/oak/trunk/oak-search/src/test/java/org/apache/jackrabbit/oak/plugins/index/FunctionIndexCommonTest.java
URL: http://svn.apache.org/viewvc/jackrabbit/oak/trunk/oak-search/src/test/java/org/apache/jackrabbit/oak/plugins/index/FunctionIndexCommonTest.java?rev=1880807&view=auto
==============================================================================
--- jackrabbit/oak/trunk/oak-search/src/test/java/org/apache/jackrabbit/oak/plugins/index/FunctionIndexCommonTest.java (added)
+++ jackrabbit/oak/trunk/oak-search/src/test/java/org/apache/jackrabbit/oak/plugins/index/FunctionIndexCommonTest.java Wed Aug 12 13:46:45 2020
@@ -0,0 +1,1188 @@
+/*
+ * 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 com.google.common.collect.Iterables;
+import com.google.common.collect.Lists;
+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.commons.junit.LogCustomizer;
+import org.apache.jackrabbit.oak.plugins.index.search.FulltextIndexConstants;
+import org.apache.jackrabbit.oak.plugins.index.search.util.IndexDefinitionBuilder;
+import org.apache.jackrabbit.oak.query.AbstractQueryTest;
+import org.junit.Assert;
+import org.junit.Test;
+import org.slf4j.event.Level;
+
+import javax.jcr.PropertyType;
+import java.text.ParseException;
+import java.util.Collections;
+import java.util.List;
+import java.util.Set;
+
+import static java.util.Arrays.asList;
+import static org.apache.jackrabbit.oak.api.QueryEngine.NO_BINDINGS;
+import static org.hamcrest.CoreMatchers.containsString;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertThat;
+
+public abstract class FunctionIndexCommonTest extends AbstractQueryTest {
+
+    protected IndexOptions indexOptions;
+    protected TestRepository repositoryOptionsUtil;
+
+    @Test
+    public void noIndexTest() throws Exception {
+        Tree test = root.getTree("/").addChild("test");
+        for (int idx = 0; idx < 3; idx++) {
+            Tree low = test.addChild("" + (char) ('a' + idx));
+            low.setProperty("jcr:primaryType", "nt:unstructured", Type.NAME);
+            Tree up = test.addChild("" + (char) ('A' + idx));
+            up.setProperty("jcr:primaryType", "nt:unstructured", Type.NAME);
+        }
+        root.commit();
+
+        String query = "select [jcr:path] from [nt:base] where lower(localname()) = 'b'";
+        assertThat(explain(query), containsString("traverse"));
+        assertQuery(query, Lists.newArrayList("/test/b", "/test/B"));
+
+        String queryXPath = "/jcr:root/test//*[fn:lower-case(fn:local-name()) = 'b']";
+        assertThat(explainXpath(queryXPath), containsString("traverse"));
+        assertQuery(queryXPath, "xpath", Lists.newArrayList("/test/b", "/test/B"));
+
+        queryXPath = "/jcr:root/test//*[fn:lower-case(fn:local-name()) > 'b']";
+        assertThat(explainXpath(queryXPath), containsString("traverse"));
+        assertQuery(queryXPath, "xpath", Lists.newArrayList("/test/c", "/test/C"));
+
+        query = "select [jcr:path] from [nt:base] where lower(localname()) = 'B'";
+        assertThat(explain(query), containsString("traverse"));
+        assertQuery(query, Lists.<String>newArrayList());
+    }
+
+    @Test
+    public void lowerCaseLocalName() throws Exception {
+        Tree luceneIndex = createIndex("lowerLocalName", Collections.<String>emptySet());
+        luceneIndex.setProperty("excludedPaths",
+                Lists.newArrayList("/jcr:system", "/oak:index"), Type.STRINGS);
+        Tree func = luceneIndex.addChild(FulltextIndexConstants.INDEX_RULES)
+                .addChild("nt:base")
+                .addChild(FulltextIndexConstants.PROP_NODE)
+                .addChild("lowerLocalName");
+        func.setProperty(FulltextIndexConstants.PROP_FUNCTION, "lower(localname())");
+
+        Tree test = root.getTree("/").addChild("test");
+        for (int idx = 0; idx < 3; idx++) {
+            Tree low = test.addChild("" + (char) ('a' + idx));
+            low.setProperty("jcr:primaryType", "nt:unstructured", Type.NAME);
+            Tree up = test.addChild("" + (char) ('A' + idx));
+            up.setProperty("jcr:primaryType", "nt:unstructured", Type.NAME);
+        }
+        root.commit();
+
+        String query = "select [jcr:path] from [nt:base] where lower(localname()) = 'b'";
+        assertThat(explain(query), containsString("lucene:lowerLocalName"));
+        assertQuery(query, Lists.newArrayList("/test/b", "/test/B"));
+
+        String queryXPath = "/jcr:root//*[fn:lower-case(fn:local-name()) = 'b']";
+        assertThat(explainXpath(queryXPath), containsString("lucene:lowerLocalName"));
+        assertQuery(queryXPath, "xpath", Lists.newArrayList("/test/b", "/test/B"));
+
+        queryXPath = "/jcr:root//*[fn:lower-case(fn:local-name()) > 'b']";
+        assertThat(explainXpath(queryXPath), containsString("lucene:lowerLocalName"));
+        assertQuery(queryXPath, "xpath", Lists.newArrayList("/test/c", "/test/C", "/test"));
+
+        query = "select [jcr:path] from [nt:base] where lower(localname()) = 'B'";
+        assertThat(explain(query), containsString("lucene:lowerLocalName"));
+        assertQuery(query, Lists.<String>newArrayList());
+    }
+
+    @Test
+    public void lengthName() throws Exception {
+        Tree luceneIndex = createIndex("lengthName", Collections.<String>emptySet());
+        luceneIndex.setProperty("excludedPaths",
+                Lists.newArrayList("/jcr:system", "/oak:index"), Type.STRINGS);
+        Tree func = luceneIndex.addChild(FulltextIndexConstants.INDEX_RULES)
+                .addChild("nt:base")
+                .addChild(FulltextIndexConstants.PROP_NODE)
+                .addChild("lengthName");
+        func.setProperty(FulltextIndexConstants.PROP_ORDERED, true);
+        func.setProperty(FulltextIndexConstants.PROP_TYPE, PropertyType.TYPENAME_LONG);
+        func.setProperty(FulltextIndexConstants.PROP_FUNCTION, "fn:string-length(fn:name())");
+
+        Tree test = root.getTree("/").addChild("test");
+        for (int idx = 1; idx < 1000; idx *= 10) {
+            Tree testNode = test.addChild("test" + idx);
+            testNode.setProperty("jcr:primaryType", "nt:unstructured", Type.NAME);
+        }
+        root.commit();
+
+        String query = "select [jcr:path] from [nt:base] where length(name()) = 6";
+        assertThat(explain(query), containsString("lucene:lengthName"));
+        assertQuery(query, Lists.newArrayList("/test/test10"));
+
+        String queryXPath = "/jcr:root//*[fn:string-length(fn:name()) = 7]";
+        assertThat(explainXpath(queryXPath), containsString("lucene:lengthName"));
+        assertQuery(queryXPath, "xpath", Lists.newArrayList("/test/test100"));
+
+        queryXPath = "/jcr:root//* order by fn:string-length(fn:name())";
+        assertThat(explainXpath(queryXPath), containsString("lucene:lengthName"));
+        assertQuery(queryXPath, "xpath", Lists.newArrayList(
+                "/test", "/test/test1", "/test/test10", "/test/test100"));
+    }
+
+    @Test
+    public void length() throws Exception {
+        Tree luceneIndex = createIndex("length", Collections.<String>emptySet());
+        luceneIndex.setProperty("excludedPaths",
+                Lists.newArrayList("/jcr:system", "/oak:index"), Type.STRINGS);
+        Tree func = luceneIndex.addChild(FulltextIndexConstants.INDEX_RULES)
+                .addChild("nt:base")
+                .addChild(FulltextIndexConstants.PROP_NODE)
+                .addChild("lengthName");
+        func.setProperty(FulltextIndexConstants.PROP_FUNCTION, "fn:string-length(@value)");
+
+        Tree test = root.getTree("/").addChild("test");
+        for (int idx = 1; idx <= 1000; idx *= 10) {
+            Tree testNode = test.addChild("test" + idx);
+            testNode.setProperty("jcr:primaryType", "nt:unstructured", Type.NAME);
+            testNode.setProperty("value", new byte[idx]);
+        }
+        root.commit();
+
+        String query = "select [jcr:path] from [nt:base] where length([value]) = 100";
+        assertThat(explain(query), containsString("lucene:length"));
+        assertQuery(query, Lists.newArrayList("/test/test100"));
+
+        String queryXPath = "/jcr:root//*[fn:string-length(@value) = 10]";
+        assertThat(explainXpath(queryXPath), containsString("lucene:length"));
+        assertQuery(queryXPath, "xpath", Lists.newArrayList("/test/test10"));
+    }
+
+    @Test
+    public void upperCase() throws Exception {
+        Tree luceneIndex = createIndex("upper", Collections.<String>emptySet());
+        Tree func = luceneIndex.addChild(FulltextIndexConstants.INDEX_RULES)
+                .addChild("nt:base")
+                .addChild(FulltextIndexConstants.PROP_NODE)
+                .addChild("upperName");
+        func.setProperty(FulltextIndexConstants.PROP_FUNCTION, "fn:upper-case(@name)");
+
+        Tree test = root.getTree("/").addChild("test");
+        test.setProperty("jcr:primaryType", "nt:unstructured", Type.NAME);
+
+        List<String> paths = Lists.newArrayList();
+        for (int idx = 0; idx < 15; idx++) {
+            Tree a = test.addChild("n" + idx);
+            a.setProperty("jcr:primaryType", "nt:unstructured", Type.NAME);
+            a.setProperty("name", "10% foo");
+            paths.add("/test/n" + idx);
+        }
+        root.commit();
+
+        String query = "select [jcr:path] from [nt:unstructured] where upper([name]) = '10% FOO'";
+        assertThat(explain(query), containsString("lucene:upper"));
+        assertQuery(query, paths);
+
+        query = "select [jcr:path] from [nt:unstructured] where upper([name]) like '10\\% FOO'";
+        assertThat(explain(query), containsString("lucene:upper"));
+        assertQuery(query, paths);
+    }
+
+    @Test
+    public void testOrdering2() throws Exception {
+        Tree index = root.getTree("/");
+        Tree indexDefn = createTestIndexNode(index, indexOptions.getIndexType());
+        TestUtil.useV2(indexDefn);
+        indexDefn.setProperty(FulltextIndexConstants.EVALUATE_PATH_RESTRICTION, true);
+        Tree props = TestUtil.newRulePropTree(indexDefn, "nt:unstructured");
+        props.getParent().setProperty(FulltextIndexConstants.INDEX_NODE_NAME, true);
+        TestUtil.enableForFullText(props, FulltextIndexConstants.REGEX_ALL_PROPS, true);
+        Tree upper = TestUtil.enableFunctionIndex(props, "upper([foo])");
+        upper.setProperty(FulltextIndexConstants.PROP_ORDERED, true);
+
+        root.commit();
+
+        Tree test = root.getTree("/").addChild("test");
+        test.setProperty("jcr:primaryType", "nt:unstructured", Type.NAME);
+
+
+        Tree a = test.addChild("n1");
+        a.setProperty("jcr:primaryType", "nt:unstructured", Type.NAME);
+        a.setProperty("foo", "hello");
+
+        a = test.addChild("n2");
+        a.setProperty("jcr:primaryType", "nt:unstructured", Type.NAME);
+        a.setProperty("foo", "World!");
+        a = test.addChild("n3");
+        a.setProperty("jcr:primaryType", "nt:unstructured", Type.NAME);
+        a.setProperty("foo", "Hallo");
+        a = test.addChild("n4");
+        a.setProperty("jcr:primaryType", "nt:unstructured", Type.NAME);
+        a.setProperty("foo", "10%");
+        a = test.addChild("n5");
+        a.setProperty("jcr:primaryType", "nt:unstructured", Type.NAME);
+        a.setProperty("foo", "10 percent");
+
+        a = test.addChild("n0");
+        a.setProperty("jcr:primaryType", "nt:unstructured", Type.NAME);
+        a = test.addChild("n9");
+        a.setProperty("jcr:primaryType", "nt:unstructured", Type.NAME);
+
+        String query = "select a.[foo]\n" +
+                "\t  from [nt:unstructured] as a\n" +
+                "\t  where a.foo is not null and isdescendantnode(a , '/test') order by upper(a.foo)";
+
+        root.commit();
+
+        assertThat(explain(query), containsString("lucene:test-index(/oak:index/test-index)"));
+
+        List<String> result = executeQuery(query, SQL2);
+        assertEquals("Ordering doesn't match", asList("10 percent", "10%", "Hallo", "hello", "World!"), result);
+
+    }
+
+
+    /*
+    Test order by func(a),func(b)
+    order by func(b),func(a)
+    func(a) DESC,func(b)
+    func(a),func(b)DESC
+    where both func(a) and func(b) have ordered set = true
+    Correct ordering is effectively served by the index
+     */
+    @Test
+    public void testOrdering3() throws Exception {
+
+        Tree index = root.getTree("/");
+        Tree indexDefn = createTestIndexNode(index, indexOptions.getIndexType());
+        TestUtil.useV2(indexDefn);
+        indexDefn.setProperty(FulltextIndexConstants.EVALUATE_PATH_RESTRICTION, true);
+        Tree props = TestUtil.newRulePropTree(indexDefn, "nt:unstructured");
+        props.getParent().setProperty(FulltextIndexConstants.INDEX_NODE_NAME, true);
+        TestUtil.enableForFullText(props, FulltextIndexConstants.REGEX_ALL_PROPS, true);
+
+        Tree upper = TestUtil.enableFunctionIndex(props, "upper([foo])");
+        upper.setProperty(FulltextIndexConstants.PROP_ORDERED, true);
+
+        Tree upper2 = TestUtil.enableFunctionIndex(props, "upper([foo2])");
+        upper2.setProperty(FulltextIndexConstants.PROP_ORDERED, true);
+
+
+        root.commit();
+
+        Tree test = root.getTree("/").addChild("test");
+        test.setProperty("jcr:primaryType", "nt:unstructured", Type.NAME);
+
+
+        Tree a = test.addChild("n1");
+        a.setProperty("jcr:primaryType", "nt:unstructured", Type.NAME);
+        a.setProperty("foo", "a1");
+        a.setProperty("foo2", "b2");
+
+        a = test.addChild("n2");
+        a.setProperty("jcr:primaryType", "nt:unstructured", Type.NAME);
+        a.setProperty("foo", "a2");
+        a.setProperty("foo2", "b3");
+
+        a = test.addChild("n3");
+        a.setProperty("jcr:primaryType", "nt:unstructured", Type.NAME);
+        a.setProperty("foo", "a3");
+        a.setProperty("foo2", "b1");
+
+        a = test.addChild("n4");
+        a.setProperty("jcr:primaryType", "nt:unstructured", Type.NAME);
+        a.setProperty("foo", "a1");
+        a.setProperty("foo2", "b3");
+
+        a = test.addChild("n5");
+        a.setProperty("jcr:primaryType", "nt:unstructured", Type.NAME);
+        a.setProperty("foo", "a1");
+        a.setProperty("foo2", "b1");
+
+
+        String query = "select a.[foo],a.[foo2]\n" +
+                "\t  from [nt:unstructured] as a\n" +
+                "\t  where a.foo is not null and isdescendantnode(a , '/test') order by upper(a.foo),upper(a.foo2)";
+
+        root.commit();
+
+        List<String> result = executeQuery(query, SQL2);
+
+        assertEquals("Ordering doesn't match", asList("a1, b1", "a1, b2", "a1, b3", "a2, b3", "a3, b1"), result);
+
+        query = "select a.[foo2],a.[foo]\n" +
+                "\t  from [nt:unstructured] as a\n" +
+                "\t  where a.foo is not null and isdescendantnode(a , '/test') order by upper(a.foo2),upper(a.foo)";
+
+        result = executeQuery(query, SQL2);
+
+        assertEquals("Ordering doesn't match", asList("b1, a1", "b1, a3", "b2, a1", "b3, a1", "b3, a2"), result);
+
+        query = "select a.[foo],a.[foo2]\n" +
+                "\t  from [nt:unstructured] as a\n" +
+                "\t  where a.foo is not null and isdescendantnode(a , '/test') order by upper(a.foo) DESC, upper(a.foo2)";
+        result = executeQuery(query, SQL2);
+
+        assertEquals("Ordering doesn't match", asList("a3, b1", "a2, b3", "a1, b1", "a1, b2", "a1, b3"), result);
+
+        query = "select a.[foo],a.[foo2]\n" +
+                "\t  from [nt:unstructured] as a\n" +
+                "\t  where a.foo is not null and isdescendantnode(a , '/test') order by upper(a.foo), upper(a.foo2) DESC";
+
+        result = executeQuery(query, SQL2);
+
+        assertEquals("Ordering doesn't match", asList("a1, b3", "a1, b2", "a1, b1", "a2, b3", "a3, b1"), result);
+
+
+    }
+
+    /*
+    Test order by func(a),func(b)
+    order by func(b),func(a)
+    func(a) DESC,func(b)
+    func(a),func(b)DESC
+    where only func(a) is ordered by index
+    The effective ordering in this case will be done by QueryEngine
+     */
+    @Test
+    public void testOrdering4() throws Exception {
+        Tree index = root.getTree("/");
+        Tree indexDefn = createTestIndexNode(index, indexOptions.getIndexType());
+        TestUtil.useV2(indexDefn);
+        indexDefn.setProperty(FulltextIndexConstants.EVALUATE_PATH_RESTRICTION, true);
+        Tree props = TestUtil.newRulePropTree(indexDefn, "nt:unstructured");
+        props.getParent().setProperty(FulltextIndexConstants.INDEX_NODE_NAME, true);
+        TestUtil.enableForFullText(props, FulltextIndexConstants.REGEX_ALL_PROPS, true);
+
+        Tree upper = TestUtil.enableFunctionIndex(props, "upper([foo])");
+        upper.setProperty(FulltextIndexConstants.PROP_ORDERED, true);
+
+        TestUtil.enableFunctionIndex(props, "upper([foo2])");
+
+
+        root.commit();
+
+        Tree test = root.getTree("/").addChild("test");
+        test.setProperty("jcr:primaryType", "nt:unstructured", Type.NAME);
+
+
+        Tree a = test.addChild("n1");
+        a.setProperty("jcr:primaryType", "nt:unstructured", Type.NAME);
+        a.setProperty("foo", "a1");
+        a.setProperty("foo2", "b2");
+
+        a = test.addChild("n2");
+        a.setProperty("jcr:primaryType", "nt:unstructured", Type.NAME);
+        a.setProperty("foo", "a2");
+        a.setProperty("foo2", "b3");
+
+        a = test.addChild("n3");
+        a.setProperty("jcr:primaryType", "nt:unstructured", Type.NAME);
+        a.setProperty("foo", "a3");
+        a.setProperty("foo2", "b1");
+
+        a = test.addChild("n4");
+        a.setProperty("jcr:primaryType", "nt:unstructured", Type.NAME);
+        a.setProperty("foo", "a1");
+        a.setProperty("foo2", "b3");
+
+        a = test.addChild("n5");
+        a.setProperty("jcr:primaryType", "nt:unstructured", Type.NAME);
+        a.setProperty("foo", "a1");
+        a.setProperty("foo2", "b1");
+
+
+        String query = "select a.[foo],a.[foo2]\n" +
+                "\t  from [nt:unstructured] as a\n" +
+                "\t  where a.foo is not null and isdescendantnode(a , '/test') order by upper(a.foo),upper(a.foo2)";
+
+        root.commit();
+
+        List<String> result = executeQuery(query, SQL2);
+
+        assertEquals("Ordering doesn't match", asList("a1, b1", "a1, b2", "a1, b3", "a2, b3", "a3, b1"), result);
+
+        query = "select a.[foo2],a.[foo]\n" +
+                "\t  from [nt:unstructured] as a\n" +
+                "\t  where a.foo is not null and isdescendantnode(a , '/test') order by upper(a.foo2),upper(a.foo)";
+
+        result = executeQuery(query, SQL2);
+
+        assertEquals("Ordering doesn't match", asList("b1, a1", "b1, a3", "b2, a1", "b3, a1", "b3, a2"), result);
+
+        query = "select a.[foo],a.[foo2]\n" +
+                "\t  from [nt:unstructured] as a\n" +
+                "\t  where a.foo is not null and isdescendantnode(a , '/test') order by upper(a.foo) DESC, upper(a.foo2)";
+        result = executeQuery(query, SQL2);
+
+        assertEquals("Ordering doesn't match", asList("a3, b1", "a2, b3", "a1, b1", "a1, b2", "a1, b3"), result);
+
+        query = "select a.[foo],a.[foo2]\n" +
+                "\t  from [nt:unstructured] as a\n" +
+                "\t  where a.foo is not null and isdescendantnode(a , '/test') order by upper(a.foo), upper(a.foo2) DESC";
+
+        result = executeQuery(query, SQL2);
+
+        assertEquals("Ordering doesn't match", asList("a1, b3", "a1, b2", "a1, b1", "a2, b3", "a3, b1"), result);
+
+    }
+
+    /*
+    Test order by func(a),b
+    order by b,func(a)
+    order by func(a) DESC,b
+    order by func(a),b DESC
+    where both b and func(a) have ordered=true
+     */
+    @Test
+    public void testOrdering5() throws Exception {
+        Tree index = root.getTree("/");
+        Tree indexDefn = createTestIndexNode(index, indexOptions.getIndexType());
+        TestUtil.useV2(indexDefn);
+        indexDefn.setProperty(FulltextIndexConstants.EVALUATE_PATH_RESTRICTION, true);
+        Tree props = TestUtil.newRulePropTree(indexDefn, "nt:unstructured");
+        props.getParent().setProperty(FulltextIndexConstants.INDEX_NODE_NAME, true);
+        TestUtil.enableForFullText(props, FulltextIndexConstants.REGEX_ALL_PROPS, true);
+
+        Tree upper = TestUtil.enableFunctionIndex(props, "upper([foo])");
+        upper.setProperty(FulltextIndexConstants.PROP_ORDERED, true);
+
+
+        Tree upper2 = TestUtil.enablePropertyIndex(props, "foo2", false);
+        upper2.setProperty(FulltextIndexConstants.PROP_ORDERED, true);
+
+
+        root.commit();
+
+        Tree test = root.getTree("/").addChild("test");
+        test.setProperty("jcr:primaryType", "nt:unstructured", Type.NAME);
+
+
+        Tree a = test.addChild("n1");
+        a.setProperty("jcr:primaryType", "nt:unstructured", Type.NAME);
+        a.setProperty("foo", "a1");
+        a.setProperty("foo2", "b2");
+
+        a = test.addChild("n2");
+        a.setProperty("jcr:primaryType", "nt:unstructured", Type.NAME);
+        a.setProperty("foo", "a2");
+        a.setProperty("foo2", "b3");
+
+        a = test.addChild("n3");
+        a.setProperty("jcr:primaryType", "nt:unstructured", Type.NAME);
+        a.setProperty("foo", "a3");
+        a.setProperty("foo2", "b1");
+
+        a = test.addChild("n4");
+        a.setProperty("jcr:primaryType", "nt:unstructured", Type.NAME);
+        a.setProperty("foo", "a1");
+        a.setProperty("foo2", "b3");
+
+        a = test.addChild("n5");
+        a.setProperty("jcr:primaryType", "nt:unstructured", Type.NAME);
+        a.setProperty("foo", "a1");
+        a.setProperty("foo2", "b1");
+
+
+        String query = "select a.[foo],a.[foo2]\n" +
+                "\t  from [nt:unstructured] as a\n" +
+                "\t  where a.foo is not null and isdescendantnode(a , '/test') order by upper(a.foo),a.foo2";
+
+        root.commit();
+
+        List<String> result = executeQuery(query, SQL2);
+
+
+        assertEquals("Ordering doesn't match", asList("a1, b1", "a1, b2", "a1, b3", "a2, b3", "a3, b1"), result);
+
+        query = "select a.[foo2],a.[foo]\n" +
+                "\t  from [nt:unstructured] as a\n" +
+                "\t  where a.foo is not null and isdescendantnode(a , '/test') order by a.foo2,upper(a.foo)";
+
+        result = executeQuery(query, SQL2);
+
+        assertEquals("Ordering doesn't match", asList("b1, a1", "b1, a3", "b2, a1", "b3, a1", "b3, a2"), result);
+
+        query = "select a.[foo],a.[foo2]\n" +
+                "\t  from [nt:unstructured] as a\n" +
+                "\t  where a.foo is not null and isdescendantnode(a , '/test') order by upper(a.foo) DESC, a.foo2";
+        result = executeQuery(query, SQL2);
+
+        assertEquals("Ordering doesn't match", asList("a3, b1", "a2, b3", "a1, b1", "a1, b2", "a1, b3"), result);
+
+        query = "select a.[foo],a.[foo2]\n" +
+                "\t  from [nt:unstructured] as a\n" +
+                "\t  where a.foo is not null and isdescendantnode(a , '/test') order by upper(a.foo), a.foo2 DESC";
+
+        result = executeQuery(query, SQL2);
+
+        assertEquals("Ordering doesn't match", asList("a1, b3", "a1, b2", "a1, b1", "a2, b3", "a3, b1"), result);
+
+    }
+
+    /*
+    Test order by func(a),b
+    orrder by b,func(a)
+    order by func(a) DESC,b
+    order by func(a),b DESC
+    where func(a) does not have ordered = true
+     */
+    @Test
+    public void testOrdering6() throws Exception {
+        Tree index = root.getTree("/");
+        Tree indexDefn = createTestIndexNode(index, indexOptions.getIndexType());
+        TestUtil.useV2(indexDefn);
+        indexDefn.setProperty(FulltextIndexConstants.EVALUATE_PATH_RESTRICTION, true);
+        Tree props = TestUtil.newRulePropTree(indexDefn, "nt:unstructured");
+        props.getParent().setProperty(FulltextIndexConstants.INDEX_NODE_NAME, true);
+        TestUtil.enableForFullText(props, FulltextIndexConstants.REGEX_ALL_PROPS, true);
+
+        TestUtil.enableFunctionIndex(props, "upper([foo])");
+
+
+        Tree upper2 = TestUtil.enablePropertyIndex(props, "foo2", false);
+        upper2.setProperty(FulltextIndexConstants.PROP_ORDERED, true);
+
+
+        root.commit();
+
+        Tree test = root.getTree("/").addChild("test");
+        test.setProperty("jcr:primaryType", "nt:unstructured", Type.NAME);
+
+
+        Tree a = test.addChild("n1");
+        a.setProperty("jcr:primaryType", "nt:unstructured", Type.NAME);
+        a.setProperty("foo", "a1");
+        a.setProperty("foo2", "b2");
+
+        a = test.addChild("n2");
+        a.setProperty("jcr:primaryType", "nt:unstructured", Type.NAME);
+        a.setProperty("foo", "a2");
+        a.setProperty("foo2", "b3");
+
+        a = test.addChild("n3");
+        a.setProperty("jcr:primaryType", "nt:unstructured", Type.NAME);
+        a.setProperty("foo", "a3");
+        a.setProperty("foo2", "b1");
+
+        a = test.addChild("n4");
+        a.setProperty("jcr:primaryType", "nt:unstructured", Type.NAME);
+        a.setProperty("foo", "a1");
+        a.setProperty("foo2", "b3");
+
+        a = test.addChild("n5");
+        a.setProperty("jcr:primaryType", "nt:unstructured", Type.NAME);
+        a.setProperty("foo", "a1");
+        a.setProperty("foo2", "b1");
+
+
+        String query = "select a.[foo],a.[foo2]\n" +
+                "\t  from [nt:unstructured] as a\n" +
+                "\t  where a.foo is not null and isdescendantnode(a , '/test') order by upper(a.foo),a.foo2";
+
+        root.commit();
+
+        List<String> result = executeQuery(query, SQL2);
+
+        assertEquals("Ordering doesn't match", asList("a1, b1", "a1, b2", "a1, b3", "a2, b3", "a3, b1"), result);
+
+        query = "select a.[foo2],a.[foo]\n" +
+                "\t  from [nt:unstructured] as a\n" +
+                "\t  where a.foo is not null and isdescendantnode(a , '/test') order by a.foo2,upper(a.foo)";
+
+        result = executeQuery(query, SQL2);
+
+        assertEquals("Ordering doesn't match", asList("b1, a1", "b1, a3", "b2, a1", "b3, a1", "b3, a2"), result);
+
+        query = "select a.[foo],a.[foo2]\n" +
+                "\t  from [nt:unstructured] as a\n" +
+                "\t  where a.foo is not null and isdescendantnode(a , '/test') order by upper(a.foo) DESC, a.foo2";
+        result = executeQuery(query, SQL2);
+
+        assertEquals("Ordering doesn't match", asList("a3, b1", "a2, b3", "a1, b1", "a1, b2", "a1, b3"), result);
+
+        query = "select a.[foo],a.[foo2]\n" +
+                "\t  from [nt:unstructured] as a\n" +
+                "\t  where a.foo is not null and isdescendantnode(a , '/test') order by upper(a.foo), a.foo2 DESC";
+
+        result = executeQuery(query, SQL2);
+
+        assertEquals("Ordering doesn't match", asList("a1, b3", "a1, b2", "a1, b1", "a2, b3", "a3, b1"), result);
+    }
+
+    /*
+    Testing order by for
+    different function implementations
+     */
+    @Test
+    public void testOrdering7() throws Exception {
+        Tree index = root.getTree("/");
+        Tree indexDefn = createTestIndexNode(index, indexOptions.getIndexType());
+        TestUtil.useV2(indexDefn);
+        indexDefn.setProperty(FulltextIndexConstants.EVALUATE_PATH_RESTRICTION, true);
+        Tree props = TestUtil.newRulePropTree(indexDefn, "nt:unstructured");
+        props.getParent().setProperty(FulltextIndexConstants.INDEX_NODE_NAME, true);
+        TestUtil.enableForFullText(props, FulltextIndexConstants.REGEX_ALL_PROPS, true);
+
+        Tree fn = TestUtil.enableFunctionIndex(props, "upper([foo])");
+        fn.setProperty(FulltextIndexConstants.PROP_ORDERED, true);
+
+        fn = TestUtil.enableFunctionIndex(props, "lower([foo])");
+        fn.setProperty(FulltextIndexConstants.PROP_ORDERED, true);
+
+        fn = TestUtil.enableFunctionIndex(props, "length([foo])");
+        fn.setProperty(FulltextIndexConstants.PROP_ORDERED, true);
+        // Any function property trying to sory by length needs to explicitly set the type to Long
+        fn.setProperty(FulltextIndexConstants.PROP_TYPE, "Long");
+
+        fn = TestUtil.enableFunctionIndex(props, "coalesce([foo2],[foo])");
+        fn.setProperty(FulltextIndexConstants.PROP_ORDERED, true);
+
+        fn = TestUtil.enableFunctionIndex(props, "name()");
+        fn.setProperty(FulltextIndexConstants.PROP_ORDERED, true);
+
+        fn = TestUtil.enableFunctionIndex(props, "localname()");
+        fn.setProperty(FulltextIndexConstants.PROP_ORDERED, true);
+
+        fn = TestUtil.enableFunctionIndex(props, "lower(coalesce([foo2], coalesce([foo], localname())))");
+        fn.setProperty(FulltextIndexConstants.PROP_ORDERED, true);
+
+        fn = TestUtil.enableFunctionIndex(props, "length(coalesce([foo], coalesce([foo2], localname())))");
+        fn.setProperty(FulltextIndexConstants.PROP_ORDERED, true);
+        fn.setProperty(FulltextIndexConstants.PROP_TYPE, "Long");
+
+        root.commit();
+
+        Tree test = root.getTree("/").addChild("test");
+        test.setProperty("jcr:primaryType", "nt:unstructured", Type.NAME);
+
+
+        Tree a = test.addChild("d1");
+        a.setProperty("jcr:primaryType", "nt:unstructured", Type.NAME);
+        a.setProperty("foo", "c");
+
+        a = test.addChild("d2");
+        a.setProperty("jcr:primaryType", "nt:unstructured", Type.NAME);
+        a.setProperty("foo", "bbbb");
+        a.setProperty("foo2", "22");
+
+
+        a = test.addChild("d3");
+        a.setProperty("jcr:primaryType", "nt:unstructured", Type.NAME);
+        a.setProperty("foo", "aa");
+
+        a = test.addChild("jcr:content");
+        a.setProperty("jcr:primaryType", "nt:unstructured", Type.NAME);
+        a.setProperty("foo", "test");
+        a.setProperty("foo2", "11");
+
+        root.commit();
+
+        String query = "select [jcr:path]\n" +
+                "\t  from [nt:unstructured] as a\n" +
+                "\t  where  isdescendantnode(a , '/test') order by coalesce([foo2],[foo]) ";
+
+        List<String> result = executeQuery(query, SQL2);
+
+        assertEquals("Ordering doesn't match", asList("/test/jcr:content", "/test/d2", "/test/d3", "/test/d1"), result);
+
+        query = "select a.[foo]\n" +
+                "\t  from [nt:unstructured] as a\n" +
+                "\t  where  isdescendantnode(a , '/test') order by lower([a].[foo])";
+
+        result = executeQuery(query, SQL2);
+
+        assertEquals("Ordering doesn't match", asList("aa", "bbbb", "c", "test"), result);
+
+        query = "select [jcr:path]\n" +
+                "\t  from [nt:unstructured] as a\n" +
+                "\t  where  isdescendantnode(a , '/test') order by localname() ";
+
+        result = executeQuery(query, SQL2);
+
+        assertEquals("Ordering doesn't match", asList("/test/jcr:content", "/test/d1", "/test/d2", "/test/d3"), result);
+
+
+        query = "select [jcr:path]\n" +
+                "\t  from [nt:unstructured] as a\n" +
+                "\t  where  isdescendantnode(a , '/test') order by name() ";
+
+        result = executeQuery(query, SQL2);
+
+        assertEquals("Ordering doesn't match", asList("/test/d1", "/test/d2", "/test/d3", "/test/jcr:content"), result);
+
+        query = "select [jcr:path]\n" +
+                "\t  from [nt:unstructured] as a\n" +
+                "\t  where  isdescendantnode(a , '/test') order by lower(coalesce([a].[foo2], coalesce([a].[foo], localname())))";
+
+        result = executeQuery(query, SQL2);
+
+        assertEquals("Ordering doesn't match", asList("/test/jcr:content", "/test/d2", "/test/d3", "/test/d1"), result);
+
+        query = "select [jcr:path]\n" +
+                "\t  from [nt:unstructured] as a\n" +
+                "\t  where  isdescendantnode(a , '/test') order by lower(coalesce([a].[foo2], coalesce([a].[foo], localname()))) DESC";
+
+        result = executeQuery(query, SQL2);
+
+        assertEquals("Ordering doesn't match", asList("/test/d1", "/test/d3", "/test/d2", "/test/jcr:content"), result);
+
+        query = "select [jcr:path]\n" +
+                "\t  from [nt:unstructured] as a\n" +
+                "\t  where  a.[foo] is not null AND isdescendantnode(a , '/test') order by length([a].[foo]) DESC, localname()";
+
+        result = executeQuery(query, SQL2);
+
+        assertEquals("Ordering doesn't match", asList("/test/jcr:content", "/test/d2", "/test/d3", "/test/d1"), result);
+
+        query = "select [jcr:path]\n" +
+                "\t  from [nt:unstructured] as a\n" +
+                "\t  where  a.[foo] is not null AND isdescendantnode(a , '/test') order by length([a].[foo]), localname()";
+
+        result = executeQuery(query, SQL2);
+
+        assertEquals("Ordering doesn't match", asList("/test/d1", "/test/d3", "/test/jcr:content", "/test/d2"), result);
+
+
+        query = "select [jcr:path]\n" +
+                "\t  from [nt:unstructured] as a\n" +
+                "\t  where  a.[foo] is not null AND isdescendantnode(a , '/test') order by length(coalesce([foo], coalesce([foo2], localname()))), localname() DESC";
+
+        result = executeQuery(query, SQL2);
+
+        assertEquals("Ordering doesn't match", asList("/test/d1", "/test/d3", "/test/d2", "/test/jcr:content"), result);
+
+
+    }
+
+    @Test
+    public void testOrdering() throws Exception {
+        Tree luceneIndex = createIndex("upper", Collections.<String>emptySet());
+        Tree nonFunc = luceneIndex.addChild(FulltextIndexConstants.INDEX_RULES)
+                .addChild("nt:base")
+                .addChild(FulltextIndexConstants.PROP_NODE)
+                .addChild("foo");
+        nonFunc.setProperty(FulltextIndexConstants.PROP_ORDERED, true);
+        nonFunc.setProperty(FulltextIndexConstants.PROP_PROPERTY_INDEX, true);
+        nonFunc.setProperty("name", "foo");
+
+        Tree func = luceneIndex.getChild(FulltextIndexConstants.INDEX_RULES)
+                .getChild("nt:base")
+                .getChild(FulltextIndexConstants.PROP_NODE)
+                .addChild("fooUpper");
+        func.setProperty(FulltextIndexConstants.PROP_ORDERED, true);
+        func.setProperty(FulltextIndexConstants.PROP_FUNCTION, "fn:upper-case(@foo)");
+        func.setProperty(FulltextIndexConstants.PROP_PROPERTY_INDEX, true);
+
+        root.commit();
+
+        Tree test = root.getTree("/").addChild("test");
+        test.setProperty("jcr:primaryType", "nt:unstructured", Type.NAME);
+
+        List<String> paths = Lists.newArrayList();
+        for (int idx = 0; idx < 10; idx++) {
+            paths.add("/test/n" + idx);
+            if (idx % 2 == 0) continue;
+            Tree a = test.addChild("n" + idx);
+            a.setProperty("jcr:primaryType", "nt:unstructured", Type.NAME);
+            a.setProperty("foo", "bar" + idx);
+
+        }
+        for (int idx = 0; idx < 10; idx++) {
+            if (idx % 2 != 0) continue;
+            Tree a = test.addChild("n" + idx);
+            a.setProperty("jcr:primaryType", "nt:unstructured", Type.NAME);
+            a.setProperty("foo", "bar" + idx);
+        }
+        root.commit();
+
+        String query = "/jcr:root//element(*, nt:unstructured) [jcr:like(fn:upper-case(@foo),'BAR%')] order by foo";
+        assertThat(explainXpath(query), containsString("lucene:upper"));
+        List<String> result = assertQuery(query, "xpath", paths);
+        assertEquals("Ordering doesn't match", paths, result);
+
+
+        query = "/jcr:root//element(*, nt:unstructured) [jcr:like(fn:upper-case(@foo),'BAR%')] order by fn:upper-case(@foo)";
+        assertThat(explainXpath(query), containsString("lucene:upper"));
+        List<String> result2 = assertQuery(query, "xpath", paths);
+        assertEquals("Ordering doesn't match", paths, result2);
+    }
+
+    @Test
+    public void upperCaseRelative() throws Exception {
+        Tree luceneIndex = createIndex("upper", Collections.<String>emptySet());
+        Tree func = luceneIndex.addChild(FulltextIndexConstants.INDEX_RULES)
+                .addChild("nt:base")
+                .addChild(FulltextIndexConstants.PROP_NODE)
+                .addChild("upperName");
+        func.setProperty(FulltextIndexConstants.PROP_FUNCTION, "upper([data/name])");
+
+        Tree test = root.getTree("/").addChild("test");
+        test.setProperty("jcr:primaryType", "nt:unstructured", Type.NAME);
+
+        List<String> paths = Lists.newArrayList();
+        for (int idx = 0; idx < 15; idx++) {
+            Tree a = test.addChild("n" + idx);
+            a.setProperty("jcr:primaryType", "nt:unstructured", Type.NAME);
+            Tree b = a.addChild("data");
+            b.setProperty("jcr:primaryType", "nt:unstructured", Type.NAME);
+            b.setProperty("name", "foo");
+            paths.add("/test/n" + idx);
+        }
+        root.commit();
+
+        String query = "select [jcr:path] from [nt:unstructured] where upper([data/name]) = 'FOO'";
+        assertThat(explain(query), containsString("lucene:upper"));
+        assertQuery(query, paths);
+
+        String queryXPath = "/jcr:root//element(*, nt:unstructured)[fn:upper-case(data/@name) = 'FOO']";
+        assertThat(explainXpath(queryXPath), containsString("lucene:upper"));
+        assertQuery(queryXPath, "xpath", paths);
+
+        for (int idx = 0; idx < 15; idx++) {
+            Tree a = test.getChild("n" + idx);
+            Tree b = a.getChild("data");
+            b.setProperty("name", "bar");
+        }
+        root.commit();
+
+        query = "select [jcr:path] from [nt:unstructured] where upper([data/name]) = 'BAR'";
+        assertThat(explain(query), containsString("lucene:upper"));
+        assertQuery(query, paths);
+
+        queryXPath = "/jcr:root//element(*, nt:unstructured)[fn:upper-case(data/@name) = 'BAR']";
+        assertThat(explainXpath(queryXPath), containsString("lucene:upper"));
+        assertQuery(queryXPath, "xpath", paths);
+    }
+
+
+    @Test
+    public void coalesceOrdering() throws Exception {
+
+        IndexDefinitionBuilder idxb = indexOptions.createIndexDefinitionBuilder().noAsync();
+        idxb.indexRule("nt:base").property("foo", null).function(
+                "coalesce([jcr:content/foo2], [jcr:content/foo])"
+        ).ordered();
+
+        Tree idx = root.getTree("/").getChild("oak:index").addChild("test1");
+        idxb.build(idx);
+        root.commit();
+
+        Tree rootTree = root.getTree("/");
+        rootTree.addChild("a").addChild("jcr:content").setProperty("foo2", "a");
+        rootTree.addChild("b").addChild("jcr:content").setProperty("foo", "b");
+        Tree child = rootTree.addChild("c").addChild("jcr:content");
+        child.setProperty("foo", "c");
+        child.setProperty("foo2", "a1");
+
+        root.commit();
+
+        assertOrderedPlanAndQuery(
+                "select * from [nt:base] order by coalesce([jcr:content/foo2], [jcr:content/foo])",
+                "lucene:test1(/oak:index/test1)", asList("/a", "/c", "/b"));
+
+        assertOrderedPlanAndQuery(
+                "select * from [nt:base] order by coalesce([jcr:content/foo2], [jcr:content/foo]) DESC",
+                "lucene:test1(/oak:index/test1)", asList("/b", "/c", "/a"));
+    }
+
+    @Test
+    public void coalesce() throws Exception {
+        IndexDefinitionBuilder idxb = indexOptions.createIndexDefinitionBuilder().noAsync();
+        idxb.indexRule("nt:base").property("foo", null).function(
+                "lower(coalesce([jcr:content/foo2], coalesce([jcr:content/foo], localname())))"
+        );
+
+        Tree idx = root.getTree("/").getChild("oak:index").addChild("test1");
+        idxb.build(idx);
+        root.commit();
+
+        Tree rootTree = root.getTree("/");
+        rootTree.addChild("a").addChild("jcr:content").setProperty("foo2", "BAR");
+        rootTree.addChild("b").addChild("jcr:content").setProperty("foo", "bAr");
+        Tree child = rootTree.addChild("c").addChild("jcr:content");
+        child.setProperty("foo", "bar");
+        child.setProperty("foo2", "bar1");
+        rootTree.addChild("bar");
+
+        root.commit();
+
+        assertPlanAndQuery(
+                "select * from [nt:base] where lower(coalesce([jcr:content/foo2], coalesce([jcr:content/foo], localname()))) = 'bar'",
+                "lucene:test1(/oak:index/test1)", asList("/a", "/b", "/bar"));
+    }
+
+    /*
+    Given an index def with 2 orderable property definitions(Relative) for same property - one with function and one without
+    Order by should give correct results
+     */
+    @Test
+    public void sameOrderableRelPropWithAndWithoutFunc_checkOrdering() throws Exception {
+
+        // Index def with same property - ordered - one with function and one without
+        Tree luceneIndex = createIndex("upper", Collections.<String>emptySet());
+        Tree nonFunc = luceneIndex.addChild(FulltextIndexConstants.INDEX_RULES)
+                .addChild("nt:base")
+                .addChild(FulltextIndexConstants.PROP_NODE)
+                .addChild("foo");
+        nonFunc.setProperty(FulltextIndexConstants.PROP_PROPERTY_INDEX, true);
+        nonFunc.setProperty(FulltextIndexConstants.PROP_ORDERED, true);
+        nonFunc.setProperty("name", "jcr:content/n/foo");
+
+        Tree func = luceneIndex.getChild(FulltextIndexConstants.INDEX_RULES)
+                .getChild("nt:base")
+                .getChild(FulltextIndexConstants.PROP_NODE)
+                .addChild("testOak");
+        func.setProperty(FulltextIndexConstants.PROP_ORDERED, true);
+        func.setProperty(FulltextIndexConstants.PROP_FUNCTION, "fn:upper-case(jcr:content/n/@foo)");
+
+        root.commit();
+
+
+        int i = 1;
+        // Create nodes that will be served by the index definition that follows
+        for (String node : asList("a", "c", "b", "e", "d")) {
+
+            Tree test = root.getTree("/").addChild(node);
+            test.setProperty("jcr:primaryType", "nt:unstructured", Type.NAME);
+
+            Tree a = test.addChild("jcr:content");
+            a.setProperty("jcr:primaryType", "nt:unstructured", Type.NAME);
+
+            Tree b = a.addChild("n");
+
+            b.setProperty("jcr:primaryType", "nt:unstructured", Type.NAME);
+            b.setProperty("foo", "bar" + i);
+            i++;
+        }
+
+        root.commit();
+
+
+        // Check ordering works for func and non func properties
+        assertOrderedPlanAndQuery(
+                "select * from [nt:base] order by upper([jcr:content/n/foo])",
+                "lucene:upper(/oak:index/upper)", asList("/a", "/c", "/b", "/e", "/d"));
+
+        assertOrderedPlanAndQuery(
+                "select * from [nt:base] order by [jcr:content/n/foo]",
+                "lucene:upper(/oak:index/upper)", asList("/a", "/c", "/b", "/e", "/d"));
+
+        assertOrderedPlanAndQuery(
+                "select * from [nt:base] order by upper([jcr:content/n/foo]) DESC",
+                "lucene:upper(/oak:index/upper)", asList("/d", "/e", "/b", "/c", "/a"));
+
+        assertOrderedPlanAndQuery(
+                "select * from [nt:base] order by [jcr:content/n/foo] DESC",
+                "lucene:upper(/oak:index/upper)", asList("/d", "/e", "/b", "/c", "/a"));
+
+
+        // Now we change the value of foo on already indexed nodes and see if changes get indexed properly.
+
+        i = 5;
+        for (String node : asList("a", "c", "b", "e", "d")) {
+
+            Tree test = root.getTree("/").getChild(node).getChild("jcr:content").getChild("n");
+
+            test.setProperty("foo", "bar" + i);
+            i--;
+        }
+        root.commit();
+
+        assertOrderedPlanAndQuery(
+                "select * from [nt:base] order by upper([jcr:content/n/foo])",
+                "lucene:upper(/oak:index/upper)", asList("/d", "/e", "/b", "/c", "/a"));
+
+        assertOrderedPlanAndQuery(
+                "select * from [nt:base] order by [jcr:content/n/foo]",
+                "lucene:upper(/oak:index/upper)", asList("/d", "/e", "/b", "/c", "/a"));
+
+        assertOrderedPlanAndQuery(
+                "select * from [nt:base] order by upper([jcr:content/n/foo]) DESC",
+                "lucene:upper(/oak:index/upper)", asList("/a", "/c", "/b", "/e", "/d"));
+
+        assertOrderedPlanAndQuery(
+                "select * from [nt:base] order by [jcr:content/n/foo] DESC",
+                "lucene:upper(/oak:index/upper)", asList("/a", "/c", "/b", "/e", "/d"));
+
+    }
+
+    /*
+Given an index def with 2 orderable property definitions(non-relative) for same property - one with function and one without
+Indexer should index any changes properly and ordering should work as expected.
+*/
+    @Test
+    public void sameOrderablePropertyWithandWithoutFunction() throws Exception {
+        LogCustomizer customLogs = LogCustomizer.forLogger(getLoggerName()).enable(org.slf4j.event.Level.WARN).create();
+        // Create nodes that will be served by the index definition that follows
+        int i = 1;
+        // Create nodes that will be served by the index definition that follows
+        for (String node : asList("a", "c", "b", "e", "d")) {
+
+            Tree test = root.getTree("/").addChild(node);
+            test.setProperty("jcr:primaryType", "nt:unstructured", Type.NAME);
+            test.setProperty("foo", "bar" + i);
+            i++;
+        }
+
+        root.commit();
+
+        // Index def with same property - ordered - one with function and one without
+        Tree luceneIndex = createIndex("upper", Collections.<String>emptySet());
+        Tree nonFunc = luceneIndex.addChild(FulltextIndexConstants.INDEX_RULES)
+                .addChild("nt:base")
+                .addChild(FulltextIndexConstants.PROP_NODE)
+                .addChild("foo");
+        nonFunc.setProperty(FulltextIndexConstants.PROP_ORDERED, true);
+        nonFunc.setProperty(FulltextIndexConstants.PROP_PROPERTY_INDEX, true);
+        nonFunc.setProperty("name", "foo");
+
+        Tree func = luceneIndex.getChild(FulltextIndexConstants.INDEX_RULES)
+                .getChild("nt:base")
+                .getChild(FulltextIndexConstants.PROP_NODE)
+                .addChild("testOak");
+        func.setProperty(FulltextIndexConstants.PROP_ORDERED, true);
+        func.setProperty(FulltextIndexConstants.PROP_FUNCTION, "fn:upper-case(@foo)");
+
+        // Now do some change in the node that are covered by above index definition
+        try {
+            customLogs.starting();
+            i = 5;
+            for (String node : asList("a", "c", "b", "e", "d")) {
+
+                Tree test = root.getTree("/").addChild(node);
+                test.setProperty("jcr:primaryType", "nt:unstructured", Type.NAME);
+
+                test.setProperty("foo", "bar" + i);
+                i--;
+            }
+
+            root.commit();
+            Assert.assertFalse(customLogs.getLogs().contains("Failed to index the node [/test]"));
+            Assert.assertTrue(customLogs.getLogs().size() == 0);
+
+            assertOrderedPlanAndQuery(
+                    "select * from [nt:base] order by upper([foo])",
+                    "lucene:upper(/oak:index/upper)", asList("/d", "/e", "/b", "/c", "/a"));
+
+            assertOrderedPlanAndQuery(
+                    "select * from [nt:base] order by [foo]",
+                    "lucene:upper(/oak:index/upper)", asList("/d", "/e", "/b", "/c", "/a"));
+
+            assertOrderedPlanAndQuery(
+                    "select * from [nt:base] order by upper([foo]) DESC",
+                    "lucene:upper(/oak:index/upper)", asList("/a", "/c", "/b", "/e", "/d"));
+
+            assertOrderedPlanAndQuery(
+                    "select * from [nt:base] order by [foo] DESC",
+                    "lucene:upper(/oak:index/upper)", asList("/a", "/c", "/b", "/e", "/d"));
+
+        } finally {
+            customLogs.finished();
+        }
+
+    }
+
+    /*
+    <OAK-8166>
+    Given an index def with 2 orderable property definitions(Relative) for same property - one with function and one without
+    Indexer should not fail to index the nodes covered by this index
+     */
+    @Test
+    public void sameOrderableRelativePropertyWithAndWithoutFunction() throws Exception {
+
+        LogCustomizer customLogs = LogCustomizer.forLogger(getLoggerName()).enable(Level.WARN).create();
+        // Create nodes that will be served by the index definition that follows
+        Tree test = root.getTree("/").addChild("test");
+        test.setProperty("jcr:primaryType", "nt:unstructured", Type.NAME);
+
+        Tree a = test.addChild("jcr:content");
+        a.setProperty("jcr:primaryType", "nt:unstructured", Type.NAME);
+
+        Tree b = a.addChild("n");
+
+        b.setProperty("jcr:primaryType", "nt:unstructured", Type.NAME);
+        b.setProperty("foo", "bar");
+
+        root.commit();
+
+        // Index def with same property - ordered - one with function and one without
+        Tree luceneIndex = createIndex("upper", Collections.<String>emptySet());
+        Tree nonFunc = luceneIndex.addChild(FulltextIndexConstants.INDEX_RULES)
+                .addChild("nt:unstructured")
+                .addChild(FulltextIndexConstants.PROP_NODE)
+                .addChild("foo");
+        nonFunc.setProperty(FulltextIndexConstants.PROP_ORDERED, true);
+        nonFunc.setProperty("name", "jcr:content/n/foo");
+
+        Tree func = luceneIndex.getChild(FulltextIndexConstants.INDEX_RULES)
+                .getChild("nt:unstructured")
+                .getChild(FulltextIndexConstants.PROP_NODE)
+                .addChild("testOak");
+        func.setProperty(FulltextIndexConstants.PROP_ORDERED, true);
+        func.setProperty(FulltextIndexConstants.PROP_FUNCTION, "fn:upper-case(jcr:content/n/@foo)");
+
+        // Now do some change in the node that are covered by above index definition
+        try {
+            customLogs.starting();
+            root.getTree("/").getChild("test").getChild("jcr:content").getChild("n").setProperty("foo", "bar2");
+            root.commit();
+            Assert.assertFalse(customLogs.getLogs().contains("Failed to index the node [/test]"));
+            Assert.assertTrue(customLogs.getLogs().size() == 0);
+        } finally {
+            customLogs.finished();
+        }
+
+    }
+
+
+    protected String explain(String query) {
+        String explain = "explain " + query;
+        return executeQuery(explain, "JCR-SQL2").get(0);
+    }
+
+    protected String explainXpath(String query) throws ParseException {
+        String explain = "explain " + query;
+        Result result = executeQuery(explain, "xpath", NO_BINDINGS);
+        ResultRow row = Iterables.getOnlyElement(result.getRows());
+        String plan = row.getValue("plan").getValue(Type.STRING);
+        return plan;
+    }
+
+    private void assertOrderedPlanAndQuery(String query, String planExpectation, List<String> paths) {
+        List<String> result = assertPlanAndQuery(query, planExpectation, paths);
+        assertEquals("Ordering doesn't match", paths, result);
+    }
+
+    private List<String> assertPlanAndQuery(String query, String planExpectation, List<String> paths) {
+        assertThat(explain(query), containsString(planExpectation));
+        return assertQuery(query, paths);
+    }
+
+    protected Tree createIndex(String name, Set<String> propNames) {
+        Tree index = root.getTree("/");
+        return createIndex(index, name, propNames);
+    }
+
+    abstract protected Tree createIndex(Tree index, String name, Set<String> propNames);
+
+    abstract protected String getLoggerName();
+}

Propchange: jackrabbit/oak/trunk/oak-search/src/test/java/org/apache/jackrabbit/oak/plugins/index/FunctionIndexCommonTest.java
------------------------------------------------------------------------------
    svn:eol-style = native

Added: jackrabbit/oak/trunk/oak-search/src/test/java/org/apache/jackrabbit/oak/plugins/index/IndexAggregation2CommonTest.java
URL: http://svn.apache.org/viewvc/jackrabbit/oak/trunk/oak-search/src/test/java/org/apache/jackrabbit/oak/plugins/index/IndexAggregation2CommonTest.java?rev=1880807&view=auto
==============================================================================
--- jackrabbit/oak/trunk/oak-search/src/test/java/org/apache/jackrabbit/oak/plugins/index/IndexAggregation2CommonTest.java (added)
+++ jackrabbit/oak/trunk/oak-search/src/test/java/org/apache/jackrabbit/oak/plugins/index/IndexAggregation2CommonTest.java Wed Aug 12 13:46:45 2020
@@ -0,0 +1,388 @@
+/*
+ * 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 com.google.common.base.Strings;
+import com.google.common.collect.Lists;
+import org.apache.jackrabbit.JcrConstants;
+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.plugins.index.aggregate.SimpleNodeAggregator;
+import org.apache.jackrabbit.oak.plugins.index.search.FulltextIndexConstants;
+import org.apache.jackrabbit.oak.plugins.memory.PropertyStates;
+import org.apache.jackrabbit.oak.plugins.tree.TreeUtil;
+import org.apache.jackrabbit.oak.query.AbstractQueryTest;
+import org.apache.jackrabbit.oak.spi.query.QueryIndex;
+import org.jetbrains.annotations.NotNull;
+import org.junit.Ignore;
+import org.junit.Test;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.util.Collections;
+import java.util.Iterator;
+import java.util.List;
+
+import static com.google.common.base.Preconditions.checkArgument;
+import static com.google.common.base.Preconditions.checkNotNull;
+import static com.google.common.collect.ImmutableList.of;
+import static com.google.common.collect.Lists.newArrayList;
+import static org.apache.jackrabbit.JcrConstants.JCR_CONTENT;
+import static org.apache.jackrabbit.JcrConstants.JCR_PRIMARYTYPE;
+import static org.apache.jackrabbit.JcrConstants.NT_FILE;
+import static org.apache.jackrabbit.JcrConstants.NT_UNSTRUCTURED;
+import static org.apache.jackrabbit.oak.api.QueryEngine.NO_BINDINGS;
+import static org.apache.jackrabbit.oak.api.Type.NAME;
+import static org.apache.jackrabbit.oak.api.Type.STRING;
+import static org.apache.jackrabbit.oak.api.Type.STRINGS;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertTrue;
+
+public abstract class IndexAggregation2CommonTest extends AbstractQueryTest {
+    private static final Logger LOG = LoggerFactory.getLogger(IndexAggregation2CommonTest.class);
+
+    private static final String NT_TEST_PAGE = "test:Page";
+    private static final String NT_TEST_PAGECONTENT = "test:PageContent";
+    private static final String NT_TEST_ASSET = "test:Asset";
+    private static final String NT_TEST_ASSETCONTENT = "test:AssetContent";
+
+    protected IndexOptions indexOptions;
+    protected TestRepository repositoryOptionsUtil;
+
+
+    @Override
+    protected void createTestIndexNode() throws Exception {
+        Tree index = root.getTree("/");
+        Tree indexDefn = createTestIndexNode(index, indexOptions.getIndexType());
+        TestUtil.useV2(indexDefn);
+        //Aggregates
+        TestUtil.newNodeAggregator(indexDefn)
+                .newRuleWithName(NT_FILE, newArrayList("jcr:content"))
+                .newRuleWithName(NT_TEST_PAGE, newArrayList("jcr:content"))
+                .newRuleWithName(NT_TEST_PAGECONTENT, newArrayList("*", "*/*", "*/*/*", "*/*/*/*"))
+                .newRuleWithName(NT_TEST_ASSET, newArrayList("jcr:content"))
+                .newRuleWithName(
+                        NT_TEST_ASSETCONTENT,
+                        newArrayList("metadata", "renditions", "renditions/original", "comments",
+                                "renditions/original/jcr:content"))
+                .newRuleWithName("rep:User", newArrayList("profile"));
+
+        Tree originalInclude = indexDefn.getChild(FulltextIndexConstants.AGGREGATES)
+                .getChild(NT_TEST_ASSET).addChild("includeOriginal");
+        originalInclude.setProperty(FulltextIndexConstants.AGG_RELATIVE_NODE, true);
+        originalInclude.setProperty(FulltextIndexConstants.AGG_PATH, "jcr:content/renditions/original");
+
+        Tree includeSingleRel = indexDefn.getChild(FulltextIndexConstants.AGGREGATES)
+                .getChild(NT_TEST_ASSET).addChild("includeFirstLevelChild");
+        includeSingleRel.setProperty(FulltextIndexConstants.AGG_RELATIVE_NODE, true);
+        includeSingleRel.setProperty(FulltextIndexConstants.AGG_PATH, "firstLevelChild");
+
+        // Include all properties for both assets and pages
+        Tree assetProps = TestUtil.newRulePropTree(indexDefn, NT_TEST_ASSET);
+        TestUtil.enableForFullText(assetProps, "jcr:content/metadata/format");
+        TestUtil.enableForFullText(assetProps, FulltextIndexConstants.REGEX_ALL_PROPS, true);
+
+        Tree pageProps = TestUtil.newRulePropTree(indexDefn, NT_TEST_PAGE);
+        TestUtil.enableForFullText(pageProps, FulltextIndexConstants.REGEX_ALL_PROPS, true);
+
+        root.commit();
+    }
+
+    protected static QueryIndex.NodeAggregator getNodeAggregator() {
+        return new SimpleNodeAggregator()
+                .newRuleWithName(NT_FILE, newArrayList("jcr:content"))
+                .newRuleWithName(NT_TEST_PAGE, newArrayList("jcr:content"))
+                .newRuleWithName(NT_TEST_PAGECONTENT, newArrayList("*", "*/*", "*/*/*", "*/*/*/*"))
+                .newRuleWithName(NT_TEST_ASSET, newArrayList("jcr:content"))
+                .newRuleWithName(
+                        NT_TEST_ASSETCONTENT,
+                        newArrayList("metadata", "renditions", "renditions/original", "comments",
+                                "renditions/original/jcr:content"))
+                .newRuleWithName("rep:User", newArrayList("profile"));
+    }
+
+    @Test
+    public void oak2226() throws Exception {
+        setTraversalEnabled(false);
+        final String statement = "/jcr:root/content//element(*, test:Asset)[" +
+                "(jcr:contains(., 'mountain')) " +
+                "and (jcr:contains(jcr:content/metadata/@format, 'image'))]";
+        Tree content = root.getTree("/").addChild("content");
+        List<String> expected = Lists.newArrayList();
+
+        /*
+         * creating structure
+         *  "/content" : {
+         *      "node" : {
+         *          "jcr:primaryType" : "test:Asset",
+         *          "jcr:content" : {
+         *              "jcr:primaryType" : "test:AssetContent",
+         *              "metadata" : {
+         *                  "jcr:primaryType" : "nt:unstructured",
+         *                  "title" : "Lorem mountain ipsum",
+         *                  "format" : "image/jpeg"
+         *              }
+         *          }
+         *      },
+         *      "mountain-node" : {
+         *          "jcr:primaryType" : "test:Asset",
+         *          "jcr:content" : {
+         *              "jcr:primaryType" : "test:AssetContent",
+         *              "metadata" : {
+         *                  "jcr:primaryType" : "nt:unstructured",
+         *                  "format" : "image/jpeg"
+         *              }
+         *          }
+         *      }
+         *  }
+         */
+
+
+        // adding a node with 'mountain' property
+        Tree node = content.addChild("node");
+        node.setProperty(JCR_PRIMARYTYPE, NT_TEST_ASSET, NAME);
+        expected.add(node.getPath());
+        node = node.addChild("jcr:content");
+        node.setProperty(JCR_PRIMARYTYPE, NT_TEST_ASSETCONTENT, NAME);
+        node = node.addChild("metadata");
+        node.setProperty(JCR_PRIMARYTYPE, NT_UNSTRUCTURED, NAME);
+        node.setProperty("title", "Lorem mountain ipsum", STRING);
+        node.setProperty("format", "image/jpeg", STRING);
+
+        // adding a node with 'mountain' name but not property
+        node = content.addChild("mountain-node");
+        node.setProperty(JCR_PRIMARYTYPE, NT_TEST_ASSET, NAME);
+        expected.add(node.getPath());
+        node = node.addChild("jcr:content");
+        node.setProperty(JCR_PRIMARYTYPE, NT_TEST_ASSETCONTENT, NAME);
+        node = node.addChild("metadata");
+        node.setProperty(JCR_PRIMARYTYPE, NT_UNSTRUCTURED, NAME);
+        node.setProperty("format", "image/jpeg", STRING);
+
+        root.commit();
+
+        assertQuery(statement, "xpath", expected);
+        setTraversalEnabled(true);
+    }
+
+    @Test
+    public void oak2249() throws Exception {
+        setTraversalEnabled(false);
+        final String statement = "//element(*, test:Asset)[ " +
+                "( " +
+                "jcr:contains(., 'summer') " +
+                "or " +
+                "jcr:content/metadata/@tags = 'namespace:season/summer' " +
+                ") and " +
+                "jcr:contains(jcr:content/metadata/@format, 'image') " +
+                "]";
+
+        Tree content = root.getTree("/").addChild("content");
+        List<String> expected = newArrayList();
+
+        Tree metadata = createAssetStructure(content, "tagged");
+        metadata.setProperty("tags", of("namespace:season/summer"), STRINGS);
+        metadata.setProperty("format", "image/jpeg", STRING);
+        expected.add("/content/tagged");
+
+        metadata = createAssetStructure(content, "titled");
+        metadata.setProperty("title", "Lorem summer ipsum", STRING);
+        metadata.setProperty("format", "image/jpeg", STRING);
+        expected.add("/content/titled");
+
+        metadata = createAssetStructure(content, "summer-node");
+        metadata.setProperty("format", "image/jpeg", STRING);
+        expected.add("/content/summer-node");
+
+        // the following is NOT expected
+        metadata = createAssetStructure(content, "winter-node");
+        metadata.setProperty("tags", of("namespace:season/winter"), STRINGS);
+        metadata.setProperty("title", "Lorem winter ipsum", STRING);
+        metadata.setProperty("format", "image/jpeg", STRING);
+
+        root.commit();
+
+        assertQuery(statement, "xpath", expected);
+        setTraversalEnabled(true);
+    }
+
+    @Test
+    public void indexRelativeNode() throws Exception {
+        setTraversalEnabled(false);
+        final String statement = "//element(*, test:Asset)[ " +
+                "jcr:contains(., 'summer') " +
+                "and jcr:contains(jcr:content/renditions/original, 'fox')" +
+                "and jcr:contains(jcr:content/metadata/@format, 'image') " +
+                "]";
+
+        Tree content = root.getTree("/").addChild("content");
+        List<String> expected = newArrayList();
+
+        Tree metadata = createAssetStructure(content, "tagged");
+        metadata.setProperty("tags", of("namespace:season/summer"), STRINGS);
+        metadata.setProperty("format", "image/jpeg", STRING);
+
+        Tree original = metadata.getParent().addChild("renditions").addChild("original");
+        original.setProperty(JcrConstants.JCR_PRIMARYTYPE, JcrConstants.NT_FILE);
+        original.addChild("jcr:content").setProperty(PropertyStates.createProperty(JcrConstants.JCR_MIMETYPE, "text/plain"));
+        original.addChild("jcr:content").setProperty(PropertyStates.createProperty("jcr:data", "fox jumps".getBytes()));
+
+        expected.add("/content/tagged");
+        root.commit();
+
+        assertQuery(statement, "xpath", expected);
+
+        //Update the reaggregated node and with that parent should be get updated
+        Tree originalContent = TreeUtil.getTree(root.getTree("/"), "/content/tagged/jcr:content/renditions/original/jcr:content");
+        originalContent.setProperty(PropertyStates.createProperty("jcr:data", "kiwi jumps".getBytes()));
+        root.commit();
+        assertQuery(statement, "xpath", Collections.<String>emptyList());
+        setTraversalEnabled(true);
+    }
+
+    @Test
+    public void indexSingleRelativeNode() throws Exception {
+        setTraversalEnabled(false);
+        final String statement = "//element(*, test:Asset)[ " +
+                "jcr:contains(firstLevelChild, 'summer') ]";
+
+        List<String> expected = newArrayList();
+
+        Tree content = root.getTree("/").addChild("content");
+        Tree page = content.addChild("pages");
+        page.setProperty(JCR_PRIMARYTYPE, NT_TEST_ASSET, NAME);
+        Tree child = page.addChild("firstLevelChild");
+        child.setProperty("tag", "summer is here", STRING);
+        root.commit();
+
+        expected.add("/content/pages");
+        assertQuery(statement, "xpath", expected);
+    }
+
+    @Ignore("OAK-6597")
+    @Test
+    public void excerpt() throws Exception {
+        setTraversalEnabled(false);
+        final String statement = "select [rep:excerpt] from [test:Page] as page where contains(*, '%s*')";
+
+        Tree content = root.getTree("/").addChild("content");
+        Tree pageContent = createPageStructure(content, "foo");
+        // contains 'aliq' but not 'tinc'
+        pageContent.setProperty("bar", "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Quisque aliquet odio varius odio "
+                + "imperdiet, non egestas ex consectetur. Fusce congue ac augue quis finibus. Sed vulputate sollicitudin neque, nec "
+                + "lobortis nisl varius eget.");
+        // doesn't contain 'aliq' but 'tinc'
+        pageContent.getParent().setProperty("bar", "Donec lacinia luctus leo, sed rutrum nulla. Sed sed hendrerit turpis. Donec ex quam, "
+                + "bibendum et metus at, tristique tincidunt leo. Nam at elit ligula. Etiam ullamcorper, elit sit amet varius molestie, "
+                + "nisl ex egestas libero, quis elementum enim mi a quam.");
+
+        root.commit();
+
+        for (String term : new String[]{"tinc", "aliq"}) {
+            Result result = executeQuery(String.format(statement, term), "JCR-SQL2", NO_BINDINGS);
+            Iterator<? extends ResultRow> rows = result.getRows().iterator();
+            assertTrue(rows.hasNext());
+            ResultRow firstHit = rows.next();
+            assertFalse(rows.hasNext()); // assert that there is only a single hit
+
+            PropertyValue excerptValue = firstHit.getValue("rep:excerpt");
+            assertNotNull(excerptValue);
+            assertFalse("Excerpt for '" + term + "' is not supposed to be empty.", "".equals(excerptValue.getValue(STRING)));
+        }
+    }
+
+    /**
+     * <p>
+     * convenience method that create an "asset" structure like
+     * </p>
+     * <p>
+     * <pre>
+     *  "parent" : {
+     *      "nodeName" : {
+     *          "jcr:primaryType" : "test:Asset",
+     *          "jcr:content" : {
+     *              "jcr:primaryType" : "test:AssetContent",
+     *              "metatada" : {
+     *                  "jcr:primaryType" : "nt:unstructured"
+     *              }
+     *          }
+     *      }
+     *  }
+     * </pre>
+     * <p>
+     * <p>
+     * and returns the {@code metadata} node
+     * </p>
+     *
+     * @param parent   the parent under which creating the node
+     * @param nodeName the node name to be used
+     * @return the {@code metadata} node. See above for details
+     */
+    private static Tree createAssetStructure(@NotNull final Tree parent,
+                                             @NotNull final String nodeName) {
+        checkNotNull(parent);
+        checkArgument(!Strings.isNullOrEmpty(nodeName), "nodeName cannot be null or empty");
+
+        Tree node = parent.addChild(nodeName);
+        node.setProperty(JCR_PRIMARYTYPE, NT_TEST_ASSET, NAME);
+        node = node.addChild(JCR_CONTENT);
+        node.setProperty(JCR_PRIMARYTYPE, NT_TEST_ASSETCONTENT, NAME);
+        node = node.addChild("metadata");
+        node.setProperty(JCR_PRIMARYTYPE, NT_UNSTRUCTURED, NAME);
+        return node;
+    }
+
+    /**
+     * <p>
+     * convenience method that create an "page" structure like
+     * </p>
+     * <p>
+     * <pre>
+     *  "parent" : {
+     *      "nodeName" : {
+     *          "jcr:primaryType" : "test:Page",
+     *          "jcr:content" : {
+     *              "jcr:primaryType" : "test:PageContent"
+     *          }
+     *      }
+     *  }
+     * </pre>
+     * <p>
+     * <p>
+     * and returns the {@code jcr:content} node
+     * </p>
+     *
+     * @param parent   the parent under which creating the node
+     * @param nodeName the node name to be used
+     * @return the {@code jcr:content} node. See above for details
+     */
+    private static Tree createPageStructure(@NotNull final Tree parent,
+                                            @NotNull final String nodeName) {
+        checkNotNull(parent);
+        checkArgument(!Strings.isNullOrEmpty(nodeName), "nodeName cannot be null or empty");
+
+        Tree node = parent.addChild(nodeName);
+        node.setProperty(JCR_PRIMARYTYPE, NT_TEST_PAGE, NAME);
+        node = node.addChild(JCR_CONTENT);
+        node.setProperty(JCR_PRIMARYTYPE, NT_TEST_PAGECONTENT, NAME);
+
+        return node;
+    }
+}

Propchange: jackrabbit/oak/trunk/oak-search/src/test/java/org/apache/jackrabbit/oak/plugins/index/IndexAggregation2CommonTest.java
------------------------------------------------------------------------------
    svn:eol-style = native