Query DSL: Text Queries (boolean, phrase, and phrase_prefix), closes #917.
This commit is contained in:
parent
e66c78ad64
commit
465036655f
|
@ -0,0 +1,122 @@
|
||||||
|
/*
|
||||||
|
* Licensed to Elastic Search and Shay Banon under one
|
||||||
|
* or more contributor license agreements. See the NOTICE file
|
||||||
|
* distributed with this work for additional information
|
||||||
|
* regarding copyright ownership. Elastic Search 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.elasticsearch.common.lucene.search;
|
||||||
|
|
||||||
|
import org.apache.lucene.index.IndexReader;
|
||||||
|
import org.apache.lucene.index.Term;
|
||||||
|
import org.apache.lucene.search.*;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.util.Set;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Query that matches no documents.
|
||||||
|
*/
|
||||||
|
public final class MatchNoDocsQuery extends Query {
|
||||||
|
|
||||||
|
public static MatchNoDocsQuery INSTANCE = new MatchNoDocsQuery();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Since all instances of this class are equal to each other,
|
||||||
|
* we have a constant hash code.
|
||||||
|
*/
|
||||||
|
private static final int HASH_CODE = 12345;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Weight implementation that matches no documents.
|
||||||
|
*/
|
||||||
|
private class MatchNoDocsWeight extends Weight {
|
||||||
|
/**
|
||||||
|
* The similarity implementation.
|
||||||
|
*/
|
||||||
|
private final Similarity similarity;
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a new weight that matches nothing.
|
||||||
|
*
|
||||||
|
* @param searcher the search to match for
|
||||||
|
*/
|
||||||
|
public MatchNoDocsWeight(final Searcher searcher) {
|
||||||
|
this.similarity = searcher.getSimilarity();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String toString() {
|
||||||
|
return "weight(" + MatchNoDocsQuery.this + ")";
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Query getQuery() {
|
||||||
|
return MatchNoDocsQuery.this;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public float getValue() {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public float sumOfSquaredWeights() {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void normalize(final float queryNorm) {
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Scorer scorer(final IndexReader reader,
|
||||||
|
final boolean scoreDocsInOrder,
|
||||||
|
final boolean topScorer) throws IOException {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Explanation explain(final IndexReader reader,
|
||||||
|
final int doc) {
|
||||||
|
return new ComplexExplanation(false, 0, "MatchNoDocs matches nothing");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Weight createWeight(final Searcher searcher) {
|
||||||
|
return new MatchNoDocsWeight(searcher);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void extractTerms(final Set<Term> terms) {
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String toString(final String field) {
|
||||||
|
return "MatchNoDocsQuery";
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean equals(final Object o) {
|
||||||
|
return o instanceof MatchAllDocsQuery;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int hashCode() {
|
||||||
|
return HASH_CODE;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,258 @@
|
||||||
|
/*
|
||||||
|
* Licensed to Elastic Search and Shay Banon under one
|
||||||
|
* or more contributor license agreements. See the NOTICE file
|
||||||
|
* distributed with this work for additional information
|
||||||
|
* regarding copyright ownership. Elastic Search 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.elasticsearch.common.lucene.search;
|
||||||
|
|
||||||
|
import org.apache.lucene.index.IndexReader;
|
||||||
|
import org.apache.lucene.index.Term;
|
||||||
|
import org.apache.lucene.index.TermEnum;
|
||||||
|
import org.apache.lucene.search.MultiPhraseQuery;
|
||||||
|
import org.apache.lucene.search.Query;
|
||||||
|
import org.apache.lucene.util.ToStringUtils;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.util.*;
|
||||||
|
|
||||||
|
public class MultiPhrasePrefixQuery extends Query {
|
||||||
|
|
||||||
|
private String field;
|
||||||
|
private ArrayList<Term[]> termArrays = new ArrayList<Term[]>();
|
||||||
|
private ArrayList<Integer> positions = new ArrayList<Integer>();
|
||||||
|
private int maxExpansions = Integer.MAX_VALUE;
|
||||||
|
|
||||||
|
private int slop = 0;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the phrase slop for this query.
|
||||||
|
*
|
||||||
|
* @see org.apache.lucene.search.PhraseQuery#setSlop(int)
|
||||||
|
*/
|
||||||
|
public void setSlop(int s) {
|
||||||
|
slop = s;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setMaxExpansions(int maxExpansions) {
|
||||||
|
this.maxExpansions = maxExpansions;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the phrase slop for this query.
|
||||||
|
*
|
||||||
|
* @see org.apache.lucene.search.PhraseQuery#getSlop()
|
||||||
|
*/
|
||||||
|
public int getSlop() {
|
||||||
|
return slop;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add a single term at the next position in the phrase.
|
||||||
|
*
|
||||||
|
* @see org.apache.lucene.search.PhraseQuery#add(Term)
|
||||||
|
*/
|
||||||
|
public void add(Term term) {
|
||||||
|
add(new Term[]{term});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add multiple terms at the next position in the phrase. Any of the terms
|
||||||
|
* may match.
|
||||||
|
*
|
||||||
|
* @see org.apache.lucene.search.PhraseQuery#add(Term)
|
||||||
|
*/
|
||||||
|
public void add(Term[] terms) {
|
||||||
|
int position = 0;
|
||||||
|
if (positions.size() > 0)
|
||||||
|
position = positions.get(positions.size() - 1).intValue() + 1;
|
||||||
|
|
||||||
|
add(terms, position);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Allows to specify the relative position of terms within the phrase.
|
||||||
|
*
|
||||||
|
* @param terms
|
||||||
|
* @param position
|
||||||
|
* @see org.apache.lucene.search.PhraseQuery#add(Term, int)
|
||||||
|
*/
|
||||||
|
public void add(Term[] terms, int position) {
|
||||||
|
if (termArrays.size() == 0)
|
||||||
|
field = terms[0].field();
|
||||||
|
|
||||||
|
for (int i = 0; i < terms.length; i++) {
|
||||||
|
if (terms[i].field() != field) {
|
||||||
|
throw new IllegalArgumentException(
|
||||||
|
"All phrase terms must be in the same field (" + field + "): "
|
||||||
|
+ terms[i]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
termArrays.add(terms);
|
||||||
|
positions.add(Integer.valueOf(position));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns a List of the terms in the multiphrase.
|
||||||
|
* Do not modify the List or its contents.
|
||||||
|
*/
|
||||||
|
public List<Term[]> getTermArrays() {
|
||||||
|
return Collections.unmodifiableList(termArrays);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the relative positions of terms in this phrase.
|
||||||
|
*/
|
||||||
|
public int[] getPositions() {
|
||||||
|
int[] result = new int[positions.size()];
|
||||||
|
for (int i = 0; i < positions.size(); i++)
|
||||||
|
result[i] = positions.get(i).intValue();
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override public Query rewrite(IndexReader reader) throws IOException {
|
||||||
|
MultiPhraseQuery query = new MultiPhraseQuery();
|
||||||
|
query.setSlop(slop);
|
||||||
|
int sizeMinus1 = termArrays.size() - 1;
|
||||||
|
for (int i = 0; i < sizeMinus1; i++) {
|
||||||
|
query.add(termArrays.get(i), positions.get(i));
|
||||||
|
}
|
||||||
|
Term[] suffixTerms = termArrays.get(sizeMinus1);
|
||||||
|
int position = positions.get(sizeMinus1);
|
||||||
|
List<Term> terms = new ArrayList<Term>();
|
||||||
|
for (Term term : suffixTerms) {
|
||||||
|
getPrefixTerms(terms, term, reader);
|
||||||
|
}
|
||||||
|
if (terms.isEmpty()) {
|
||||||
|
return MatchNoDocsQuery.INSTANCE;
|
||||||
|
}
|
||||||
|
query.add(terms.toArray(new Term[terms.size()]), position);
|
||||||
|
return query.rewrite(reader);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void getPrefixTerms(List<Term> terms, final Term prefix, final IndexReader reader) throws IOException {
|
||||||
|
TermEnum enumerator = reader.terms(prefix);
|
||||||
|
try {
|
||||||
|
do {
|
||||||
|
Term term = enumerator.term();
|
||||||
|
if (term != null
|
||||||
|
&& term.text().startsWith(prefix.text())
|
||||||
|
&& term.field().equals(field)) {
|
||||||
|
terms.add(term);
|
||||||
|
} else {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
if (terms.size() > maxExpansions) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
} while (enumerator.next());
|
||||||
|
} finally {
|
||||||
|
enumerator.close();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public final String toString(String f) {
|
||||||
|
StringBuilder buffer = new StringBuilder();
|
||||||
|
if (field == null || !field.equals(f)) {
|
||||||
|
buffer.append(field);
|
||||||
|
buffer.append(":");
|
||||||
|
}
|
||||||
|
|
||||||
|
buffer.append("\"");
|
||||||
|
Iterator<Term[]> i = termArrays.iterator();
|
||||||
|
while (i.hasNext()) {
|
||||||
|
Term[] terms = i.next();
|
||||||
|
if (terms.length > 1) {
|
||||||
|
buffer.append("(");
|
||||||
|
for (int j = 0; j < terms.length; j++) {
|
||||||
|
buffer.append(terms[j].text());
|
||||||
|
if (j < terms.length - 1)
|
||||||
|
buffer.append(" ");
|
||||||
|
}
|
||||||
|
buffer.append(")");
|
||||||
|
} else {
|
||||||
|
buffer.append(terms[0].text());
|
||||||
|
}
|
||||||
|
if (i.hasNext())
|
||||||
|
buffer.append(" ");
|
||||||
|
}
|
||||||
|
buffer.append("\"");
|
||||||
|
|
||||||
|
if (slop != 0) {
|
||||||
|
buffer.append("~");
|
||||||
|
buffer.append(slop);
|
||||||
|
}
|
||||||
|
|
||||||
|
buffer.append(ToStringUtils.boost(getBoost()));
|
||||||
|
|
||||||
|
return buffer.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns true if <code>o</code> is equal to this.
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public boolean equals(Object o) {
|
||||||
|
if (!(o instanceof MultiPhrasePrefixQuery)) return false;
|
||||||
|
MultiPhrasePrefixQuery other = (MultiPhrasePrefixQuery) o;
|
||||||
|
return this.getBoost() == other.getBoost()
|
||||||
|
&& this.slop == other.slop
|
||||||
|
&& termArraysEquals(this.termArrays, other.termArrays)
|
||||||
|
&& this.positions.equals(other.positions);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns a hash code value for this object.
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public int hashCode() {
|
||||||
|
return Float.floatToIntBits(getBoost())
|
||||||
|
^ slop
|
||||||
|
^ termArraysHashCode()
|
||||||
|
^ positions.hashCode()
|
||||||
|
^ 0x4AC65113;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Breakout calculation of the termArrays hashcode
|
||||||
|
private int termArraysHashCode() {
|
||||||
|
int hashCode = 1;
|
||||||
|
for (final Term[] termArray : termArrays) {
|
||||||
|
hashCode = 31 * hashCode
|
||||||
|
+ (termArray == null ? 0 : Arrays.hashCode(termArray));
|
||||||
|
}
|
||||||
|
return hashCode;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Breakout calculation of the termArrays equals
|
||||||
|
private boolean termArraysEquals(List<Term[]> termArrays1, List<Term[]> termArrays2) {
|
||||||
|
if (termArrays1.size() != termArrays2.size()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
ListIterator<Term[]> iterator1 = termArrays1.listIterator();
|
||||||
|
ListIterator<Term[]> iterator2 = termArrays2.listIterator();
|
||||||
|
while (iterator1.hasNext()) {
|
||||||
|
Term[] termArray1 = iterator1.next();
|
||||||
|
Term[] termArray2 = iterator2.next();
|
||||||
|
if (!(termArray1 == null ? termArray2 == null : Arrays.equals(termArray1,
|
||||||
|
termArray2))) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
|
@ -220,6 +220,7 @@ public class IndexQueryParserModule extends AbstractModule {
|
||||||
|
|
||||||
private static class DefaultQueryProcessors extends QueryParsersProcessor {
|
private static class DefaultQueryProcessors extends QueryParsersProcessor {
|
||||||
@Override public void processXContentQueryParsers(XContentQueryParsersBindings bindings) {
|
@Override public void processXContentQueryParsers(XContentQueryParsersBindings bindings) {
|
||||||
|
bindings.processXContentQueryParser(TextQueryParser.NAME, TextQueryParser.class);
|
||||||
bindings.processXContentQueryParser(HasChildQueryParser.NAME, HasChildQueryParser.class);
|
bindings.processXContentQueryParser(HasChildQueryParser.NAME, HasChildQueryParser.class);
|
||||||
bindings.processXContentQueryParser(TopChildrenQueryParser.NAME, TopChildrenQueryParser.class);
|
bindings.processXContentQueryParser(TopChildrenQueryParser.NAME, TopChildrenQueryParser.class);
|
||||||
bindings.processXContentQueryParser(DisMaxQueryParser.NAME, DisMaxQueryParser.class);
|
bindings.processXContentQueryParser(DisMaxQueryParser.NAME, DisMaxQueryParser.class);
|
||||||
|
|
|
@ -33,6 +33,36 @@ public abstract class QueryBuilders {
|
||||||
return new MatchAllQueryBuilder();
|
return new MatchAllQueryBuilder();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a text query with type "BOOLEAN" for the provided field name and text.
|
||||||
|
*
|
||||||
|
* @param name The field name.
|
||||||
|
* @param text The query text (to be analyzed).
|
||||||
|
*/
|
||||||
|
public static TextQueryBuilder text(String name, Object text) {
|
||||||
|
return new TextQueryBuilder(name, text).type(TextQueryBuilder.Type.BOOLEAN);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a text query with type "PHRASE" for the provided field name and text.
|
||||||
|
*
|
||||||
|
* @param name The field name.
|
||||||
|
* @param text The query text (to be analyzed).
|
||||||
|
*/
|
||||||
|
public static TextQueryBuilder textPhrase(String name, Object text) {
|
||||||
|
return new TextQueryBuilder(name, text).type(TextQueryBuilder.Type.PHRASE);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a text query with type "PHRASE_PREFIX" for the provided field name and text.
|
||||||
|
*
|
||||||
|
* @param name The field name.
|
||||||
|
* @param text The query text (to be analyzed).
|
||||||
|
*/
|
||||||
|
public static TextQueryBuilder textPhrasePrefix(String name, Object text) {
|
||||||
|
return new TextQueryBuilder(name, text).type(TextQueryBuilder.Type.PHRASE_PREFIX);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A query that generates the union of documents produced by its sub-queries, and that scores each document
|
* A query that generates the union of documents produced by its sub-queries, and that scores each document
|
||||||
* with the maximum score for that document as produced by any sub-query, plus a tie breaking increment for any
|
* with the maximum score for that document as produced by any sub-query, plus a tie breaking increment for any
|
||||||
|
|
|
@ -0,0 +1,158 @@
|
||||||
|
/*
|
||||||
|
* Licensed to Elastic Search and Shay Banon under one
|
||||||
|
* or more contributor license agreements. See the NOTICE file
|
||||||
|
* distributed with this work for additional information
|
||||||
|
* regarding copyright ownership. Elastic Search 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.elasticsearch.index.query.xcontent;
|
||||||
|
|
||||||
|
import org.elasticsearch.common.xcontent.XContentBuilder;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Text query is a query that analyzes the text and constructs a query as the result of the analysis. It
|
||||||
|
* can construct different queries based on the type provided.
|
||||||
|
*/
|
||||||
|
public class TextQueryBuilder extends BaseQueryBuilder {
|
||||||
|
|
||||||
|
public static enum Operator {
|
||||||
|
OR,
|
||||||
|
AND
|
||||||
|
}
|
||||||
|
|
||||||
|
public static enum Type {
|
||||||
|
/**
|
||||||
|
* The text is analyzed and terms are added to a boolean query.
|
||||||
|
*/
|
||||||
|
BOOLEAN,
|
||||||
|
/**
|
||||||
|
* The text is analyzed and used as a phrase query.
|
||||||
|
*/
|
||||||
|
PHRASE,
|
||||||
|
/**
|
||||||
|
* The text is analyzed and used in a phrase query, with the last term acting as a prefix.
|
||||||
|
*/
|
||||||
|
PHRASE_PREFIX
|
||||||
|
}
|
||||||
|
|
||||||
|
private final String name;
|
||||||
|
|
||||||
|
private final Object text;
|
||||||
|
|
||||||
|
private Type type;
|
||||||
|
|
||||||
|
private Operator operator;
|
||||||
|
|
||||||
|
private String analyzer;
|
||||||
|
|
||||||
|
private Integer slop;
|
||||||
|
|
||||||
|
private String fuzziness;
|
||||||
|
|
||||||
|
private Integer prefixLength;
|
||||||
|
|
||||||
|
private Integer maxExpansions;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Constructs a new text query.
|
||||||
|
*/
|
||||||
|
public TextQueryBuilder(String name, Object text) {
|
||||||
|
this.name = name;
|
||||||
|
this.text = text;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the type of the text query.
|
||||||
|
*/
|
||||||
|
public TextQueryBuilder type(Type type) {
|
||||||
|
this.type = type;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the operator to use when using a boolean query. Defaults to <tt>OR</tt>.
|
||||||
|
*/
|
||||||
|
public TextQueryBuilder operator(Operator operator) {
|
||||||
|
this.operator = operator;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Explicitly set the analyzer to use. Defaults to use explicit mapping config for the field, or, if not
|
||||||
|
* set, the default search analyzer.
|
||||||
|
*/
|
||||||
|
public TextQueryBuilder analyzer(String analyzer) {
|
||||||
|
this.analyzer = analyzer;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set the phrase slop if evaluated to a phrase query type.
|
||||||
|
*/
|
||||||
|
public TextQueryBuilder slop(int slop) {
|
||||||
|
this.slop = slop;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the minimum similarity used when evaluated to a fuzzy query type. Defaults to "0.5".
|
||||||
|
*/
|
||||||
|
public TextQueryBuilder fuzziness(Object fuzziness) {
|
||||||
|
this.fuzziness = fuzziness.toString();
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* When using fuzzy or prefix type query, the number of term expansions to use. Defaults to unbounded
|
||||||
|
* so its recommended to set it to a reasonable value for faster execution.
|
||||||
|
*/
|
||||||
|
public TextQueryBuilder maxExpansions(int maxExpansions) {
|
||||||
|
this.maxExpansions = maxExpansions;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override public void doXContent(XContentBuilder builder, Params params) throws IOException {
|
||||||
|
builder.startObject(TextQueryParser.NAME);
|
||||||
|
builder.startObject(name);
|
||||||
|
|
||||||
|
builder.field("text", text);
|
||||||
|
if (type != null) {
|
||||||
|
builder.field("type", type.toString().toLowerCase());
|
||||||
|
}
|
||||||
|
if (operator != null) {
|
||||||
|
builder.field("operator", operator.toString());
|
||||||
|
}
|
||||||
|
if (analyzer != null) {
|
||||||
|
builder.field("analyzer", analyzer);
|
||||||
|
}
|
||||||
|
if (slop != null) {
|
||||||
|
builder.field("slop", slop);
|
||||||
|
}
|
||||||
|
if (fuzziness != null) {
|
||||||
|
builder.field("fuzziness", fuzziness);
|
||||||
|
}
|
||||||
|
if (prefixLength != null) {
|
||||||
|
builder.field("prefix_length", prefixLength);
|
||||||
|
}
|
||||||
|
if (maxExpansions != null) {
|
||||||
|
builder.field("max_expansions", maxExpansions);
|
||||||
|
}
|
||||||
|
|
||||||
|
builder.endObject();
|
||||||
|
builder.endObject();
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,138 @@
|
||||||
|
/*
|
||||||
|
* Licensed to Elastic Search and Shay Banon under one
|
||||||
|
* or more contributor license agreements. See the NOTICE file
|
||||||
|
* distributed with this work for additional information
|
||||||
|
* regarding copyright ownership. Elastic Search 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.elasticsearch.index.query.xcontent;
|
||||||
|
|
||||||
|
import org.apache.lucene.search.BooleanClause;
|
||||||
|
import org.apache.lucene.search.FuzzyQuery;
|
||||||
|
import org.apache.lucene.search.Query;
|
||||||
|
import org.elasticsearch.common.inject.Inject;
|
||||||
|
import org.elasticsearch.common.settings.Settings;
|
||||||
|
import org.elasticsearch.common.xcontent.XContentParser;
|
||||||
|
import org.elasticsearch.index.AbstractIndexComponent;
|
||||||
|
import org.elasticsearch.index.Index;
|
||||||
|
import org.elasticsearch.index.query.QueryParsingException;
|
||||||
|
import org.elasticsearch.index.settings.IndexSettings;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @author kimchy (shay.banon)
|
||||||
|
*/
|
||||||
|
public class TextQueryParser extends AbstractIndexComponent implements XContentQueryParser {
|
||||||
|
|
||||||
|
public static final String NAME = "text";
|
||||||
|
|
||||||
|
@Inject public TextQueryParser(Index index, @IndexSettings Settings settings) {
|
||||||
|
super(index, settings);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override public String[] names() {
|
||||||
|
return new String[]{NAME, "text_phrase", "textPhrase", "text_phrase_prefix", "textPhrasePrefix", "fuzzyText", "fuzzy_text"};
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override public Query parse(QueryParseContext parseContext) throws IOException, QueryParsingException {
|
||||||
|
XContentParser parser = parseContext.parser();
|
||||||
|
|
||||||
|
org.elasticsearch.index.search.TextQueryParser.Type type = org.elasticsearch.index.search.TextQueryParser.Type.BOOLEAN;
|
||||||
|
if ("text_phrase".equals(parser.currentName()) || "textPhrase".equals(parser.currentName())) {
|
||||||
|
type = org.elasticsearch.index.search.TextQueryParser.Type.PHRASE;
|
||||||
|
} else if ("text_phrase_prefix".equals(parser.currentName()) || "textPhrasePrefix".equals(parser.currentName())) {
|
||||||
|
type = org.elasticsearch.index.search.TextQueryParser.Type.PHRASE_PREFIX;
|
||||||
|
}
|
||||||
|
|
||||||
|
XContentParser.Token token = parser.nextToken();
|
||||||
|
assert token == XContentParser.Token.FIELD_NAME;
|
||||||
|
String fieldName = parser.currentName();
|
||||||
|
|
||||||
|
String text = null;
|
||||||
|
float boost = 1.0f;
|
||||||
|
int phraseSlop = 0;
|
||||||
|
String analyzer = null;
|
||||||
|
String fuzziness = null;
|
||||||
|
int prefixLength = FuzzyQuery.defaultPrefixLength;
|
||||||
|
int maxExpansions = FuzzyQuery.defaultMaxExpansions;
|
||||||
|
BooleanClause.Occur occur = BooleanClause.Occur.SHOULD;
|
||||||
|
|
||||||
|
token = parser.nextToken();
|
||||||
|
if (token == XContentParser.Token.START_OBJECT) {
|
||||||
|
String currentFieldName = null;
|
||||||
|
while ((token = parser.nextToken()) != XContentParser.Token.END_OBJECT) {
|
||||||
|
if (token == XContentParser.Token.FIELD_NAME) {
|
||||||
|
currentFieldName = parser.currentName();
|
||||||
|
} else if (token.isValue()) {
|
||||||
|
if ("query".equals(currentFieldName)) {
|
||||||
|
text = parser.text();
|
||||||
|
} else if ("type".equals(currentFieldName)) {
|
||||||
|
String tStr = parser.text();
|
||||||
|
if ("boolean".equals(tStr)) {
|
||||||
|
type = org.elasticsearch.index.search.TextQueryParser.Type.BOOLEAN;
|
||||||
|
} else if ("phrase".equals(tStr)) {
|
||||||
|
type = org.elasticsearch.index.search.TextQueryParser.Type.PHRASE;
|
||||||
|
} else if ("phrase_prefix".equals(tStr) || "phrasePrefix".equals(currentFieldName)) {
|
||||||
|
type = org.elasticsearch.index.search.TextQueryParser.Type.PHRASE_PREFIX;
|
||||||
|
}
|
||||||
|
} else if ("analyzer".equals(currentFieldName)) {
|
||||||
|
analyzer = parser.textOrNull();
|
||||||
|
} else if ("boost".equals(currentFieldName)) {
|
||||||
|
boost = parser.floatValue();
|
||||||
|
} else if ("slop".equals(currentFieldName) || "phrase_slop".equals(currentFieldName) || "phraseSlop".equals(currentFieldName)) {
|
||||||
|
phraseSlop = parser.intValue();
|
||||||
|
} else if ("fuzziness".equals(currentFieldName)) {
|
||||||
|
fuzziness = parser.textOrNull();
|
||||||
|
} else if ("prefix_length".equals(currentFieldName) || "prefixLength".equals(currentFieldName)) {
|
||||||
|
prefixLength = parser.intValue();
|
||||||
|
} else if ("max_expansions".equals(currentFieldName) || "maxExpansions".equals(currentFieldName)) {
|
||||||
|
maxExpansions = parser.intValue();
|
||||||
|
} else if ("operator".equals(currentFieldName)) {
|
||||||
|
String op = parser.text();
|
||||||
|
if ("or".equalsIgnoreCase(op)) {
|
||||||
|
occur = BooleanClause.Occur.SHOULD;
|
||||||
|
} else if ("and".equalsIgnoreCase(op)) {
|
||||||
|
occur = BooleanClause.Occur.MUST;
|
||||||
|
} else {
|
||||||
|
throw new QueryParsingException(index, "text query requires operator to be either 'and' or 'or', not [" + op + "]");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
parser.nextToken();
|
||||||
|
} else {
|
||||||
|
text = parser.text();
|
||||||
|
// move to the next token
|
||||||
|
parser.nextToken();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (text == null) {
|
||||||
|
throw new QueryParsingException(index, "No text specified for text query");
|
||||||
|
}
|
||||||
|
|
||||||
|
org.elasticsearch.index.search.TextQueryParser tQP = new org.elasticsearch.index.search.TextQueryParser(parseContext, fieldName, text);
|
||||||
|
tQP.setPhraseSlop(phraseSlop);
|
||||||
|
tQP.setAnalyzer(analyzer);
|
||||||
|
tQP.setFuzziness(fuzziness);
|
||||||
|
tQP.setFuzzyPrefixLength(prefixLength);
|
||||||
|
tQP.setMaxExpansions(maxExpansions);
|
||||||
|
tQP.setOccur(occur);
|
||||||
|
|
||||||
|
Query query = tQP.parse(type);
|
||||||
|
query.setBoost(boost);
|
||||||
|
return query;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,353 @@
|
||||||
|
/*
|
||||||
|
* Licensed to Elastic Search and Shay Banon under one
|
||||||
|
* or more contributor license agreements. See the NOTICE file
|
||||||
|
* distributed with this work for additional information
|
||||||
|
* regarding copyright ownership. Elastic Search 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.elasticsearch.index.search;
|
||||||
|
|
||||||
|
import org.apache.lucene.analysis.Analyzer;
|
||||||
|
import org.apache.lucene.analysis.CachingTokenFilter;
|
||||||
|
import org.apache.lucene.analysis.TokenStream;
|
||||||
|
import org.apache.lucene.analysis.tokenattributes.CharTermAttribute;
|
||||||
|
import org.apache.lucene.analysis.tokenattributes.PositionIncrementAttribute;
|
||||||
|
import org.apache.lucene.index.Term;
|
||||||
|
import org.apache.lucene.search.*;
|
||||||
|
import org.elasticsearch.ElasticSearchIllegalArgumentException;
|
||||||
|
import org.elasticsearch.ElasticSearchIllegalStateException;
|
||||||
|
import org.elasticsearch.common.Nullable;
|
||||||
|
import org.elasticsearch.common.io.FastStringReader;
|
||||||
|
import org.elasticsearch.common.lucene.search.MatchNoDocsQuery;
|
||||||
|
import org.elasticsearch.common.lucene.search.MultiPhrasePrefixQuery;
|
||||||
|
import org.elasticsearch.index.mapper.FieldMapper;
|
||||||
|
import org.elasticsearch.index.mapper.MapperService;
|
||||||
|
import org.elasticsearch.index.query.xcontent.QueryParseContext;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
import static org.elasticsearch.index.query.support.QueryParsers.*;
|
||||||
|
|
||||||
|
public class TextQueryParser {
|
||||||
|
|
||||||
|
public static enum Type {
|
||||||
|
BOOLEAN,
|
||||||
|
PHRASE,
|
||||||
|
PHRASE_PREFIX
|
||||||
|
}
|
||||||
|
|
||||||
|
private final QueryParseContext parseContext;
|
||||||
|
|
||||||
|
private final String fieldName;
|
||||||
|
|
||||||
|
private final String text;
|
||||||
|
|
||||||
|
private String analyzer;
|
||||||
|
|
||||||
|
private BooleanClause.Occur occur = BooleanClause.Occur.SHOULD;
|
||||||
|
|
||||||
|
private boolean enablePositionIncrements = true;
|
||||||
|
|
||||||
|
private int phraseSlop = 0;
|
||||||
|
|
||||||
|
private String fuzziness = null;
|
||||||
|
private int fuzzyPrefixLength = FuzzyQuery.defaultPrefixLength;
|
||||||
|
private int maxExpansions = FuzzyQuery.defaultMaxExpansions;
|
||||||
|
|
||||||
|
public TextQueryParser(QueryParseContext parseContext, String fieldName, String text) {
|
||||||
|
this.parseContext = parseContext;
|
||||||
|
this.fieldName = fieldName;
|
||||||
|
this.text = text;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setAnalyzer(String analyzer) {
|
||||||
|
this.analyzer = analyzer;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setOccur(BooleanClause.Occur occur) {
|
||||||
|
this.occur = occur;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setEnablePositionIncrements(boolean enablePositionIncrements) {
|
||||||
|
this.enablePositionIncrements = enablePositionIncrements;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setPhraseSlop(int phraseSlop) {
|
||||||
|
this.phraseSlop = phraseSlop;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setFuzziness(String fuzziness) {
|
||||||
|
this.fuzziness = fuzziness;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setFuzzyPrefixLength(int fuzzyPrefixLength) {
|
||||||
|
this.fuzzyPrefixLength = fuzzyPrefixLength;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setMaxExpansions(int maxExpansions) {
|
||||||
|
this.maxExpansions = maxExpansions;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Query parse(Type type) {
|
||||||
|
FieldMapper mapper = null;
|
||||||
|
String field = fieldName;
|
||||||
|
MapperService.SmartNameFieldMappers smartNameFieldMappers = parseContext.smartFieldMappers(fieldName);
|
||||||
|
if (smartNameFieldMappers != null) {
|
||||||
|
if (smartNameFieldMappers.hasMapper()) {
|
||||||
|
mapper = smartNameFieldMappers.mapper();
|
||||||
|
if (mapper != null) {
|
||||||
|
field = mapper.names().indexName();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (mapper != null && mapper.useFieldQueryWithQueryString()) {
|
||||||
|
return wrapSmartNameQuery(mapper.fieldQuery(text, parseContext), smartNameFieldMappers, parseContext);
|
||||||
|
}
|
||||||
|
|
||||||
|
Analyzer analyzer = null;
|
||||||
|
if (this.analyzer == null) {
|
||||||
|
if (mapper != null) {
|
||||||
|
analyzer = mapper.searchAnalyzer();
|
||||||
|
}
|
||||||
|
if (analyzer == null) {
|
||||||
|
analyzer = parseContext.mapperService().searchAnalyzer();
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
analyzer = parseContext.mapperService().analysisService().analyzer(this.analyzer);
|
||||||
|
if (analyzer == null) {
|
||||||
|
throw new ElasticSearchIllegalArgumentException("No analyzer found for [" + this.analyzer + "]");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Logic similar to QueryParser#getFieldQuery
|
||||||
|
|
||||||
|
TokenStream source;
|
||||||
|
try {
|
||||||
|
source = analyzer.reusableTokenStream(field, new FastStringReader(text));
|
||||||
|
source.reset();
|
||||||
|
} catch (IOException e) {
|
||||||
|
source = analyzer.tokenStream(field, new FastStringReader(text));
|
||||||
|
}
|
||||||
|
CachingTokenFilter buffer = new CachingTokenFilter(source);
|
||||||
|
CharTermAttribute termAtt = null;
|
||||||
|
PositionIncrementAttribute posIncrAtt = null;
|
||||||
|
int numTokens = 0;
|
||||||
|
|
||||||
|
boolean success = false;
|
||||||
|
try {
|
||||||
|
buffer.reset();
|
||||||
|
success = true;
|
||||||
|
} catch (IOException e) {
|
||||||
|
// success==false if we hit an exception
|
||||||
|
}
|
||||||
|
if (success) {
|
||||||
|
if (buffer.hasAttribute(CharTermAttribute.class)) {
|
||||||
|
termAtt = buffer.getAttribute(CharTermAttribute.class);
|
||||||
|
}
|
||||||
|
if (buffer.hasAttribute(PositionIncrementAttribute.class)) {
|
||||||
|
posIncrAtt = buffer.getAttribute(PositionIncrementAttribute.class);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
int positionCount = 0;
|
||||||
|
boolean severalTokensAtSamePosition = false;
|
||||||
|
|
||||||
|
boolean hasMoreTokens = false;
|
||||||
|
if (termAtt != null) {
|
||||||
|
try {
|
||||||
|
hasMoreTokens = buffer.incrementToken();
|
||||||
|
while (hasMoreTokens) {
|
||||||
|
numTokens++;
|
||||||
|
int positionIncrement = (posIncrAtt != null) ? posIncrAtt.getPositionIncrement() : 1;
|
||||||
|
if (positionIncrement != 0) {
|
||||||
|
positionCount += positionIncrement;
|
||||||
|
} else {
|
||||||
|
severalTokensAtSamePosition = true;
|
||||||
|
}
|
||||||
|
hasMoreTokens = buffer.incrementToken();
|
||||||
|
}
|
||||||
|
} catch (IOException e) {
|
||||||
|
// ignore
|
||||||
|
}
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
// rewind the buffer stream
|
||||||
|
buffer.reset();
|
||||||
|
|
||||||
|
// close original stream - all tokens buffered
|
||||||
|
source.close();
|
||||||
|
} catch (IOException e) {
|
||||||
|
// ignore
|
||||||
|
}
|
||||||
|
|
||||||
|
Term termFactory = new Term(field);
|
||||||
|
if (numTokens == 0) {
|
||||||
|
return MatchNoDocsQuery.INSTANCE;
|
||||||
|
} else if (type == Type.BOOLEAN) {
|
||||||
|
if (numTokens == 1) {
|
||||||
|
String term = null;
|
||||||
|
try {
|
||||||
|
boolean hasNext = buffer.incrementToken();
|
||||||
|
assert hasNext == true;
|
||||||
|
term = termAtt.toString();
|
||||||
|
} catch (IOException e) {
|
||||||
|
// safe to ignore, because we know the number of tokens
|
||||||
|
}
|
||||||
|
Query q = newTermQuery(mapper, termFactory.createTerm(term));
|
||||||
|
return wrapSmartNameQuery(q, smartNameFieldMappers, parseContext);
|
||||||
|
}
|
||||||
|
BooleanQuery q = new BooleanQuery(positionCount == 1);
|
||||||
|
for (int i = 0; i < numTokens; i++) {
|
||||||
|
String term = null;
|
||||||
|
try {
|
||||||
|
boolean hasNext = buffer.incrementToken();
|
||||||
|
assert hasNext == true;
|
||||||
|
term = termAtt.toString();
|
||||||
|
} catch (IOException e) {
|
||||||
|
// safe to ignore, because we know the number of tokens
|
||||||
|
}
|
||||||
|
|
||||||
|
Query currentQuery = newTermQuery(mapper, termFactory.createTerm(term));
|
||||||
|
q.add(currentQuery, occur);
|
||||||
|
}
|
||||||
|
return wrapSmartNameQuery(q, smartNameFieldMappers, parseContext);
|
||||||
|
} else if (type == Type.PHRASE) {
|
||||||
|
if (severalTokensAtSamePosition) {
|
||||||
|
MultiPhraseQuery mpq = new MultiPhraseQuery();
|
||||||
|
mpq.setSlop(phraseSlop);
|
||||||
|
List<Term> multiTerms = new ArrayList<Term>();
|
||||||
|
int position = -1;
|
||||||
|
for (int i = 0; i < numTokens; i++) {
|
||||||
|
String term = null;
|
||||||
|
int positionIncrement = 1;
|
||||||
|
try {
|
||||||
|
boolean hasNext = buffer.incrementToken();
|
||||||
|
assert hasNext == true;
|
||||||
|
term = termAtt.toString();
|
||||||
|
if (posIncrAtt != null) {
|
||||||
|
positionIncrement = posIncrAtt.getPositionIncrement();
|
||||||
|
}
|
||||||
|
} catch (IOException e) {
|
||||||
|
// safe to ignore, because we know the number of tokens
|
||||||
|
}
|
||||||
|
|
||||||
|
if (positionIncrement > 0 && multiTerms.size() > 0) {
|
||||||
|
if (enablePositionIncrements) {
|
||||||
|
mpq.add(multiTerms.toArray(new Term[multiTerms.size()]), position);
|
||||||
|
} else {
|
||||||
|
mpq.add(multiTerms.toArray(new Term[multiTerms.size()]));
|
||||||
|
}
|
||||||
|
multiTerms.clear();
|
||||||
|
}
|
||||||
|
position += positionIncrement;
|
||||||
|
multiTerms.add(termFactory.createTerm(term));
|
||||||
|
}
|
||||||
|
if (enablePositionIncrements) {
|
||||||
|
mpq.add(multiTerms.toArray(new Term[multiTerms.size()]), position);
|
||||||
|
} else {
|
||||||
|
mpq.add(multiTerms.toArray(new Term[multiTerms.size()]));
|
||||||
|
}
|
||||||
|
return wrapSmartNameQuery(mpq, smartNameFieldMappers, parseContext);
|
||||||
|
} else {
|
||||||
|
PhraseQuery pq = new PhraseQuery();
|
||||||
|
pq.setSlop(phraseSlop);
|
||||||
|
int position = -1;
|
||||||
|
|
||||||
|
|
||||||
|
for (int i = 0; i < numTokens; i++) {
|
||||||
|
String term = null;
|
||||||
|
int positionIncrement = 1;
|
||||||
|
|
||||||
|
try {
|
||||||
|
boolean hasNext = buffer.incrementToken();
|
||||||
|
assert hasNext == true;
|
||||||
|
term = termAtt.toString();
|
||||||
|
if (posIncrAtt != null) {
|
||||||
|
positionIncrement = posIncrAtt.getPositionIncrement();
|
||||||
|
}
|
||||||
|
} catch (IOException e) {
|
||||||
|
// safe to ignore, because we know the number of tokens
|
||||||
|
}
|
||||||
|
|
||||||
|
if (enablePositionIncrements) {
|
||||||
|
position += positionIncrement;
|
||||||
|
pq.add(termFactory.createTerm(term), position);
|
||||||
|
} else {
|
||||||
|
pq.add(termFactory.createTerm(term));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return wrapSmartNameQuery(pq, smartNameFieldMappers, parseContext);
|
||||||
|
}
|
||||||
|
} else if (type == Type.PHRASE_PREFIX) {
|
||||||
|
MultiPhrasePrefixQuery mpq = new MultiPhrasePrefixQuery();
|
||||||
|
mpq.setSlop(phraseSlop);
|
||||||
|
mpq.setMaxExpansions(maxExpansions);
|
||||||
|
List<Term> multiTerms = new ArrayList<Term>();
|
||||||
|
int position = -1;
|
||||||
|
for (int i = 0; i < numTokens; i++) {
|
||||||
|
String term = null;
|
||||||
|
int positionIncrement = 1;
|
||||||
|
try {
|
||||||
|
boolean hasNext = buffer.incrementToken();
|
||||||
|
assert hasNext == true;
|
||||||
|
term = termAtt.toString();
|
||||||
|
if (posIncrAtt != null) {
|
||||||
|
positionIncrement = posIncrAtt.getPositionIncrement();
|
||||||
|
}
|
||||||
|
} catch (IOException e) {
|
||||||
|
// safe to ignore, because we know the number of tokens
|
||||||
|
}
|
||||||
|
|
||||||
|
if (positionIncrement > 0 && multiTerms.size() > 0) {
|
||||||
|
if (enablePositionIncrements) {
|
||||||
|
mpq.add(multiTerms.toArray(new Term[multiTerms.size()]), position);
|
||||||
|
} else {
|
||||||
|
mpq.add(multiTerms.toArray(new Term[multiTerms.size()]));
|
||||||
|
}
|
||||||
|
multiTerms.clear();
|
||||||
|
}
|
||||||
|
position += positionIncrement;
|
||||||
|
multiTerms.add(termFactory.createTerm(term));
|
||||||
|
}
|
||||||
|
if (enablePositionIncrements) {
|
||||||
|
mpq.add(multiTerms.toArray(new Term[multiTerms.size()]), position);
|
||||||
|
} else {
|
||||||
|
mpq.add(multiTerms.toArray(new Term[multiTerms.size()]));
|
||||||
|
}
|
||||||
|
return wrapSmartNameQuery(mpq, smartNameFieldMappers, parseContext);
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new ElasticSearchIllegalStateException("No type found for [" + type + "]");
|
||||||
|
}
|
||||||
|
|
||||||
|
private Query newTermQuery(@Nullable FieldMapper mapper, Term term) {
|
||||||
|
if (fuzziness != null) {
|
||||||
|
if (mapper != null) {
|
||||||
|
return mapper.fuzzyQuery(term.text(), fuzziness, fuzzyPrefixLength, maxExpansions);
|
||||||
|
}
|
||||||
|
return new FuzzyQuery(term, Float.parseFloat(fuzziness), fuzzyPrefixLength, maxExpansions);
|
||||||
|
}
|
||||||
|
if (mapper != null) {
|
||||||
|
Query termQuery = mapper.queryStringTermQuery(term);
|
||||||
|
if (termQuery != null) {
|
||||||
|
return termQuery;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return new TermQuery(term);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,67 @@
|
||||||
|
/*
|
||||||
|
* Licensed to Elastic Search and Shay Banon under one
|
||||||
|
* or more contributor license agreements. See the NOTICE file
|
||||||
|
* distributed with this work for additional information
|
||||||
|
* regarding copyright ownership. Elastic Search 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.elasticsearch.common.lucene.search;
|
||||||
|
|
||||||
|
import org.apache.lucene.document.Document;
|
||||||
|
import org.apache.lucene.document.Field;
|
||||||
|
import org.apache.lucene.index.IndexReader;
|
||||||
|
import org.apache.lucene.index.IndexWriter;
|
||||||
|
import org.apache.lucene.index.IndexWriterConfig;
|
||||||
|
import org.apache.lucene.index.Term;
|
||||||
|
import org.apache.lucene.search.IndexSearcher;
|
||||||
|
import org.apache.lucene.store.RAMDirectory;
|
||||||
|
import org.elasticsearch.common.lucene.Lucene;
|
||||||
|
import org.testng.annotations.Test;
|
||||||
|
|
||||||
|
import static org.hamcrest.MatcherAssert.*;
|
||||||
|
import static org.hamcrest.Matchers.*;
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public class MultiPhrasePrefixQueryTests {
|
||||||
|
|
||||||
|
@Test public void simpleTests() throws Exception {
|
||||||
|
IndexWriter writer = new IndexWriter(new RAMDirectory(), new IndexWriterConfig(Lucene.VERSION, Lucene.STANDARD_ANALYZER));
|
||||||
|
Document doc = new Document();
|
||||||
|
doc.add(new Field("field", "aaa bbb ccc ddd", Field.Store.NO, Field.Index.ANALYZED));
|
||||||
|
writer.addDocument(doc);
|
||||||
|
IndexReader reader = IndexReader.open(writer, true);
|
||||||
|
IndexSearcher searcher = new IndexSearcher(reader);
|
||||||
|
|
||||||
|
MultiPhrasePrefixQuery query = new MultiPhrasePrefixQuery();
|
||||||
|
query.add(new Term("field", "aa"));
|
||||||
|
assertThat(Lucene.count(searcher, query, 0), equalTo(1l));
|
||||||
|
|
||||||
|
query = new MultiPhrasePrefixQuery();
|
||||||
|
query.add(new Term("field", "aaa"));
|
||||||
|
query.add(new Term("field", "bb"));
|
||||||
|
assertThat(Lucene.count(searcher, query, 0), equalTo(1l));
|
||||||
|
|
||||||
|
query = new MultiPhrasePrefixQuery();
|
||||||
|
query.setSlop(1);
|
||||||
|
query.add(new Term("field", "aaa"));
|
||||||
|
query.add(new Term("field", "cc"));
|
||||||
|
assertThat(Lucene.count(searcher, query, 0), equalTo(1l));
|
||||||
|
|
||||||
|
query = new MultiPhrasePrefixQuery();
|
||||||
|
query.setSlop(1);
|
||||||
|
query.add(new Term("field", "xxx"));
|
||||||
|
assertThat(Lucene.count(searcher, query, 0), equalTo(0l));
|
||||||
|
}
|
||||||
|
}
|
|
@ -390,6 +390,58 @@ public class SimpleIndexQueryParserTests {
|
||||||
assertThat(fieldQuery.includesMin(), equalTo(true));
|
assertThat(fieldQuery.includesMin(), equalTo(true));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test public void testTextQuery1() throws IOException {
|
||||||
|
IndexQueryParser queryParser = queryParser();
|
||||||
|
String query = copyToStringFromClasspath("/org/elasticsearch/index/query/xcontent/text1.json");
|
||||||
|
Query parsedQuery = queryParser.parse(query).query();
|
||||||
|
assertThat(parsedQuery, instanceOf(BooleanQuery.class));
|
||||||
|
BooleanQuery booleanQuery = (BooleanQuery) parsedQuery;
|
||||||
|
assertThat((double) booleanQuery.getBoost(), closeTo(1.0d, 0.00001d));
|
||||||
|
assertThat(((TermQuery) booleanQuery.getClauses()[0].getQuery()).getTerm(), equalTo(new Term("name.first", "aaa")));
|
||||||
|
assertThat(((TermQuery) booleanQuery.getClauses()[1].getQuery()).getTerm(), equalTo(new Term("name.first", "bbb")));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test public void testTextQuery2() throws IOException {
|
||||||
|
IndexQueryParser queryParser = queryParser();
|
||||||
|
String query = copyToStringFromClasspath("/org/elasticsearch/index/query/xcontent/text2.json");
|
||||||
|
Query parsedQuery = queryParser.parse(query).query();
|
||||||
|
assertThat(parsedQuery, instanceOf(BooleanQuery.class));
|
||||||
|
BooleanQuery booleanQuery = (BooleanQuery) parsedQuery;
|
||||||
|
assertThat((double) booleanQuery.getBoost(), closeTo(1.5d, 0.00001d));
|
||||||
|
assertThat(((TermQuery) booleanQuery.getClauses()[0].getQuery()).getTerm(), equalTo(new Term("name.first", "aaa")));
|
||||||
|
assertThat(((TermQuery) booleanQuery.getClauses()[1].getQuery()).getTerm(), equalTo(new Term("name.first", "bbb")));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test public void testTextQuery3() throws IOException {
|
||||||
|
IndexQueryParser queryParser = queryParser();
|
||||||
|
String query = copyToStringFromClasspath("/org/elasticsearch/index/query/xcontent/text3.json");
|
||||||
|
Query parsedQuery = queryParser.parse(query).query();
|
||||||
|
assertThat(parsedQuery, instanceOf(PhraseQuery.class));
|
||||||
|
PhraseQuery phraseQuery = (PhraseQuery) parsedQuery;
|
||||||
|
assertThat(phraseQuery.getTerms()[0], equalTo(new Term("name.first", "aaa")));
|
||||||
|
assertThat(phraseQuery.getTerms()[1], equalTo(new Term("name.first", "bbb")));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test public void testTextQuery4() throws IOException {
|
||||||
|
IndexQueryParser queryParser = queryParser();
|
||||||
|
String query = copyToStringFromClasspath("/org/elasticsearch/index/query/xcontent/text4.json");
|
||||||
|
Query parsedQuery = queryParser.parse(query).query();
|
||||||
|
assertThat(parsedQuery, instanceOf(MultiPhrasePrefixQuery.class));
|
||||||
|
MultiPhrasePrefixQuery phraseQuery = (MultiPhrasePrefixQuery) parsedQuery;
|
||||||
|
assertThat(phraseQuery.getTermArrays().get(0)[0], equalTo(new Term("name.first", "aaa")));
|
||||||
|
assertThat(phraseQuery.getTermArrays().get(1)[0], equalTo(new Term("name.first", "bbb")));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test public void testTextQuery4_2() throws IOException {
|
||||||
|
IndexQueryParser queryParser = queryParser();
|
||||||
|
String query = copyToStringFromClasspath("/org/elasticsearch/index/query/xcontent/text4_2.json");
|
||||||
|
Query parsedQuery = queryParser.parse(query).query();
|
||||||
|
assertThat(parsedQuery, instanceOf(MultiPhrasePrefixQuery.class));
|
||||||
|
MultiPhrasePrefixQuery phraseQuery = (MultiPhrasePrefixQuery) parsedQuery;
|
||||||
|
assertThat(phraseQuery.getTermArrays().get(0)[0], equalTo(new Term("name.first", "aaa")));
|
||||||
|
assertThat(phraseQuery.getTermArrays().get(1)[0], equalTo(new Term("name.first", "bbb")));
|
||||||
|
}
|
||||||
|
|
||||||
@Test public void testTermWithBoostQueryBuilder() throws IOException {
|
@Test public void testTermWithBoostQueryBuilder() throws IOException {
|
||||||
IndexQueryParser queryParser = queryParser();
|
IndexQueryParser queryParser = queryParser();
|
||||||
Query parsedQuery = queryParser.parse(termQuery("age", 34).boost(2.0f)).query();
|
Query parsedQuery = queryParser.parse(termQuery("age", 34).boost(2.0f)).query();
|
||||||
|
|
|
@ -0,0 +1,5 @@
|
||||||
|
{
|
||||||
|
"text" : {
|
||||||
|
"name.first" : "aaa bbb"
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,8 @@
|
||||||
|
{
|
||||||
|
"text" : {
|
||||||
|
"name.first" : {
|
||||||
|
"query" : "aaa bbb",
|
||||||
|
"boost" : 1.5
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,8 @@
|
||||||
|
{
|
||||||
|
"text" : {
|
||||||
|
"name.first" : {
|
||||||
|
"query" : "aaa bbb",
|
||||||
|
"type" : "phrase"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,8 @@
|
||||||
|
{
|
||||||
|
"text" : {
|
||||||
|
"name.first" : {
|
||||||
|
"query" : "aaa bbb",
|
||||||
|
"type" : "phrase_prefix"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,7 @@
|
||||||
|
{
|
||||||
|
"text_phrase_prefix" : {
|
||||||
|
"name.first" : {
|
||||||
|
"query" : "aaa bbb"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue