#594 - ElasticSearch - Add match / TextExpression support (Full text search expression)

This commit is contained in:
Robin Bygrave
2016-03-09 21:48:22 +13:00
parent 3c63b79324
commit 34dcc98908
37 changed files with 860 additions and 79 deletions
@@ -1,10 +1,8 @@
package com.avaje.ebean;
import java.io.Serializable;
/**
* An expression that is part of a WHERE or HAVING clause.
*/
public interface Expression extends Serializable {
public interface Expression {
}
@@ -1,5 +1,7 @@
package com.avaje.ebean;
import com.avaje.ebean.search.Match;
import java.util.Collection;
import java.util.List;
import java.util.Map;
@@ -285,6 +287,11 @@ public interface ExpressionFactory {
*/
Expression raw(String raw);
/**
* Create a Text Match expression (currently doc store/Elastic only).
*/
Expression textMatch(String propertyName, String search, Match options);
/**
* And - join two expressions with a logical and.
*/
@@ -319,4 +326,12 @@ public interface ExpressionFactory {
* Return a list of expressions that will be joined by OR's.
*/
<T> Junction<T> disjunction(Query<T> query, ExpressionList<T> parent);
/**
* Return a Text query junction for MUST, SHOULD or MUST NOT.
* <p>
* This is doc store Elastic only.
* </p>
*/
<T> TextJunction<T> textJunction(Query<T> query, TextExpressionList<T> parent, TextJunction.Type type);
}
@@ -3,7 +3,6 @@ package com.avaje.ebean;
import org.jetbrains.annotations.Nullable;
import javax.persistence.NonUniqueResultException;
import java.io.Serializable;
import java.sql.Timestamp;
import java.util.Collection;
import java.util.List;
@@ -30,7 +29,7 @@ import java.util.Set;
*
* @see Query#where()
*/
public interface ExpressionList<T> extends Serializable {
public interface ExpressionList<T> {
/**
* Return the query that owns this expression list.
+21 -1
View File
@@ -3,7 +3,6 @@ package com.avaje.ebean;
import org.jetbrains.annotations.Nullable;
import javax.persistence.NonUniqueResultException;
import java.io.Serializable;
import java.sql.Timestamp;
import java.util.List;
import java.util.Map;
@@ -1042,6 +1041,27 @@ public interface Query<T> {
*/
ExpressionList<T> where();
/**
* Add Full text search expressions for Document store queries.
* <p>
* This is currently ElasticSearch only and provides the full text
* expressions such as Match and Multi-Match.
* </p>
* <p>
* This automatically makes this query a "Doc Store" query and will execute
* against the document store (ElasticSearch).
* </p>
* <p>
* Expressions added here are added to the "query" section of an ElasticSearch
* query rather than the "filter" section.
* </p>
* <p>
* Expressions added to the where() are added to the "filter" section of an
* ElasticSearch query.
* </p>
*/
TextExpressionList<T> text();
/**
* This applies a filter on the 'many' property list rather than the root
* level objects.
@@ -0,0 +1,58 @@
package com.avaje.ebean;
import com.avaje.ebean.search.Match;
/**
* An list of Full text query expressions.
* <p>
* For ElasticSearch these expression go into the "query" section rather than the "filter" section.
* </p>
*/
public interface TextExpressionList<T> extends ExpressionList<T> {
/**
* Add a match expression.
*
* @param propertyName The property name for the match
* @param search The search value
*/
TextExpressionList<T> match(String propertyName, String search);
/**
* Add a match expression with options.
*
* @param propertyName The property name for the match
* @param search The search value
*/
TextExpressionList<T> match(String propertyName, String search, Match options);
/**
* Start a list of expressions that will be joined by MUST.
*/
TextJunction<T> must();
/**
* Start a list of expressions that will be joined by SHOULD.
*/
TextJunction<T> should();
/**
* Start a list of expressions that will be joined by MUST NOT.
*/
TextJunction<T> mustNot();
/**
* End the list of MUST expressions.
*/
TextExpressionList<T> endMust();
/**
* End the list of SHOULD expressions.
*/
TextExpressionList<T> endShould();
/**
* End the list of MUST NOT expressions.
*/
TextExpressionList<T> endMustNot();
}
@@ -0,0 +1,41 @@
package com.avaje.ebean;
/**
* A Full text MUST, MUST NOT or SHOULD group of expressions.
*/
public interface TextJunction<T> extends Junction<T>, TextExpressionList<T> {
/**
* The type of Junction used in full text expressions.
*/
enum Type {
/**
* Logically a AND group.
*/
MUST("must"),
/**
* Logically a NOT group.
*/
MUST_NOT("must_not"),
/**
* Logically a OR group.
*/
SHOULD("should");
String literal;
Type(String literal) {
this.literal = literal;
}
/**
* Return the literal value for this type.
*/
public String literal() {
return literal;
}
}
}
@@ -0,0 +1,201 @@
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;
protected boolean phrase;
protected boolean phrasePrefix;
protected int maxExpansions;
/**
* Create and return Match options using AND operator.
*/
public static Match AND() {
return new Match().opAnd();
}
/**
* Create and return Match options using OR operator.
*/
public static Match OR() {
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.
*/
public Match phrase() {
phrase = true;
return this;
}
/**
* Set this to be a "Phrase Prefix" type expression.
*/
public Match phrasePrefix() {
phrasePrefix = true;
return this;
}
/**
* Set the zero terms.
*/
public Match zeroTerms(String zeroTerms) {
this.zeroTerms = zeroTerms;
return this;
}
/**
* Set the cutoff frequency.
*/
public Match cutoffFrequency(double cutoffFrequency) {
this.cutoffFrequency = cutoffFrequency;
return this;
}
/**
* Set the max expansions (for phrase prefix only).
*/
public Match maxExpansions(int maxExpansions) {
this.maxExpansions = maxExpansions;
return this;
}
/**
* Set the Analyzer to use for this expression.
*/
public Match analyzer(String analyzer) {
this.analyzer = analyzer;
return this;
}
/**
* 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;
}
public String getZeroTerms() {
return zeroTerms;
}
public double getCutoffFrequency() {
return cutoffFrequency;
}
public boolean isPhrase() {
return phrase;
}
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;
if (o == null || getClass() != o.getClass()) return false;
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;
}
@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);
result = 31 * result + (phrasePrefix ? 1 : 0);
result = 31 * result + maxExpansions;
return result;
}
}
@@ -509,6 +509,11 @@ public interface SpiQuery<T> extends Query<T> {
*/
SpiExpressionList<T> getHavingExpressions();
/**
* Return the text expressions.
*/
SpiExpressionList<T> getTextExpression();
/**
* Return additional having clause. Where raw String expressions are added
* to having clause rather than Expression objects.
@@ -0,0 +1,10 @@
package com.avaje.ebeaninternal.api;
import com.avaje.ebean.TextExpressionList;
/**
* SPI extensions to Full text expression list.
*/
public interface SpiTextExpressionList<T> extends TextExpressionList<T> {
}
@@ -0,0 +1,18 @@
package com.avaje.ebeaninternal.api;
import com.avaje.ebean.TextJunction;
import com.avaje.ebeaninternal.server.expression.DocQueryContext;
import java.io.IOException;
/**
* SPI extension to the full text junctions (MUST, MUST NOT, SHOULD).
*/
public interface SpiTextJunction<T> extends TextJunction<T> {
/**
* Write the junction expression to the query context.
*/
void writeDocQueryJunction(DocQueryContext context) throws IOException;
}
@@ -14,8 +14,6 @@ import com.avaje.ebeaninternal.server.el.ElPropertyValue;
*/
public abstract class AbstractExpression implements SpiExpression {
private static final long serialVersionUID = 4072786211853856174L;
protected final String propName;
protected AbstractExpression(String propName) {
@@ -15,8 +15,6 @@ import java.util.Map.Entry;
class AllEqualsExpression extends NonPrepareExpression {
private static final long serialVersionUID = -8691773558205937025L;
private final Map<String, Object> propMap;
AllEqualsExpression(Map<String, Object> propMap) {
@@ -8,8 +8,6 @@ import java.io.IOException;
class BetweenExpression extends AbstractExpression {
private static final long serialVersionUID = 2078918165221454910L;
private static final String BETWEEN = " between ";
private final Object valueHigh;
@@ -15,8 +15,6 @@ import java.io.IOException;
*/
class BetweenPropertyExpression extends NonPrepareExpression {
private static final long serialVersionUID = 2078918165221454910L;
private static final String BETWEEN = " between ";
private final String lowProperty;
@@ -9,8 +9,6 @@ import java.io.IOException;
class CaseInsensitiveEqualExpression extends AbstractExpression {
private static final long serialVersionUID = -6406036750998971064L;
private final String value;
CaseInsensitiveEqualExpression(String propertyName, String value) {
@@ -0,0 +1,84 @@
package com.avaje.ebeaninternal.server.expression;
import com.avaje.ebean.Query;
import com.avaje.ebean.TextExpressionList;
import com.avaje.ebean.TextJunction;
import com.avaje.ebean.search.Match;
import com.avaje.ebeaninternal.api.SpiExpression;
import com.avaje.ebeaninternal.api.SpiTextJunction;
import java.io.IOException;
import java.util.List;
/**
* Implementation of SpiTextJunction (Must, Must Not or Should group).
*/
class DTextJunction<T> extends JunctionExpression<T> implements SpiTextJunction<T> {
private final TextJunction.Type type;
DTextJunction(Query<T> query, TextExpressionList<T> parent, TextJunction.Type type) {
super(query, parent);
this.type = type;
}
@Override
public void writeDocQueryJunction(DocQueryContext context) throws IOException {
context.startBoolGroupList(type);
List<SpiExpression> list = exprList.internalList();
for (int i = 0; i < list.size(); i++) {
list.get(i).writeDocQuery(context);
}
context.endBoolGroupList();
}
@Override
public SpiExpression copyForPlanKey() {
return this;
}
@Override
public TextExpressionList<T> match(String propertyName, String search) {
return match(propertyName, search, null);
}
@Override
public TextExpressionList<T> match(String propertyName, String search, Match options) {
return exprList.match(propertyName, search, options);
}
@Override
public TextJunction<T> must() {
return exprList.must();
}
@Override
public TextJunction<T> should() {
return exprList.should();
}
@Override
public TextJunction<T> mustNot() {
return exprList.mustNot();
}
@Override
public TextExpressionList<T> endMust() {
return endTextJunction();
}
@Override
public TextExpressionList<T> endShould() {
return endTextJunction();
}
@Override
public TextExpressionList<T> endMustNot() {
return endTextJunction();
}
private TextExpressionList<T> endTextJunction() {
return exprList.endTextJunction();
}
}
@@ -42,8 +42,6 @@ import com.avaje.ebeaninternal.server.query.SplitName;
*/
public class DefaultExampleExpression implements SpiExpression, ExampleExpression {
private static final long serialVersionUID = 1L;
/**
* The example bean containing the properties.
*/
@@ -7,7 +7,10 @@ import com.avaje.ebean.ExpressionList;
import com.avaje.ebean.Junction;
import com.avaje.ebean.LikeType;
import com.avaje.ebean.Query;
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.ebeaninternal.api.SpiExpressionFactory;
import com.avaje.ebeaninternal.api.SpiQuery;
@@ -36,6 +39,11 @@ public class DefaultExpressionFactory implements SpiExpressionFactory {
return "sql";
}
@Override
public Expression textMatch(String propertyName, String search, Match options) {
return new TextMatchExpression(propertyName, search, options);
}
public Expression jsonExists(String propertyName, String path) {
return new JsonPathExpression(propertyName, path, Op.EXISTS, null);
}
@@ -434,4 +442,11 @@ public class DefaultExpressionFactory implements SpiExpressionFactory {
return new JunctionExpression.Disjunction<T>(query, parent);
}
/**
* Create and return a Full text junction (Must, Must Not or Should).
*/
@Override
public <T> TextJunction<T> textJunction(Query<T> query, TextExpressionList<T> parent, TextJunction.Type type) {
return new DTextJunction<T>(query, parent, type);
}
}
@@ -2,12 +2,15 @@ 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.ebeaninternal.api.HashQueryPlanBuilder;
import com.avaje.ebeaninternal.api.ManyWhereJoins;
import com.avaje.ebeaninternal.api.SpiExpression;
import com.avaje.ebeaninternal.api.SpiExpressionList;
import com.avaje.ebeaninternal.api.SpiExpressionRequest;
import com.avaje.ebeaninternal.api.SpiExpressionValidation;
import com.avaje.ebeaninternal.api.SpiTextExpressionList;
import com.avaje.ebeaninternal.api.SpiTextJunction;
import com.avaje.ebeaninternal.server.deploy.BeanDescriptor;
import java.io.IOException;
@@ -21,15 +24,13 @@ import java.util.Set;
/**
* Default implementation of ExpressionList.
*/
public class DefaultExpressionList<T> implements SpiExpressionList<T> {
private static final long serialVersionUID = -6992345500247035947L;
public class DefaultExpressionList<T> implements SpiExpressionList<T>, SpiTextExpressionList<T> {
protected final List<SpiExpression> list;
protected final Query<T> query;
protected final ExpressionList<T> parentExprList;
protected final TextExpressionList<T> parentExprList;
protected transient ExpressionFactory expr;
@@ -37,15 +38,32 @@ public class DefaultExpressionList<T> implements SpiExpressionList<T> {
private final String listAndEnd;
private final String listAndJoin;
public DefaultExpressionList(Query<T> query, ExpressionList<T> parentExprList) {
/**
* Set to true for the "Text" root expression list.
*/
private final boolean textRoot;
/**
* Construct for Text root expression list - this handles implicit Bool Should, Must etc.
*/
public DefaultExpressionList(Query<T> query) {
this(query, query.getExpressionFactory(), null, new ArrayList<SpiExpression>(), true);
}
public DefaultExpressionList(Query<T> query, TextExpressionList<T> parentExprList) {
this(query, query.getExpressionFactory(), parentExprList);
}
public DefaultExpressionList(Query<T> query, ExpressionFactory expr, ExpressionList<T> parentExprList) {
public DefaultExpressionList(Query<T> query, ExpressionFactory expr, TextExpressionList<T> parentExprList) {
this(query, expr, parentExprList, new ArrayList<SpiExpression>());
}
protected DefaultExpressionList(Query<T> query, ExpressionFactory expr, ExpressionList<T> parentExprList, List<SpiExpression> list) {
protected DefaultExpressionList(Query<T> query, ExpressionFactory expr, TextExpressionList<T> parentExprList, List<SpiExpression> list) {
this(query, expr, parentExprList, list, false);
}
private DefaultExpressionList(Query<T> query, ExpressionFactory expr, TextExpressionList<T> parentExprList, List<SpiExpression> list, boolean textRoot) {
this.textRoot = textRoot;
this.list = list;
this.query = query;
this.expr = expr;
@@ -60,19 +78,66 @@ public class DefaultExpressionList<T> implements SpiExpressionList<T> {
this(null, null, null, new ArrayList<SpiExpression>());
}
/**
* Write being aware if it is the Top level "text" expressions.
* <p>
* If this is the Top level "text" expressions then it detects if explicit or implicit Bool Should, Must etc is required
* to wrap the expressions.
* </p>
* <p>
* If implicit Bool is required SHOULD is used.
* </p>
*/
@Override
public void writeDocQuery(DocQueryContext context) throws IOException {
writeDocQuery(context, null);
if (!textRoot) {
writeDocQuery(context, null);
} else {
// this is a Top level "text" expressions so we may need to wrap in Bool SHOULD etc.
if (list.isEmpty()) throw new IllegalStateException("empty expression list?");
int size = list.size();
SpiExpression first = list.get(0);
boolean explicitBool = first instanceof SpiTextJunction<?>;
boolean implicitBool = !explicitBool && size > 1;
if (implicitBool || explicitBool) {
context.startBoolGroup();
}
if (implicitBool) {
context.startBoolGroupList(TextJunction.Type.SHOULD);
}
for (int i = 0; i < size; i++) {
SpiExpression expr = list.get(i);
if (explicitBool) {
try {
((SpiTextJunction<?>) expr).writeDocQueryJunction(context);
} catch (ClassCastException e) {
throw new IllegalStateException("The top level text() expressions should be all be 'Must', 'Should' or 'Must Not' or none of them should be.", e);
}
} else {
expr.writeDocQuery(context);
}
}
if (implicitBool) {
context.endBoolGroupList();
}
if (implicitBool || explicitBool) {
context.endBoolGroup();
}
}
}
public void writeDocQuery(DocQueryContext context, SpiExpression idEquals) throws IOException {
int size = list.size();
if (size == 1 && idEquals == null) {
// only 1 expression - skip bool must
// only 1 expression - skip bool
list.get(0).writeDocQuery(context);
} else if (size == 0 && idEquals != null) {
// only idEquals - skip bool must
// only idEquals - skip bool
idEquals.writeDocQuery(context);
} else {
// bool must wrap all the children
@@ -139,6 +204,10 @@ public class DefaultExpressionList<T> implements SpiExpressionList<T> {
return parentExprList == null ? this : parentExprList;
}
protected TextExpressionList<T> endTextJunction() {
return parentExprList == null ? this : parentExprList;
}
@Override
public Query<T> query() {
return query;
@@ -574,6 +643,12 @@ public class DefaultExpressionList<T> implements SpiExpressionList<T> {
return this;
}
public TextJunction<T> textJunction(TextJunction.Type type) {
TextJunction<T> junction = expr.textJunction(query, this, type);
add(junction);
return junction;
}
@Override
public Junction<T> conjunction() {
Junction<T> conjunction = expr.conjunction(query, this);
@@ -778,4 +853,44 @@ public class DefaultExpressionList<T> implements SpiExpressionList<T> {
return this;
}
@Override
public TextExpressionList<T> match(String propertyName, String search) {
return match(propertyName, search, null);
}
@Override
public TextExpressionList<T> match(String propertyName, String search, Match options) {
add(expr.textMatch(propertyName, search, options));
return this;
}
@Override
public TextJunction<T> must() {
return textJunction(TextJunction.Type.MUST);
}
@Override
public TextJunction<T> should() {
return textJunction(TextJunction.Type.SHOULD);
}
@Override
public TextJunction<T> mustNot() {
return textJunction(TextJunction.Type.MUST_NOT);
}
@Override
public TextExpressionList<T> endMust() {
return endTextJunction();
}
@Override
public TextExpressionList<T> endShould() {
return endTextJunction();
}
@Override
public TextExpressionList<T> endMustNot() {
return endTextJunction();
}
}
@@ -1,6 +1,8 @@
package com.avaje.ebeaninternal.server.expression;
import com.avaje.ebean.LikeType;
import com.avaje.ebean.TextJunction;
import com.avaje.ebean.search.Match;
import java.io.IOException;
import java.util.List;
@@ -90,4 +92,29 @@ public interface DocQueryContext {
* Write a Like expression.
*/
void writeLike(String propName, String val, LikeType type, boolean caseInsensitive) throws IOException;
/**
* Write a Match expression.
*/
void writeMatch(String propName, String search, Match options) throws IOException;
/**
* Start a Bool which may contain some of Must, Must Not, Should.
*/
void startBoolGroup() throws IOException;
/**
* Start a Must, Must Not or Should list.
*/
void startBoolGroupList(TextJunction.Type type) throws IOException;
/**
* End a Must, Must Not or Should list.
*/
void endBoolGroupList() throws IOException;
/**
* End the Bool group.
*/
void endBoolGroup() throws IOException;
}
@@ -16,8 +16,6 @@ import com.avaje.ebeaninternal.server.query.CQuery;
class ExistsQueryExpression implements SpiExpression, UnsupportedDocStoreExpression {
private static final long serialVersionUID = 666990277309851644L;
protected final boolean not;
protected final SpiQuery<?> subQuery;
@@ -10,8 +10,6 @@ import java.util.Set;
public class FilterExpressionList<T> extends DefaultExpressionList<T> {
private static final long serialVersionUID = 2226895827150099020L;
private static final String notAllowedMessage = "This method is not allowed on a filter";
private final Query<T> rootQuery;
@@ -14,8 +14,6 @@ import java.io.IOException;
*/
class IdExpression extends NonPrepareExpression implements SpiExpression {
private static final long serialVersionUID = -3065936341718489842L;
private final Object value;
IdExpression(Object value) {
@@ -16,8 +16,6 @@ import java.util.List;
*/
public class IdInExpression extends NonPrepareExpression {
private static final long serialVersionUID = 1L;
private final List<?> idList;
public IdInExpression(List<?> idList) {
@@ -11,8 +11,6 @@ import java.util.Collection;
class InExpression extends AbstractExpression {
private static final long serialVersionUID = 3150665801693551260L;
private final boolean not;
private final Object[] values;
@@ -16,8 +16,6 @@ import java.util.List;
*/
class InQueryExpression extends AbstractExpression implements UnsupportedDocStoreExpression {
private static final long serialVersionUID = 666990277309851644L;
private final boolean not;
private final SpiQuery<?> subQuery;
@@ -1,19 +1,6 @@
package com.avaje.ebeaninternal.server.expression;
import com.avaje.ebean.Expression;
import com.avaje.ebean.ExpressionList;
import com.avaje.ebean.FetchPath;
import com.avaje.ebean.FutureIds;
import com.avaje.ebean.FutureList;
import com.avaje.ebean.FutureRowCount;
import com.avaje.ebean.Junction;
import com.avaje.ebean.OrderBy;
import com.avaje.ebean.PagedList;
import com.avaje.ebean.Query;
import com.avaje.ebean.QueryEachConsumer;
import com.avaje.ebean.QueryEachWhileConsumer;
import com.avaje.ebean.QueryIterator;
import com.avaje.ebean.Version;
import com.avaje.ebean.*;
import com.avaje.ebean.event.BeanQueryRequest;
import com.avaje.ebeaninternal.api.HashQueryPlanBuilder;
import com.avaje.ebeaninternal.api.ManyWhereJoins;
@@ -34,16 +21,12 @@ import java.util.Set;
*/
abstract class JunctionExpression<T> implements Junction<T>, SpiExpression, ExpressionList<T> {
private static final long serialVersionUID = -7422204102750462676L;
static final String OR = " or ";
private static final String OR = " or ";
private static final String AND = " and ";
static final String AND = " and ";
static class Conjunction<T> extends JunctionExpression<T> {
private static final long serialVersionUID = -645619859900030678L;
Conjunction(Query<T> query, ExpressionList<T> parent) {
super(false, AND, query, parent);
}
@@ -59,8 +42,6 @@ abstract class JunctionExpression<T> implements Junction<T>, SpiExpression, Expr
static class Disjunction<T> extends JunctionExpression<T> {
private static final long serialVersionUID = -8464470066692221413L;
Disjunction(Query<T> query, ExpressionList<T> parent) {
super(true, OR, query, parent);
}
@@ -84,10 +65,16 @@ abstract class JunctionExpression<T> implements Junction<T>, SpiExpression, Expr
*/
private final boolean disjunction;
JunctionExpression(Query<T> query, TextExpressionList<T> parent) {
this.disjunction = true;
this.joinType = OR;
this.exprList = new DefaultExpressionList<T>(query, parent);
}
JunctionExpression(boolean disjunction, String joinType, Query<T> query, ExpressionList<T> parent) {
this.disjunction = disjunction;
this.joinType = joinType;
this.exprList = new DefaultExpressionList<T>(query, parent);
this.exprList = new DefaultExpressionList<T>(query, (TextExpressionList<T>)parent);
}
/**
@@ -10,8 +10,6 @@ import java.io.IOException;
class LikeExpression extends AbstractExpression {
private static final long serialVersionUID = -5398151809111172380L;
private final String val;
private final boolean caseInsensitive;
@@ -16,15 +16,11 @@ import java.io.IOException;
*/
abstract class LogicExpression implements SpiExpression {
private static final long serialVersionUID = 616860781960645251L;
static final String AND = " and ";
static final String OR = " or ";
static class And extends LogicExpression {
private static final long serialVersionUID = -3832889676798526444L;
And(Expression expOne, Expression expTwo) {
super(AND, expOne, expTwo);
}
@@ -38,8 +34,6 @@ abstract class LogicExpression implements SpiExpression {
static class Or extends LogicExpression {
private static final long serialVersionUID = -6871993143194094819L;
Or(Expression expOne, Expression expTwo) {
super(OR, expOne, expTwo);
}
@@ -13,8 +13,6 @@ import java.io.IOException;
final class NotExpression implements SpiExpression {
private static final long serialVersionUID = 5648926732402355781L;
private static final String NOT_START = "not (";
private static final String NOT_END = ") ";
@@ -13,8 +13,6 @@ import java.io.IOException;
*/
class NullExpression extends AbstractExpression {
private static final long serialVersionUID = 4246991057451128269L;
private final boolean notNull;
NullExpression(String propertyName, boolean notNull) {
@@ -11,8 +11,6 @@ import java.io.IOException;
class RawExpression extends NonPrepareExpression {
private static final long serialVersionUID = 7973903141340334606L;
private final String sql;
private final Object[] values;
@@ -10,8 +10,6 @@ import java.io.IOException;
public class SimpleExpression extends AbstractExpression {
private static final long serialVersionUID = -382881395755603790L;
private final Op type;
private final Object value;
@@ -0,0 +1,75 @@
package com.avaje.ebeaninternal.server.expression;
import com.avaje.ebean.search.Match;
import com.avaje.ebeaninternal.api.HashQueryPlanBuilder;
import com.avaje.ebeaninternal.api.SpiExpression;
import com.avaje.ebeaninternal.api.SpiExpressionRequest;
import java.io.IOException;
/**
* Full text MATCH expression.
*/
public class TextMatchExpression extends AbstractExpression {
private final String search;
private final Match options;
public TextMatchExpression(String propertyName, String search, Match options) {
super(propertyName);
this.search = search;
this.options = options;
}
@Override
public void writeDocQuery(DocQueryContext context) throws IOException {
context.writeMatch(propName, 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(TextMatchExpression.class).add(propName).add(search);
if (options != null) {
builder.add(options.isAnd());
builder.add(options.getBoost());
builder.add(options.getMinShouldMatch());
}
}
@Override
public int queryBindHash() {
return search.hashCode();
}
@Override
public boolean isSameByPlan(SpiExpression other) {
if (!(other instanceof TextMatchExpression)) {
return false;
}
TextMatchExpression that = (TextMatchExpression) other;
return this.propName.equals(that.propName)
&& this.search.equals(that.search)
&& this.options == null ? that.options == null : options.equals(that.options);
}
@Override
public boolean isSameByBind(SpiExpression other) {
TextMatchExpression that = (TextMatchExpression) other;
return search.equals(that.search);
}
}
@@ -158,6 +158,8 @@ public class DefaultOrmQuery<T> implements SpiQuery<T> {
*/
private BindParams bindParams;
private DefaultExpressionList<T> textExpressions;
private DefaultExpressionList<T> whereExpressions;
private DefaultExpressionList<T> havingExpressions;
@@ -1313,6 +1315,15 @@ public class DefaultOrmQuery<T> implements SpiQuery<T> {
return this;
}
@Override
public TextExpressionList text() {
if (textExpressions == null) {
useDocStore = true;
textExpressions = new DefaultExpressionList<T>(this);
}
return textExpressions;
}
public ExpressionList<T> where() {
if (whereExpressions == null) {
whereExpressions = new DefaultExpressionList<T>(this, null);
@@ -1352,6 +1363,11 @@ public class DefaultOrmQuery<T> implements SpiQuery<T> {
return whereExpressions;
}
@Override
public SpiExpressionList<T> getTextExpression() {
return textExpressions;
}
public String getGeneratedSql() {
return generatedSql;
}
@@ -0,0 +1,62 @@
package com.avaje.ebean;
import com.avaje.ebean.search.Match;
import com.avaje.tests.model.basic.Order;
import org.junit.Test;
public class TextExpressionListTest {
@Test
public void syntax() {
Ebean.find(Order.class)
.text().match("name", "rob");
Ebean.find(Order.class)
.text().should()
.match("name", "rob")
.match("note", "war and peace");
Ebean.find(Order.class)
.text()
.should()
.match("title", "war and peace")
.match("author", "leo tolstoy")
.should()
.match("translator", "Constance Garnett")
.match("translator", "Louise Maude");
Ebean.find(Order.class)
.text()
.should()
.match("title", "war and peace")
.match("author", "leo tolstoy")
.should()
.match("translator", "Constance Garnett", Match.AND().boost(2).minShouldMatch("75%"))
.match("translator", "Louise Maude")
.where()
.gt("reviewDate", 12345);
Ebean.find(Order.class)
.text()
.must()
.match("title", "quick")
.endMust()
.should()
.match("title", "brown")
.match("title", "dog")
.endShould()
.mustNot()
.match("title", "lazy")
.endMustNot()
.where()
.gt("reviewDate", 12345);
}
}
@@ -0,0 +1,75 @@
package com.avaje.ebean.search;
import org.junit.Test;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotEquals;
public class MatchTest {
Match match() {
return Match.AND()
.analyzer("whitespace")
.boost(2)
.cutoffFrequency(1)
.minShouldMatch("50%")
.maxExpansions(3)
.zeroTerms("all");
}
@Test
public void equals_when_allSet() {
assertEquals(match(), match());
assertEquals(match().hashCode(), match().hashCode());
}
@Test
public void notEquals_when_analyzer() {
assertNotEquals(match(), match().analyzer("foo"));
assertNotEquals(match().hashCode(), match().analyzer("foo").hashCode());
assertNotEquals(match(), match().analyzer(null));
assertNotEquals(match().hashCode(), match().analyzer(null).hashCode());
}
@Test
public void notEquals_when_boost() {
assertNotEquals(match(), match().boost(3));
}
@Test
public void notEquals_when_cutoffFrequency() {
assertNotEquals(match(), match().cutoffFrequency(3));
}
@Test
public void notEquals_when_maxExpansions() {
assertNotEquals(match(), match().maxExpansions(100));
}
@Test
public void notEquals_when_minShouldMatch() {
assertNotEquals(match(), match().minShouldMatch("12%"));
}
@Test
public void notEquals_when_zeroTerms() {
assertNotEquals(match(), match().zeroTerms("none"));
}
@Test
public void notEquals_when_phrase() {
assertNotEquals(match(), match().phrase());
}
@Test
public void notEquals_when_phrasePrefix() {
assertNotEquals(match(), match().phrasePrefix());
}
@Test
public void notEquals_when_operator() {
assertNotEquals(match(), match().opOr());
}
}