Index: oak-lucene/src/main/java/org/apache/jackrabbit/oak/plugins/index/lucene/FieldFactory.java =================================================================== --- oak-lucene/src/main/java/org/apache/jackrabbit/oak/plugins/index/lucene/FieldFactory.java (revision 1656661) +++ oak-lucene/src/main/java/org/apache/jackrabbit/oak/plugins/index/lucene/FieldFactory.java (working copy) @@ -118,6 +118,17 @@ return new IntField(FieldNames.PATH_DEPTH, PathUtils.getDepth(path), NO); } + public static Field newSuggestField(String... values) { + StringBuilder builder = new StringBuilder(); + for (String v : values) { + if (builder.length() > 0) { + builder.append('\n'); + } + builder.append(v); + } + return new OakTextField(FieldNames.SUGGEST, builder.toString(), true); + } + /** * Date values are saved with sec resolution * @param date jcr data string Index: oak-lucene/src/main/java/org/apache/jackrabbit/oak/plugins/index/lucene/FieldNames.java =================================================================== --- oak-lucene/src/main/java/org/apache/jackrabbit/oak/plugins/index/lucene/FieldNames.java (revision 1656661) +++ oak-lucene/src/main/java/org/apache/jackrabbit/oak/plugins/index/lucene/FieldNames.java (working copy) @@ -26,6 +26,7 @@ */ public final class FieldNames { + /** * Private constructor. */ @@ -54,6 +55,16 @@ public static final String FULLTEXT = ":fulltext"; /** + * Name of the field that contains the suggest index. + */ + public static final String SUGGEST = ":suggest"; + + /** + * Name of the field that contains the spellcheck index. + */ + public static final String SPELLCHECK = ":spellcheck"; + + /** * Prefix for all field names that are fulltext indexed by property name. */ public static final String ANALYZED_FIELD_PREFIX = "full:"; 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 1656661) +++ oak-lucene/src/main/java/org/apache/jackrabbit/oak/plugins/index/lucene/LuceneIndex.java (working copy) @@ -50,6 +50,7 @@ import com.google.common.collect.AbstractIterator; +import com.google.common.collect.Iterables; import com.google.common.collect.Queues; import com.google.common.collect.Sets; @@ -59,6 +60,7 @@ 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.plugins.index.lucene.util.SuggestHelper; import org.apache.jackrabbit.oak.query.QueryEngineSettings; import org.apache.jackrabbit.oak.query.QueryImpl; import org.apache.jackrabbit.oak.query.fulltext.FullTextAnd; @@ -102,6 +104,7 @@ import org.apache.lucene.search.TopDocs; import org.apache.lucene.search.WildcardQuery; import org.apache.lucene.search.spell.SuggestWord; +import org.apache.lucene.search.suggest.Lookup; import org.apache.lucene.util.BytesRef; import org.apache.lucene.util.automaton.Automaton; import org.apache.lucene.util.automaton.CompiledAutomaton; @@ -264,7 +267,7 @@ public Cursor query(final IndexPlan plan, NodeState rootState) { final Filter filter = plan.getFilter(); FullTextExpression ft = filter.getFullTextConstraint(); - Set relPaths = getRelativePaths(ft); + final Set relPaths = getRelativePaths(ft); if (relPaths.size() > 1) { return new MultiLuceneIndex(filter, rootState, relPaths).query(); } @@ -360,14 +363,24 @@ } lastDocToRecord = doc; } - } else if (luceneRequestFacade.getLuceneRequest() instanceof SuggestWord[]) { - SuggestWord[] intent = (SuggestWord[]) luceneRequestFacade.getLuceneRequest(); - Collection suggestedWords = new ArrayList(intent.length); - for (SuggestWord suggestWord : intent) { + } else if (luceneRequestFacade.getLuceneRequest() instanceof SpellcheckHelper.SpellcheckQuery) { + SpellcheckHelper.SpellcheckQuery spellcheckQuery = (SpellcheckHelper.SpellcheckQuery) luceneRequestFacade.getLuceneRequest(); + SuggestWord[] suggestWords = SpellcheckHelper.getSpellcheck(spellcheckQuery, searcher.getIndexReader()); + Collection suggestedWords = new ArrayList(suggestWords.length); + for (SuggestWord suggestWord : suggestWords) { suggestedWords.add(suggestWord.string); } queue.add(new LuceneResultRow(suggestedWords)); noDocs = true; + } else if (luceneRequestFacade.getLuceneRequest() instanceof SuggestHelper.SuggestQuery) { + SuggestHelper.SuggestQuery suggestQuery = (SuggestHelper.SuggestQuery) luceneRequestFacade.getLuceneRequest(); + List lookupResults = SuggestHelper.getSuggestions(suggestQuery); + Collection suggestedWords = new ArrayList(lookupResults.size()); + for (Lookup.LookupResult suggestWord : lookupResults) { + suggestedWords.add(suggestWord.key.toString()); + } + queue.add(new LuceneResultRow(suggestedWords)); + noDocs = true; } } catch (IOException e) { LOG.warn("query via {} failed.", LuceneIndex.this, e); @@ -481,10 +494,14 @@ if (query.startsWith("spellcheck?")) { String spellcheckQueryString = query.replace("spellcheck?", ""); if (reader != null) { - return new LuceneRequestFacade(SpellcheckHelper.getSpellcheck(spellcheckQueryString, reader)); + return new LuceneRequestFacade(SpellcheckHelper.getSpellcheckQuery(spellcheckQueryString)); } - } - else { + } else if (query.startsWith("suggest?")) { + String suggestQueryString = query.replace("suggest?", ""); + if (reader != null) { + return new LuceneRequestFacade(SuggestHelper.getSuggestQuery(suggestQueryString, reader)); + } + } else { try { qs.add(queryParser.parse(query)); } catch (ParseException e) { @@ -1050,8 +1067,8 @@ if (QueryImpl.JCR_SCORE.equals(columnName)) { return PropertyValues.newDouble(currentRow.score); } - if (QueryImpl.REP_SPELLCHECK.equals(columnName)) { - return PropertyValues.newString(currentRow.suggestWords); + if (QueryImpl.REP_SPELLCHECK.equals(columnName) || QueryImpl.REP_SUGGEST.equals(columnName)) { + return PropertyValues.newString(Iterables.toString(currentRow.suggestWords)); } return pathRow.getValue(columnName); } Index: oak-lucene/src/main/java/org/apache/jackrabbit/oak/plugins/index/lucene/LuceneIndexConstants.java =================================================================== --- oak-lucene/src/main/java/org/apache/jackrabbit/oak/plugins/index/lucene/LuceneIndexConstants.java (revision 1656661) +++ oak-lucene/src/main/java/org/apache/jackrabbit/oak/plugins/index/lucene/LuceneIndexConstants.java (working copy) @@ -226,4 +226,14 @@ * By default, no more than 10,000 terms will be indexed for a field. */ String MAX_FIELD_LENGTH = "maxFieldLength"; + + /** + * whether use this property values for suggestions + */ + String PROP_USE_IN_SUGGEST = "useInSuggest"; + + /** + * whether use this property values for spellchecking + */ + String PROP_USE_IN_SPELLCHECK = "useInSuggest"; } Index: oak-lucene/src/main/java/org/apache/jackrabbit/oak/plugins/index/lucene/LuceneIndexEditor.java =================================================================== --- oak-lucene/src/main/java/org/apache/jackrabbit/oak/plugins/index/lucene/LuceneIndexEditor.java (revision 1656661) +++ oak-lucene/src/main/java/org/apache/jackrabbit/oak/plugins/index/lucene/LuceneIndexEditor.java (working copy) @@ -62,6 +62,7 @@ import org.apache.lucene.document.NumericDocValuesField; import org.apache.lucene.document.SortedDocValuesField; import org.apache.lucene.document.StringField; +import org.apache.lucene.document.TextField; import org.apache.lucene.index.IndexWriter; import org.apache.lucene.search.PrefixQuery; import org.apache.lucene.util.BytesRef; @@ -329,9 +330,22 @@ document.add(newDepthField(path)); } + // because of LUCENE-5833 we have to merge the suggest fields into a single one + Field suggestField = null; for (Field f : fields) { - document.add(f); + if (FieldNames.SUGGEST.endsWith(f.name())) { + if (suggestField == null) { + suggestField = f; + } else { + suggestField = FieldFactory.newSuggestField(suggestField.stringValue(), f.stringValue()); + } + } else { + document.add(f); + } } + if (suggestField != null) { + document.add(suggestField); + } //TODO Boost at document level @@ -365,6 +379,14 @@ fields.add(newPropertyField(analyzedPropName, value, !pd.skipTokenization(pname), pd.stored)); } + if (pd.useInSuggest) { + fields.add(newPropertyField(FieldNames.SUGGEST, value, true, true)); + } + + if (pd.useInSpellcheck) { + fields.add(newPropertyField(FieldNames.SPELLCHECK, value, true, true)); + } + if (pd.nodeScopeIndex) { Field field = newFulltextField(value); field.setBoost(pd.boost); 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 1656661) +++ oak-lucene/src/main/java/org/apache/jackrabbit/oak/plugins/index/lucene/LucenePropertyIndex.java (working copy) @@ -34,6 +34,7 @@ import javax.jcr.PropertyType; import com.google.common.collect.AbstractIterator; +import com.google.common.collect.Iterables; import com.google.common.collect.Lists; import com.google.common.collect.Queues; import com.google.common.collect.Sets; @@ -45,6 +46,7 @@ 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.plugins.index.lucene.util.SuggestHelper; import org.apache.jackrabbit.oak.query.QueryEngineSettings; import org.apache.jackrabbit.oak.query.QueryImpl; import org.apache.jackrabbit.oak.query.fulltext.FullTextAnd; @@ -88,6 +90,8 @@ import org.apache.lucene.search.TopDocs; import org.apache.lucene.search.WildcardQuery; import org.apache.lucene.search.spell.SuggestWord; +import org.apache.lucene.search.suggest.Lookup; +import org.apache.lucene.util.Version; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -332,14 +336,24 @@ } 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; + } else if (luceneRequestFacade.getLuceneRequest() instanceof SpellcheckHelper.SpellcheckQuery) { + SpellcheckHelper.SpellcheckQuery spellcheckQuery = (SpellcheckHelper.SpellcheckQuery) luceneRequestFacade.getLuceneRequest(); + SuggestWord[] suggestWords = SpellcheckHelper.getSpellcheck(spellcheckQuery, searcher.getIndexReader()); + Collection suggestedWords = new ArrayList(suggestWords.length); + for (SuggestWord suggestWord : suggestWords) { + suggestedWords.add(suggestWord.string); } - queue.add(new LuceneResultRow(suggestedWordsStrings)); + queue.add(new LuceneResultRow(suggestedWords)); noDocs = true; + } else if (luceneRequestFacade.getLuceneRequest() instanceof SuggestHelper.SuggestQuery) { + SuggestHelper.SuggestQuery suggestQuery = (SuggestHelper.SuggestQuery) luceneRequestFacade.getLuceneRequest(); + List lookupResults = SuggestHelper.getSuggestions(suggestQuery); + Collection suggestedWords = new ArrayList(lookupResults.size()); + for (Lookup.LookupResult suggestWord : lookupResults) { + suggestedWords.add(suggestWord.key.toString()); + } + queue.add(new LuceneResultRow(suggestedWords)); + noDocs = true; } } catch (IOException e) { LOG.warn("query via {} failed.", LucenePropertyIndex.this, e); @@ -478,10 +492,15 @@ } else if (query.startsWith("spellcheck?")) { String spellcheckQueryString = query.replace("spellcheck?", ""); if (reader != null) { - return new LuceneRequestFacade(SpellcheckHelper.getSpellcheck(spellcheckQueryString, reader)); + return new LuceneRequestFacade(SpellcheckHelper.getSpellcheckQuery(spellcheckQueryString)); } - } - else { + } else if (query.startsWith("suggest?")) { + String suggestQueryString = query.replace("suggest?", ""); + if (reader != null) { + return new LuceneRequestFacade(SuggestHelper.getSuggestQuery(suggestQueryString, + reader)); + } + } else { try { qs.add(queryParser.parse(query)); } catch (ParseException e) { @@ -834,7 +853,7 @@ // (a "non-local return") final AtomicReference result = new AtomicReference(); ft.accept(new FullTextVisitor() { - + @Override public boolean visit(FullTextContains contains) { visitTerm(contains.getPropertyName(), contains.getRawText(), null, false); @@ -880,7 +899,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 = getLuceneFieldName(propertyName, pr); Query q = tokenToQuery(text, p, analyzer); @@ -1002,15 +1021,15 @@ static class LuceneResultRow { final String path; final double score; - final String[] suggestWords; + final Iterable suggestWords; LuceneResultRow(String path, double score) { this.path = path; this.score = score; - this.suggestWords = new String[0]; + this.suggestWords = Collections.emptySet(); } - LuceneResultRow(String[] suggestWords) { + LuceneResultRow(Iterable suggestWords) { this.path = "/"; this.score = 1.0d; this.suggestWords = suggestWords; @@ -1021,17 +1040,17 @@ 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; private final String pathPrefix; LuceneResultRow currentRow; - + LucenePathCursor(final Iterator it, final IndexPlan plan, QueryEngineSettings settings) { pathPrefix = plan.getPathPrefix(); Iterator pathIterator = new Iterator() { @@ -1043,7 +1062,7 @@ @Override public String next() { - currentRow = it.next(); + currentRow = it.next(); return currentRow.path; } @@ -1051,12 +1070,12 @@ public void remove() { it.remove(); } - + }; pathCursor = new PathCursor(pathIterator, true, settings); } - + @Override public boolean hasNext() { return pathCursor.hasNext(); @@ -1088,12 +1107,12 @@ if (QueryImpl.JCR_SCORE.equals(columnName)) { return PropertyValues.newDouble(currentRow.score); } - if (QueryImpl.REP_SPELLCHECK.equals(columnName)) { - return PropertyValues.newString(Arrays.toString(currentRow.suggestWords)); + if (QueryImpl.REP_SPELLCHECK.equals(columnName) || QueryImpl.REP_SUGGEST.equals(columnName)) { + return PropertyValues.newString(Iterables.toString(currentRow.suggestWords)); } return pathRow.getValue(columnName); } - + }; } } Index: oak-lucene/src/main/java/org/apache/jackrabbit/oak/plugins/index/lucene/PropertyDefinition.java =================================================================== --- oak-lucene/src/main/java/org/apache/jackrabbit/oak/plugins/index/lucene/PropertyDefinition.java (revision 1656661) +++ oak-lucene/src/main/java/org/apache/jackrabbit/oak/plugins/index/lucene/PropertyDefinition.java (working copy) @@ -70,6 +70,10 @@ final int includedPropertyTypes; + boolean useInSuggest; + + boolean useInSpellcheck; + public PropertyDefinition(IndexingRule idxDefn, String name, NodeState defn) { this.isRegexp = getOptionalValue(defn, PROP_IS_REGEX, false); this.name = getName(defn, name); @@ -90,6 +94,8 @@ //TODO Add test case for above cases this.propertyType = getPropertyType(idxDefn, name, defn); + this.useInSuggest = getOptionalValue(defn, LuceneIndexConstants.PROP_USE_IN_SUGGEST, false); + this.useInSpellcheck = getOptionalValue(defn, LuceneIndexConstants.PROP_USE_IN_SPELLCHECK, false); } /** @@ -149,6 +155,8 @@ ", propertyIndex=" + propertyIndex + ", analyzed=" + analyzed + ", ordered=" + ordered + + ", useInSuggest=" + useInSuggest+ + ", useInSpellcheck=" + useInSpellcheck+ '}'; } Index: oak-lucene/src/main/java/org/apache/jackrabbit/oak/plugins/index/lucene/util/CRTokenizer.java =================================================================== --- oak-lucene/src/main/java/org/apache/jackrabbit/oak/plugins/index/lucene/util/CRTokenizer.java (revision 0) +++ oak-lucene/src/main/java/org/apache/jackrabbit/oak/plugins/index/lucene/util/CRTokenizer.java (working copy) @@ -0,0 +1,38 @@ +/* + * 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.util; + +import java.io.Reader; + +import org.apache.lucene.analysis.util.CharTokenizer; +import org.apache.lucene.util.Version; + +/** + * A {@link org.apache.lucene.analysis.util.CharTokenizer} dividing tokens at \n. + */ +public class CRTokenizer extends CharTokenizer { + public CRTokenizer(Version matchVersion, Reader input) { + super(matchVersion, input); + } + + @Override + protected boolean isTokenChar(int c) { + return c != '\n'; + } +} Property changes on: oak-lucene/src/main/java/org/apache/jackrabbit/oak/plugins/index/lucene/util/CRTokenizer.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 1656661) +++ oak-lucene/src/main/java/org/apache/jackrabbit/oak/plugins/index/lucene/util/SpellcheckHelper.java (working copy) @@ -29,28 +29,53 @@ * under the hood. */ public class SpellcheckHelper { - public static SuggestWord[] getSpellcheck(String spellcheckQueryString, IndexReader reader) { - DirectSpellChecker spellChecker = new DirectSpellChecker(); + public static SuggestWord[] getSpellcheck(SpellcheckQuery spellcheckQuery, IndexReader reader) { 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]; - } + DirectSpellChecker spellChecker = new DirectSpellChecker(); + return spellChecker.suggestSimilar(spellcheckQuery.getTerm(), spellcheckQuery.getCount(), reader); + } catch (Exception e) { + throw new RuntimeException("could not handle Spellcheck query " + spellcheckQuery, e); + } + } + + public static SpellcheckQuery getSpellcheckQuery(String spellcheckQueryString) { + 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]; - } + } + return new SpellcheckHelper.SpellcheckQuery(new Term(FieldNames.SPELLCHECK, text), 10); + } - } catch (Exception e) { - throw new RuntimeException("could not handle Spellcheck query " + spellcheckQueryString); + public static class SpellcheckQuery { + private final Term term; + private final int count; + + public SpellcheckQuery(Term term, int count) { + this.term = term; + this.count = count; } + + public Term getTerm() { + return term; + } + + public int getCount() { + return count; + } + + @Override + public String toString() { + return "SpellcheckQuery{" + + "term=" + term + + ", count=" + count + + '}'; + } } } Index: oak-lucene/src/main/java/org/apache/jackrabbit/oak/plugins/index/lucene/util/SuggestHelper.java =================================================================== --- oak-lucene/src/main/java/org/apache/jackrabbit/oak/plugins/index/lucene/util/SuggestHelper.java (revision 0) +++ oak-lucene/src/main/java/org/apache/jackrabbit/oak/plugins/index/lucene/util/SuggestHelper.java (working copy) @@ -0,0 +1,137 @@ +/* + * 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.util; + +import java.io.Reader; +import java.util.Collections; +import java.util.List; + +import org.apache.jackrabbit.oak.plugins.index.lucene.FieldNames; +import org.apache.lucene.analysis.Analyzer; +import org.apache.lucene.analysis.core.KeywordAnalyzer; +import org.apache.lucene.analysis.standard.ClassicAnalyzer; +import org.apache.lucene.analysis.standard.StandardAnalyzer; +import org.apache.lucene.index.IndexReader; +import org.apache.lucene.search.suggest.DocumentDictionary; +import org.apache.lucene.search.suggest.InputIterator; +import org.apache.lucene.search.suggest.Lookup; +import org.apache.lucene.search.suggest.analyzing.AnalyzingInfixSuggester; +import org.apache.lucene.search.suggest.analyzing.FreeTextSuggester; +import org.apache.lucene.search.suggest.analyzing.FuzzySuggester; +import org.apache.lucene.search.suggest.fst.WFSTCompletionLookup; +import org.apache.lucene.util.BytesRef; +import org.apache.lucene.util.Version; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Helper class for getting suggest results for a given term, calling a {@link org.apache.lucene.search.suggest.Lookup} + * implementation under the hood. + */ +public class SuggestHelper { + + private static final Logger log = LoggerFactory.getLogger(SuggestHelper.class); + + public static List getSuggestions(SuggestQuery suggestQuery) { + FreeTextSuggester suggester = new FreeTextSuggester(new Analyzer() { + @Override + protected TokenStreamComponents createComponents(String fieldName, Reader reader) { + return new TokenStreamComponents(new CRTokenizer(Version.LUCENE_47, reader)); + } + }); + try { + DocumentDictionary dictionary = suggestQuery.getDictionary(); + suggester.build(dictionary); // TODO : it should be possible to avoid rebuilding the index every time + } catch (Exception e) { + log.warn("could not build suggester from the passed dictionary ", e); + } + try { + long count = suggester.getCount(); + if (count > 0) { + return suggester.lookup(suggestQuery.getText(), false, 10); + } else { + return Collections.emptyList(); + } + } catch (Exception e) { + throw new RuntimeException("could not handle Suggest query " + suggestQuery, e); + } + } + + public static SuggestQuery getSuggestQuery(String suggestQueryString, IndexReader reader) { + try { + String text = null; + for (String param : suggestQueryString.split("&")) { + String[] keyValuePair = param.split("="); + if (keyValuePair.length != 2 || keyValuePair[0] == null || keyValuePair[1] == null) { + throw new RuntimeException("Unparsable native Lucene Suggest query: " + suggestQueryString); + } else { + if ("term".equals(keyValuePair[0])) { + text = keyValuePair[1]; + } + } + } + if (text != null) { + return new SuggestQuery(new DocumentDictionary(reader, FieldNames.SUGGEST, FieldNames.PATH_DEPTH), text, new Analyzer() { + @Override + protected TokenStreamComponents createComponents(String fieldName, Reader reader) { + return new TokenStreamComponents(new CRTokenizer(Version.LUCENE_47, reader)); + } + }); + } else { + return null; + } + + } catch (Exception e) { + throw new RuntimeException("could not build SuggestQuery " + suggestQueryString, e); + } + } + + public static class SuggestQuery { + + private final DocumentDictionary dictionary; + private final String text; + private final Analyzer analyzer; + + public SuggestQuery(DocumentDictionary dictionary, String text, Analyzer analyzer) { + this.dictionary = dictionary; + this.text = text; + this.analyzer = analyzer; + } + + public DocumentDictionary getDictionary() { + return dictionary; + } + + public String getText() { + return text; + } + + public Analyzer getAnalyzer() { + return analyzer; + } + + @Override + public String toString() { + return "SuggestQuery{" + + "dictionary=" + dictionary + + ", text='" + text + '\'' + + '}'; + } + } +} Property changes on: oak-lucene/src/main/java/org/apache/jackrabbit/oak/plugins/index/lucene/util/SuggestHelper.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/LuceneOakRepositoryStub.java =================================================================== --- oak-lucene/src/test/java/org/apache/jackrabbit/oak/jcr/LuceneOakRepositoryStub.java (revision 1656661) +++ oak-lucene/src/test/java/org/apache/jackrabbit/oak/jcr/LuceneOakRepositoryStub.java (working copy) @@ -100,6 +100,8 @@ .setProperty(LuceneIndexConstants.PROP_NODE_SCOPE_INDEX, true) .setProperty(LuceneIndexConstants.PROP_USE_IN_EXCERPT, true) .setProperty(LuceneIndexConstants.PROP_PROPERTY_INDEX, true) + .setProperty(LuceneIndexConstants.PROP_USE_IN_SPELLCHECK, true) + .setProperty(LuceneIndexConstants.PROP_USE_IN_SUGGEST, true) .setProperty(LuceneIndexConstants.PROP_NAME, LuceneIndexConstants.REGEX_ALL_PROPS) .setProperty(LuceneIndexConstants.PROP_IS_REGEX, true); } Index: oak-lucene/src/test/java/org/apache/jackrabbit/oak/jcr/query/SuggestTest.java =================================================================== --- oak-lucene/src/test/java/org/apache/jackrabbit/oak/jcr/query/SuggestTest.java (revision 0) +++ oak-lucene/src/test/java/org/apache/jackrabbit/oak/jcr/query/SuggestTest.java (working copy) @@ -0,0 +1,84 @@ +/* + * 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 suggest support. + */ +public class SuggestTest extends AbstractQueryTest { + + public void testSuggestSql() throws Exception { + Session session = superuser; + Node n1 = testRootNode.addNode("node1"); + n1.setProperty("jcr:title", "in 2015 my fox is red, like mike's fox and john's fox"); + Node n2 = testRootNode.addNode("node2"); + n2.setProperty("jcr:title", "in 2015 a red fox is still a fox"); + session.save(); + + QueryManager qm = session.getWorkspace().getQueryManager(); + String sql = "SELECT [rep:suggest()] FROM nt:base WHERE [jcr:path] = '/' AND SUGGEST('in 201')"; + Query q = qm.createQuery(sql, Query.SQL); + String result = getResult(q.execute(), "rep:suggest()"); + assertNotNull(result); + assertEquals("[in 2015 a red fox is still a fox, in 2015 my fox is red, like mike's fox and john's fox]", + result); + } + + public void testSuggestXPath() throws Exception { + Session session = superuser; + QueryManager qm = session.getWorkspace().getQueryManager(); + Node n1 = testRootNode.addNode("node1"); + n1.setProperty("jcr:title", "in 2015 my fox is red, like mike's fox and john's fox"); + Node n2 = testRootNode.addNode("node2"); + n2.setProperty("jcr:title", "in 2015 a red fox is still a fox"); + session.save(); + + String xpath = "/jcr:root[rep:suggest('in 201')]/(rep:suggest())"; + Query q = qm.createQuery(xpath, Query.XPATH); + String result = getResult(q.execute(), "rep:suggest()"); + assertNotNull(result); + assertEquals("[in 2015 a red fox is still a fox, in 2015 my fox is red, like mike's fox and john's fox]", + 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/SuggestTest.java ___________________________________________________________________ Added: svn:eol-style ## -0,0 +1 ## +native \ No newline at end of property