Index: oak-core/src/main/java/org/apache/jackrabbit/oak/query/ast/ComparisonImpl.java =================================================================== --- oak-core/src/main/java/org/apache/jackrabbit/oak/query/ast/ComparisonImpl.java (revision 1571250) +++ oak-core/src/main/java/org/apache/jackrabbit/oak/query/ast/ComparisonImpl.java (working copy) @@ -81,9 +81,15 @@ @Override public boolean evaluate() { + PropertyValue p1 = operand1.currentProperty(); + + // functions-as-properties support (e.g. for native queries) + if (operand1.toString().contains("native('")) { + return true; + } + // JCR 2.0 spec, 6.7.16 Comparison: // "operand1 may evaluate to an array of values" - PropertyValue p1 = operand1.currentProperty(); if (p1 == null) { return false; } Index: oak-core/src/test/resources/org/apache/jackrabbit/oak/query/sql2_native.txt =================================================================== --- oak-core/src/test/resources/org/apache/jackrabbit/oak/query/sql2_native.txt (revision 0) +++ oak-core/src/test/resources/org/apache/jackrabbit/oak/query/sql2_native.txt (working copy) @@ -0,0 +1,42 @@ +# Licensed to the Apache Software Foundation (ASF) under one or more +# contributor license agreements. See the NOTICE file distributed with +# this work for additional information regarding copyright ownership. +# The ASF licenses this file to You under the Apache License, Version 2.0 +# (the "License"); you may not use this file except in compliance with +# the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# Syntax: +# * lines that start with spaces belong to the previous line +# * lines starting with "#" are remarks. +# * lines starting with "select" are queries, followed by expected results and an empty line +# * lines starting with "explain" are followed by expected query plan and an empty line +# * lines starting with "sql1" are run using the sql1 language +# * lines starting with "xpath2sql" are just converted + from xpath to sql2 +# * all other lines are are committed into the microkernel (line by line) +# * new tests are typically be added on top, after the syntax docs +# * use ascii character only + +# union, distinct + +commit / + "test": { "a": { "name": "Hello" }, "b": { "name" : "World" }} + +select [jcr:path] + from [nt:base] + where [native('solr')] = 'name:(Hello OR World)' +/test/a +/test/b + +select [jcr:path] + from [nt:base] + where [native('solr')] = 'path_child:\/test _val_:"recip(rord(name),1,2,3)"' +/test/a +/test/b \ No newline at end of file Property changes on: oak-core/src/test/resources/org/apache/jackrabbit/oak/query/sql2_native.txt ___________________________________________________________________ Added: svn:eol-style ## -0,0 +1 ## +native \ No newline at end of property Index: oak-lucene/src/main/java/org/apache/jackrabbit/oak/plugins/index/lucene/LuceneIndex.java =================================================================== --- oak-lucene/src/main/java/org/apache/jackrabbit/oak/plugins/index/lucene/LuceneIndex.java (revision 1571250) +++ oak-lucene/src/main/java/org/apache/jackrabbit/oak/plugins/index/lucene/LuceneIndex.java (working copy) @@ -16,25 +16,6 @@ */ package org.apache.jackrabbit.oak.plugins.index.lucene; -import static org.apache.jackrabbit.JcrConstants.JCR_MIXINTYPES; -import static org.apache.jackrabbit.JcrConstants.JCR_PRIMARYTYPE; -import static org.apache.jackrabbit.oak.plugins.index.IndexConstants.INDEX_DEFINITIONS_NAME; -import static org.apache.jackrabbit.oak.plugins.index.IndexConstants.TYPE_PROPERTY_NAME; -import static org.apache.jackrabbit.oak.plugins.index.lucene.FieldNames.PATH; -import static org.apache.jackrabbit.oak.plugins.index.lucene.FieldNames.PATH_SELECTOR; -import static org.apache.jackrabbit.oak.plugins.index.lucene.LuceneIndexConstants.INDEX_DATA_CHILD_NAME; -import static org.apache.jackrabbit.oak.plugins.index.lucene.LuceneIndexConstants.PERSISTENCE_FILE; -import static org.apache.jackrabbit.oak.plugins.index.lucene.LuceneIndexConstants.PERSISTENCE_NAME; -import static org.apache.jackrabbit.oak.plugins.index.lucene.LuceneIndexConstants.PERSISTENCE_OAK; -import static org.apache.jackrabbit.oak.plugins.index.lucene.LuceneIndexConstants.PERSISTENCE_PATH; -import static org.apache.jackrabbit.oak.plugins.index.lucene.LuceneIndexConstants.TYPE_LUCENE; -import static org.apache.jackrabbit.oak.plugins.index.lucene.TermFactory.newFulltextTerm; -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; import java.io.IOException; import java.io.StringReader; @@ -45,7 +26,6 @@ import java.util.List; import java.util.Set; import java.util.concurrent.atomic.AtomicReference; - import org.apache.jackrabbit.JcrConstants; import org.apache.jackrabbit.oak.api.Type; import org.apache.jackrabbit.oak.commons.PathUtils; @@ -74,6 +54,8 @@ import org.apache.lucene.index.Term; import org.apache.lucene.index.Terms; import org.apache.lucene.index.TermsEnum; +import org.apache.lucene.queryparser.classic.ParseException; +import org.apache.lucene.queryparser.classic.QueryParser; import org.apache.lucene.search.BooleanClause; import org.apache.lucene.search.BooleanQuery; import org.apache.lucene.search.IndexSearcher; @@ -90,11 +72,31 @@ import org.apache.lucene.store.Directory; import org.apache.lucene.store.FSDirectory; import org.apache.lucene.util.BytesRef; +import org.apache.lucene.util.Version; import org.apache.lucene.util.automaton.Automaton; import org.apache.lucene.util.automaton.CompiledAutomaton; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import static org.apache.jackrabbit.JcrConstants.JCR_MIXINTYPES; +import static org.apache.jackrabbit.JcrConstants.JCR_PRIMARYTYPE; +import static org.apache.jackrabbit.oak.plugins.index.IndexConstants.INDEX_DEFINITIONS_NAME; +import static org.apache.jackrabbit.oak.plugins.index.IndexConstants.TYPE_PROPERTY_NAME; +import static org.apache.jackrabbit.oak.plugins.index.lucene.FieldNames.PATH; +import static org.apache.jackrabbit.oak.plugins.index.lucene.FieldNames.PATH_SELECTOR; +import static org.apache.jackrabbit.oak.plugins.index.lucene.LuceneIndexConstants.INDEX_DATA_CHILD_NAME; +import static org.apache.jackrabbit.oak.plugins.index.lucene.LuceneIndexConstants.PERSISTENCE_FILE; +import static org.apache.jackrabbit.oak.plugins.index.lucene.LuceneIndexConstants.PERSISTENCE_NAME; +import static org.apache.jackrabbit.oak.plugins.index.lucene.LuceneIndexConstants.PERSISTENCE_OAK; +import static org.apache.jackrabbit.oak.plugins.index.lucene.LuceneIndexConstants.PERSISTENCE_PATH; +import static org.apache.jackrabbit.oak.plugins.index.lucene.LuceneIndexConstants.TYPE_LUCENE; +import static org.apache.jackrabbit.oak.plugins.index.lucene.TermFactory.newFulltextTerm; +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; + /** * Provides a QueryIndex that does lookups against a Lucene-based index * @@ -138,6 +140,7 @@ private static final Logger LOG = LoggerFactory .getLogger(LuceneIndex.class); + public static final String NATIVE_QUERY_FUNCTION = "native('lucene')"; private final Analyzer analyzer; @@ -423,7 +426,17 @@ } else { qs.add(getFullTextQuery(ft, analyzer, reader)); } - if (nonFullTextConstraints) { + PropertyRestriction pr = filter.getPropertyRestriction(NATIVE_QUERY_FUNCTION); + if (pr != null) { + QueryParser queryParser = new QueryParser(Version.LUCENE_46, "", new OakAnalyzer(Version.LUCENE_46)); + String query = String.valueOf(pr.first.getValue(pr.first.getType())); + try { + qs.add(queryParser.parse(query)); + } catch (ParseException e) { + throw new RuntimeException(e); + } + } + else if (nonFullTextConstraints) { addNonFullTextConstraints(qs, filter, reader); } if (qs.size() == 0) { Index: oak-lucene/src/test/java/org/apache/jackrabbit/oak/plugins/index/lucene/LuceneIndexQueryTest.java =================================================================== --- oak-lucene/src/test/java/org/apache/jackrabbit/oak/plugins/index/lucene/LuceneIndexQueryTest.java (revision 1571250) +++ oak-lucene/src/test/java/org/apache/jackrabbit/oak/plugins/index/lucene/LuceneIndexQueryTest.java (working copy) @@ -16,14 +16,8 @@ */ package org.apache.jackrabbit.oak.plugins.index.lucene; -import static java.util.Arrays.asList; -import static junit.framework.Assert.assertEquals; -import static org.apache.jackrabbit.oak.api.Type.STRINGS; -import static org.junit.Assert.assertFalse; -import static org.junit.Assert.assertTrue; - +import com.google.common.collect.ImmutableList; import java.util.Iterator; - import org.apache.jackrabbit.oak.Oak; import org.apache.jackrabbit.oak.api.ContentRepository; import org.apache.jackrabbit.oak.api.Tree; @@ -32,7 +26,11 @@ import org.apache.jackrabbit.oak.spi.security.OpenSecurityProvider; import org.junit.Test; -import com.google.common.collect.ImmutableList; +import static java.util.Arrays.asList; +import static junit.framework.Assert.assertEquals; +import static org.apache.jackrabbit.oak.api.Type.STRINGS; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; /** * Tests the query engine using the default index implementation: the @@ -241,4 +239,18 @@ ImmutableList.of(one.getPath(), two.getPath())); } + @Test + public void testNativeLuceneQuery() throws Exception { + String nativeQueryString = "select [jcr:path] from [nt:base] where [native('lucene')] = 'title:foo -title:bar'"; + Tree test = root.getTree("/").addChild("test"); + test.addChild("a").setProperty("title", "foo"); + test.addChild("b").setProperty("title", "bar"); + root.commit(); + + Iterator result = executeQuery(nativeQueryString, "JCR-SQL2").iterator(); + assertTrue(result.hasNext()); + assertEquals("/test/a", result.next()); + assertFalse(result.hasNext()); + } + } Index: oak-solr-core/src/main/java/org/apache/jackrabbit/oak/plugins/index/solr/query/SolrQueryIndex.java =================================================================== --- oak-solr-core/src/main/java/org/apache/jackrabbit/oak/plugins/index/solr/query/SolrQueryIndex.java (revision 1571250) +++ oak-solr-core/src/main/java/org/apache/jackrabbit/oak/plugins/index/solr/query/SolrQueryIndex.java (working copy) @@ -17,7 +17,6 @@ package org.apache.jackrabbit.oak.plugins.index.solr.query; import java.util.Collection; - import org.apache.jackrabbit.oak.api.PropertyValue; import org.apache.jackrabbit.oak.plugins.index.solr.configuration.OakSolrConfiguration; import org.apache.jackrabbit.oak.spi.query.Cursor; @@ -39,6 +38,7 @@ */ public class SolrQueryIndex implements QueryIndex { + public static final String NATIVE_QUERY_FUNCTION = "native('solr')"; private final Logger log = LoggerFactory.getLogger(SolrQueryIndex.class); public static final String TYPE = "solr"; @@ -95,40 +95,45 @@ Collection propertyRestrictions = filter.getPropertyRestrictions(); if (propertyRestrictions != null && !propertyRestrictions.isEmpty()) { for (Filter.PropertyRestriction pr : propertyRestrictions) { - if (pr.propertyName.contains("/")) { - // cannot handle child-level property restrictions - continue; - } - String first = null; - if (pr.first != null) { - first = partialEscape(String.valueOf(pr.first.getValue(pr.first.getType()))).toString(); - } - String last = null; - if (pr.last != null) { - last = partialEscape(String.valueOf(pr.last.getValue(pr.last.getType()))).toString(); - } + // native query support + if (NATIVE_QUERY_FUNCTION.equals(pr.propertyName)) { + queryBuilder.append(String.valueOf(pr.first.getValue(pr.first.getType()))); + } else { + if (pr.propertyName.contains("/")) { + // cannot handle child-level property restrictions + continue; + } + String first = null; + if (pr.first != null) { + first = partialEscape(String.valueOf(pr.first.getValue(pr.first.getType()))).toString(); + } + String last = null; + if (pr.last != null) { + last = partialEscape(String.valueOf(pr.last.getValue(pr.last.getType()))).toString(); + } - String prField = configuration.getFieldForPropertyRestriction(pr); - CharSequence fieldName = partialEscape(prField != null ? - prField : pr.propertyName); - if ("jcr\\:path".equals(fieldName.toString())) { - queryBuilder.append(configuration.getPathField()); - queryBuilder.append(':'); - queryBuilder.append(first); - } else { - queryBuilder.append(fieldName).append(':'); - if (pr.first != null && pr.last != null && pr.first.equals(pr.last)) { + String prField = configuration.getFieldForPropertyRestriction(pr); + CharSequence fieldName = partialEscape(prField != null ? + prField : pr.propertyName); + if ("jcr\\:path".equals(fieldName.toString())) { + queryBuilder.append(configuration.getPathField()); + queryBuilder.append(':'); queryBuilder.append(first); - } else if (pr.first == null && pr.last == null) { - queryBuilder.append('*'); - } else if ((pr.first != null && pr.last == null) || (pr.last != null && pr.first == null) || (!pr.first.equals(pr.last))) { - // TODO : need to check if this works for all field types (most likely not!) - queryBuilder.append(createRangeQuery(first, last, pr.firstIncluding, pr.lastIncluding)); - } else if (pr.isLike) { - // TODO : the current parameter substitution is not expected to work well - queryBuilder.append(partialEscape(String.valueOf(pr.first.getValue(pr.first.getType())).replace('%', '*').replace('_', '?'))); } else { - throw new RuntimeException("[unexpected!] not handled case"); + queryBuilder.append(fieldName).append(':'); + if (pr.first != null && pr.last != null && pr.first.equals(pr.last)) { + queryBuilder.append(first); + } else if (pr.first == null && pr.last == null) { + queryBuilder.append('*'); + } else if ((pr.first != null && pr.last == null) || (pr.last != null && pr.first == null) || (!pr.first.equals(pr.last))) { + // TODO : need to check if this works for all field types (most likely not!) + queryBuilder.append(createRangeQuery(first, last, pr.firstIncluding, pr.lastIncluding)); + } else if (pr.isLike) { + // TODO : the current parameter substitution is not expected to work well + queryBuilder.append(partialEscape(String.valueOf(pr.first.getValue(pr.first.getType())).replace('%', '*').replace('_', '?'))); + } else { + throw new RuntimeException("[unexpected!] not handled case"); + } } } queryBuilder.append(" "); @@ -139,7 +144,7 @@ for (String fulltextCondition : fulltextConditions) { queryBuilder.append(fulltextCondition).append(" "); } - if(queryBuilder.length() == 0) { + if (queryBuilder.length() == 0) { queryBuilder.append("*:*"); } String escapedQuery = queryBuilder.toString(); @@ -198,6 +203,9 @@ log.debug("sending query {}", query); } QueryResponse queryResponse = solrServer.query(query); + if (log.isDebugEnabled()) { + log.debug("getting response {}", queryResponse); + } cursor = new SolrCursor(queryResponse); } catch (Exception e) { throw new RuntimeException(e); Index: oak-solr-core/src/main/resources/solr/oak/conf/schema.xml =================================================================== --- oak-solr-core/src/main/resources/solr/oak/conf/schema.xml (revision 1571250) +++ oak-solr-core/src/main/resources/solr/oak/conf/schema.xml (working copy) @@ -104,7 +104,7 @@ - + Index: oak-solr-core/src/test/java/org/apache/jackrabbit/oak/plugins/index/solr/query/SolrIndexQueryTest.java =================================================================== --- oak-solr-core/src/test/java/org/apache/jackrabbit/oak/plugins/index/solr/query/SolrIndexQueryTest.java (revision 1571250) +++ oak-solr-core/src/test/java/org/apache/jackrabbit/oak/plugins/index/solr/query/SolrIndexQueryTest.java (working copy) @@ -162,4 +162,66 @@ assertEquals("/test/resource", strings.next()); assertFalse(strings.hasNext()); } + + @Test + public void testNativeSolrQuery() throws Exception { + String nativeQueryString = "select [jcr:path] from [nt:base] where [native('solr')] = 'name:(Hello OR World)'"; + + Tree tree = root.getTree("/"); + Tree test = tree.addChild("test"); + test.addChild("a").setProperty("name", "Hello"); + test.addChild("b").setProperty("name", "World"); + tree.addChild("c"); + root.commit(); + + Iterator strings = executeQuery(nativeQueryString, "JCR-SQL2").iterator(); + assertTrue(strings.hasNext()); + assertEquals("/test/a", strings.next()); + assertTrue(strings.hasNext()); + assertEquals("/test/b", strings.next()); + assertFalse(strings.hasNext()); + } + + @Test + public void testNativeSolrFunctionQuery() throws Exception { + String nativeQueryString = "select [jcr:path] from [nt:base] where [native('solr')] = 'path_child:\\/test _val_:\"recip(rord(name),1,2,3)\"'"; + + Tree tree = root.getTree("/"); + Tree test = tree.addChild("test"); + test.addChild("a").setProperty("name", "Hello"); + test.addChild("b").setProperty("name", "World"); + tree.addChild("c"); + root.commit(); + + Iterator strings = executeQuery(nativeQueryString, "JCR-SQL2").iterator(); + assertTrue(strings.hasNext()); + assertEquals("/test/a", strings.next()); + assertTrue(strings.hasNext()); + assertEquals("/test/b", strings.next()); + assertFalse(strings.hasNext()); + } + + @Test + public void testNativeSolrNestedQuery() throws Exception { + String nativeQueryString = "select [jcr:path] from [nt:base] where [native('solr')] = '_query_:\"{!dismax qf=catch_all q.op=OR}hello world\"'"; + + Tree tree = root.getTree("/"); + Tree test = tree.addChild("test"); + test.addChild("a").setProperty("name", "Hello"); + test.addChild("b").setProperty("name", "World"); + tree.addChild("c"); + root.commit(); + + Iterator strings = executeQuery(nativeQueryString, "JCR-SQL2").iterator(); + assertTrue(strings.hasNext()); + assertEquals("/test/a", strings.next()); + assertTrue(strings.hasNext()); + assertEquals("/test/b", strings.next()); + assertFalse(strings.hasNext()); + } + + @Test + public void sql2Native() throws Exception { + test("sql2_native.txt"); + } } Index: oak-solr-core/src/test/resources/solr/oak/conf/schema.xml =================================================================== --- oak-solr-core/src/test/resources/solr/oak/conf/schema.xml (revision 1571250) +++ oak-solr-core/src/test/resources/solr/oak/conf/schema.xml (working copy) @@ -104,7 +104,7 @@ - +