You are viewing a plain text version of this content. The canonical link for it is here.
Posted to oak-commits@jackrabbit.apache.org by th...@apache.org on 2013/07/23 11:01:20 UTC

svn commit: r1505940 - in /jackrabbit/oak/trunk: oak-core/src/main/java/org/apache/jackrabbit/oak/query/ast/ oak-core/src/main/java/org/apache/jackrabbit/oak/query/fulltext/ oak-core/src/main/java/org/apache/jackrabbit/oak/query/index/ oak-core/src/mai...

Author: thomasm
Date: Tue Jul 23 09:01:20 2013
New Revision: 1505940

URL: http://svn.apache.org/r1505940
Log:
OAK-890 Query: advanced fulltext search conditions (disabled for now)

Added:
    jackrabbit/oak/trunk/oak-core/src/main/java/org/apache/jackrabbit/oak/query/fulltext/FullTextVisitor.java
    jackrabbit/oak/trunk/oak-lucene/src/main/java/org/apache/jackrabbit/oak/plugins/index/lucene/MultiLuceneIndex.java
Modified:
    jackrabbit/oak/trunk/oak-core/src/main/java/org/apache/jackrabbit/oak/query/ast/AndImpl.java
    jackrabbit/oak/trunk/oak-core/src/main/java/org/apache/jackrabbit/oak/query/ast/ConstraintImpl.java
    jackrabbit/oak/trunk/oak-core/src/main/java/org/apache/jackrabbit/oak/query/ast/FullTextSearchImpl.java
    jackrabbit/oak/trunk/oak-core/src/main/java/org/apache/jackrabbit/oak/query/ast/OrImpl.java
    jackrabbit/oak/trunk/oak-core/src/main/java/org/apache/jackrabbit/oak/query/ast/SelectorImpl.java
    jackrabbit/oak/trunk/oak-core/src/main/java/org/apache/jackrabbit/oak/query/fulltext/FullTextAnd.java
    jackrabbit/oak/trunk/oak-core/src/main/java/org/apache/jackrabbit/oak/query/fulltext/FullTextExpression.java
    jackrabbit/oak/trunk/oak-core/src/main/java/org/apache/jackrabbit/oak/query/fulltext/FullTextOr.java
    jackrabbit/oak/trunk/oak-core/src/main/java/org/apache/jackrabbit/oak/query/fulltext/FullTextParser.java
    jackrabbit/oak/trunk/oak-core/src/main/java/org/apache/jackrabbit/oak/query/fulltext/FullTextTerm.java
    jackrabbit/oak/trunk/oak-core/src/main/java/org/apache/jackrabbit/oak/query/index/FilterImpl.java
    jackrabbit/oak/trunk/oak-core/src/main/java/org/apache/jackrabbit/oak/spi/query/Filter.java
    jackrabbit/oak/trunk/oak-jcr/src/test/java/org/apache/jackrabbit/oak/jcr/query/QueryFulltextTest.java
    jackrabbit/oak/trunk/oak-lucene/src/main/java/org/apache/jackrabbit/oak/plugins/index/lucene/LuceneIndex.java
    jackrabbit/oak/trunk/oak-lucene/src/test/java/org/apache/jackrabbit/oak/jcr/query/QueryFulltextTest.java

Modified: jackrabbit/oak/trunk/oak-core/src/main/java/org/apache/jackrabbit/oak/query/ast/AndImpl.java
URL: http://svn.apache.org/viewvc/jackrabbit/oak/trunk/oak-core/src/main/java/org/apache/jackrabbit/oak/query/ast/AndImpl.java?rev=1505940&r1=1505939&r2=1505940&view=diff
==============================================================================
--- jackrabbit/oak/trunk/oak-core/src/main/java/org/apache/jackrabbit/oak/query/ast/AndImpl.java (original)
+++ jackrabbit/oak/trunk/oak-core/src/main/java/org/apache/jackrabbit/oak/query/ast/AndImpl.java Tue Jul 23 09:01:20 2013
@@ -18,10 +18,13 @@
  */
 package org.apache.jackrabbit.oak.query.ast;
 
+import java.util.ArrayList;
 import java.util.Map;
 import java.util.Map.Entry;
 import java.util.Set;
 
+import org.apache.jackrabbit.oak.query.fulltext.FullTextAnd;
+import org.apache.jackrabbit.oak.query.fulltext.FullTextExpression;
 import org.apache.jackrabbit.oak.query.index.FilterImpl;
 
 import com.google.common.collect.Maps;
@@ -67,6 +70,21 @@ public class AndImpl extends ConstraintI
     }
     
     @Override
+    public FullTextExpression getFullTextConstraint(SelectorImpl s) {
+        FullTextExpression f1 = constraint1.getFullTextConstraint(s);
+        FullTextExpression f2 = constraint2.getFullTextConstraint(s);
+        if (f1 == null) {
+            return f2;
+        } else if (f2 == null) {
+            return f1;
+        }
+        ArrayList<FullTextExpression> list = new ArrayList<FullTextExpression>();
+        list.add(f1);
+        list.add(f2);
+        return new FullTextAnd(list);
+    }
+    
+    @Override
     public Set<SelectorImpl> getSelectors() {
         Set<SelectorImpl> s1 = constraint1.getSelectors();
         Set<SelectorImpl> s2 = constraint1.getSelectors();

Modified: jackrabbit/oak/trunk/oak-core/src/main/java/org/apache/jackrabbit/oak/query/ast/ConstraintImpl.java
URL: http://svn.apache.org/viewvc/jackrabbit/oak/trunk/oak-core/src/main/java/org/apache/jackrabbit/oak/query/ast/ConstraintImpl.java?rev=1505940&r1=1505939&r2=1505940&view=diff
==============================================================================
--- jackrabbit/oak/trunk/oak-core/src/main/java/org/apache/jackrabbit/oak/query/ast/ConstraintImpl.java (original)
+++ jackrabbit/oak/trunk/oak-core/src/main/java/org/apache/jackrabbit/oak/query/ast/ConstraintImpl.java Tue Jul 23 09:01:20 2013
@@ -19,6 +19,7 @@ package org.apache.jackrabbit.oak.query.
 import java.util.Map;
 import java.util.Set;
 
+import org.apache.jackrabbit.oak.query.fulltext.FullTextExpression;
 import org.apache.jackrabbit.oak.query.index.FilterImpl;
 
 /**
@@ -55,6 +56,21 @@ public abstract class ConstraintImpl ext
     public abstract Set<PropertyExistenceImpl> getPropertyExistenceConditions();
     
     /**
+     * Get the (combined) full-text constraint. For constraints of the form
+     * "contains(*, 'x') or contains(*, 'y')", the combined expression is
+     * returned. If there is none, null is returned. For constraints of the form
+     * "contains(*, 'x') or z=1", null is returned as the full-text index cannot
+     * be used in this case for filtering (as it might filter out the z=1
+     * nodes).
+     * 
+     * @param s the selector
+     * @return the full-text constraint, if there is any, or null if not
+     */
+    protected FullTextExpression getFullTextConstraint(SelectorImpl s) {
+        return null;
+    }
+    
+    /**
      * Get the set of selectors for the given condition.
      * 
      * @return the set of selectors (possibly empty)

Modified: jackrabbit/oak/trunk/oak-core/src/main/java/org/apache/jackrabbit/oak/query/ast/FullTextSearchImpl.java
URL: http://svn.apache.org/viewvc/jackrabbit/oak/trunk/oak-core/src/main/java/org/apache/jackrabbit/oak/query/ast/FullTextSearchImpl.java?rev=1505940&r1=1505939&r2=1505940&view=diff
==============================================================================
--- jackrabbit/oak/trunk/oak-core/src/main/java/org/apache/jackrabbit/oak/query/ast/FullTextSearchImpl.java (original)
+++ jackrabbit/oak/trunk/oak-core/src/main/java/org/apache/jackrabbit/oak/query/ast/FullTextSearchImpl.java Tue Jul 23 09:01:20 2013
@@ -41,6 +41,29 @@ import org.apache.jackrabbit.oak.spi.que
  * A fulltext "contains(...)" condition.
  */
 public class FullTextSearchImpl extends ConstraintImpl {
+    
+    /**
+     * Feature flag. 
+     * Disabled until OAK-890 is fully implemented.
+     * Enable for testing OAK-890 related changes.
+     */
+    public static final boolean OAK_890_ADVANCED_FT_SEARCH = false;
+
+    /**
+     * Compatibility for Jackrabbit 2.0 single quoted phrase queries.
+     * (contains(., "word ''hello world'' word") 
+     * These are queries that delimit a phrase with a single quote
+     * instead, as in the spec, using double quotes.
+     */
+    public static final boolean JACKRABBIT_2_SINGLE_QUOTED_PHRASE = true;
+
+    /**
+     * Compatibility for Jackrabbit 2.0 queries with ampersand.
+     * (contains(., "max&moritz"))
+     * The ampersand is converted to a space, and a search is made for the 
+     * two words "max" and "moritz" (not a phrase search).
+     */
+    public static final boolean JACKRABBIT_2_AMPERSAND_TO_SPACE = true;
 
     private final String selectorName;
     private final String relativePath;
@@ -64,10 +87,12 @@ public class FullTextSearchImpl extends 
             propertyName = propertyName.substring(slash + 1);
         }
 
-        // temporary workaround to support using an index for
-        // "contains(a/*, 'x') or contains(a/a, x') or contains(a/b, 'x')"
-        // TODO this behavior does not match the specification
-        propertyName = null;
+        if (!OAK_890_ADVANCED_FT_SEARCH) {
+            // temporary workaround to support using an index for
+            // "contains(a/*, 'x') or contains(a/a, x') or contains(a/b, 'x')"
+            // TODO this behavior does not match the specification
+            propertyName = null;
+        }
 
         if (propertyName == null || "*".equals(propertyName)) {
             this.propertyName = null;
@@ -118,6 +143,28 @@ public class FullTextSearchImpl extends 
     }
     
     @Override
+    public FullTextExpression getFullTextConstraint(SelectorImpl s) {
+        if (s != selector) {
+            return null;
+        }
+        PropertyValue v = fullTextSearchExpression.currentValue();
+        try {
+            String p = propertyName;
+            if (OAK_890_ADVANCED_FT_SEARCH) {
+                if (relativePath != null) {
+                    if (p == null) {
+                        p = "*";
+                    }
+                    p = PathUtils.concat(relativePath, p);
+                }
+            }
+            return FullTextParser.parse(p, v.getValue(Type.STRING));
+        } catch (ParseException e) {
+            throw new IllegalArgumentException("Invalid expression: " + fullTextSearchExpression, e);
+        }
+    }
+    
+    @Override
     public Set<SelectorImpl> getSelectors() {
         return Collections.singleton(selector);
     }
@@ -129,11 +176,17 @@ public class FullTextSearchImpl extends 
 
     @Override
     public boolean evaluate() {
-        // disable evaluation if a fulltext index is used, as
-        // we don't know what exact options are used in the fulltext index
-        // (stop word, special characters,...)
-        if (selector.index instanceof FulltextQueryIndex) {
-            return true;
+        if (OAK_890_ADVANCED_FT_SEARCH) {
+            // the LuceneIndex implementation doesn't currently support
+            // queries that search for words *within* fields; the
+            // field value must match exactly
+        } else {
+            // disable evaluation if a fulltext index is used, as
+            // we don't know what exact options are used in the fulltext index
+            // (stop word, special characters,...)
+            if (selector.index instanceof FulltextQueryIndex) {
+                return true;
+            }
         }
         
         StringBuilder buff = new StringBuilder();
@@ -166,15 +219,9 @@ public class FullTextSearchImpl extends 
                 }
             }
         }
-        PropertyValue v = fullTextSearchExpression.currentValue();
-        try {
-            FullTextExpression expr = FullTextParser.parse(propertyName, v.getValue(Type.STRING));
-            return expr.evaluate(buff.toString());
-        } catch (ParseException e) {
-            throw new IllegalArgumentException("Invalid expression: " + fullTextSearchExpression, e);
-        }
+        return getFullTextConstraint(selector).evaluate(buff.toString());
     }
