diff --git a/src/main/java/com/avaje/ebean/Expression.java b/src/main/java/com/avaje/ebean/Expression.java index c6c38ab2c..306bccb93 100644 --- a/src/main/java/com/avaje/ebean/Expression.java +++ b/src/main/java/com/avaje/ebean/Expression.java @@ -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 { } diff --git a/src/main/java/com/avaje/ebean/ExpressionFactory.java b/src/main/java/com/avaje/ebean/ExpressionFactory.java index 6fcd5a590..884bab4db 100644 --- a/src/main/java/com/avaje/ebean/ExpressionFactory.java +++ b/src/main/java/com/avaje/ebean/ExpressionFactory.java @@ -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. */ Junction disjunction(Query query, ExpressionList parent); + + /** + * Return a Text query junction for MUST, SHOULD or MUST NOT. + *

+ * This is doc store Elastic only. + *

+ */ + TextJunction textJunction(Query query, TextExpressionList parent, TextJunction.Type type); } diff --git a/src/main/java/com/avaje/ebean/ExpressionList.java b/src/main/java/com/avaje/ebean/ExpressionList.java index 51e79fd1d..6577751e6 100644 --- a/src/main/java/com/avaje/ebean/ExpressionList.java +++ b/src/main/java/com/avaje/ebean/ExpressionList.java @@ -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 extends Serializable { +public interface ExpressionList { /** * Return the query that owns this expression list. diff --git a/src/main/java/com/avaje/ebean/Query.java b/src/main/java/com/avaje/ebean/Query.java index a1fc3fc04..2b6ebe314 100644 --- a/src/main/java/com/avaje/ebean/Query.java +++ b/src/main/java/com/avaje/ebean/Query.java @@ -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 { */ ExpressionList where(); + /** + * Add Full text search expressions for Document store queries. + *

+ * This is currently ElasticSearch only and provides the full text + * expressions such as Match and Multi-Match. + *

+ *

+ * This automatically makes this query a "Doc Store" query and will execute + * against the document store (ElasticSearch). + *

+ *

+ * Expressions added here are added to the "query" section of an ElasticSearch + * query rather than the "filter" section. + *

+ *

+ * Expressions added to the where() are added to the "filter" section of an + * ElasticSearch query. + *

+ */ + TextExpressionList text(); + /** * This applies a filter on the 'many' property list rather than the root * level objects. diff --git a/src/main/java/com/avaje/ebean/TextExpressionList.java b/src/main/java/com/avaje/ebean/TextExpressionList.java new file mode 100644 index 000000000..a30fc7179 --- /dev/null +++ b/src/main/java/com/avaje/ebean/TextExpressionList.java @@ -0,0 +1,58 @@ +package com.avaje.ebean; + +import com.avaje.ebean.search.Match; + +/** + * An list of Full text query expressions. + *

+ * For ElasticSearch these expression go into the "query" section rather than the "filter" section. + *

+ */ +public interface TextExpressionList extends ExpressionList { + + /** + * Add a match expression. + * + * @param propertyName The property name for the match + * @param search The search value + */ + TextExpressionList 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 match(String propertyName, String search, Match options); + + /** + * Start a list of expressions that will be joined by MUST. + */ + TextJunction must(); + + /** + * Start a list of expressions that will be joined by SHOULD. + */ + TextJunction should(); + + /** + * Start a list of expressions that will be joined by MUST NOT. + */ + TextJunction mustNot(); + + /** + * End the list of MUST expressions. + */ + TextExpressionList endMust(); + + /** + * End the list of SHOULD expressions. + */ + TextExpressionList endShould(); + + /** + * End the list of MUST NOT expressions. + */ + TextExpressionList endMustNot(); +} diff --git a/src/main/java/com/avaje/ebean/TextJunction.java b/src/main/java/com/avaje/ebean/TextJunction.java new file mode 100644 index 000000000..537d0bbb4 --- /dev/null +++ b/src/main/java/com/avaje/ebean/TextJunction.java @@ -0,0 +1,41 @@ +package com.avaje.ebean; + +/** + * A Full text MUST, MUST NOT or SHOULD group of expressions. + */ +public interface TextJunction extends Junction, TextExpressionList { + + /** + * 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; + } + } +} diff --git a/src/main/java/com/avaje/ebean/search/Match.java b/src/main/java/com/avaje/ebean/search/Match.java new file mode 100644 index 000000000..4de641ea6 --- /dev/null +++ b/src/main/java/com/avaje/ebean/search/Match.java @@ -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; + } +} diff --git a/src/main/java/com/avaje/ebeaninternal/api/SpiQuery.java b/src/main/java/com/avaje/ebeaninternal/api/SpiQuery.java index 0aeaad4c0..fec134ac0 100644 --- a/src/main/java/com/avaje/ebeaninternal/api/SpiQuery.java +++ b/src/main/java/com/avaje/ebeaninternal/api/SpiQuery.java @@ -509,6 +509,11 @@ public interface SpiQuery extends Query { */ SpiExpressionList getHavingExpressions(); + /** + * Return the text expressions. + */ + SpiExpressionList getTextExpression(); + /** * Return additional having clause. Where raw String expressions are added * to having clause rather than Expression objects. diff --git a/src/main/java/com/avaje/ebeaninternal/api/SpiTextExpressionList.java b/src/main/java/com/avaje/ebeaninternal/api/SpiTextExpressionList.java new file mode 100644 index 000000000..2c958e06e --- /dev/null +++ b/src/main/java/com/avaje/ebeaninternal/api/SpiTextExpressionList.java @@ -0,0 +1,10 @@ +package com.avaje.ebeaninternal.api; + +import com.avaje.ebean.TextExpressionList; + +/** + * SPI extensions to Full text expression list. + */ +public interface SpiTextExpressionList extends TextExpressionList { + +} diff --git a/src/main/java/com/avaje/ebeaninternal/api/SpiTextJunction.java b/src/main/java/com/avaje/ebeaninternal/api/SpiTextJunction.java new file mode 100644 index 000000000..7d3e1c086 --- /dev/null +++ b/src/main/java/com/avaje/ebeaninternal/api/SpiTextJunction.java @@ -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 extends TextJunction { + + /** + * Write the junction expression to the query context. + */ + void writeDocQueryJunction(DocQueryContext context) throws IOException; + +} diff --git a/src/main/java/com/avaje/ebeaninternal/server/expression/AbstractExpression.java b/src/main/java/com/avaje/ebeaninternal/server/expression/AbstractExpression.java index d471a8d57..f2fac018e 100644 --- a/src/main/java/com/avaje/ebeaninternal/server/expression/AbstractExpression.java +++ b/src/main/java/com/avaje/ebeaninternal/server/expression/AbstractExpression.java @@ -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) { diff --git a/src/main/java/com/avaje/ebeaninternal/server/expression/AllEqualsExpression.java b/src/main/java/com/avaje/ebeaninternal/server/expression/AllEqualsExpression.java index b21d18816..2883412e4 100644 --- a/src/main/java/com/avaje/ebeaninternal/server/expression/AllEqualsExpression.java +++ b/src/main/java/com/avaje/ebeaninternal/server/expression/AllEqualsExpression.java @@ -15,8 +15,6 @@ import java.util.Map.Entry; class AllEqualsExpression extends NonPrepareExpression { - private static final long serialVersionUID = -8691773558205937025L; - private final Map propMap; AllEqualsExpression(Map propMap) { diff --git a/src/main/java/com/avaje/ebeaninternal/server/expression/BetweenExpression.java b/src/main/java/com/avaje/ebeaninternal/server/expression/BetweenExpression.java index 255fd117f..089f2c058 100644 --- a/src/main/java/com/avaje/ebeaninternal/server/expression/BetweenExpression.java +++ b/src/main/java/com/avaje/ebeaninternal/server/expression/BetweenExpression.java @@ -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; diff --git a/src/main/java/com/avaje/ebeaninternal/server/expression/BetweenPropertyExpression.java b/src/main/java/com/avaje/ebeaninternal/server/expression/BetweenPropertyExpression.java index 133904a84..d121437b6 100644 --- a/src/main/java/com/avaje/ebeaninternal/server/expression/BetweenPropertyExpression.java +++ b/src/main/java/com/avaje/ebeaninternal/server/expression/BetweenPropertyExpression.java @@ -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; diff --git a/src/main/java/com/avaje/ebeaninternal/server/expression/CaseInsensitiveEqualExpression.java b/src/main/java/com/avaje/ebeaninternal/server/expression/CaseInsensitiveEqualExpression.java index 399e79d42..b10c2c57a 100644 --- a/src/main/java/com/avaje/ebeaninternal/server/expression/CaseInsensitiveEqualExpression.java +++ b/src/main/java/com/avaje/ebeaninternal/server/expression/CaseInsensitiveEqualExpression.java @@ -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) { diff --git a/src/main/java/com/avaje/ebeaninternal/server/expression/DTextJunction.java b/src/main/java/com/avaje/ebeaninternal/server/expression/DTextJunction.java new file mode 100644 index 000000000..baba32c88 --- /dev/null +++ b/src/main/java/com/avaje/ebeaninternal/server/expression/DTextJunction.java @@ -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 extends JunctionExpression implements SpiTextJunction { + + private final TextJunction.Type type; + + DTextJunction(Query query, TextExpressionList parent, TextJunction.Type type) { + super(query, parent); + this.type = type; + } + + @Override + public void writeDocQueryJunction(DocQueryContext context) throws IOException { + context.startBoolGroupList(type); + List 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 match(String propertyName, String search) { + return match(propertyName, search, null); + } + + @Override + public TextExpressionList match(String propertyName, String search, Match options) { + return exprList.match(propertyName, search, options); + } + + @Override + public TextJunction must() { + return exprList.must(); + } + + @Override + public TextJunction should() { + return exprList.should(); + } + + @Override + public TextJunction mustNot() { + return exprList.mustNot(); + } + + @Override + public TextExpressionList endMust() { + return endTextJunction(); + } + + @Override + public TextExpressionList endShould() { + return endTextJunction(); + } + + @Override + public TextExpressionList endMustNot() { + return endTextJunction(); + } + + private TextExpressionList endTextJunction() { + return exprList.endTextJunction(); + } + +} diff --git a/src/main/java/com/avaje/ebeaninternal/server/expression/DefaultExampleExpression.java b/src/main/java/com/avaje/ebeaninternal/server/expression/DefaultExampleExpression.java index 020fc457f..9a887dea2 100644 --- a/src/main/java/com/avaje/ebeaninternal/server/expression/DefaultExampleExpression.java +++ b/src/main/java/com/avaje/ebeaninternal/server/expression/DefaultExampleExpression.java @@ -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. */ diff --git a/src/main/java/com/avaje/ebeaninternal/server/expression/DefaultExpressionFactory.java b/src/main/java/com/avaje/ebeaninternal/server/expression/DefaultExpressionFactory.java index 0226c93ea..4a9bcd512 100644 --- a/src/main/java/com/avaje/ebeaninternal/server/expression/DefaultExpressionFactory.java +++ b/src/main/java/com/avaje/ebeaninternal/server/expression/DefaultExpressionFactory.java @@ -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(query, parent); } + /** + * Create and return a Full text junction (Must, Must Not or Should). + */ + @Override + public TextJunction textJunction(Query query, TextExpressionList parent, TextJunction.Type type) { + return new DTextJunction(query, parent, type); + } } diff --git a/src/main/java/com/avaje/ebeaninternal/server/expression/DefaultExpressionList.java b/src/main/java/com/avaje/ebeaninternal/server/expression/DefaultExpressionList.java index 2bf3a44cb..fae3ae3e5 100644 --- a/src/main/java/com/avaje/ebeaninternal/server/expression/DefaultExpressionList.java +++ b/src/main/java/com/avaje/ebeaninternal/server/expression/DefaultExpressionList.java @@ -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 implements SpiExpressionList { - - private static final long serialVersionUID = -6992345500247035947L; +public class DefaultExpressionList implements SpiExpressionList, SpiTextExpressionList { protected final List list; protected final Query query; - protected final ExpressionList parentExprList; + protected final TextExpressionList parentExprList; protected transient ExpressionFactory expr; @@ -37,15 +38,32 @@ public class DefaultExpressionList implements SpiExpressionList { private final String listAndEnd; private final String listAndJoin; - public DefaultExpressionList(Query query, ExpressionList 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 query) { + this(query, query.getExpressionFactory(), null, new ArrayList(), true); + } + + public DefaultExpressionList(Query query, TextExpressionList parentExprList) { this(query, query.getExpressionFactory(), parentExprList); } - public DefaultExpressionList(Query query, ExpressionFactory expr, ExpressionList parentExprList) { + public DefaultExpressionList(Query query, ExpressionFactory expr, TextExpressionList parentExprList) { this(query, expr, parentExprList, new ArrayList()); } - protected DefaultExpressionList(Query query, ExpressionFactory expr, ExpressionList parentExprList, List list) { + protected DefaultExpressionList(Query query, ExpressionFactory expr, TextExpressionList parentExprList, List list) { + this(query, expr, parentExprList, list, false); + } + + private DefaultExpressionList(Query query, ExpressionFactory expr, TextExpressionList parentExprList, List list, boolean textRoot) { + this.textRoot = textRoot; this.list = list; this.query = query; this.expr = expr; @@ -60,19 +78,66 @@ public class DefaultExpressionList implements SpiExpressionList { this(null, null, null, new ArrayList()); } + /** + * Write being aware if it is the Top level "text" expressions. + *

+ * 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. + *

+ *

+ * If implicit Bool is required SHOULD is used. + *

+ */ @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 implements SpiExpressionList { return parentExprList == null ? this : parentExprList; } + protected TextExpressionList endTextJunction() { + return parentExprList == null ? this : parentExprList; + } + @Override public Query query() { return query; @@ -574,6 +643,12 @@ public class DefaultExpressionList implements SpiExpressionList { return this; } + public TextJunction textJunction(TextJunction.Type type) { + TextJunction junction = expr.textJunction(query, this, type); + add(junction); + return junction; + } + @Override public Junction conjunction() { Junction conjunction = expr.conjunction(query, this); @@ -778,4 +853,44 @@ public class DefaultExpressionList implements SpiExpressionList { return this; } + @Override + public TextExpressionList match(String propertyName, String search) { + return match(propertyName, search, null); + } + + @Override + public TextExpressionList match(String propertyName, String search, Match options) { + add(expr.textMatch(propertyName, search, options)); + return this; + } + + @Override + public TextJunction must() { + return textJunction(TextJunction.Type.MUST); + } + + @Override + public TextJunction should() { + return textJunction(TextJunction.Type.SHOULD); + } + + @Override + public TextJunction mustNot() { + return textJunction(TextJunction.Type.MUST_NOT); + } + + @Override + public TextExpressionList endMust() { + return endTextJunction(); + } + + @Override + public TextExpressionList endShould() { + return endTextJunction(); + } + + @Override + public TextExpressionList endMustNot() { + return endTextJunction(); + } } diff --git a/src/main/java/com/avaje/ebeaninternal/server/expression/DocQueryContext.java b/src/main/java/com/avaje/ebeaninternal/server/expression/DocQueryContext.java index ffea20340..6943fa0c6 100644 --- a/src/main/java/com/avaje/ebeaninternal/server/expression/DocQueryContext.java +++ b/src/main/java/com/avaje/ebeaninternal/server/expression/DocQueryContext.java @@ -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; } diff --git a/src/main/java/com/avaje/ebeaninternal/server/expression/ExistsQueryExpression.java b/src/main/java/com/avaje/ebeaninternal/server/expression/ExistsQueryExpression.java index 1a2992d28..ec937239d 100644 --- a/src/main/java/com/avaje/ebeaninternal/server/expression/ExistsQueryExpression.java +++ b/src/main/java/com/avaje/ebeaninternal/server/expression/ExistsQueryExpression.java @@ -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; diff --git a/src/main/java/com/avaje/ebeaninternal/server/expression/FilterExpressionList.java b/src/main/java/com/avaje/ebeaninternal/server/expression/FilterExpressionList.java index 2c8fd7358..91ffc2406 100644 --- a/src/main/java/com/avaje/ebeaninternal/server/expression/FilterExpressionList.java +++ b/src/main/java/com/avaje/ebeaninternal/server/expression/FilterExpressionList.java @@ -10,8 +10,6 @@ import java.util.Set; public class FilterExpressionList extends DefaultExpressionList { - private static final long serialVersionUID = 2226895827150099020L; - private static final String notAllowedMessage = "This method is not allowed on a filter"; private final Query rootQuery; diff --git a/src/main/java/com/avaje/ebeaninternal/server/expression/IdExpression.java b/src/main/java/com/avaje/ebeaninternal/server/expression/IdExpression.java index b55fd7952..73692a5f5 100644 --- a/src/main/java/com/avaje/ebeaninternal/server/expression/IdExpression.java +++ b/src/main/java/com/avaje/ebeaninternal/server/expression/IdExpression.java @@ -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) { diff --git a/src/main/java/com/avaje/ebeaninternal/server/expression/IdInExpression.java b/src/main/java/com/avaje/ebeaninternal/server/expression/IdInExpression.java index c63fc1f38..b9c7e09e5 100644 --- a/src/main/java/com/avaje/ebeaninternal/server/expression/IdInExpression.java +++ b/src/main/java/com/avaje/ebeaninternal/server/expression/IdInExpression.java @@ -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) { diff --git a/src/main/java/com/avaje/ebeaninternal/server/expression/InExpression.java b/src/main/java/com/avaje/ebeaninternal/server/expression/InExpression.java index 1ed7ef702..ebd31b768 100644 --- a/src/main/java/com/avaje/ebeaninternal/server/expression/InExpression.java +++ b/src/main/java/com/avaje/ebeaninternal/server/expression/InExpression.java @@ -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; diff --git a/src/main/java/com/avaje/ebeaninternal/server/expression/InQueryExpression.java b/src/main/java/com/avaje/ebeaninternal/server/expression/InQueryExpression.java index a486b558c..7768da6cf 100644 --- a/src/main/java/com/avaje/ebeaninternal/server/expression/InQueryExpression.java +++ b/src/main/java/com/avaje/ebeaninternal/server/expression/InQueryExpression.java @@ -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; diff --git a/src/main/java/com/avaje/ebeaninternal/server/expression/JunctionExpression.java b/src/main/java/com/avaje/ebeaninternal/server/expression/JunctionExpression.java index 7a3249c29..38a2927e8 100644 --- a/src/main/java/com/avaje/ebeaninternal/server/expression/JunctionExpression.java +++ b/src/main/java/com/avaje/ebeaninternal/server/expression/JunctionExpression.java @@ -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 implements Junction, SpiExpression, ExpressionList { - 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 extends JunctionExpression { - private static final long serialVersionUID = -645619859900030678L; - Conjunction(Query query, ExpressionList parent) { super(false, AND, query, parent); } @@ -59,8 +42,6 @@ abstract class JunctionExpression implements Junction, SpiExpression, Expr static class Disjunction extends JunctionExpression { - private static final long serialVersionUID = -8464470066692221413L; - Disjunction(Query query, ExpressionList parent) { super(true, OR, query, parent); } @@ -84,10 +65,16 @@ abstract class JunctionExpression implements Junction, SpiExpression, Expr */ private final boolean disjunction; + JunctionExpression(Query query, TextExpressionList parent) { + this.disjunction = true; + this.joinType = OR; + this.exprList = new DefaultExpressionList(query, parent); + } + JunctionExpression(boolean disjunction, String joinType, Query query, ExpressionList parent) { this.disjunction = disjunction; this.joinType = joinType; - this.exprList = new DefaultExpressionList(query, parent); + this.exprList = new DefaultExpressionList(query, (TextExpressionList)parent); } /** diff --git a/src/main/java/com/avaje/ebeaninternal/server/expression/LikeExpression.java b/src/main/java/com/avaje/ebeaninternal/server/expression/LikeExpression.java index 91bac41b0..b8b54d3e5 100644 --- a/src/main/java/com/avaje/ebeaninternal/server/expression/LikeExpression.java +++ b/src/main/java/com/avaje/ebeaninternal/server/expression/LikeExpression.java @@ -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; diff --git a/src/main/java/com/avaje/ebeaninternal/server/expression/LogicExpression.java b/src/main/java/com/avaje/ebeaninternal/server/expression/LogicExpression.java index b29f848a0..73f7f9c3f 100644 --- a/src/main/java/com/avaje/ebeaninternal/server/expression/LogicExpression.java +++ b/src/main/java/com/avaje/ebeaninternal/server/expression/LogicExpression.java @@ -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); } diff --git a/src/main/java/com/avaje/ebeaninternal/server/expression/NotExpression.java b/src/main/java/com/avaje/ebeaninternal/server/expression/NotExpression.java index be503ab97..5e4b1c09a 100644 --- a/src/main/java/com/avaje/ebeaninternal/server/expression/NotExpression.java +++ b/src/main/java/com/avaje/ebeaninternal/server/expression/NotExpression.java @@ -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 = ") "; diff --git a/src/main/java/com/avaje/ebeaninternal/server/expression/NullExpression.java b/src/main/java/com/avaje/ebeaninternal/server/expression/NullExpression.java index 8035ef881..213b59b3f 100644 --- a/src/main/java/com/avaje/ebeaninternal/server/expression/NullExpression.java +++ b/src/main/java/com/avaje/ebeaninternal/server/expression/NullExpression.java @@ -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) { diff --git a/src/main/java/com/avaje/ebeaninternal/server/expression/RawExpression.java b/src/main/java/com/avaje/ebeaninternal/server/expression/RawExpression.java index bc381ee59..bbb48f21c 100644 --- a/src/main/java/com/avaje/ebeaninternal/server/expression/RawExpression.java +++ b/src/main/java/com/avaje/ebeaninternal/server/expression/RawExpression.java @@ -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; diff --git a/src/main/java/com/avaje/ebeaninternal/server/expression/SimpleExpression.java b/src/main/java/com/avaje/ebeaninternal/server/expression/SimpleExpression.java index ae771a497..dee7b0d04 100644 --- a/src/main/java/com/avaje/ebeaninternal/server/expression/SimpleExpression.java +++ b/src/main/java/com/avaje/ebeaninternal/server/expression/SimpleExpression.java @@ -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; diff --git a/src/main/java/com/avaje/ebeaninternal/server/expression/TextMatchExpression.java b/src/main/java/com/avaje/ebeaninternal/server/expression/TextMatchExpression.java new file mode 100644 index 000000000..47dc9e6f2 --- /dev/null +++ b/src/main/java/com/avaje/ebeaninternal/server/expression/TextMatchExpression.java @@ -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); + } +} diff --git a/src/main/java/com/avaje/ebeaninternal/server/querydefn/DefaultOrmQuery.java b/src/main/java/com/avaje/ebeaninternal/server/querydefn/DefaultOrmQuery.java index 16c196532..2c10120c6 100644 --- a/src/main/java/com/avaje/ebeaninternal/server/querydefn/DefaultOrmQuery.java +++ b/src/main/java/com/avaje/ebeaninternal/server/querydefn/DefaultOrmQuery.java @@ -158,6 +158,8 @@ public class DefaultOrmQuery implements SpiQuery { */ private BindParams bindParams; + private DefaultExpressionList textExpressions; + private DefaultExpressionList whereExpressions; private DefaultExpressionList havingExpressions; @@ -1313,6 +1315,15 @@ public class DefaultOrmQuery implements SpiQuery { return this; } + @Override + public TextExpressionList text() { + if (textExpressions == null) { + useDocStore = true; + textExpressions = new DefaultExpressionList(this); + } + return textExpressions; + } + public ExpressionList where() { if (whereExpressions == null) { whereExpressions = new DefaultExpressionList(this, null); @@ -1352,6 +1363,11 @@ public class DefaultOrmQuery implements SpiQuery { return whereExpressions; } + @Override + public SpiExpressionList getTextExpression() { + return textExpressions; + } + public String getGeneratedSql() { return generatedSql; } diff --git a/src/test/java/com/avaje/ebean/TextExpressionListTest.java b/src/test/java/com/avaje/ebean/TextExpressionListTest.java new file mode 100644 index 000000000..41a9770e1 --- /dev/null +++ b/src/test/java/com/avaje/ebean/TextExpressionListTest.java @@ -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); + + + } +} \ No newline at end of file diff --git a/src/test/java/com/avaje/ebean/search/MatchTest.java b/src/test/java/com/avaje/ebean/search/MatchTest.java new file mode 100644 index 000000000..7906ef3a1 --- /dev/null +++ b/src/test/java/com/avaje/ebean/search/MatchTest.java @@ -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()); + } + +} \ No newline at end of file