#594 - ElasticSearch - multi match support

This commit is contained in:
Robin Bygrave
2016-03-09 23:44:23 +13:00
parent 34dcc98908
commit faf7df1c69
12 changed files with 524 additions and 95 deletions
@@ -1,6 +1,7 @@
package com.avaje.ebean;
import com.avaje.ebean.search.Match;
import com.avaje.ebean.search.MultiMatch;
import java.util.Collection;
import java.util.List;
@@ -292,6 +293,11 @@ public interface ExpressionFactory {
*/
Expression textMatch(String propertyName, String search, Match options);
/**
* Create a Text Multi match expression (currently doc store/Elastic only).
*/
Expression textMultiMatch(String query, MultiMatch options);
/**
* And - join two expressions with a logical and.
*/
@@ -334,4 +340,5 @@ public interface ExpressionFactory {
* </p>
*/
<T> TextJunction<T> textJunction(Query<T> query, TextExpressionList<T> parent, TextJunction.Type type);
}
@@ -1,6 +1,7 @@
package com.avaje.ebean;
import com.avaje.ebean.search.Match;
import com.avaje.ebean.search.MultiMatch;
/**
* An list of Full text query expressions.
@@ -26,6 +27,16 @@ public interface TextExpressionList<T> extends ExpressionList<T> {
*/
TextExpressionList<T> match(String propertyName, String search, Match options);
/**
* Add a multi-match expression.
*/
TextExpressionList<T> multiMatch(String search, String... properties);
/**
* Add a multi-match expression using options.
*/
TextExpressionList<T> multiMatch(String search, MultiMatch options);
/**
* Start a list of expressions that will be joined by MUST.
*/
@@ -55,4 +66,5 @@ public interface TextExpressionList<T> extends ExpressionList<T> {
* End the list of MUST NOT expressions.
*/
TextExpressionList<T> endMustNot();
}
@@ -0,0 +1,130 @@
package com.avaje.ebean.search;
/**
* Options for the text match expression.
*/
public abstract class BaseMatch {
protected boolean and;
protected String analyzer;
protected double boost;
protected String minShouldMatch;
protected int maxExpansions;
protected String zeroTerms;
protected double cutoffFrequency;
protected String fuzziness;
protected int prefixLength;
protected String rewrite;
/**
* Return true if using the AND operator otherwise using the OR operator.
*/
public boolean isAnd() {
return and;
}
/**
* Return the boost.
*/
public double getBoost() {
return boost;
}
/**
* Return the minimum should match.
*/
public String getMinShouldMatch() {
return minShouldMatch;
}
/**
* Return the zero terms option.
*/
public String getZeroTerms() {
return zeroTerms;
}
/**
* Return the cutoff frequency.
*/
public double getCutoffFrequency() {
return cutoffFrequency;
}
/**
* Return the max expansions.
*/
public int getMaxExpansions() {
return maxExpansions;
}
/**
* Return the analyzer.
*/
public String getAnalyzer() {
return analyzer;
}
/**
* Return the fuzziness.
*/
public String getFuzziness() {
return fuzziness;
}
/**
* Return the prefix length.
*/
public int getPrefixLength() {
return prefixLength;
}
/**
* Return the rewrite option.
*/
public String getRewrite() {
return rewrite;
}
protected boolean baseEquals(BaseMatch baseMatch) {
if (and != baseMatch.and) return false;
if (Double.compare(baseMatch.boost, boost) != 0) return false;
if (maxExpansions != baseMatch.maxExpansions) return false;
if (Double.compare(baseMatch.cutoffFrequency, cutoffFrequency) != 0) return false;
if (prefixLength != baseMatch.prefixLength) return false;
if (analyzer != null ? !analyzer.equals(baseMatch.analyzer) : baseMatch.analyzer != null) return false;
if (minShouldMatch != null ? !minShouldMatch.equals(baseMatch.minShouldMatch) : baseMatch.minShouldMatch != null) return false;
if (zeroTerms != null ? !zeroTerms.equals(baseMatch.zeroTerms) : baseMatch.zeroTerms != null) return false;
if (fuzziness != null ? !fuzziness.equals(baseMatch.fuzziness) : baseMatch.fuzziness != null) return false;
return rewrite != null ? rewrite.equals(baseMatch.rewrite) : baseMatch.rewrite == null;
}
protected int baseHashCode() {
int result;
long temp;
result = (and ? 1 : 0);
result = 31 * result + (analyzer != null ? analyzer.hashCode() : 0);
temp = Double.doubleToLongBits(boost);
result = 31 * result + (int) (temp ^ (temp >>> 32));
result = 31 * result + (minShouldMatch != null ? minShouldMatch.hashCode() : 0);
result = 31 * result + maxExpansions;
result = 31 * result + (zeroTerms != null ? zeroTerms.hashCode() : 0);
temp = Double.doubleToLongBits(cutoffFrequency);
result = 31 * result + (int) (temp ^ (temp >>> 32));
result = 31 * result + (fuzziness != null ? fuzziness.hashCode() : 0);
result = 31 * result + prefixLength;
result = 31 * result + (rewrite != null ? rewrite.hashCode() : 0);
return result;
}
}
+38 -92
View File
@@ -3,26 +3,12 @@ package com.avaje.ebean.search;
/**
* Options for the text match expression.
*/
public class Match {
protected boolean and;
protected double boost;
protected String minShouldMatch;
protected String zeroTerms;
protected double cutoffFrequency;
protected String analyzer;
public class Match extends BaseMatch {
protected boolean phrase;
protected boolean phrasePrefix;
protected int maxExpansions;
/**
* Create and return Match options using AND operator.
*/
@@ -37,38 +23,6 @@ public class Match {
return new Match().opOr();
}
/**
* Use the AND operator (rather than OR).
*/
public Match opAnd() {
and = true;
return this;
}
/**
* Use the OR operator (rather than AND).
*/
public Match opOr() {
and = false;
return this;
}
/**
* Set the minimum should match value.
*/
public Match minShouldMatch(String minShouldMatch) {
this.minShouldMatch = minShouldMatch;
return this;
}
/**
* Set the boost.
*/
public Match boost(double boost) {
this.boost = boost;
return this;
}
/**
* Set this to be a "Phrase" type expression.
*/
@@ -85,6 +39,22 @@ public class Match {
return this;
}
/**
* Use the AND operator (rather than OR).
*/
public Match opAnd() {
and = true;
return this;
}
/**
* Use the OR operator (rather than AND).
*/
public Match opOr() {
and = false;
return this;
}
/**
* Set the zero terms.
*/
@@ -118,50 +88,43 @@ public class Match {
}
/**
* Return true if using the AND operator otherwise using the OR operator.
* Set the boost.
*/
public boolean isAnd() {
return and;
public Match boost(double boost) {
this.boost = boost;
return this;
}
/**
* Return the boost.
* Set the rewrite to use.
*/
public double getBoost() {
return boost;
public Match minShouldMatch(String minShouldMatch) {
this.minShouldMatch = minShouldMatch;
return this;
}
/**
* Return the minimum should match.
* Set the rewrite to use.
*/
public String getMinShouldMatch() {
return minShouldMatch;
}
public String getZeroTerms() {
return zeroTerms;
}
public double getCutoffFrequency() {
return cutoffFrequency;
public Match rewrite(String rewrite) {
this.rewrite = rewrite;
return this;
}
/**
* Return true if this is a phrase query.
*/
public boolean isPhrase() {
return phrase;
}
/**
* Return true if this is a phrase prefix query.
*/
public boolean isPhrasePrefix() {
return phrasePrefix;
}
public int getMaxExpansions() {
return maxExpansions;
}
public String getAnalyzer() {
return analyzer;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
@@ -169,33 +132,16 @@ public class Match {
Match match = (Match) o;
if (and != match.and) return false;
if (Double.compare(match.boost, boost) != 0) return false;
if (Double.compare(match.cutoffFrequency, cutoffFrequency) != 0) return false;
if (phrase != match.phrase) return false;
if (phrasePrefix != match.phrasePrefix) return false;
if (maxExpansions != match.maxExpansions) return false;
if (minShouldMatch != null ? !minShouldMatch.equals(match.minShouldMatch) : match.minShouldMatch != null)
return false;
if (zeroTerms != null ? !zeroTerms.equals(match.zeroTerms) : match.zeroTerms != null) return false;
return analyzer != null ? analyzer.equals(match.analyzer) : match.analyzer == null;
return baseEquals(match);
}
@Override
public int hashCode() {
int result;
long temp;
result = (and ? 1 : 0);
temp = Double.doubleToLongBits(boost);
result = 31 * result + (int) (temp ^ (temp >>> 32));
result = 31 * result + (minShouldMatch != null ? minShouldMatch.hashCode() : 0);
result = 31 * result + (zeroTerms != null ? zeroTerms.hashCode() : 0);
temp = Double.doubleToLongBits(cutoffFrequency);
result = 31 * result + (int) (temp ^ (temp >>> 32));
result = 31 * result + (analyzer != null ? analyzer.hashCode() : 0);
result = 31 * result + (phrase ? 1 : 0);
int result = (phrase ? 1 : 0);
result = 31 * result + (phrasePrefix ? 1 : 0);
result = 31 * result + maxExpansions;
result = 31 * result + baseHashCode();
return result;
}
}
@@ -0,0 +1,186 @@
package com.avaje.ebean.search;
import java.util.Arrays;
/**
* Options for the text match expression.
*/
public class MultiMatch extends BaseMatch {
/**
* The MultiMatch type.
*/
public enum Type {
BEST_FIELDS,
MOST_FIELDS,
CROSS_FIELDS,
PHRASE,
PHRASE_PREFIX
}
protected Type type = Type.BEST_FIELDS;
protected String[] fields;
protected double tieBreaker;
/**
* Create and return Match options using AND operator.
*/
public static MultiMatch fields(String... fields) {
return new MultiMatch(fields);
}
/**
* Create and return Match options using OR operator.
*/
public static MultiMatch OR() {
return new MultiMatch().opOr();
}
/**
* Construct with a set of fields.
*/
public MultiMatch(String... fields) {
this.fields = fields;
}
/**
* Set the type of query.
*/
public MultiMatch type(Type type) {
this.type = type;
return this;
}
/**
* Set the tieBreaker to use.
*/
public MultiMatch tieBreaker(double tieBreaker) {
this.tieBreaker = tieBreaker;
return this;
}
/**
* Use the AND operator (rather than OR).
*/
public MultiMatch opAnd() {
and = true;
return this;
}
/**
* Use the OR operator (rather than AND).
*/
public MultiMatch opOr() {
and = false;
return this;
}
/**
* Set the minimum should match value.
*/
public MultiMatch minShouldMatch(String minShouldMatch) {
this.minShouldMatch = minShouldMatch;
return this;
}
/**
* Set the boost.
*/
public MultiMatch boost(double boost) {
this.boost = boost;
return this;
}
/**
* Set the zero terms.
*/
public MultiMatch zeroTerms(String zeroTerms) {
this.zeroTerms = zeroTerms;
return this;
}
/**
* Set the cutoff frequency.
*/
public MultiMatch cutoffFrequency(double cutoffFrequency) {
this.cutoffFrequency = cutoffFrequency;
return this;
}
/**
* Set the max expansions (for phrase prefix only).
*/
public MultiMatch maxExpansions(int maxExpansions) {
this.maxExpansions = maxExpansions;
return this;
}
/**
* Set the Analyzer to use for this expression.
*/
public MultiMatch analyzer(String analyzer) {
this.analyzer = analyzer;
return this;
}
/**
* Set the rewrite to use.
*/
public MultiMatch rewrite(String rewrite) {
this.rewrite = rewrite;
return this;
}
/**
* Return the type.
*/
public Type getType() {
return type;
}
/**
* Return the fields to search.
*/
public String[] getFields() {
return fields;
}
/**
* Return the tie breaker.
*/
public double getTieBreaker() {
return tieBreaker;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
MultiMatch that = (MultiMatch) o;
if (Double.compare(that.tieBreaker, tieBreaker) != 0) return false;
if (type != that.type) return false;
if (fields.length != that.fields.length) return false;
for (int i = 0; i < fields.length; i++) {
if (!fields[i].equals(that.fields[i])) {
return false;
}
}
return baseEquals(that);
}
@Override
public int hashCode() {
int result;
long temp;
result = type.hashCode();
result = 31 * result + Arrays.hashCode(fields);
temp = Double.doubleToLongBits(tieBreaker);
result = 31 * result + (int) (temp ^ (temp >>> 32));
result = 31 * result + baseHashCode();
return result;
}
}
@@ -4,6 +4,7 @@ import com.avaje.ebean.Query;
import com.avaje.ebean.TextExpressionList;
import com.avaje.ebean.TextJunction;
import com.avaje.ebean.search.Match;
import com.avaje.ebean.search.MultiMatch;
import com.avaje.ebeaninternal.api.SpiExpression;
import com.avaje.ebeaninternal.api.SpiTextJunction;
@@ -47,6 +48,16 @@ class DTextJunction<T> extends JunctionExpression<T> implements SpiTextJunction<
return exprList.match(propertyName, search, options);
}
@Override
public TextExpressionList<T> multiMatch(String query, String... properties) {
return exprList.multiMatch(query, properties);
}
@Override
public TextExpressionList<T> multiMatch(String query, MultiMatch options) {
return exprList.multiMatch(query, options);
}
@Override
public TextJunction<T> must() {
return exprList.must();
@@ -11,6 +11,7 @@ import com.avaje.ebean.TextExpressionList;
import com.avaje.ebean.TextJunction;
import com.avaje.ebean.bean.EntityBean;
import com.avaje.ebean.search.Match;
import com.avaje.ebean.search.MultiMatch;
import com.avaje.ebeaninternal.api.SpiExpressionFactory;
import com.avaje.ebeaninternal.api.SpiQuery;
@@ -44,6 +45,11 @@ public class DefaultExpressionFactory implements SpiExpressionFactory {
return new TextMatchExpression(propertyName, search, options);
}
@Override
public Expression textMultiMatch(String query, MultiMatch options) {
return new TextMultiMatchExpression(query, options);
}
public Expression jsonExists(String propertyName, String path) {
return new JsonPathExpression(propertyName, path, Op.EXISTS, null);
}
@@ -3,6 +3,7 @@ package com.avaje.ebeaninternal.server.expression;
import com.avaje.ebean.*;
import com.avaje.ebean.event.BeanQueryRequest;
import com.avaje.ebean.search.Match;
import com.avaje.ebean.search.MultiMatch;
import com.avaje.ebeaninternal.api.HashQueryPlanBuilder;
import com.avaje.ebeaninternal.api.ManyWhereJoins;
import com.avaje.ebeaninternal.api.SpiExpression;
@@ -864,6 +865,17 @@ public class DefaultExpressionList<T> implements SpiExpressionList<T>, SpiTextEx
return this;
}
@Override
public TextExpressionList<T> multiMatch(String query, String... fields) {
return multiMatch(query, new MultiMatch().fields(fields));
}
@Override
public TextExpressionList<T> multiMatch(String query, MultiMatch options) {
add(expr.textMultiMatch(query, options));
return this;
}
@Override
public TextJunction<T> must() {
return textJunction(TextJunction.Type.MUST);
@@ -3,6 +3,7 @@ package com.avaje.ebeaninternal.server.expression;
import com.avaje.ebean.LikeType;
import com.avaje.ebean.TextJunction;
import com.avaje.ebean.search.Match;
import com.avaje.ebean.search.MultiMatch;
import java.io.IOException;
import java.util.List;
@@ -98,6 +99,11 @@ public interface DocQueryContext {
*/
void writeMatch(String propName, String search, Match options) throws IOException;
/**
* Write a Multi-match expression.
*/
void writeMultiMatch(String search, MultiMatch options) throws IOException;
/**
* Start a Bool which may contain some of Must, Must Not, Should.
*/
@@ -117,4 +123,5 @@ public interface DocQueryContext {
* End the Bool group.
*/
void endBoolGroup() throws IOException;
}
@@ -1,5 +1,6 @@
package com.avaje.ebeaninternal.server.expression;
import com.avaje.ebean.search.BaseMatch;
import com.avaje.ebean.search.Match;
import com.avaje.ebeaninternal.api.HashQueryPlanBuilder;
import com.avaje.ebeaninternal.api.SpiExpression;
@@ -44,9 +45,9 @@ public class TextMatchExpression extends AbstractExpression {
public void queryPlanHash(HashQueryPlanBuilder builder) {
builder.add(TextMatchExpression.class).add(propName).add(search);
if (options != null) {
builder.add(options.isAnd());
builder.add(options.getBoost());
builder.add(options.getMinShouldMatch());
builder.add(options.isPhrase());
builder.add(options.isPhrasePrefix());
addHash(builder, options);
}
}
@@ -72,4 +73,20 @@ public class TextMatchExpression extends AbstractExpression {
TextMatchExpression that = (TextMatchExpression) other;
return search.equals(that.search);
}
/**
* Add the hash to the builder for the base/common options.
*/
public static void addHash(HashQueryPlanBuilder builder, BaseMatch options) {
builder.add(options.isAnd());
builder.add(options.getAnalyzer());
builder.add(options.getBoost());
builder.add(options.getCutoffFrequency());
builder.add(options.getFuzziness());
builder.add(options.getMaxExpansions());
builder.add(options.getMinShouldMatch());
builder.add(options.getPrefixLength());
builder.add(options.getRewrite());
builder.add(options.getZeroTerms());
}
}
@@ -0,0 +1,77 @@
package com.avaje.ebeaninternal.server.expression;
import com.avaje.ebean.search.MultiMatch;
import com.avaje.ebeaninternal.api.HashQueryPlanBuilder;
import com.avaje.ebeaninternal.api.SpiExpression;
import com.avaje.ebeaninternal.api.SpiExpressionRequest;
import java.io.IOException;
/**
* Full text Multi-Match expression.
*/
public class TextMultiMatchExpression extends AbstractExpression {
private final String search;
private final MultiMatch options;
public TextMultiMatchExpression(String search, MultiMatch options) {
super(null);
this.search = search;
this.options = options;
}
@Override
public void writeDocQuery(DocQueryContext context) throws IOException {
context.writeMultiMatch(search, options);
}
@Override
public void addSql(SpiExpressionRequest request) {
throw new IllegalStateException("Not implemented - DocStore/Elastic only");
}
@Override
public void addBindValues(SpiExpressionRequest request) {
throw new IllegalStateException("Not implemented - DocStore/Elastic only");
}
/**
* Based on the type and propertyName.
*/
@Override
public void queryPlanHash(HashQueryPlanBuilder builder) {
builder.add(TextMultiMatchExpression.class).add(search);
builder.add(options.getType());
builder.add(options.getTieBreaker());
String[] fields = options.getFields();
builder.add(fields.length);
for (int i = 0; i < fields.length; i++) {
builder.add(fields[i]);
}
TextMatchExpression.addHash(builder, options);
}
@Override
public int queryBindHash() {
return search.hashCode();
}
@Override
public boolean isSameByPlan(SpiExpression other) {
if (!(other instanceof TextMultiMatchExpression)) {
return false;
}
TextMultiMatchExpression that = (TextMultiMatchExpression) other;
return this.search.equals(that.search)
&& this.options == null ? that.options == null : options.equals(that.options);
}
@Override
public boolean isSameByBind(SpiExpression other) {
TextMultiMatchExpression that = (TextMultiMatchExpression) other;
return search.equals(that.search);
}
}
@@ -2,6 +2,7 @@ package com.avaje.ebean;
import com.avaje.ebean.search.Match;
import com.avaje.ebean.search.MultiMatch;
import com.avaje.tests.model.basic.Order;
import org.junit.Test;
@@ -59,4 +60,21 @@ public class TextExpressionListTest {
}
@Test
public void syntax_multiMatch() {
Ebean.find(Order.class)
.text()
.multiMatch("Will Smith", "title", "*name");
MultiMatch match = MultiMatch.fields("title", "*name")
.opAnd()
.type(MultiMatch.Type.PHRASE_PREFIX);
Ebean.find(Order.class)
.text()
.multiMatch("Will Smith", match);
}
}