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 1655982) +++ 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 1655982) +++ 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 1655982) +++ 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 1655982) +++ 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 1655982) +++ 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 1655982) +++ 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 1655982) +++ 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 1655982) +++ 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 1655982) +++ 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-core/src/test/resources/org/apache/jackrabbit/oak/query/xpath.txt =================================================================== --- oak-core/src/test/resources/org/apache/jackrabbit/oak/query/xpath.txt (revision 1655982) +++ oak-core/src/test/resources/org/apache/jackrabbit/oak/query/xpath.txt (working copy) @@ -1353,7 +1353,7 @@ invalid: Query: //element(*, my:type)[@my:value = [(*)'x']; expected: @, true, false, -, +, *, ., @, ( xpath2sql //element(*, my:type)[jcr:strike(@title,'%Java%')] -invalid: Query: //element(*, my:type)[jcr:strike(@(*)title,'%Java%')]; expected: jcr:like | jcr:contains | jcr:score | xs:dateTime | fn:lower-case | fn:upper-case | fn:name +invalid: Query: //element(*, my:type)[jcr:strike(@(*)title,'%Java%')]; expected: jcr:like | jcr:contains | jcr:score | xs:dateTime | fn:lower-case | fn:upper-case | fn:name | rep:similar | rep:spellcheck xpath2sql //element(*, my:type)[ invalid: Query: //element(*, my:type)(*)[; expected: fn:not, not, (, @, true, false, -, +, *, ., @, ( Index: oak-lucene/pom.xml =================================================================== --- oak-lucene/pom.xml (revision 1655982) +++ oak-lucene/pom.xml (working copy) @@ -200,6 +200,12 @@ ${lucene.version} provided + + org.apache.lucene + lucene-suggest + ${lucene.version} + provided + 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 1655982) +++ oak-lucene/src/main/java/org/apache/jackrabbit/oak/plugins/index/lucene/LuceneIndex.java (working copy) @@ -58,6 +58,7 @@ import org.apache.jackrabbit.oak.plugins.index.aggregate.NodeAggregator; import org.apache.jackrabbit.oak.plugins.index.lucene.IndexDefinition.IndexingRule; import org.apache.jackrabbit.oak.plugins.index.lucene.util.MoreLikeThisHelper; +import org.apache.jackrabbit.oak.plugins.index.lucene.util.SpellcheckHelper; import org.apache.jackrabbit.oak.query.QueryEngineSettings; import org.apache.jackrabbit.oak.query.QueryImpl; import org.apache.jackrabbit.oak.query.fulltext.FullTextAnd; @@ -100,6 +101,7 @@ import org.apache.lucene.search.TermRangeQuery; import org.apache.lucene.search.TopDocs; import org.apache.lucene.search.WildcardQuery; +import org.apache.lucene.search.spell.SuggestWord; import org.apache.lucene.util.BytesRef; import org.apache.lucene.util.automaton.Automaton; import org.apache.lucene.util.automaton.CompiledAutomaton; @@ -243,7 +245,7 @@ // we only restrict non-full-text conditions if there is // no relative property in the full-text constraint boolean nonFullTextConstraints = parent.isEmpty(); - String planDesc = getQuery(filter, null, nonFullTextConstraints, index.getDefinition()) + " ft:(" + ft + ")"; + String planDesc = getLuceneRequest(filter, null, nonFullTextConstraints, index.getDefinition()) + " ft:(" + ft + ")"; if (!parent.isEmpty()) { planDesc += " parent:" + parent; } @@ -278,6 +280,7 @@ private final Set seenPaths = Sets.newHashSet(); private ScoreDoc lastDoc; private int nextBatchSize = LUCENE_QUERY_BATCH_SIZE; + private boolean noDocs = false; @Override protected LuceneResultRow computeNext() { @@ -322,6 +325,11 @@ * @return true if any document is loaded */ private boolean loadDocs() { + + if (noDocs) { + return false; + } + ScoreDoc lastDocToRecord = null; IndexNode indexNode = tracker.acquireIndexNode((String) plan.getAttribute(ATTR_INDEX_PATH)); @@ -328,27 +336,38 @@ checkState(indexNode != null); try { IndexSearcher searcher = indexNode.getSearcher(); - Query query = getQuery(filter, searcher.getIndexReader(), + LuceneRequestFacade luceneRequestFacade = getLuceneRequest(filter, searcher.getIndexReader(), nonFullTextConstraints, indexNode.getDefinition()); - TopDocs docs; - long time = System.currentTimeMillis(); - if (lastDoc != null) { - LOG.debug("loading the next {} entries for query {}", nextBatchSize, query); - docs = searcher.searchAfter(lastDoc, query, nextBatchSize); - } else { - LOG.debug("loading the first {} entries for query {}", nextBatchSize, query); - docs = searcher.search(query, nextBatchSize); - } - time = System.currentTimeMillis() - time; - LOG.debug("... took {} ms", time); - nextBatchSize = (int) Math.min(nextBatchSize * 2L, 100000); + if (luceneRequestFacade.getLuceneRequest() instanceof Query) { + Query query = (Query) luceneRequestFacade.getLuceneRequest(); + TopDocs docs; + long time = System.currentTimeMillis(); + if (lastDoc != null) { + LOG.debug("loading the next {} entries for query {}", nextBatchSize, query); + docs = searcher.searchAfter(lastDoc, query, nextBatchSize); + } else { + LOG.debug("loading the first {} entries for query {}", nextBatchSize, query); + docs = searcher.search(query, nextBatchSize); + } + time = System.currentTimeMillis() - time; + LOG.debug("... took {} ms", time); + nextBatchSize = (int) Math.min(nextBatchSize * 2L, 100000); - for (ScoreDoc doc : docs.scoreDocs) { - LuceneResultRow row = convertToRow(doc, searcher); - if(row != null) { - queue.add(row); + for (ScoreDoc doc : docs.scoreDocs) { + LuceneResultRow row = convertToRow(doc, searcher); + if (row != null) { + queue.add(row); + } + lastDocToRecord = doc; } - lastDocToRecord = doc; + } else if (luceneRequestFacade.getLuceneRequest() instanceof SuggestWord[]) { + SuggestWord[] intent = (SuggestWord[]) luceneRequestFacade.getLuceneRequest(); + Collection suggestedWords = new ArrayList(intent.length); + for (SuggestWord suggestWord : intent) { + suggestedWords.add(suggestWord.string); + } + queue.add(new LuceneResultRow(suggestedWords)); + noDocs = true; } } catch (IOException e) { LOG.warn("query via {} failed.", LuceneIndex.this, e); @@ -431,12 +450,11 @@ * @param nonFullTextConstraints whether non-full-text constraints (such a * path, node type, and so on) should be added to the Lucene * query - * @param analyzer the Lucene analyzer used for building the fulltext query * @param indexDefinition nodestate that contains the index definition * @return the Lucene query */ - private static Query getQuery(Filter filter, IndexReader reader, - boolean nonFullTextConstraints, IndexDefinition indexDefinition) { + private static LuceneRequestFacade getLuceneRequest(Filter filter, IndexReader reader, + boolean nonFullTextConstraints, IndexDefinition indexDefinition) { List qs = new ArrayList(); Analyzer analyzer = indexDefinition.getAnalyzer(); FullTextExpression ft = filter.getFullTextConstraint(); @@ -460,6 +478,12 @@ } } } + if (query.startsWith("spellcheck?")) { + String spellcheckQueryString = query.replace("spellcheck?", ""); + if (reader != null) { + return new LuceneRequestFacade(SpellcheckHelper.getSpellcheck(spellcheckQueryString, reader)); + } + } else { try { qs.add(queryParser.parse(query)); @@ -472,16 +496,16 @@ indexDefinition); } if (qs.size() == 0) { - return new MatchAllDocsQuery(); + return new LuceneRequestFacade(new MatchAllDocsQuery()); } if (qs.size() == 1) { - return qs.get(0); + return new LuceneRequestFacade(qs.get(0)); } BooleanQuery bq = new BooleanQuery(); for (Query q : qs) { bq.add(q, MUST); } - return bq; + return new LuceneRequestFacade(bq); } private static void addNonFullTextConstraints(List qs, @@ -702,7 +726,7 @@ // (a "non-local return") final AtomicReference result = new AtomicReference(); ft.accept(new FullTextVisitor() { - + @Override public boolean visit(FullTextContains contains) { return contains.getBase().accept(this); @@ -747,7 +771,7 @@ public boolean visit(FullTextTerm term) { return visitTerm(term.getPropertyName(), term.getText(), term.getBoost(), term.isNot()); } - + private boolean visitTerm(String propertyName, String text, String boost, boolean not) { String p = propertyName; if (p != null && p.indexOf('/') >= 0) { @@ -947,27 +971,35 @@ static class LuceneResultRow { final String path; final double score; + final Iterable suggestWords; LuceneResultRow(String path, double score) { this.path = path; this.score = score; + this.suggestWords = Collections.emptySet(); } + LuceneResultRow(Iterable suggestWords) { + this.path = "/"; + this.score = 1.0d; + this.suggestWords = suggestWords; + } + @Override public String toString() { return String.format("%s (%1.2f)", path, score); } } - + /** * A cursor over Lucene results. The result includes the path, * and the jcr:score pseudo-property as returned by Lucene. */ static class LucenePathCursor implements Cursor { - + private final Cursor pathCursor; LuceneResultRow currentRow; - + LucenePathCursor(final Iterator it, QueryEngineSettings settings) { Iterator pathIterator = new Iterator() { @@ -978,7 +1010,7 @@ @Override public String next() { - currentRow = it.next(); + currentRow = it.next(); return currentRow.path; } @@ -986,12 +1018,12 @@ public void remove() { it.remove(); } - + }; pathCursor = new PathCursor(pathIterator, true, settings); } - + @Override public boolean hasNext() { return pathCursor.hasNext(); @@ -1018,9 +1050,12 @@ if (QueryImpl.JCR_SCORE.equals(columnName)) { return PropertyValues.newDouble(currentRow.score); } + if (QueryImpl.REP_SPELLCHECK.equals(columnName)) { + return PropertyValues.newString(currentRow.suggestWords); + } return pathRow.getValue(columnName); } - + }; } } Index: oak-lucene/src/main/java/org/apache/jackrabbit/oak/plugins/index/lucene/LucenePropertyIndex.java =================================================================== --- oak-lucene/src/main/java/org/apache/jackrabbit/oak/plugins/index/lucene/LucenePropertyIndex.java (revision 1655982) +++ oak-lucene/src/main/java/org/apache/jackrabbit/oak/plugins/index/lucene/LucenePropertyIndex.java (working copy) @@ -20,7 +20,9 @@ import java.io.IOException; import java.util.ArrayList; +import java.util.Arrays; import java.util.Collection; +import java.util.Collections; import java.util.Deque; import java.util.Iterator; import java.util.List; @@ -42,6 +44,7 @@ import org.apache.jackrabbit.oak.plugins.index.lucene.IndexDefinition.IndexingRule; import org.apache.jackrabbit.oak.plugins.index.lucene.IndexPlanner.PlanResult; import org.apache.jackrabbit.oak.plugins.index.lucene.util.MoreLikeThisHelper; +import org.apache.jackrabbit.oak.plugins.index.lucene.util.SpellcheckHelper; import org.apache.jackrabbit.oak.query.QueryEngineSettings; import org.apache.jackrabbit.oak.query.QueryImpl; import org.apache.jackrabbit.oak.query.fulltext.FullTextAnd; @@ -84,6 +87,7 @@ import org.apache.lucene.search.TermRangeQuery; import org.apache.lucene.search.TopDocs; import org.apache.lucene.search.WildcardQuery; +import org.apache.lucene.search.spell.SuggestWord; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -217,7 +221,7 @@ .append("(") .append(path) .append(") "); - sb.append(getQuery(plan, null)); + sb.append(getLuceneRequest(plan, null)); if(plan.getSortOrder() != null && !plan.getSortOrder().isEmpty()){ sb.append(" ordering:").append(plan.getSortOrder()); } @@ -246,6 +250,7 @@ private final Set seenPaths = Sets.newHashSet(); private ScoreDoc lastDoc; private int nextBatchSize = LUCENE_QUERY_BATCH_SIZE; + private boolean noDocs = false; @Override protected LuceneResultRow computeNext() { @@ -285,6 +290,11 @@ * @return true if any document is loaded */ private boolean loadDocs() { + + if (noDocs) { + return false; + } + ScoreDoc lastDocToRecord = null; IndexNode indexNode = acquireIndexNode(plan); @@ -291,34 +301,45 @@ checkState(indexNode != null); try { IndexSearcher searcher = indexNode.getSearcher(); - Query query = getQuery(plan, searcher.getIndexReader()); - TopDocs docs; - long time = System.currentTimeMillis(); - if (lastDoc != null) { - LOG.debug("loading the next {} entries for query {}", nextBatchSize, query); - if (sort == null) { - docs = searcher.searchAfter(lastDoc, query, nextBatchSize); + LuceneRequestFacade luceneRequestFacade = getLuceneRequest(plan, searcher.getIndexReader()); + if (luceneRequestFacade.getLuceneRequest() instanceof Query) { + Query query = (Query) luceneRequestFacade.getLuceneRequest(); + TopDocs docs; + long time = System.currentTimeMillis(); + if (lastDoc != null) { + LOG.debug("loading the next {} entries for query {}", nextBatchSize, query); + if (sort == null) { + docs = searcher.searchAfter(lastDoc, query, nextBatchSize); + } else { + docs = searcher.searchAfter(lastDoc, query, LUCENE_QUERY_BATCH_SIZE, sort); + } } else { - docs = searcher.searchAfter(lastDoc, query, LUCENE_QUERY_BATCH_SIZE, sort); + LOG.debug("loading the first {} entries for query {}", nextBatchSize, query); + if (sort == null) { + docs = searcher.search(query, nextBatchSize); + } else { + docs = searcher.search(query, LUCENE_QUERY_BATCH_SIZE, sort); + } } - } else { - LOG.debug("loading the first {} entries for query {}", nextBatchSize, query); - if (sort == null) { - docs = searcher.search(query, nextBatchSize); - } else { - docs = searcher.search(query, LUCENE_QUERY_BATCH_SIZE, sort); - } - } - time = System.currentTimeMillis() - time; - LOG.debug("... took {} ms", time); - nextBatchSize = (int) Math.min(nextBatchSize * 2L, 100000); + time = System.currentTimeMillis() - time; + LOG.debug("... took {} ms", time); + nextBatchSize = (int) Math.min(nextBatchSize * 2L, 100000); - for (ScoreDoc doc : docs.scoreDocs) { - LuceneResultRow row = convertToRow(doc, searcher); - if(row != null) { - queue.add(row); + for (ScoreDoc doc : docs.scoreDocs) { + LuceneResultRow row = convertToRow(doc, searcher); + if (row != null) { + queue.add(row); + } + lastDocToRecord = doc; } - lastDocToRecord = doc; + } else if (luceneRequestFacade.getLuceneRequest() instanceof SuggestWord[]) { + SuggestWord[] suggestWords = (SuggestWord[]) luceneRequestFacade.getLuceneRequest(); + String[] suggestedWordsStrings = new String[suggestWords.length]; + for (int i = 0; i < suggestWords.length; i++) { + suggestedWordsStrings[i] = suggestWords[i].string; + } + queue.add(new LuceneResultRow(suggestedWordsStrings)); + noDocs = true; } } catch (IOException e) { LOG.warn("query via {} failed.", LucenePropertyIndex.this, e); @@ -419,14 +440,9 @@ * * @param plan index plan containing filter details * @param reader the Lucene reader - * @param nonFullTextConstraints whether non-full-text constraints (such a - * path, node type, and so on) should be added to the Lucene - * query - * @param analyzer the Lucene analyzer used for building the fulltext query - * @param defn nodestate that contains the index definition * @return the Lucene query */ - private static Query getQuery(IndexPlan plan, IndexReader reader) { + private static LuceneRequestFacade getLuceneRequest(IndexPlan plan, IndexReader reader) { List qs = new ArrayList(); Filter filter = plan.getFilter(); FullTextExpression ft = filter.getFullTextConstraint(); @@ -459,6 +475,11 @@ qs.add(moreLikeThis); } } + } else if (query.startsWith("spellcheck?")) { + String spellcheckQueryString = query.replace("spellcheck?", ""); + if (reader != null) { + return new LuceneRequestFacade(SpellcheckHelper.getSpellcheck(spellcheckQueryString, reader)); + } } else { try { @@ -495,16 +516,16 @@ if (!defn.isFullTextEnabled()) { throw new IllegalStateException("No query created for filter " + filter); } - return new MatchAllDocsQuery(); + return new LuceneRequestFacade(new MatchAllDocsQuery()); } if (qs.size() == 1) { - return qs.get(0); + return new LuceneRequestFacade(qs.get(0)); } BooleanQuery bq = new BooleanQuery(); for (Query q : qs) { bq.add(q, MUST); } - return bq; + return new LuceneRequestFacade(bq); } private static void addNonFullTextConstraints(List qs, @@ -981,12 +1002,20 @@ static class LuceneResultRow { final String path; final double score; + final String[] suggestWords; LuceneResultRow(String path, double score) { this.path = path; this.score = score; + this.suggestWords = new String[0]; } + LuceneResultRow(String[] suggestWords) { + this.path = "/"; + this.score = 1.0d; + this.suggestWords = suggestWords; + } + @Override public String toString() { return String.format("%s (%1.2f)", path, score); @@ -1059,6 +1088,9 @@ if (QueryImpl.JCR_SCORE.equals(columnName)) { return PropertyValues.newDouble(currentRow.score); } + if (QueryImpl.REP_SPELLCHECK.equals(columnName)) { + return PropertyValues.newString(Arrays.toString(currentRow.suggestWords)); + } return pathRow.getValue(columnName); } Index: oak-lucene/src/main/java/org/apache/jackrabbit/oak/plugins/index/lucene/LuceneRequestFacade.java =================================================================== --- oak-lucene/src/main/java/org/apache/jackrabbit/oak/plugins/index/lucene/LuceneRequestFacade.java (revision 0) +++ oak-lucene/src/main/java/org/apache/jackrabbit/oak/plugins/index/lucene/LuceneRequestFacade.java (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. + */ +package org.apache.jackrabbit.oak.plugins.index.lucene; + +import javax.annotation.Nonnull; + +/** + * Facade for possible requests to be done to Lucene, like queries, spellchecking requests, etc.. + * + * @param the actual Lucene class representing the request / use case. + */ +class LuceneRequestFacade { + + private final T luceneRequest; + + LuceneRequestFacade(@Nonnull T luceneRequest) { + this.luceneRequest = luceneRequest; + } + + T getLuceneRequest() { + return luceneRequest; + } + + @Override + public String toString() { + return luceneRequest.toString(); + } +} \ No newline at end of file Property changes on: oak-lucene/src/main/java/org/apache/jackrabbit/oak/plugins/index/lucene/LuceneRequestFacade.java ___________________________________________________________________ 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/util/SpellcheckHelper.java =================================================================== --- oak-lucene/src/main/java/org/apache/jackrabbit/oak/plugins/index/lucene/util/SpellcheckHelper.java (revision 0) +++ oak-lucene/src/main/java/org/apache/jackrabbit/oak/plugins/index/lucene/util/SpellcheckHelper.java (working copy) @@ -0,0 +1,38 @@ +package org.apache.jackrabbit.oak.plugins.index.lucene.util; + +import org.apache.jackrabbit.oak.plugins.index.lucene.FieldNames; +import org.apache.lucene.index.IndexReader; +import org.apache.lucene.index.Term; +import org.apache.lucene.search.spell.DirectSpellChecker; +import org.apache.lucene.search.spell.SuggestWord; + +/** + * Helper class for getting spellcheck results for a given term, calling a {@link org.apache.lucene.search.spell.DirectSpellChecker} + * under the hood. + */ +public class SpellcheckHelper { + public static SuggestWord[] getSpellcheck(String spellcheckQueryString, IndexReader reader) { + DirectSpellChecker spellChecker = new DirectSpellChecker(); + try { + String text = null; + for (String param : spellcheckQueryString.split("&")) { + String[] keyValuePair = param.split("="); + if (keyValuePair.length != 2 || keyValuePair[0] == null || keyValuePair[1] == null) { + throw new RuntimeException("Unparsable native Lucene Spellcheck query: " + spellcheckQueryString); + } else { + if ("term".equals(keyValuePair[0])) { + text = keyValuePair[1]; + } + } + } + if (text != null) { + return spellChecker.suggestSimilar(new Term(FieldNames.FULLTEXT, text), 10, reader); + } else { + return new SuggestWord[0]; + } + + } catch (Exception e) { + throw new RuntimeException("could not handle Spellcheck query " + spellcheckQueryString); + } + } +} Property changes on: oak-lucene/src/main/java/org/apache/jackrabbit/oak/plugins/index/lucene/util/SpellcheckHelper.java ___________________________________________________________________ Added: svn:eol-style ## -0,0 +1 ## +native \ No newline at end of property Index: oak-lucene/src/test/java/org/apache/jackrabbit/oak/jcr/query/SpellcheckTest.java =================================================================== --- oak-lucene/src/test/java/org/apache/jackrabbit/oak/jcr/query/SpellcheckTest.java (revision 0) +++ oak-lucene/src/test/java/org/apache/jackrabbit/oak/jcr/query/SpellcheckTest.java (working copy) @@ -0,0 +1,82 @@ +/* + * 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.jcr.query; + +import javax.jcr.Node; +import javax.jcr.RepositoryException; +import javax.jcr.Session; +import javax.jcr.query.Query; +import javax.jcr.query.QueryManager; +import javax.jcr.query.QueryResult; +import javax.jcr.query.Row; +import javax.jcr.query.RowIterator; + +import org.apache.jackrabbit.core.query.AbstractQueryTest; + +/** + * Tests the spellcheck support. + */ +public class SpellcheckTest extends AbstractQueryTest { + + public void testSpellcheckSql() throws Exception { + Session session = superuser; + QueryManager qm = session.getWorkspace().getQueryManager(); + Node n1 = testRootNode.addNode("node1"); + n1.setProperty("text", "hello hello hello alt"); + Node n2 = testRootNode.addNode("node2"); + n2.setProperty("text", "hello"); + session.save(); + + String sql = "SELECT [rep:spellcheck()] FROM nt:base WHERE [jcr:path] = '/' AND SPELLCHECK('helo')"; + Query q = qm.createQuery(sql, Query.SQL); + String result = getResult(q.execute(), "rep:spellcheck()"); + assertNotNull(result); + assertEquals("[hello, hold]", result); + } + + public void testSpellcheckXPath() throws Exception { + Session session = superuser; + QueryManager qm = session.getWorkspace().getQueryManager(); + Node n1 = testRootNode.addNode("node1"); + n1.setProperty("text", "hello hello hello alt"); + Node n2 = testRootNode.addNode("node2"); + n2.setProperty("text", "hello"); + session.save(); + + String xpath = "/jcr:root[rep:spellcheck('helo')]/(rep:spellcheck())"; + Query q = qm.createQuery(xpath, Query.XPATH); + String result = getResult(q.execute(), "rep:spellcheck()"); + assertNotNull(result); + assertEquals("[hello, hold]", result); + } + + static String getResult(QueryResult result, String propertyName) throws RepositoryException { + StringBuilder buff = new StringBuilder(); + RowIterator it = result.getRows(); + while (it.hasNext()) { + if (buff.length() > 0) { + buff.append(", "); + } + Row row = it.nextRow(); + buff.append(row.getValue(propertyName).getString()); + } + return buff.toString(); + } + +} \ No newline at end of file Property changes on: oak-lucene/src/test/java/org/apache/jackrabbit/oak/jcr/query/SpellcheckTest.java ___________________________________________________________________ Added: svn:eol-style ## -0,0 +1 ## +native \ No newline at end of property Index: oak-lucene/src/test/resources/org/apache/jackrabbit/oak/query/sql1.txt =================================================================== --- oak-lucene/src/test/resources/org/apache/jackrabbit/oak/query/sql1.txt (revision 0) +++ oak-lucene/src/test/resources/org/apache/jackrabbit/oak/query/sql1.txt (working copy) @@ -0,0 +1,53 @@ +# 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 + +# 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') +[jackrabbit], /, 1.0 + +sql1 select prop1 from nt:unstructured where prop1 is not null order by prop1 asc + +sql1 select excerpt(.) from nt:resource where contains(., 'jackrabbit') + +sql1 select * from nt:base + where jcr:path like '/testroot/%' + and birth > timestamp '1976-01-01T00:00:00.000+01:00' + +sql1 select * from nt:base + where jcr:path like '/testroot/%' + and value like 'foo\_bar' escape '\' + +sql1 select * from nt:unstructured + where "jcr:path" = '/testroot/foo' and contains(., 'fox') + +sql1 select * from nt:unstructured + where "jcr:path" like '/testroot/%' and contains(., 'fox test') + +# not supported currently +# sql1 select [jcr:path], [jcr:score], * from [nt:base] +# where (0 is not null) and isdescendantnode('/testroot') + Property changes on: oak-lucene/src/test/resources/org/apache/jackrabbit/oak/query/sql1.txt ___________________________________________________________________ Added: svn:eol-style ## -0,0 +1 ## +native \ No newline at end of property 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 1655982) +++ 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 1655982) +++ 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 1655982) +++ 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 noDocs = 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 (noDocs) { + 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)); + noDocs = 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 1655982) +++ oak-solr-core/src/main/resources/solr/oak/conf/schema.xml (working copy) @@ -71,6 +71,13 @@ + + + + + + + @@ -110,6 +117,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 1655982) +++ oak-solr-core/src/main/resources/solr/oak/conf/solrconfig.xml (working copy) @@ -851,6 +851,10 @@ nameOfCustomComponent2 --> + + mlt + spellcheck + @@ -1148,6 +1152,65 @@ --> + + + 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 + + + + Index: oak-solr-core/src/test/resources/solr/oak/conf/solrconfig.xml =================================================================== --- oak-solr-core/src/test/resources/solr/oak/conf/solrconfig.xml (revision 1655982) +++ 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 @@ -1220,9 +1224,7 @@ 4 0.01 - + @@ -1287,9 +1289,9 @@ See http://wiki.apache.org/solr/SpellCheckComponent for details on the request parameters. --> - + - text + catch_all