Index: oak-core/src/main/java/org/apache/jackrabbit/oak/query/QueryImpl.java =================================================================== --- oak-core/src/main/java/org/apache/jackrabbit/oak/query/QueryImpl.java (revision 1655594) +++ oak-core/src/main/java/org/apache/jackrabbit/oak/query/QueryImpl.java (working copy) @@ -62,6 +62,7 @@ import org.apache.jackrabbit.oak.query.ast.SelectorImpl; import org.apache.jackrabbit.oak.query.ast.SimilarImpl; import org.apache.jackrabbit.oak.query.ast.SourceImpl; +import org.apache.jackrabbit.oak.query.ast.SpellcheckImpl; import org.apache.jackrabbit.oak.query.ast.UpperCaseImpl; import org.apache.jackrabbit.oak.query.index.FilterImpl; import org.apache.jackrabbit.oak.query.index.TraversingIndex; @@ -101,6 +102,11 @@ */ public static final String REP_EXCERPT = "rep:excerpt"; + /** + * The "rep:spellcheck" pseudo-property. + */ + public static final String REP_SPELLCHECK = "rep:spellcheck()"; + private static final Logger LOG = LoggerFactory.getLogger(QueryImpl.class); SourceImpl source; @@ -236,6 +242,13 @@ node.bindSelector(source); return super.visit(node); } + + @Override + public boolean visit(SpellcheckImpl node) { + node.setQuery(query); + node.bindSelector(source); + return super.visit(node); + } @Override public boolean visit(FullTextSearchScoreImpl node) { Index: oak-core/src/main/java/org/apache/jackrabbit/oak/query/SQL2Parser.java =================================================================== --- oak-core/src/main/java/org/apache/jackrabbit/oak/query/SQL2Parser.java (revision 1655594) +++ oak-core/src/main/java/org/apache/jackrabbit/oak/query/SQL2Parser.java (working copy) @@ -557,6 +557,15 @@ String language = readString().getValue(Type.STRING); read(","); c = factory.nativeFunction(selectorName, language, parseStaticOperand()); + } else if ("SPELLCHECK".equalsIgnoreCase(functionName)) { + String selectorName; + if (currentTokenType == IDENTIFIER) { + selectorName = readName(); + read(","); + } else { + selectorName = getOnlySelectorName(); + } + c = factory.spellcheck(selectorName, parseStaticOperand()); } else { return null; } @@ -829,17 +838,22 @@ } read(")"); } + readOptionalAlias(column); } else { column.propertyName = readName(); - if (readIf(".")) { + if (column.propertyName.equals("rep:spellcheck")) { + if (readIf("(")) { + read(")"); + column.propertyName = ":spellcheck"; + } + readOptionalAlias(column); + } else if (readIf(".")) { column.selectorName = column.propertyName; if (readIf("*")) { column.propertyName = null; } else { column.propertyName = readName(); - if (readIf("AS")) { - column.columnName = readName(); - } else { + if (!readOptionalAlias(column)) { column.columnName = column.selectorName + "." + column.propertyName; @@ -846,9 +860,7 @@ } } } else { - if (readIf("AS")) { - column.columnName = readName(); - } + readOptionalAlias(column); } } list.add(column); @@ -856,6 +868,14 @@ } return list; } + + private boolean readOptionalAlias(ColumnOrWildcard column) throws ParseException { + if (readIf("AS")) { + column.columnName = readName(); + return true; + } + return false; + } private ColumnImpl[] resolveColumns(ArrayList list) throws ParseException { ArrayList columns = new ArrayList(); Index: oak-core/src/main/java/org/apache/jackrabbit/oak/query/ast/AstElementFactory.java =================================================================== --- oak-core/src/main/java/org/apache/jackrabbit/oak/query/ast/AstElementFactory.java (revision 1655594) +++ oak-core/src/main/java/org/apache/jackrabbit/oak/query/ast/AstElementFactory.java (working copy) @@ -157,4 +157,8 @@ return new SimilarImpl(selectorName, propertyName, path); } + public ConstraintImpl spellcheck(String selectorName, StaticOperandImpl expression) { + return new SpellcheckImpl(selectorName, expression); + } + } Index: oak-core/src/main/java/org/apache/jackrabbit/oak/query/ast/AstVisitor.java =================================================================== --- oak-core/src/main/java/org/apache/jackrabbit/oak/query/ast/AstVisitor.java (revision 1655594) +++ oak-core/src/main/java/org/apache/jackrabbit/oak/query/ast/AstVisitor.java (working copy) @@ -79,8 +79,10 @@ boolean visit(UpperCaseImpl node); - boolean visit(NativeFunctionImpl nativeFunctionImpl); + boolean visit(NativeFunctionImpl node); - boolean visit(SimilarImpl similarImpl); + boolean visit(SimilarImpl node); + + boolean visit(SpellcheckImpl node); } \ No newline at end of file Index: oak-core/src/main/java/org/apache/jackrabbit/oak/query/ast/AstVisitorBase.java =================================================================== --- oak-core/src/main/java/org/apache/jackrabbit/oak/query/ast/AstVisitorBase.java (revision 1655594) +++ oak-core/src/main/java/org/apache/jackrabbit/oak/query/ast/AstVisitorBase.java (working copy) @@ -82,6 +82,15 @@ node.getPathExpression().accept(this); return true; } + + /** + * Calls accept on the static operand in the spellcheck search constraint. + */ + @Override + public boolean visit(SpellcheckImpl node) { + node.getExpression().accept(this); + return true; + } /** * Calls accept on the two sources and the join condition in the join node. Index: oak-core/src/main/java/org/apache/jackrabbit/oak/query/ast/SelectorImpl.java =================================================================== --- oak-core/src/main/java/org/apache/jackrabbit/oak/query/ast/SelectorImpl.java (revision 1655594) +++ oak-core/src/main/java/org/apache/jackrabbit/oak/query/ast/SelectorImpl.java (working copy) @@ -634,6 +634,8 @@ result = currentRow.getValue(QueryImpl.JCR_SCORE); } else if (oakPropertyName.equals(QueryImpl.REP_EXCERPT)) { result = currentRow.getValue(QueryImpl.REP_EXCERPT); + } else if (oakPropertyName.equals(QueryImpl.REP_SPELLCHECK)) { + result = currentRow.getValue(QueryImpl.REP_SPELLCHECK); } else { result = PropertyValues.create(t.getProperty(oakPropertyName)); } Index: oak-core/src/main/java/org/apache/jackrabbit/oak/query/ast/SpellcheckImpl.java =================================================================== --- oak-core/src/main/java/org/apache/jackrabbit/oak/query/ast/SpellcheckImpl.java (revision 0) +++ oak-core/src/main/java/org/apache/jackrabbit/oak/query/ast/SpellcheckImpl.java (working copy) @@ -0,0 +1,112 @@ +/* + * 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.ast; + +import java.util.Collections; +import java.util.Set; + +import org.apache.jackrabbit.oak.api.PropertyValue; +import org.apache.jackrabbit.oak.api.Type; +import org.apache.jackrabbit.oak.query.index.FilterImpl; +import org.apache.jackrabbit.oak.spi.query.PropertyValues; +import org.apache.jackrabbit.oak.spi.query.QueryIndex.FulltextQueryIndex; + +/** + * Support for "similar(...) + */ +public class SpellcheckImpl extends ConstraintImpl { + + public static final String NATIVE_LUCENE_LANGUAGE = "lucene"; + + public static final String SPELLCHECK_PREFIX = "spellcheck?term="; + + private final String selectorName; + private final StaticOperandImpl expression; + private SelectorImpl selector; + + SpellcheckImpl(String selectorName, StaticOperandImpl expression) { + this.selectorName = selectorName; + this.expression = expression; + } + + @Override + public String toString() { + StringBuilder builder = new StringBuilder(); + builder.append("spellcheck("); + builder.append(quote(selectorName)); + builder.append(", "); + builder.append(getExpression()); + builder.append(')'); + return builder.toString(); + } + + @Override + public boolean evaluate() { + // disable evaluation if a fulltext index is used, + // and because we don't know how to process native + // conditions + if (!(selector.getIndex() instanceof FulltextQueryIndex)) { + throw new IllegalArgumentException("No full-text index was found that can process the condition " + toString()); + } + // we assume the index only returns the requested entries + return true; + } + + @Override + public Set getPropertyExistenceConditions() { + return Collections.emptySet(); + } + + @Override + public void restrict(FilterImpl f) { + if (f.getSelector().equals(selector)) { + PropertyValue p = expression.currentValue(); + String term = p.getValue(Type.STRING); + String query = SPELLCHECK_PREFIX + term; + PropertyValue v = PropertyValues.newString(query); + f.restrictProperty(NativeFunctionImpl.NATIVE_PREFIX + NATIVE_LUCENE_LANGUAGE, Operator.EQUAL, v); + } + } + + @Override + public void restrictPushDown(SelectorImpl s) { + if (s.equals(selector)) { + selector.restrictSelector(this); + } + } + + @Override + public Set getSelectors() { + return Collections.emptySet(); + } + + @Override + boolean accept(AstVisitor v) { + return v.visit(this); + } + + public void bindSelector(SourceImpl source) { + selector = source.getExistingSelector(selectorName); + } + + public StaticOperandImpl getExpression() { + return expression; + } + +} \ No newline at end of file Property changes on: oak-core/src/main/java/org/apache/jackrabbit/oak/query/ast/SpellcheckImpl.java ___________________________________________________________________ Added: svn:eol-style ## -0,0 +1 ## +native \ No newline at end of property Index: oak-core/src/main/java/org/apache/jackrabbit/oak/query/xpath/Expression.java =================================================================== --- oak-core/src/main/java/org/apache/jackrabbit/oak/query/xpath/Expression.java (revision 1655594) +++ oak-core/src/main/java/org/apache/jackrabbit/oak/query/xpath/Expression.java (working copy) @@ -544,9 +544,40 @@ return false; } - } + } /** + * A rep:spellcheck condition. + */ + static class Spellcheck extends Expression { + + final Expression term; + + Spellcheck(Expression term) { + this.term = term; + } + + @Override + public String toString() { + StringBuilder buff = new StringBuilder("spellcheck("); + buff.append(term); + buff.append(')'); + return buff.toString(); + } + + @Override + boolean isCondition() { + return true; + } + + @Override + boolean isName() { + return false; + } + + } + + /** * A function call. */ static class Function extends Expression { Index: oak-core/src/main/java/org/apache/jackrabbit/oak/query/xpath/XPathToSQL2Converter.java =================================================================== --- oak-core/src/main/java/org/apache/jackrabbit/oak/query/xpath/XPathToSQL2Converter.java (revision 1655594) +++ oak-core/src/main/java/org/apache/jackrabbit/oak/query/xpath/XPathToSQL2Converter.java (working copy) @@ -209,7 +209,12 @@ readExcerpt(); Expression.Property p = new Expression.Property(currentSelector, "rep:excerpt", false); statement.addSelectColumn(p); + } else if (readIf("rep:spellcheck")) { + readExcerpt(); + Expression.Property p = new Expression.Property(currentSelector, "rep:spellcheck()", false); + statement.addSelectColumn(p); } + } while (readIf("|")); read(")"); } else if (currentTokenType == IDENTIFIER) { @@ -622,12 +627,12 @@ Expression.Similar f = new Expression.Similar(property, path); return f; } else if ("rep:spellcheck".equals(functionName)) { - // TODO maybe support rep:spellcheck as in - // /jcr:root[rep:spellcheck('${query}')]/(rep:spellcheck()) - throw getSyntaxError("rep:spellcheck is not supported"); + Expression term = parseExpression(); + read(")"); + return new Expression.Spellcheck(term); } else { throw getSyntaxError("jcr:like | jcr:contains | jcr:score | xs:dateTime | " + - "fn:lower-case | fn:upper-case | fn:name"); + "fn:lower-case | fn:upper-case | fn:name | rep:similar | rep:spellcheck"); } } Index: oak-core/src/test/resources/org/apache/jackrabbit/oak/query/sql1.txt =================================================================== --- oak-core/src/test/resources/org/apache/jackrabbit/oak/query/sql1.txt (revision 1655594) +++ oak-core/src/test/resources/org/apache/jackrabbit/oak/query/sql1.txt (working copy) @@ -26,6 +26,9 @@ # sql-1 query (nt:unstructured needs to be escaped in sql-2) +sql1 SELECT rep:spellcheck() FROM nt:base WHERE jcr:path = '/' AND SPELLCHECK('jackrabit') +java.lang.IllegalArgumentException: No full-text index was found that can process the condition spellcheck([nt:base], 'jackrabit') + sql1 select prop1 from nt:unstructured where prop1 is not null order by prop1 asc sql1 select excerpt(.) from nt:resource where contains(., 'jackrabbit') Index: oak-solr-core/src/main/java/org/apache/jackrabbit/oak/plugins/index/solr/query/AdvancedSolrQueryIndex.java =================================================================== --- oak-solr-core/src/main/java/org/apache/jackrabbit/oak/plugins/index/solr/query/AdvancedSolrQueryIndex.java (revision 1655594) +++ oak-solr-core/src/main/java/org/apache/jackrabbit/oak/plugins/index/solr/query/AdvancedSolrQueryIndex.java (working copy) @@ -101,7 +101,7 @@ } @Override - void onRetrievedResults(Filter filter, SolrDocumentList docs) { + void onRetrievedDocs(Filter filter, SolrDocumentList docs) { // update estimates cache cache.put(filter.toString(), docs.getNumFound()); } Index: oak-solr-core/src/main/java/org/apache/jackrabbit/oak/plugins/index/solr/query/FilterQueryParser.java =================================================================== --- oak-solr-core/src/main/java/org/apache/jackrabbit/oak/plugins/index/solr/query/FilterQueryParser.java (revision 1655594) +++ oak-solr-core/src/main/java/org/apache/jackrabbit/oak/plugins/index/solr/query/FilterQueryParser.java (working copy) @@ -79,26 +79,38 @@ if (kv.length != 2) { throw new RuntimeException("Unparsable native HTTP Solr query"); } else { - if ("stream.body".equals(kv[0])) { - kv[0] = "q"; - String mltFlString = "mlt.fl="; - int mltFlIndex = parameterString.indexOf(mltFlString); - if (mltFlIndex > -1) { - int beginIndex = mltFlIndex + mltFlString.length(); - int endIndex = parameterString.indexOf('&', beginIndex); - String fields; - if (endIndex > beginIndex) { - fields = parameterString.substring(beginIndex, endIndex); - } else { - fields = parameterString.substring(beginIndex); + // more like this + if ("/mlt".equals(requestHandlerString)) { + if ("stream.body".equals(kv[0])) { + kv[0] = "q"; + String mltFlString = "mlt.fl="; + int mltFlIndex = parameterString.indexOf(mltFlString); + if (mltFlIndex > -1) { + int beginIndex = mltFlIndex + mltFlString.length(); + int endIndex = parameterString.indexOf('&', beginIndex); + String fields; + if (endIndex > beginIndex) { + fields = parameterString.substring(beginIndex, endIndex); + } else { + fields = parameterString.substring(beginIndex); + } + kv[1] = "_query_:\"{!dismax qf=" + fields + " q.op=OR}" + kv[1] + "\""; } - kv[1] = "_query_:\"{!dismax qf=" + fields + " q.op=OR}" + kv[1] + "\""; } + if ("mlt.fl".equals(kv[0]) && ":path".equals(kv[1])) { + // rep:similar passes the path of the node to find similar documents for in the :path + // but needs its indexed content to find similar documents + kv[1] = configuration.getCatchAllField(); + } } - if ("mlt.fl".equals(kv[0]) && ":path".equals(kv[1])) { - // rep:similar passes the path of the node to find similar documents for in the :path - // but needs its indexed content to find similar documents - kv[1] = configuration.getCatchAllField(); + if ("/spellcheck".equals(requestHandlerString)) { + if ("term".equals(kv[0])) { + kv[0] = "spellcheck.q"; + } + solrQuery.setParam("spellcheck", true); + + // TODO : this should not be always passed to avoid building the dictionary on each spellcheck request + solrQuery.setParam("spellcheck.build", true); } solrQuery.setParam(kv[0], kv[1]); } @@ -291,7 +303,7 @@ private static boolean isSupportedHttpRequest(String nativeQueryString) { // the query string starts with ${supported-handler.selector}? - return nativeQueryString.matches("(mlt|query|select|get)\\\\?.*"); + return nativeQueryString.matches("(spellcheck|mlt|query|select|get)\\\\?.*"); } private static void setDefaults(SolrQuery solrQuery, OakSolrConfiguration configuration) { 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 1655594) +++ oak-solr-core/src/main/java/org/apache/jackrabbit/oak/plugins/index/solr/query/SolrQueryIndex.java (working copy) @@ -46,6 +46,8 @@ import org.apache.jackrabbit.oak.spi.state.NodeState; import org.apache.solr.client.solrj.SolrQuery; import org.apache.solr.client.solrj.SolrServer; +import org.apache.solr.client.solrj.response.QueryResponse; +import org.apache.solr.client.solrj.response.SpellCheckResponse; import org.apache.solr.common.SolrDocument; import org.apache.solr.common.SolrDocumentList; import org.slf4j.Logger; @@ -193,10 +195,11 @@ private final Deque queue = Queues.newArrayDeque(); private SolrDocument lastDoc; private int offset = 0; + private boolean stop = false; @Override protected SolrResultRow computeNext() { - while (!queue.isEmpty() || loadDocs()) { + if (!queue.isEmpty() || loadDocs()) { return queue.remove(); } return endOfData(); @@ -232,6 +235,11 @@ * @return true if any document is loaded */ private boolean loadDocs() { + + if (stop) { + return false; + } + SolrDocument lastDocToRecord = null; try { @@ -247,10 +255,12 @@ if (log.isDebugEnabled()) { log.debug("sending query {}", query); } - SolrDocumentList docs = solrServer.query(query).getResults(); + QueryResponse queryResponse = solrServer.query(query); - onRetrievedResults(filter, docs); + SolrDocumentList docs = queryResponse.getResults(); + onRetrievedDocs(filter, docs); + if (log.isDebugEnabled()) { log.debug("getting docs {}", docs); } @@ -262,6 +272,19 @@ } lastDocToRecord = doc; } + + // handle spellcheck + SpellCheckResponse spellCheckResponse = queryResponse.getSpellCheckResponse(); + if (spellCheckResponse != null && spellCheckResponse.getSuggestions() != null && + spellCheckResponse.getSuggestions().size() > 0) { + SolrDocument fakeDoc = new SolrDocument(); + for (SpellCheckResponse.Suggestion suggestion : spellCheckResponse.getSuggestions()) { + fakeDoc.addField("rep:spellcheck()", suggestion.getAlternatives()); + } + queue.add(new SolrResultRow("/", 1.0, fakeDoc)); + stop = true; + } + } catch (Exception e) { if (log.isWarnEnabled()) { log.warn("query via {} failed.", solrServer, e); @@ -281,7 +304,7 @@ return cursor; } - void onRetrievedResults(Filter filter, SolrDocumentList docs) { + void onRetrievedDocs(Filter filter, SolrDocumentList docs) { // do nothing } @@ -300,11 +323,12 @@ } static boolean isIgnoredProperty(String propertyName, OakSolrConfiguration configuration) { - return !configuration.useForPropertyRestrictions() // Solr index not used for properties - || (configuration.getUsedProperties().size() > 0 && !configuration.getUsedProperties().contains(propertyName)) // not explicitly contained in the used properties - || propertyName.contains("/") // no child-level property restrictions - || "rep:excerpt".equals(propertyName) // rep:excerpt is handled by the query engine - || configuration.getIgnoredProperties().contains(propertyName); // property is explicitly ignored + return !(NATIVE_LUCENE_QUERY.equals(propertyName) || NATIVE_SOLR_QUERY.equals(propertyName)) && + (!configuration.useForPropertyRestrictions() // Solr index not used for properties + || (configuration.getUsedProperties().size() > 0 && !configuration.getUsedProperties().contains(propertyName)) // not explicitly contained in the used properties + || propertyName.contains("/") // no child-level property restrictions + || "rep:excerpt".equals(propertyName) // rep:excerpt is handled by the query engine + || configuration.getIgnoredProperties().contains(propertyName)); } static class SolrResultRow { Index: oak-solr-core/src/main/resources/solr/oak/conf/schema.xml =================================================================== --- oak-solr-core/src/main/resources/solr/oak/conf/schema.xml (revision 1655594) +++ oak-solr-core/src/main/resources/solr/oak/conf/schema.xml (working copy) @@ -110,6 +110,7 @@ + Index: oak-solr-core/src/main/resources/solr/oak/conf/solrconfig.xml =================================================================== --- oak-solr-core/src/main/resources/solr/oak/conf/solrconfig.xml (revision 1655594) +++ oak-solr-core/src/main/resources/solr/oak/conf/solrconfig.xml (working copy) @@ -851,6 +851,10 @@ nameOfCustomComponent2 --> + + mlt + spellcheck + @@ -1148,6 +1152,111 @@ --> + + + text_general + + + + + + default + catch_all + solr.DirectSolrSpellChecker + + internal + + 0.5 + + 2 + + 1 + + 5 + + 4 + + 0.01 + + + + + + wordbreak + solr.WordBreakSolrSpellChecker + name + true + true + 10 + + + + + + + + + + + + + + + catch_all + + default + wordbreak + on + true + 10 + 5 + 5 + true + true + 10 + 5 + + + spellcheck + + + + @@ -145,6 +146,7 @@ + path_exact Index: oak-solr-core/src/test/resources/solr/oak/conf/solrconfig.xml =================================================================== --- oak-solr-core/src/test/resources/solr/oak/conf/solrconfig.xml (revision 1655594) +++ oak-solr-core/src/test/resources/solr/oak/conf/solrconfig.xml (working copy) @@ -807,6 +807,10 @@ nameOfCustomComponent2 --> + + mlt + spellcheck + @@ -1195,7 +1199,7 @@ --> - textSpell + text_general default - name + catch_all solr.DirectSolrSpellChecker internal @@ -1287,9 +1291,9 @@ See http://wiki.apache.org/solr/SpellCheckComponent for details on the request parameters. --> - + - text + catch_all