-
+    
     private static void appendString(StringBuilder buff, PropertyValue p) {
         if (p.isArray()) {
             for (String v : p.getValue(STRINGS)) {

Modified: jackrabbit/oak/trunk/oak-core/src/main/java/org/apache/jackrabbit/oak/query/ast/OrImpl.java
URL: http://svn.apache.org/viewvc/jackrabbit/oak/trunk/oak-core/src/main/java/org/apache/jackrabbit/oak/query/ast/OrImpl.java?rev=1505940&r1=1505939&r2=1505940&view=diff
==============================================================================
--- jackrabbit/oak/trunk/oak-core/src/main/java/org/apache/jackrabbit/oak/query/ast/OrImpl.java (original)
+++ jackrabbit/oak/trunk/oak-core/src/main/java/org/apache/jackrabbit/oak/query/ast/OrImpl.java Tue Jul 23 09:01:20 2013
@@ -18,10 +18,13 @@
  */
 package org.apache.jackrabbit.oak.query.ast;
 
+import java.util.ArrayList;
 import java.util.Map;
 import java.util.Map.Entry;
 import java.util.Set;
 
+import org.apache.jackrabbit.oak.query.fulltext.FullTextExpression;
+import org.apache.jackrabbit.oak.query.fulltext.FullTextOr;
 import org.apache.jackrabbit.oak.query.index.FilterImpl;
 
 import com.google.common.collect.Lists;
@@ -74,6 +77,21 @@ public class OrImpl extends ConstraintIm
     }
     
     @Override
+    public FullTextExpression getFullTextConstraint(SelectorImpl s) {
+        FullTextExpression f1 = constraint1.getFullTextConstraint(s);
+        FullTextExpression f2 = constraint2.getFullTextConstraint(s);
+        if (f1 == null || f2 == null) {
+            // the full-text index can not be used for conditions of the form
+            // "contains(a, 'x') or b=123"
+            return null;
+        }
+        ArrayList<FullTextExpression> list = new ArrayList<FullTextExpression>();
+        list.add(f1);
+        list.add(f2);
+        return new FullTextOr(list);
+    }
+    
+    @Override
     public Set<SelectorImpl> getSelectors() {
         Set<SelectorImpl> s1 = constraint1.getSelectors();
         Set<SelectorImpl> s2 = constraint1.getSelectors();
@@ -124,11 +142,10 @@ public class OrImpl extends ConstraintIm
     @Override
     public void restrict(FilterImpl f) {
         Set<PropertyExistenceImpl> set = getPropertyExistenceConditions();
-        if (set.isEmpty()) {
-            return;
-        }
-        for (PropertyExistenceImpl p : set) {
-            p.restrict(f);
+        if (!set.isEmpty()) {
+            for (PropertyExistenceImpl p : set) {
+                p.restrict(f);
+            }
         }
     }
 

Modified: jackrabbit/oak/trunk/oak-core/src/main/java/org/apache/jackrabbit/oak/query/ast/SelectorImpl.java
URL: http://svn.apache.org/viewvc/jackrabbit/oak/trunk/oak-core/src/main/java/org/apache/jackrabbit/oak/query/ast/SelectorImpl.java?rev=1505940&r1=1505939&r2=1505940&view=diff
==============================================================================
--- jackrabbit/oak/trunk/oak-core/src/main/java/org/apache/jackrabbit/oak/query/ast/SelectorImpl.java (original)
+++ jackrabbit/oak/trunk/oak-core/src/main/java/org/apache/jackrabbit/oak/query/ast/SelectorImpl.java Tue Jul 23 09:01:20 2013
@@ -42,6 +42,7 @@ import org.apache.jackrabbit.oak.api.Pro
 import org.apache.jackrabbit.oak.api.Tree;
 import org.apache.jackrabbit.oak.commons.PathUtils;
 import org.apache.jackrabbit.oak.query.QueryImpl;
+import org.apache.jackrabbit.oak.query.fulltext.FullTextExpression;
 import org.apache.jackrabbit.oak.query.index.FilterImpl;
 import org.apache.jackrabbit.oak.spi.query.Cursor;
 import org.apache.jackrabbit.oak.spi.query.Cursors;
@@ -220,6 +221,8 @@ public class SelectorImpl extends Source
         // (".. is null" must be written as "not .. is not null").
         if (queryConstraint != null) {
             queryConstraint.restrict(f);
+            FullTextExpression ft = queryConstraint.getFullTextConstraint(this);
+            f.setFullTextConstraint(ft);
         }
 
         return f;

Modified: jackrabbit/oak/trunk/oak-core/src/main/java/org/apache/jackrabbit/oak/query/fulltext/FullTextAnd.java
URL: http://svn.apache.org/viewvc/jackrabbit/oak/trunk/oak-core/src/main/java/org/apache/jackrabbit/oak/query/fulltext/FullTextAnd.java?rev=1505940&r1=1505939&r2=1505940&view=diff
==============================================================================
--- jackrabbit/oak/trunk/oak-core/src/main/java/org/apache/jackrabbit/oak/query/fulltext/FullTextAnd.java (original)
+++ jackrabbit/oak/trunk/oak-core/src/main/java/org/apache/jackrabbit/oak/query/fulltext/FullTextAnd.java Tue Jul 23 09:01:20 2013
@@ -19,7 +19,7 @@
 package org.apache.jackrabbit.oak.query.fulltext;
 
 import java.util.ArrayList;
-import java.util.LinkedHashSet;
+import java.util.Set;
 
 /**
  * A fulltext "and" condition.
@@ -44,16 +44,13 @@ public class FullTextAnd extends FullTex
 
     @Override
     public FullTextExpression simplify() {
-        // remove duplicates
-        LinkedHashSet<FullTextExpression> newList = new LinkedHashSet<FullTextExpression>();
-        for (int i = 0; i < list.size(); i++) {
-            newList.add(list.get(i).simplify());
+        Set<FullTextExpression> set = FullTextOr.getSortedAndUniqueSet(list);
+        if (set.size() == 1) {
+            return set.iterator().next();
         }
-        if (newList.size() == 1) {
-            return newList.iterator().next();
-        }
-        ArrayList<FullTextExpression> l = new ArrayList<FullTextExpression>(newList.size());
-        l.addAll(newList);
+        ArrayList<FullTextExpression> l = new ArrayList<FullTextExpression>(
+                set.size());
+        l.addAll(set);
         return new FullTextAnd(l);
     }
 
@@ -80,5 +77,10 @@ public class FullTextAnd extends FullTex
     public int getPrecedence() {
         return PRECEDENCE_AND;
     }
+    
+    @Override
+    public boolean accept(FullTextVisitor v) {
+        return v.visit(this);
+    }
 
 }
\ No newline at end of file

Modified: jackrabbit/oak/trunk/oak-core/src/main/java/org/apache/jackrabbit/oak/query/fulltext/FullTextExpression.java
URL: http://svn.apache.org/viewvc/jackrabbit/oak/trunk/oak-core/src/main/java/org/apache/jackrabbit/oak/query/fulltext/FullTextExpression.java?rev=1505940&r1=1505939&r2=1505940&view=diff
==============================================================================
--- jackrabbit/oak/trunk/oak-core/src/main/java/org/apache/jackrabbit/oak/query/fulltext/FullTextExpression.java (original)
+++ jackrabbit/oak/trunk/oak-core/src/main/java/org/apache/jackrabbit/oak/query/fulltext/FullTextExpression.java Tue Jul 23 09:01:20 2013
@@ -81,4 +81,12 @@ public abstract class FullTextExpression
         return toString().hashCode();
     }
     
+    /**
+     * Let the expression call the applicable visit method of the visitor. 
+     * 
+     * @param v the visitor
+     * @return true if the visit method returned true
+     */
+    public abstract boolean accept(FullTextVisitor v);
+    
 }
\ No newline at end of file

Modified: jackrabbit/oak/trunk/oak-core/src/main/java/org/apache/jackrabbit/oak/query/fulltext/FullTextOr.java
URL: http://svn.apache.org/viewvc/jackrabbit/oak/trunk/oak-core/src/main/java/org/apache/jackrabbit/oak/query/fulltext/FullTextOr.java?rev=1505940&r1=1505939&r2=1505940&view=diff
==============================================================================
--- jackrabbit/oak/trunk/oak-core/src/main/java/org/apache/jackrabbit/oak/query/fulltext/FullTextOr.java (original)
+++ jackrabbit/oak/trunk/oak-core/src/main/java/org/apache/jackrabbit/oak/query/fulltext/FullTextOr.java Tue Jul 23 09:01:20 2013
@@ -19,7 +19,10 @@
 package org.apache.jackrabbit.oak.query.fulltext;
 
 import java.util.ArrayList;
-import java.util.LinkedHashSet;
+import java.util.Comparator;
+import java.util.List;
+import java.util.Set;
+import java.util.TreeSet;
 
 /**
  * A fulltext "or" condition.
@@ -44,18 +47,33 @@ public class FullTextOr extends FullText
 
     @Override
     public FullTextExpression simplify() {
-        // remove duplicates
-        LinkedHashSet<FullTextExpression> newList = new LinkedHashSet<FullTextExpression>();
-        for (int i = 0; i < list.size(); i++) {
-            newList.add(list.get(i).simplify());
-        }
-        if (newList.size() == 1) {
-            return newList.iterator().next();
+        Set<FullTextExpression> set = getSortedAndUniqueSet(list);
+        if (set.size() == 1) {
+            return set.iterator().next();
         }
-        ArrayList<FullTextExpression> l = new ArrayList<FullTextExpression>(newList.size());
-        l.addAll(newList);
+        ArrayList<FullTextExpression> l = new ArrayList<FullTextExpression>(
+                set.size());
+        l.addAll(set);
         return new FullTextOr(l);
     }
+    
+    static Set<FullTextExpression> getSortedAndUniqueSet(
+            List<FullTextExpression> list) {
+        // sort and remove duplicates
+        TreeSet<FullTextExpression> set = new TreeSet<FullTextExpression>(
+                new Comparator<FullTextExpression>() {
+
+                    @Override
+                    public int compare(FullTextExpression o1,
+                            FullTextExpression o2) {
+                        return o1.toString().compareTo(o2.toString());
+                    }
+                });
+        for (int i = 0; i < list.size(); i++) {
+            set.add(list.get(i).simplify());
+        }
+        return set;
+    }
 
     @Override
     public String toString() {
@@ -80,5 +98,10 @@ public class FullTextOr extends FullText
     public int getPrecedence() {
         return PRECEDENCE_OR;
     }
+    
+    @Override
+    public boolean accept(FullTextVisitor v) {
+        return v.visit(this);
+    }
 
 }
\ No newline at end of file

Modified: jackrabbit/oak/trunk/oak-core/src/main/java/org/apache/jackrabbit/oak/query/fulltext/FullTextParser.java
URL: http://svn.apache.org/viewvc/jackrabbit/oak/trunk/oak-core/src/main/java/org/apache/jackrabbit/oak/query/fulltext/FullTextParser.java?rev=1505940&r1=1505939&r2=1505940&view=diff
==============================================================================
--- jackrabbit/oak/trunk/oak-core/src/main/java/org/apache/jackrabbit/oak/query/fulltext/FullTextParser.java (original)
+++ jackrabbit/oak/trunk/oak-core/src/main/java/org/apache/jackrabbit/oak/query/fulltext/FullTextParser.java Tue Jul 23 09:01:20 2013
@@ -21,6 +21,8 @@ package org.apache.jackrabbit.oak.query.
 import java.text.ParseException;
 import java.util.ArrayList;
 
+import org.apache.jackrabbit.oak.query.ast.FullTextSearchImpl;
+
 
 /**
  * A parser for fulltext condition literals. The grammar is defined in the
@@ -43,6 +45,11 @@ public class FullTextParser {
     public static FullTextExpression parse(String propertyName, String text) throws ParseException {
         FullTextParser p = new FullTextParser();
         p.propertyName = propertyName;
+        if (FullTextSearchImpl.JACKRABBIT_2_AMPERSAND_TO_SPACE) {
+            if (text.indexOf('&') >= 0) {
+                text = text.replace('&', ' ');
+            }
+        }
         p.text = text;
         FullTextExpression e = p.parseOr();
         return e;
@@ -119,6 +126,35 @@ public class FullTextParser {
                     buff.append(c);
                 }
             }
+        } else if (c == '\'' && FullTextSearchImpl.JACKRABBIT_2_SINGLE_QUOTED_PHRASE) {
+            // basically the same as double quote
+            parseIndex++;
+            while (true) {
+                if (parseIndex >= text.length()) {
+                    throw getSyntaxError("single quote");
+                }
+                c = text.charAt(parseIndex++);
+                if (c == '\\') {
+                    escaped = true;
+                    if (parseIndex >= text.length()) {
+                        throw getSyntaxError("escaped char");
+                    }
+                    c = text.charAt(parseIndex++);
+                    buff.append(c);
+                } else if (c == '\'') {
+                    if (parseIndex < text.length()) {
+                        if (text.charAt(parseIndex) == '^') {
+                            boost = "";
+                        } else if (text.charAt(parseIndex) != ' ') {
+                            throw getSyntaxError("space");
+                        }
+                    }
+                    parseIndex++;
+                    break;
+                } else {
+                    buff.append(c);
+                }
+            }
         } else {
             do {
                 c = text.charAt(parseIndex++);

Modified: jackrabbit/oak/trunk/oak-core/src/main/java/org/apache/jackrabbit/oak/query/fulltext/FullTextTerm.java
URL: http://svn.apache.org/viewvc/jackrabbit/oak/trunk/oak-core/src/main/java/org/apache/jackrabbit/oak/query/fulltext/FullTextTerm.java?rev=1505940&r1=1505939&r2=1505940&view=diff
==============================================================================
--- jackrabbit/oak/trunk/oak-core/src/main/java/org/apache/jackrabbit/oak/query/fulltext/FullTextTerm.java (original)
+++ jackrabbit/oak/trunk/oak-core/src/main/java/org/apache/jackrabbit/oak/query/fulltext/FullTextTerm.java Tue Jul 23 09:01:20 2013
@@ -96,9 +96,7 @@ public class FullTextTerm extends FullTe
         if (not) {
             buff.append('-');
         }
-        if (propertyName != null && "*".equals(propertyName)) {
-            // TODO support property name conditions
-            // (currently disabled)
+        if (propertyName != null && !"*".equals(propertyName)) {
             buff.append(propertyName).append(':');
         }
         buff.append('\"');
@@ -122,9 +120,26 @@ public class FullTextTerm extends FullTe
         return propertyName;
     }
     
+    public String getBoost() {
+        return boost;
+    }
+    
+    public boolean isNot() {
+        return not;
+    }
+    
+    public String getText() {
+        return text;
+    }
+    
     @Override
     public int getPrecedence() {
         return PRECEDENCE_TERM;
     }
+    
+    @Override
+    public boolean accept(FullTextVisitor v) {
+        return v.visit(this);
+    }
 
 }
\ No newline at end of file

Added: jackrabbit/oak/trunk/oak-core/src/main/java/org/apache/jackrabbit/oak/query/fulltext/FullTextVisitor.java
URL: http://svn.apache.org/viewvc/jackrabbit/oak/trunk/oak-core/src/main/java/org/apache/jackrabbit/oak/query/fulltext/FullTextVisitor.java?rev=1505940&view=auto
==============================================================================
--- jackrabbit/oak/trunk/oak-core/src/main/java/org/apache/jackrabbit/oak/query/fulltext/FullTextVisitor.java (added)
+++ jackrabbit/oak/trunk/oak-core/src/main/java/org/apache/jackrabbit/oak/query/fulltext/FullTextVisitor.java Tue Jul 23 09:01:20 2013
@@ -0,0 +1,79 @@
+/*
+ * 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.query.fulltext;
+
+/**
+ * A visitor for full-text expressions. This class is abstract because at least
+ * one of the methods needs to be implemented to make anything useful, most
+ * likely visit(FullTextTerm).
+ */
+public interface FullTextVisitor {
+
+    /**
+     * Visit an "and" expression.
+     * 
+     * @param and the "and" expression
+     * @return true if visiting should continue
+     */
+    boolean visit(FullTextAnd and);
+
+    /**
+     * Visit an "or" expression.
+     * 
+     * @param or the "or" expression
+     * @return true if visiting should continue
+     */
+    boolean visit(FullTextOr or);
+
+    /**
+     * Visit a term
+     * 
+     * @param term the term
+     * @return true if visiting should continue
+     */
+    boolean visit(FullTextTerm term);
+    
+    /**
+     * The base implementation of a full-text visitor.
+     */
+    public abstract static class FullTextVisitorBase implements FullTextVisitor {
+
+        @Override
+        public boolean visit(FullTextAnd and) {
+            for (FullTextExpression e : and.list) {
+                if (!e.accept(this)) {
+                    return false;
+                }
+            }
+            return true;
+        }
+
+        @Override
+        public boolean visit(FullTextOr or) {
+            for (FullTextExpression e : or.list) {
+                if (!e.accept(this)) {
+                    return false;
+                }
+            }
+            return true;
+        }
+
+    }
+    
+}

Modified: jackrabbit/oak/trunk/oak-core/src/main/java/org/apache/jackrabbit/oak/query/index/FilterImpl.java
URL: http://svn.apache.org/viewvc/jackrabbit/oak/trunk/oak-core/src/main/java/org/apache/jackrabbit/oak/query/index/FilterImpl.java?rev=1505940&r1=1505939&r2=1505940&view=diff
==============================================================================
--- jackrabbit/oak/trunk/oak-core/src/main/java/org/apache/jackrabbit/oak/query/index/FilterImpl.java (original)
+++ jackrabbit/oak/trunk/oak-core/src/main/java/org/apache/jackrabbit/oak/query/index/FilterImpl.java Tue Jul 23 09:01:20 2013
@@ -34,6 +34,7 @@ import org.apache.jackrabbit.oak.api.Pro
 import org.apache.jackrabbit.oak.commons.PathUtils;
 import org.apache.jackrabbit.oak.query.ast.Operator;
 import org.apache.jackrabbit.oak.query.ast.SelectorImpl;
+import org.apache.jackrabbit.oak.query.fulltext.FullTextExpression;
 import org.apache.jackrabbit.oak.spi.query.Filter;
 
 /**
@@ -64,6 +65,8 @@ public class FilterImpl implements Filte
      * The fulltext search conditions, if any.
      */
     private final ArrayList<String> fulltextConditions = new ArrayList<String>();
+    
+    private FullTextExpression fullTextConstraint;
 
     private final HashMap<String, PropertyRestriction> propertyRestrictions =
             new HashMap<String, PropertyRestriction>();
@@ -298,6 +301,9 @@ public class FilterImpl implements Filte
         if (queryStatement != null) {
             buff.append("query=").append(queryStatement);
         }
+        if (fullTextConstraint != null) {
+            buff.append("fullText=").append(fullTextConstraint);
+        }
         buff.append(", path=").append(path).append(pathRestriction);
         if (!propertyRestrictions.isEmpty()) {
             buff.append(", property=[");
@@ -442,6 +448,15 @@ public class FilterImpl implements Filte
     public void restrictFulltextCondition(String condition) {
         fulltextConditions.add(condition);
     }
+    
+    public void setFullTextConstraint(FullTextExpression constraint) {
+        this.fullTextConstraint = constraint;
+    }
+    
+    @Override
+    public FullTextExpression getFullTextConstraint() {
+        return fullTextConstraint;
+    }
 
     @Override
     @Nullable

Modified: jackrabbit/oak/trunk/oak-core/src/main/java/org/apache/jackrabbit/oak/spi/query/Filter.java
URL: http://svn.apache.org/viewvc/jackrabbit/oak/trunk/oak-core/src/main/java/org/apache/jackrabbit/oak/spi/query/Filter.java?rev=1505940&r1=1505939&r2=1505940&view=diff
==============================================================================
--- jackrabbit/oak/trunk/oak-core/src/main/java/org/apache/jackrabbit/oak/spi/query/Filter.java (original)
+++ jackrabbit/oak/trunk/oak-core/src/main/java/org/apache/jackrabbit/oak/spi/query/Filter.java Tue Jul 23 09:01:20 2013
@@ -27,6 +27,7 @@ import javax.annotation.Nullable;
 import javax.jcr.PropertyType;
 
 import org.apache.jackrabbit.oak.api.PropertyValue;
+import org.apache.jackrabbit.oak.query.fulltext.FullTextExpression;
 
 /**
  * The filter for an index lookup that contains a number of restrictions that
@@ -54,6 +55,13 @@ public interface Filter {
      * @return the conditions (an empty collection if not used)
      */
     Collection<String> getFulltextConditions();
+    
+    /**
+     * Get the fulltext search condition expression, if any.
+     * 
+     * @return the condition (null if none)
+     */
+    FullTextExpression getFullTextConstraint();
 
     /**
      * Get the property restriction for the given property, if any.

Modified: jackrabbit/oak/trunk/oak-jcr/src/test/java/org/apache/jackrabbit/oak/jcr/query/QueryFulltextTest.java
URL: http://svn.apache.org/viewvc/jackrabbit/oak/trunk/oak-jcr/src/test/java/org/apache/jackrabbit/oak/jcr/query/QueryFulltextTest.java?rev=1505940&r1=1505939&r2=1505940&view=diff
==============================================================================
--- jackrabbit/oak/trunk/oak-jcr/src/test/java/org/apache/jackrabbit/oak/jcr/query/QueryFulltextTest.java (original)
+++ jackrabbit/oak/trunk/oak-jcr/src/test/java/org/apache/jackrabbit/oak/jcr/query/QueryFulltextTest.java Tue Jul 23 09:01:20 2013
@@ -30,6 +30,7 @@ import javax.jcr.query.RowIterator;
 
 import org.apache.jackrabbit.oak.jcr.AbstractRepositoryTest;
 import org.apache.jackrabbit.oak.jcr.NodeStoreFixture;
+import org.apache.jackrabbit.oak.query.ast.FullTextSearchImpl;
 import org.junit.Test;
 
 /**
@@ -60,9 +61,15 @@ public class QueryFulltextTest extends A
         Query q;
         
         q = qm.createQuery("explain " + sql2, Query.JCR_SQL2);
-        assertEquals("[nt:base] as [nt:base] /* traverse \"*\" " + 
-                "where contains([nt:base].[*], cast('hello OR hallo' as string)) */", 
-                getResult(q.execute(), "plan"));
+        if (FullTextSearchImpl.OAK_890_ADVANCED_FT_SEARCH) {
+            assertEquals("[nt:base] as [nt:base] /* traverse \"*\" " + 
+                    "where contains([nt:base].[text], cast('hello OR hallo' as string)) */", 
+                    getResult(q.execute(), "plan"));
+        } else {
+            assertEquals("[nt:base] as [nt:base] /* traverse \"*\" " + 
+                    "where contains([nt:base].[*], cast('hello OR hallo' as string)) */", 
+                    getResult(q.execute(), "plan"));
+        }
         
         // verify the result
         // uppercase "OR" mean logical "or"

Modified: jackrabbit/oak/trunk/oak-lucene/src/main/java/org/apache/jackrabbit/oak/plugins/index/lucene/LuceneIndex.java
URL: http://svn.apache.org/viewvc/jackrabbit/oak/trunk/oak-lucene/src/main/java/org/apache/jackrabbit/oak/plugins/index/lucene/LuceneIndex.java?rev=1505940&r1=1505939&r2=1505940&view=diff
==============================================================================
--- jackrabbit/oak/trunk/oak-lucene/src/main/java/org/apache/jackrabbit/oak/plugins/index/lucene/LuceneIndex.java (original)
+++ jackrabbit/oak/trunk/oak-lucene/src/main/java/org/apache/jackrabbit/oak/plugins/index/lucene/LuceneIndex.java Tue Jul 23 09:01:20 2013
@@ -33,6 +33,7 @@ import static org.apache.jackrabbit.oak.
 import static org.apache.jackrabbit.oak.plugins.index.lucene.TermFactory.newPathTerm;
 import static org.apache.jackrabbit.oak.query.QueryImpl.JCR_PATH;
 import static org.apache.lucene.search.BooleanClause.Occur.MUST;
+import static org.apache.lucene.search.BooleanClause.Occur.MUST_NOT;
 import static org.apache.lucene.search.BooleanClause.Occur.SHOULD;
 
 import java.io.File;
@@ -40,11 +41,20 @@ import java.io.IOException;
 import java.util.ArrayList;
 import java.util.Collection;
 import java.util.Collections;
+import java.util.HashSet;
 import java.util.Iterator;
 import java.util.List;
+import java.util.Set;
+import java.util.concurrent.atomic.AtomicReference;
 
 import org.apache.jackrabbit.oak.api.Type;
 import org.apache.jackrabbit.oak.commons.PathUtils;
+import org.apache.jackrabbit.oak.query.ast.FullTextSearchImpl;
+import org.apache.jackrabbit.oak.query.fulltext.FullTextAnd;
+import org.apache.jackrabbit.oak.query.fulltext.FullTextExpression;
+import org.apache.jackrabbit.oak.query.fulltext.FullTextOr;
+import org.apache.jackrabbit.oak.query.fulltext.FullTextTerm;
+import org.apache.jackrabbit.oak.query.fulltext.FullTextVisitor;
 import org.apache.jackrabbit.oak.spi.query.Cursor;
 import org.apache.jackrabbit.oak.spi.query.Cursors;
 import org.apache.jackrabbit.oak.spi.query.Filter;
@@ -58,6 +68,7 @@ import org.apache.lucene.index.Directory
 import org.apache.lucene.index.IndexReader;
 import org.apache.lucene.index.MultiFields;
 import org.apache.lucene.index.Term;
+import org.apache.lucene.search.BooleanClause;
 import org.apache.lucene.search.BooleanQuery;
 import org.apache.lucene.search.IndexSearcher;
 import org.apache.lucene.search.MatchAllDocsQuery;
@@ -126,6 +137,83 @@ public class LuceneIndex implements Full
 
     @Override
     public double getCost(Filter filter, NodeState root) {
+        if (!FullTextSearchImpl.OAK_890_ADVANCED_FT_SEARCH) {
+            return getCostOld(filter, root);
+        }
+        if (!isLive(root)) {
+            // unusable index
+            return Double.POSITIVE_INFINITY;
+        }
+        FullTextExpression ft = filter.getFullTextConstraint();
+        if (ft == null) {
+            // no full-text condition: don't use this index,
+            // as there might be a better one
+            return Double.POSITIVE_INFINITY;
+        }
+        Set<String> relPaths = getRelativePaths(ft);
+        if (relPaths.size() > 1) {
+            // there are multiple "parents", as in
+            // "contains(a/x, 'hello') and contains(b/x, 'world')"
+            return new MultiLuceneIndex(filter, root, relPaths).getCost();
+        }
+        String parent = relPaths.iterator().next();
+        if (parent.isEmpty()) {
+            // no relative properties
+            return 10;
+        }
+        // all relative properties have the same "parent", as in
+        // "contains(a/x, 'hello') and contains(a/y, 'world')" or
+        // "contains(a/x, 'hello') or contains(a/*, 'world')"
+        // TODO: proper cost calculation
+        // we assume this will cause more read operations,
+        // as we need to read the node and then the parent
+        return 15;        
+    }
+    
+    /**
+     * Get the set of relative paths of a full-text condition. For example, for
+     * the condition "contains(a/b, 'hello') and contains(c/d, 'world'), the set
+     * { "a", "c" } is returned. If there are no relative properties, then one entry
+     * is returned. If there is no expression, then an empty set is returned.
+     * 
+     * @param ft the full-text expression
+     * @return the set of relative paths (possibly empty)
+     */
+    private static Set<String> getRelativePaths(FullTextExpression ft) {
+        if (ft == null) {
+            // there might be no full-text constraint when using the
+            // LowCostLuceneIndexProvider which is used for testing
+            // TODO if the LowCostLuceneIndexProvider is removed, we should do
+            // the following instead:
+            
+            // throw new IllegalStateException("Lucene index is used even when no full-text conditions are used for filter " + filter);
+            
+            return Collections.emptySet();
+        }
+        final HashSet<String> relPaths = new HashSet<String>();
+        ft.accept(new FullTextVisitor.FullTextVisitorBase() {
+
+            @Override
+            public boolean visit(FullTextTerm term) {
+                String p = term.getPropertyName();
+                if (p == null) {
+                    relPaths.add("");
+                } else if (p.startsWith("../") || p.startsWith("./")) {
+                    throw new IllegalArgumentException("Relative parent is not supported:" + p);
+                } else if (PathUtils.getDepth(p) > 1) {
+                    String parent = PathUtils.getParentPath(p);
+                    relPaths.add(parent);
+                } else {
+                    relPaths.add("");
+                }
+                return true;
+            }
+            
+        });        
+        return relPaths;
+    }
+        
+    private double getCostOld(Filter filter, NodeState root) {
         // TODO: proper cost calculation
         if (!isLive(root)) {
             // unusable index
@@ -211,11 +299,102 @@ public class LuceneIndex implements Full
 
     @Override
     public String getPlan(Filter filter, NodeState root) {
-        return getQuery(filter, root, null).toString();
+        if (FullTextSearchImpl.OAK_890_ADVANCED_FT_SEARCH) {
+            FullTextExpression ft = filter.getFullTextConstraint();
+            Set<String> relPaths = getRelativePaths(ft);
+            if (relPaths.size() > 1) {
+                return new MultiLuceneIndex(filter, root, relPaths).getPlan();
+            } 
+            String parent = relPaths.size() == 0 ? "" : relPaths.iterator().next();
+            // we only restrict non-full-text conditions if there is
+            // no relative property in the full-text constraint 
+            boolean nonFullTextConstraints = parent.isEmpty();
+            String plan = getQuery(filter, null, nonFullTextConstraints) + " ft:(" + ft + ")";
+            if (!parent.isEmpty()) {
+                plan += " parent:" + parent;
+            }
+            return plan;
+        }
+        return getQueryOld(filter, null).toString();
     }
 
     @Override
     public Cursor query(Filter filter, NodeState root) {
+        if (!FullTextSearchImpl.OAK_890_ADVANCED_FT_SEARCH) {
+            return queryOld(filter, root);
+        }
+        if (!isLive(root)) {
+            throw new IllegalStateException("Lucene index is not live");
+        }
+        FullTextExpression ft = filter.getFullTextConstraint();
+        Set<String> relPaths = getRelativePaths(ft);
+        if (relPaths.size() > 1) {
+            return new MultiLuceneIndex(filter, root, relPaths).query();
+        }
+        String parent = relPaths.size() == 0 ? "" : relPaths.iterator().next();
+        // we only restrict non-full-text conditions if there is
+        // no relative property in the full-text constraint 
+        boolean nonFullTextConstraints = parent.isEmpty();
+        Directory directory = newDirectory(root);
+        if (directory == null) {
+            return Cursors.newPathCursor(Collections.<String> emptySet());
+        }
+        long s = System.currentTimeMillis();
+        try {
+            try {
+                IndexReader reader = DirectoryReader.open(directory);
+                try {
+                    IndexSearcher searcher = new IndexSearcher(reader);
+                    Collection<String> paths = new ArrayList<String>();
+                    HashSet<String> seenPaths = new HashSet<String>();
+                    Query query = getQuery(filter, reader, nonFullTextConstraints);
+                    if (query != null) {
+                        TopDocs docs = searcher
+                                .search(query, Integer.MAX_VALUE);
+                        for (ScoreDoc doc : docs.scoreDocs) {
+                            String path = reader.document(doc.doc,
+                                    PATH_SELECTOR).get(PATH);
+                            if (path != null) {
+                                if ("".equals(path)) {
+                                    path = "/";
+                                }
+                                if (!parent.isEmpty()) {
+                                    // ensure the path ends with the given
+                                    // relative path
+                                    if (!path.endsWith("/" + parent)) {
+                                        continue;
+                                    }
+                                    // get the base path
+                                    for (int i = 0, size = PathUtils
+                                            .getDepth(parent); i < size; i++) {
+                                        path = PathUtils.getParentPath(path);
+                                    }
+                                    // avoid duplicate entries
+                                    if (seenPaths.contains(path)) {
+                                        continue;
+                                    }
+                                    seenPaths.add(path);
+                                }
+                                paths.add(path);
+                            }
+                        }
+                    }
+                    LOG.debug("query via {} took {} ms.", this,
+                            System.currentTimeMillis() - s);
+                    return Cursors.newPathCursor(paths);
+                } finally {
+                    reader.close();
+                }
+            } finally {
+                directory.close();
+            }
+        } catch (IOException e) {
+            e.printStackTrace();
+            return Cursors.newPathCursor(Collections.<String> emptySet());
+        }        
+    }
+    
+    private Cursor queryOld(Filter filter, NodeState root) {
         Directory directory = newDirectory(root);
         if (directory == null) {
             return Cursors.newPathCursor(Collections.<String> emptySet());
@@ -228,7 +407,7 @@ public class LuceneIndex implements Full
                     IndexSearcher searcher = new IndexSearcher(reader);
                     Collection<String> paths = new ArrayList<String>();
 
-                    Query query = getQuery(filter, root, reader);
+                    Query query = getQueryOld(filter, reader);
                     if (query != null) {
                         TopDocs docs = searcher
                                 .search(query, Integer.MAX_VALUE);
@@ -256,11 +435,51 @@ public class LuceneIndex implements Full
             return Cursors.newPathCursor(Collections.<String> emptySet());
         }
     }
-
-    private static Query getQuery(Filter filter, NodeState root,
-            IndexReader reader) {
+    
+    private static Query getQuery(Filter filter, IndexReader reader, boolean nonFullTextConstraints) {
         List<Query> qs = new ArrayList<Query>();
-
+        FullTextExpression ft = filter.getFullTextConstraint();
+        if (ft == null) {
+            // there might be no full-text constraint
+            // when using the LowCostLuceneIndexProvider
+            // which is used for testing
+        } else {
+            qs.add(getFullTextQuery(ft));
+        }
+        if (nonFullTextConstraints) {
+            addNonFullTextConstraints(qs, filter, reader);
+        }
+        if (qs.size() == 0) {
+            return new MatchAllDocsQuery();
+        }
+        if (qs.size() == 1) {
+            return qs.get(0);
+        }
+        BooleanQuery bq = new BooleanQuery();
+        for (Query q : qs) {
+            bq.add(q, MUST);
+        }
+        return bq;
+    }
+    
+    private static Query getQueryOld(Filter filter, IndexReader reader) {
+        List<Query> qs = new ArrayList<Query>();
+        addNonFullTextConstraints(qs, filter, reader);
+        addFullTextConstraintsOld(qs, filter);
+        if (qs.size() == 0) {
+            return new MatchAllDocsQuery();
+        }
+        if (qs.size() == 1) {
+            return qs.get(0);
+        }
+        BooleanQuery bq = new BooleanQuery();
+        for (Query q : qs) {
+            bq.add(q, MUST);
+        }
+        return bq;
+    }
+    
+    private static void addNonFullTextConstraints(List<Query> qs, Filter filter, IndexReader reader) {
         if (!filter.matchesAllTypes()) {
             addNodeTypeConstraints(qs, filter);
         }
@@ -289,9 +508,12 @@ public class LuceneIndex implements Full
         case PARENT:
             if (PathUtils.denotesRoot(path)) {
                 // there's no parent of the root node
-                return null;
+                // we add a path that can not possibly occur because there
+                // is no way to say "match no documents" in Lucene
+                qs.add(new TermQuery(new Term(FieldNames.PATH, "///")));
+            } else {
+                qs.add(new TermQuery(newPathTerm(PathUtils.getParentPath(path))));
             }
-            qs.add(new TermQuery(newPathTerm(PathUtils.getParentPath(path))));
             break;
         case NO_RESTRICTION:
             break;
@@ -363,23 +585,9 @@ public class LuceneIndex implements Full
 
             qs.add(TermRangeQuery.newStringRange(name, first, last,
                     pr.firstIncluding, pr.lastIncluding));
-        }
-
-        addFulltextConstraints(qs, filter);
-
-        if (qs.size() == 0) {
-            return new MatchAllDocsQuery();
-        }
-        if (qs.size() == 1) {
-            return qs.get(0);
-        }
-        BooleanQuery bq = new BooleanQuery();
-        for (Query q : qs) {
-            bq.add(q, MUST);
-        }
-        return bq;
+        }        
     }
-
+    
     private static void addReferenceConstraint(String uuid, List<Query> qs,
             IndexReader reader) {
         if (reader == null) {
@@ -407,8 +615,85 @@ public class LuceneIndex implements Full
         }
         qs.add(bq);
     }
+    
+    static Query getFullTextQuery(FullTextExpression ft) {
+        // a reference to the query, so it can be set in the visitor
+        // (a "non-local return")
+        final AtomicReference<Query> result = new AtomicReference<Query>();
+        ft.accept(new FullTextVisitor() {
+
+            @Override
+            public boolean visit(FullTextOr or) {
+                BooleanQuery q = new BooleanQuery();
+                q.setMinimumNumberShouldMatch(1);
+                for (FullTextExpression e : or.list) {
+                    Query x = getFullTextQuery(e);
+                    q.add(x, SHOULD);
+                }
+                result.set(q);
+                return true;     
+            }
+
+            @Override
+            public boolean visit(FullTextAnd and) {
+                BooleanQuery q = new BooleanQuery();
+                for (FullTextExpression e : and.list) {
+                    Query x = getFullTextQuery(e);
+                    // Lucene can't deal with "must(must_not(x))"
+                    if (x instanceof BooleanQuery) {
+                        BooleanQuery bq = (BooleanQuery) x;
+                        for (BooleanClause c : bq.clauses()) {
+                            q.add(c);
+                        }
+                    } else {
+                        q.add(x, MUST);
+                    }
+                }
+                result.set(q);
+                return true;     
+            }
 
-    private static void addFulltextConstraints(List<Query> qs, Filter filter) {
+            @Override
+            public boolean visit(FullTextTerm term) {
+                Query q;
+                String p = term.getPropertyName();
+                if (p != null && p.indexOf('/') >= 0) {
+                    p = PathUtils.getName(p);
+                }
+                String text = term.getText();
+                if (text.indexOf(' ') >= 0) {
+                    PhraseQuery pq = new PhraseQuery();
+                    for (String t : text.split(" ")) {
+                        pq.add(newFulltextTerm(t));
+                    }
+                    q = pq;
+                } else {
+                    // q = new TermQuery(newFulltextTerm(text));
+                    if (!text.endsWith("*")) {
+                        text = text + "*";
+                    }
+                    text = text.toLowerCase();
+                    q = new WildcardQuery(newFulltextTerm(text));
+                }
+                String boost = term.getBoost();
+                if (boost != null) {
+                    q.setBoost(Float.parseFloat(boost));
+                }
+                if (term.isNot()) {
+                    BooleanQuery bq = new BooleanQuery();
+                    bq.add(q, MUST_NOT);
+                    result.set(bq);
+                } else {
+                    result.set(q);
+                }
+                return true;
+            }
+            
+        });
+        return result.get();
+    }
+    
+    private static void addFullTextConstraintsOld(List<Query> qs, Filter filter) {
         if (filter.getFulltextConditions() == null
                 || filter.getFulltextConditions().isEmpty()) {
             return;

Added: jackrabbit/oak/trunk/oak-lucene/src/main/java/org/apache/jackrabbit/oak/plugins/index/lucene/MultiLuceneIndex.java
URL: http://svn.apache.org/viewvc/jackrabbit/oak/trunk/oak-lucene/src/main/java/org/apache/jackrabbit/oak/plugins/index/lucene/MultiLuceneIndex.java?rev=1505940&view=auto
==============================================================================
--- jackrabbit/oak/trunk/oak-lucene/src/main/java/org/apache/jackrabbit/oak/plugins/index/lucene/MultiLuceneIndex.java (added)
+++ jackrabbit/oak/trunk/oak-lucene/src/main/java/org/apache/jackrabbit/oak/plugins/index/lucene/MultiLuceneIndex.java Tue Jul 23 09:01:20 2013
@@ -0,0 +1,56 @@
+/*
+ * 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 java.util.Set;
+
+import org.apache.jackrabbit.oak.spi.query.Cursor;
+import org.apache.jackrabbit.oak.spi.query.Filter;
+import org.apache.jackrabbit.oak.spi.state.NodeState;
+
+/**
+ * A full-text search that is used when the query contains there are multiple
+ * "parents", as in "contains(a/x, 'hello') and contains(b/x, 'world')". In this
+ * case multiple Lucene queries are run, and the result is combined.
+ */
+public class MultiLuceneIndex {
+    
+    private final Filter filter;
+    private final NodeState root;
+    private final Set<String> relPaths;
+    
+    MultiLuceneIndex(Filter filter, NodeState root,
+            Set<String> relPaths) {
+        this.filter = filter;
+        this.root = root;
+        this.relPaths = relPaths;
+    }
+    
+    public String getPlan() {
+        return "Not yet implemented";
+    }
+
+    public double getCost() {
+        return Double.POSITIVE_INFINITY;
+    }
+
+    public Cursor query() {
+        throw new IllegalStateException(
+                "Queries that use multiple relative properties is not yet supported");
+    }
+
+}

Modified: jackrabbit/oak/trunk/oak-lucene/src/test/java/org/apache/jackrabbit/oak/jcr/query/QueryFulltextTest.java
URL: http://svn.apache.org/viewvc/jackrabbit/oak/trunk/oak-lucene/src/test/java/org/apache/jackrabbit/oak/jcr/query/QueryFulltextTest.java?rev=1505940&r1=1505939&r2=1505940&view=diff
==============================================================================
--- jackrabbit/oak/trunk/oak-lucene/src/test/java/org/apache/jackrabbit/oak/jcr/query/QueryFulltextTest.java (original)
+++ jackrabbit/oak/trunk/oak-lucene/src/test/java/org/apache/jackrabbit/oak/jcr/query/QueryFulltextTest.java Tue Jul 23 09:01:20 2013
@@ -27,6 +27,7 @@ import javax.jcr.query.QueryResult;
 import javax.jcr.query.RowIterator;
 
 import org.apache.jackrabbit.core.query.AbstractQueryTest;
+import org.apache.jackrabbit.oak.query.ast.FullTextSearchImpl;
 
 /**
  * Tests the fulltext index.
@@ -50,16 +51,33 @@ public class QueryFulltextTest extends A
         Query q;
         
         q = qm.createQuery("explain " + sql2, Query.JCR_SQL2);
-        assertEquals("[nt:base] as [nt:base] /* +:fulltext:hello +:fulltext:or +:fulltext:hallo " + 
-                "where contains([nt:base].[*], cast('hello OR hallo' as string)) */", 
-                getResult(q.execute(), "plan"));
-        
-        // verify the result
-        // uppercase "OR" mean logical "or"
-        q = qm.createQuery(sql2, Query.JCR_SQL2);
-        // TODO OAK-902
-        // assertEquals("/testroot/node1, /testroot/node2, /testroot/node3",
-        //        getResult(q.execute(), "path"));
+        if (FullTextSearchImpl.OAK_890_ADVANCED_FT_SEARCH) {
+            // TODO the plan should actually be:
+//            assertEquals("[nt:base] as [nt:base] /* " + 
+//                    "+((text:hallo text:hello)~1) +text:{* TO *} " + 
+//                    "ft:(text:\"hallo\" OR text:\"hello\") " +
+//                    "where contains([nt:base].[text], cast('hello OR hallo' as string)) */", 
+//                    getResult(q.execute(), "plan"));
+            assertEquals("[nt:base] as [nt:base] /* " + 
+                    "+((:fulltext:hallo* :fulltext:hello*)~1) +text:{* TO *} " + 
+                    "ft:(text:\"hallo\" OR text:\"hello\") " +
+                    "where contains([nt:base].[text], cast('hello OR hallo' as string)) */", 
+                    getResult(q.execute(), "plan"));
+        } else {
+            assertEquals("[nt:base] as [nt:base] /* " + 
+                    "+:fulltext:hello +:fulltext:or +:fulltext:hallo " + 
+                    "where contains([nt:base].[*], cast('hello OR hallo' as string)) */", 
+                    getResult(q.execute(), "plan"));
+            
+            // verify the result
+            // uppercase "OR" mean logical "or"
+            q = qm.createQuery(sql2, Query.JCR_SQL2);
+            if (FullTextSearchImpl.OAK_890_ADVANCED_FT_SEARCH) {
+                assertEquals("/testroot/node1, /testroot/node2, /testroot/node3",
+                        getResult(q.execute(), "path"));
+            }
+
+        }
 
         // lowercase "or" mean search for the term "or"
         sql2 = "select [jcr:path] as [path] from [nt:base] " + 
@@ -75,17 +93,92 @@ public class QueryFulltextTest extends A
         QueryManager qm = session.getWorkspace().getQueryManager();
         Node n1 = testRootNode.addNode("node1");
         n1.setProperty("text", "hello");
+        Node n2 = testRootNode.addNode("node2");
+        n2.setProperty("text", "hallo");
+        Node n3 = testRootNode.addNode("node3");
+        n3.setProperty("text", "hello hallo");
         session.save();
-       
+
+        Query q;
+
         String sql2 = "select [jcr:path] as [path] from [nt:base] " + 
-                "where contains([node1/text], 'hello') order by [jcr:path]";
+                "where ISCHILDNODE([/testroot])" + 
+                " AND CONTAINS(text, 'hallo')";
+        if (FullTextSearchImpl.OAK_890_ADVANCED_FT_SEARCH) {
+            q = qm.createQuery("explain " + sql2, Query.JCR_SQL2);
+            // TODO the plan should actually be:
+//          assertEquals("[nt:base] as [nt:base] /* " + 
+//                  "+text:hallo +:path:/testroot/* +text:{* TO *} " + 
+//                  "ft:(text:\"hallo\") " + 
+//                  "where (ischildnode([nt:base], [/testroot])) " + 
+//                  "and (contains([nt:base].[text], cast('hallo' as string))) */", 
+//                  getResult(q.execute(), "plan"));
+            assertEquals("[nt:base] as [nt:base] /* " + 
+                    "+:fulltext:hallo* +:path:/testroot/* +text:{* TO *} " + 
+                    "ft:(text:\"hallo\") " + 
+                    "where (ischildnode([nt:base], [/testroot])) " + 
+                    "and (contains([nt:base].[text], cast('hallo' as string))) */", 
+                    getResult(q.execute(), "plan"));
+        }
         
-        Query q;
+        q = qm.createQuery(sql2, Query.JCR_SQL2);
+        assertEquals("/testroot/node2, /testroot/node3", getResult(q.execute(), "path"));
+       
+        sql2 = "select [jcr:path] as [path] from [nt:base] " + 
+                "where contains([node1/text], 'hello') order by [jcr:path]";
         
         q = qm.createQuery(sql2, Query.JCR_SQL2);
-        // TODO OAK-913
-        // assertEquals("/testroot", getResult(q.execute(), "path"));
-
+        if (FullTextSearchImpl.OAK_890_ADVANCED_FT_SEARCH) {
+            assertEquals("/testroot", getResult(q.execute(), "path"));
+        }
+        
+        sql2 = "select [jcr:path] as [path] from [nt:base] " + 
+                "where contains([node2/text], 'hello OR hallo') order by [jcr:path]";
+        q = qm.createQuery("explain " + sql2, Query.JCR_SQL2);
+        if (FullTextSearchImpl.OAK_890_ADVANCED_FT_SEARCH) {
+            // TODO the plan should actually be:
+//            assertEquals("[nt:base] as [nt:base] /* " + 
+//                    "(text:hallo text:hello)~1 " + 
+//                    "ft:(node2/text:\"hallo\" OR node2/text:\"hello\") " + 
+//                    "parent:node2 " + 
+//                    "where contains([nt:base].[node2/text], cast('hello OR hallo' as string)) */", 
+//                    getResult(q.execute(), "plan"));
+            assertEquals("[nt:base] as [nt:base] /* " + 
+                    "(:fulltext:hallo* :fulltext:hello*)~1 " + 
+                    "ft:(node2/text:\"hallo\" OR node2/text:\"hello\") " + 
+                    "parent:node2 " + 
+                    "where contains([nt:base].[node2/text], cast('hello OR hallo' as string)) */", 
+          getResult(q.execute(), "plan"));
+            q = qm.createQuery(sql2, Query.JCR_SQL2);
+            assertEquals("/testroot", 
+                    getResult(q.execute(), "path"));            
+        } else {
+            assertEquals("[nt:base] as [nt:base] /* " + 
+                    "+:fulltext:hello +:fulltext:or +:fulltext:hallo " + 
+                    "where contains([nt:base].[node2/*], cast('hello OR hallo' as string)) */", 
+                    getResult(q.execute(), "plan"));
+            q = qm.createQuery(sql2, Query.JCR_SQL2);
+            assertEquals("", 
+                    getResult(q.execute(), "path"));            
+        }
+        
+        sql2 = "select [jcr:path] as [path] from [nt:base] " + 
+                "where contains([node1/text], 'hello') " + 
+                "and contains([node2/text], 'hallo') " + 
+                "order by [jcr:path]";
+        q = qm.createQuery("explain " + sql2, Query.JCR_SQL2);
+        if (FullTextSearchImpl.OAK_890_ADVANCED_FT_SEARCH) {
+            // TODO OAK-890
+            assertEquals("[nt:base] as [nt:base] /* " + 
+                    "Not yet implemented " + 
+                    "where (contains([nt:base].[node1/text], cast('hello' as string))) " + 
+                    "and (contains([nt:base].[node2/text], cast('hallo' as string))) */", 
+                    getResult(q.execute(), "plan"));
+//            q = qm.createQuery(sql2, Query.JCR_SQL2);
+//            assertEquals("/testroot", 
+//                    getResult(q.execute(), "path"));            
+        }
+        
     }
     
     static String getResult(QueryResult result, String propertyName) throws RepositoryException {