From 9a69fb3045a3f007fb977d419eeefb0e1495a7e6 Mon Sep 17 00:00:00 2001 From: M Sazzadul Hoque <7600764+sazzad16@users.noreply.github.com> Date: Tue, 20 Sep 2022 11:17:31 +0600 Subject: [PATCH] Re-work FT.SEARCH (#3138) * Add FTSearchParams and SortingOrder * Add FTAggregateParams (w/o GroupBy, Reduce) * Remove FTAggregateParams Jedis params pattern does not comply with FT.AGGREGATE arguments. * format SearchWithParamsTest * changes * LazyRawable changes * fixes * fix test * edit * Payload related arguments are deprecated --- .../redis/clients/jedis/CommandObjects.java | 10 + .../clients/jedis/MultiNodePipelineBase.java | 10 + .../java/redis/clients/jedis/Pipeline.java | 10 + .../redis/clients/jedis/TransactionBase.java | 10 + .../redis/clients/jedis/UnifiedJedis.java | 10 + .../clients/jedis/search/FTCreateParams.java | 14 - .../clients/jedis/search/FTSearchParams.java | 589 +++++++++++++++++ .../clients/jedis/search/IndexDefinition.java | 4 + .../clients/jedis/search/LazyRawable.java | 17 + .../redis/clients/jedis/search/Query.java | 28 +- .../jedis/search/RediSearchCommands.java | 8 + .../search/RediSearchPipelineCommands.java | 8 + .../clients/jedis/search/SearchProtocol.java | 16 +- .../jedis/search/aggr/AggregationResult.java | 11 +- .../jedis/search/schemafields/TagField.java | 8 +- .../redis/clients/jedis/util/KeyValue.java | 4 + .../search/AggregationBuilderTest.java | 1 - .../jedis/modules/search/SearchTest.java | 17 +- .../modules/search/SearchWithParamsTest.java | 625 +++++++++++++++--- 19 files changed, 1258 insertions(+), 142 deletions(-) create mode 100644 src/main/java/redis/clients/jedis/search/FTSearchParams.java create mode 100644 src/main/java/redis/clients/jedis/search/LazyRawable.java diff --git a/src/main/java/redis/clients/jedis/CommandObjects.java b/src/main/java/redis/clients/jedis/CommandObjects.java index 79b797b517..807fc8227d 100644 --- a/src/main/java/redis/clients/jedis/CommandObjects.java +++ b/src/main/java/redis/clients/jedis/CommandObjects.java @@ -3089,6 +3089,16 @@ public CommandObject ftAlter(String indexName, Iterable sch return new CommandObject<>(args, BuilderFactory.STRING); } + public CommandObject ftSearch(String indexName, String query) { + return new CommandObject<>(commandArguments(SearchCommand.SEARCH).add(indexName).add(query), + new SearchResultBuilder(true, false, false, true)); + } + + public CommandObject ftSearch(String indexName, String query, FTSearchParams params) { + return new CommandObject<>(commandArguments(SearchCommand.SEARCH).add(indexName).add(query).addParams(params), + new SearchResultBuilder(!params.getNoContent(), params.getWithScores(), false, true)); + } + public CommandObject ftSearch(String indexName, Query query) { return new CommandObject<>(commandArguments(SearchCommand.SEARCH).add(indexName).addParams(query), new SearchResultBuilder(!query.getNoContent(), query.getWithScores(), query.getWithPayloads(), true)); diff --git a/src/main/java/redis/clients/jedis/MultiNodePipelineBase.java b/src/main/java/redis/clients/jedis/MultiNodePipelineBase.java index e0b855f8e2..aa3c429a5a 100644 --- a/src/main/java/redis/clients/jedis/MultiNodePipelineBase.java +++ b/src/main/java/redis/clients/jedis/MultiNodePipelineBase.java @@ -3357,6 +3357,16 @@ public Response ftAlter(String indexName, Iterable schemaFi return appendCommand(commandObjects.ftAlter(indexName, schemaFields)); } + @Override + public Response ftSearch(String indexName, String query) { + return appendCommand(commandObjects.ftSearch(indexName, query)); + } + + @Override + public Response ftSearch(String indexName, String query, FTSearchParams searchParams) { + return appendCommand(commandObjects.ftSearch(indexName, query, searchParams)); + } + @Override public Response ftSearch(String indexName, Query query) { return appendCommand(commandObjects.ftSearch(indexName, query)); diff --git a/src/main/java/redis/clients/jedis/Pipeline.java b/src/main/java/redis/clients/jedis/Pipeline.java index c8b0e3affe..a92b004901 100644 --- a/src/main/java/redis/clients/jedis/Pipeline.java +++ b/src/main/java/redis/clients/jedis/Pipeline.java @@ -3336,6 +3336,16 @@ public Response ftAlter(String indexName, Iterable schemaFi return appendCommand(commandObjects.ftAlter(indexName, schemaFields)); } + @Override + public Response ftSearch(String indexName, String query) { + return appendCommand(commandObjects.ftSearch(indexName, query)); + } + + @Override + public Response ftSearch(String indexName, String query, FTSearchParams searchParams) { + return appendCommand(commandObjects.ftSearch(indexName, query, searchParams)); + } + @Override public Response ftSearch(String indexName, Query query) { return appendCommand(commandObjects.ftSearch(indexName, query)); diff --git a/src/main/java/redis/clients/jedis/TransactionBase.java b/src/main/java/redis/clients/jedis/TransactionBase.java index 91196e3d89..61cefb3ec7 100644 --- a/src/main/java/redis/clients/jedis/TransactionBase.java +++ b/src/main/java/redis/clients/jedis/TransactionBase.java @@ -3403,6 +3403,16 @@ public Response ftAlter(String indexName, Iterable schemaFi return appendCommand(commandObjects.ftAlter(indexName, schemaFields)); } + @Override + public Response ftSearch(String indexName, String query) { + return appendCommand(commandObjects.ftSearch(indexName, query)); + } + + @Override + public Response ftSearch(String indexName, String query, FTSearchParams searchParams) { + return appendCommand(commandObjects.ftSearch(indexName, query, searchParams)); + } + @Override public Response ftSearch(String indexName, Query query) { return appendCommand(commandObjects.ftSearch(indexName, query)); diff --git a/src/main/java/redis/clients/jedis/UnifiedJedis.java b/src/main/java/redis/clients/jedis/UnifiedJedis.java index 784c6724d8..d175bcd2f2 100644 --- a/src/main/java/redis/clients/jedis/UnifiedJedis.java +++ b/src/main/java/redis/clients/jedis/UnifiedJedis.java @@ -3469,6 +3469,16 @@ public String ftAlter(String indexName, Iterable schemaFields) { return executeCommand(commandObjects.ftAlter(indexName, schemaFields)); } + @Override + public SearchResult ftSearch(String indexName, String query) { + return executeCommand(commandObjects.ftSearch(indexName, query)); + } + + @Override + public SearchResult ftSearch(String indexName, String query, FTSearchParams params) { + return executeCommand(commandObjects.ftSearch(indexName, query, params)); + } + @Override public SearchResult ftSearch(String indexName, Query query) { return executeCommand(commandObjects.ftSearch(indexName, query)); diff --git a/src/main/java/redis/clients/jedis/search/FTCreateParams.java b/src/main/java/redis/clients/jedis/search/FTCreateParams.java index 8707272960..5ea17e7ab0 100644 --- a/src/main/java/redis/clients/jedis/search/FTCreateParams.java +++ b/src/main/java/redis/clients/jedis/search/FTCreateParams.java @@ -19,7 +19,6 @@ public class FTCreateParams implements IParams { private String languageField; private Double score; private String scoreField; - private byte[] payloadField; private boolean maxTextFields; private boolean noOffsets; private Long temporary; @@ -110,15 +109,6 @@ public FTCreateParams scoreField(String scoreField) { return this; } - /** - * Document attribute that you use as a binary safe payload string to the document that can be - * evaluated at query time by a custom scoring function or retrieved to the client. - */ - public FTCreateParams payloadField(byte[] payloadAttribute) { - this.payloadField = Arrays.copyOf(payloadAttribute, payloadAttribute.length); - return this; - } - /** * Forces RediSearch to encode indexes as if there were more than 32 text attributes. */ @@ -230,10 +220,6 @@ public void addParams(CommandArguments args) { args.add(SCORE_FIELD).add(scoreField); } - if (payloadField != null) { - args.add(PAYLOAD_FIELD).add(payloadField); - } - if (maxTextFields) { args.add(MAXTEXTFIELDS); } diff --git a/src/main/java/redis/clients/jedis/search/FTSearchParams.java b/src/main/java/redis/clients/jedis/search/FTSearchParams.java new file mode 100644 index 0000000000..e88926e1da --- /dev/null +++ b/src/main/java/redis/clients/jedis/search/FTSearchParams.java @@ -0,0 +1,589 @@ +package redis.clients.jedis.search; + +import static redis.clients.jedis.search.SearchProtocol.SearchKeyword.*; + +import java.util.*; + +import redis.clients.jedis.CommandArguments; +import redis.clients.jedis.Protocol; +import redis.clients.jedis.args.GeoUnit; +import redis.clients.jedis.args.SortingOrder; +import redis.clients.jedis.params.IParams; + +/** + * Query represents query parameters and filters to load results from the engine + */ +public class FTSearchParams implements IParams { + + private boolean noContent = false; + private boolean verbatim = false; + private boolean noStopwords = false; + private boolean withScores = false; + private final List filters = new LinkedList<>(); + private Collection inKeys; + private Collection inFields; + private Collection returnFields; + private Collection returnFieldNames; + private boolean summarize; + private SummarizeParams summarizeParams; + private boolean highlight; + private HighlightParams highlightParams; + private Integer slop; + private Long timeout; + private boolean inOrder; + private String language; + private String expander; + private String scorer; + // private boolean explainScore; // TODO + private String sortBy; + private SortingOrder sortOrder; + private int[] limit; + private Map params; + private Integer dialect; + + public FTSearchParams() { + } + + public static FTSearchParams searchParams() { + return new FTSearchParams(); + } + + @Override + public void addParams(CommandArguments args) { + + if (noContent) { + args.add(NOCONTENT); + } + if (verbatim) { + args.add(VERBATIM); + } + if (noStopwords) { + args.add(NOSTOPWORDS); + } + if (withScores) { + args.add(WITHSCORES); + } + + if (!filters.isEmpty()) { + filters.forEach(filter -> filter.addParams(args)); + } + + if (inKeys != null && !inKeys.isEmpty()) { + args.add(INKEYS).add(inKeys.size()).addObjects(inKeys); + } + + if (inFields != null && !inFields.isEmpty()) { + args.add(INFIELDS).add(inFields.size()).addObjects(inFields); + } + + if (returnFieldNames != null && !returnFieldNames.isEmpty()) { + args.add(RETURN); + LazyRawable returnCountObject = new LazyRawable(); + args.add(returnCountObject); // holding a place for setting the total count later. + int returnCount = 0; + for (FieldName fn : returnFieldNames) { + returnCount += fn.addCommandArguments(args); + } + returnCountObject.setRaw(Protocol.toByteArray(returnCount)); + } else if (returnFields != null && !returnFields.isEmpty()) { + args.add(RETURN).add(returnFields.size()).addObjects(returnFields); + } + + if (summarizeParams != null) { + args.addParams(summarizeParams); + } else if (summarize) { + args.add(SUMMARIZE); + } + + if (highlightParams != null) { + args.addParams(highlightParams); + } else if (highlight) { + args.add(HIGHLIGHT); + } + + if (slop != null) { + args.add(SLOP).add(slop); + } + + if (timeout != null) { + args.add(TIMEOUT).add(timeout); + } + + if (inOrder) { + args.add(INORDER); + } + + if (language != null) { + args.add(LANGUAGE).add(language); + } + + if (expander != null) { + args.add(EXPANDER).add(expander); + } + + if (scorer != null) { + args.add(SCORER).add(scorer); + } +// +// if (explainScore) { +// args.add(EXPLAINSCORE); +// } + + if (sortBy != null) { + args.add(SORTBY).add(sortBy); + if (sortOrder != null) { + args.add(sortOrder); + } + } + + if (limit != null) { + args.add(LIMIT).add(limit[0]).add(limit[1]); + } + + if (params != null && !params.isEmpty()) { + args.add(PARAMS).add(params.size() * 2); + params.entrySet().forEach(entry -> args.add(entry.getKey()).add(entry.getValue())); + } + + if (dialect != null) { + args.add(DIALECT).add(dialect); + } + } + + /** + * Set the query not to return the contents of documents, and rather just return the ids + * + * @return the query itself + */ + public FTSearchParams noContent() { + this.noContent = true; + return this; + } + + /** + * Set the query to verbatim mode, disabling stemming and query expansion + * + * @return the query object + */ + public FTSearchParams verbatim() { + this.verbatim = true; + return this; + } + + /** + * Set the query not to filter for stopwords. In general this should not be used + * + * @return the query object + */ + public FTSearchParams noStopwords() { + this.noStopwords = true; + return this; + } + + /** + * Set the query to return a factored score for each results. This is useful to merge results from + * multiple queries. + * + * @return the query object itself + */ + public FTSearchParams withScores() { + this.withScores = true; + return this; + } + + public FTSearchParams filter(String field, double min, double max) { + return filter(new NumericFilter(field, min, max)); + } + + public FTSearchParams filter(String field, double min, boolean exclusiveMin, double max, boolean exclusiveMax) { + return filter(new NumericFilter(field, min, exclusiveMin, max, exclusiveMax)); + } + + public FTSearchParams filter(NumericFilter numericFilter) { + filters.add(numericFilter); + return this; + } + + public FTSearchParams geoFilter(String field, double lon, double lat, double radius, GeoUnit unit) { + return geoFilter(new GeoFilter(field, lon, lat, radius, unit)); + } + + public FTSearchParams geoFilter(GeoFilter geoFilter) { + filters.add(geoFilter); + return this; + } + + /** + * Limit the query to results that are limited to a specific set of keys + * + * @param keys a list of TEXT fields in the schemas + * @return the query object itself + */ + public FTSearchParams inKeys(String... keys) { + return inKeys(Arrays.asList(keys)); + } + + public FTSearchParams inKeys(Collection keys) { + this.inKeys = keys; + return this; + } + + /** + * Limit the query to results that are limited to a specific set of fields + * + * @param fields a list of TEXT fields in the schemas + * @return the query object itself + */ + public FTSearchParams inFields(String... fields) { + return inFields(Arrays.asList(fields)); + } + + public FTSearchParams inFields(Collection fields) { + if (this.inFields == null) { + this.inFields = new ArrayList<>(fields); + } else { + this.inFields.addAll(fields); + } + return this; + } + + /** + * Result's projection - the fields to return by the query + * + * @param fields a list of TEXT fields in the schemas + * @return the query object itself + */ + public FTSearchParams returnFields(String... fields) { + if (returnFieldNames != null) { + Arrays.stream(fields).forEach(f -> returnFieldNames.add(FieldName.of(f))); + } else { + if (returnFields == null) { + returnFields = new ArrayList<>(); + } + Arrays.stream(fields).forEach(f -> returnFields.add(f)); + } + return this; + } + + public FTSearchParams returnField(FieldName field) { + initReturnFieldNames(); + returnFieldNames.add(field); + return this; + } + + public FTSearchParams returnFields(FieldName... fields) { + return returnFields(Arrays.asList(fields)); + } + + public FTSearchParams returnFields(Collection fields) { + initReturnFieldNames(); + returnFieldNames.addAll(fields); + return this; + } + + private void initReturnFieldNames() { + if (returnFieldNames == null) { + returnFieldNames = new ArrayList<>(); + } + if (returnFields != null) { + returnFields.forEach(f -> returnFieldNames.add(FieldName.of(f))); + returnFields = null; + } + } + + public FTSearchParams summarize() { + this.summarize = true; + return this; + } + + public FTSearchParams summarize(SummarizeParams summarizeParams) { + this.summarizeParams = summarizeParams; + return this; + } + + public FTSearchParams highlight() { + this.highlight = true; + return this; + } + + public FTSearchParams highlight(HighlightParams highlightParams) { + this.highlightParams = highlightParams; + return this; + } + + /** + * Set the query custom scorer + *

+ * See http://redisearch.io for documentation on extending RediSearch + * + * @param scorer a custom scorer. + * + * @return the query object itself + */ + public FTSearchParams scorer(String scorer) { + this.scorer = scorer; + return this; + } +// +// public FTSearchParams explainScore() { +// this.explainScore = true; +// return this; +// } + + public FTSearchParams slop(int slop) { + this.slop = slop; + return this; + } + + public FTSearchParams timeout(long timeout) { + this.timeout = timeout; + return this; + } + + public FTSearchParams inOrder() { + this.inOrder = true; + return this; + } + + /** + * Set the query language, for stemming purposes + *

+ * See http://redisearch.io for documentation on languages and stemming + * + * @param language a language. + * + * @return the query object itself + */ + public FTSearchParams language(String language) { + this.language = language; + return this; + } + + /** + * Set the query to be sorted by a Sortable field defined in the schema + * + * @param sortBy the sorting field's name + * @param order the sorting order + * @return the query object itself + */ + public FTSearchParams sortBy(String sortBy, SortingOrder order) { + this.sortBy = sortBy; + this.sortOrder = order; + return this; + } + + /** + * Limit the results to a certain offset and limit + * + * @param offset the first result to show, zero based indexing + * @param num how many results we want to show + * @return the query itself, for builder-style syntax + */ + public FTSearchParams limit(int offset, int num) { + this.limit = new int[]{offset, num}; + return this; + } + + /** + * Parameters can be referenced in the query string by a $ , followed by the parameter name, + * e.g., $user , and each such reference in the search query to a parameter name is substituted + * by the corresponding parameter value. + * + * @param name + * @param value can be String, long or float + * @return the query object itself + */ + public FTSearchParams addParam(String name, Object value) { + if (params == null) { + params = new HashMap<>(); + } + params.put(name, value); + return this; + } + + public FTSearchParams params(Map paramValues) { + if (this.params == null) { + this.params = new HashMap<>(paramValues); + } else { + this.params.putAll(params); + } + return this; + } + + /** + * Set the dialect version to execute the query accordingly + * + * @param dialect integer + * @return the query object itself + */ + public FTSearchParams dialect(int dialect) { + this.dialect = dialect; + return this; + } + + public boolean getNoContent() { + return noContent; + } + + public boolean getWithScores() { + return withScores; + } + + /** + * NumericFilter wraps a range filter on a numeric field. It can be inclusive or exclusive + */ + public static class NumericFilter implements IParams { + + private final String field; + private final double min; + private final boolean exclusiveMin; + private final double max; + private final boolean exclusiveMax; + + public NumericFilter(String field, double min, double max) { + this(field, min, false, max, false); + } + + public NumericFilter(String field, double min, boolean exclusiveMin, double max, boolean exclusiveMax) { + this.field = field; + this.min = min; + this.max = max; + this.exclusiveMax = exclusiveMax; + this.exclusiveMin = exclusiveMin; + } + + @Override + public void addParams(CommandArguments args) { + args.add(FILTER).add(field) + .add(formatNum(min, exclusiveMin)) + .add(formatNum(max, exclusiveMax)); + } + + private Object formatNum(double num, boolean exclude) { + return exclude ? ("(" + num) : Protocol.toByteArray(num); + } + } + + /** + * GeoFilter encapsulates a radius filter on a geographical indexed fields + */ + public static class GeoFilter implements IParams { + + private final String field; + private final double lon; + private final double lat; + private final double radius; + private final GeoUnit unit; + + public GeoFilter(String field, double lon, double lat, double radius, GeoUnit unit) { + this.field = field; + this.lon = lon; + this.lat = lat; + this.radius = radius; + this.unit = unit; + } + + @Override + public void addParams(CommandArguments args) { + args.add(GEOFILTER).add(field) + .add(lon).add(lat) + .add(radius).add(unit); + } + } + + public static class SummarizeParams implements IParams { + + private Collection fields; + private Integer fragsNum; + private Integer fragSize; + private String separator; + + public SummarizeParams() { + } + + public SummarizeParams fields(String... fields) { + return fields(Arrays.asList(fields)); + } + + public SummarizeParams fields(Collection fields) { + this.fields = fields; + return this; + } + + public SummarizeParams fragsNum(int num) { + this.fragsNum = num; + return this; + } + + public SummarizeParams fragSize(int size) { + this.fragSize = size; + return this; + } + + public SummarizeParams separator(String separator) { + this.separator = separator; + return this; + } + + @Override + public void addParams(CommandArguments args) { + args.add(SUMMARIZE); + + if (fields != null) { + args.add(FIELDS).add(fields.size()).addObjects(fields); + } + if (fragsNum != null) { + args.add(FRAGS).add(fragsNum); + } + if (fragSize != null) { + args.add(LEN).add(fragSize); + } + if (separator != null) { + args.add(SEPARATOR).add(separator); + } + } + } + + public static SummarizeParams summarizeParams() { + return new SummarizeParams(); + } + + public static class HighlightParams implements IParams { + + private Collection fields; + private String[] tags; + + public HighlightParams() { + } + + public HighlightParams fields(String fields) { + return fields(Arrays.asList(fields)); + } + + public HighlightParams fields(Collection fields) { + this.fields = fields; + return this; + } + + public HighlightParams tags(String open, String close) { + this.tags = new String[]{open, close}; + return this; + } + + @Override + public void addParams(CommandArguments args) { + args.add(HIGHLIGHT); + + if (fields != null) { + args.add(FIELDS).add(fields.size()).addObjects(fields); + } + if (tags != null) { + args.add(TAGS).add(tags[0]).add(tags[1]); + } + } + } + + public static HighlightParams highlightParams() { + return new HighlightParams(); + } +} diff --git a/src/main/java/redis/clients/jedis/search/IndexDefinition.java b/src/main/java/redis/clients/jedis/search/IndexDefinition.java index 73a4f6ae8e..c652453da5 100644 --- a/src/main/java/redis/clients/jedis/search/IndexDefinition.java +++ b/src/main/java/redis/clients/jedis/search/IndexDefinition.java @@ -104,6 +104,10 @@ public String getPayloadField() { return payloadField; } + /** + * @deprecated Since RediSearch 2.0.0, PAYLOAD_FIELD option is deprecated. + */ + @Deprecated public IndexDefinition setPayloadField(String payloadField) { this.payloadField = payloadField; return this; diff --git a/src/main/java/redis/clients/jedis/search/LazyRawable.java b/src/main/java/redis/clients/jedis/search/LazyRawable.java new file mode 100644 index 0000000000..bc1a346800 --- /dev/null +++ b/src/main/java/redis/clients/jedis/search/LazyRawable.java @@ -0,0 +1,17 @@ +package redis.clients.jedis.search; + +import redis.clients.jedis.args.Rawable; + +class LazyRawable implements Rawable { + + private byte[] raw = null; + + public void setRaw(byte[] raw) { + this.raw = raw; + } + + @Override + public byte[] getRaw() { + return raw; + } +} diff --git a/src/main/java/redis/clients/jedis/search/Query.java b/src/main/java/redis/clients/jedis/search/Query.java index ec9e929967..06b2339523 100644 --- a/src/main/java/redis/clients/jedis/search/Query.java +++ b/src/main/java/redis/clients/jedis/search/Query.java @@ -7,7 +7,6 @@ import redis.clients.jedis.CommandArguments; import redis.clients.jedis.Protocol; -import redis.clients.jedis.args.Rawable; import redis.clients.jedis.params.IParams; import redis.clients.jedis.search.SearchProtocol.SearchKeyword; import redis.clients.jedis.util.SafeEncoder; @@ -289,7 +288,7 @@ public void addParams(CommandArguments args) { } else if (returnFieldNames != null && returnFieldNames.length > 0) { args.add(SearchKeyword.RETURN.getRaw()); // final int returnCountIndex = args.size(); - DelayedRawable returnCountObject = new DelayedRawable(); + LazyRawable returnCountObject = new LazyRawable(); // args.add(null); // holding a place for setting the total count later. args.add(returnCountObject); // holding a place for setting the total count later. int returnCount = 0; @@ -334,20 +333,6 @@ public void addParams(CommandArguments args) { } } - private static class DelayedRawable implements Rawable { - - private byte[] raw = null; - - public void setRaw(byte[] raw) { - this.raw = raw; - } - - @Override - public byte[] getRaw() { - return raw; - } - } - /** * Limit the results to a certain offset and limit * @@ -372,7 +357,13 @@ public Query addFilter(Filter f) { return this; } - /* Set the query payload to be evaluated by the scoring function */ + /** + * Set the query payload to be evaluated by the scoring function + * + * @return the query object itself + * @deprecated Since RediSearch 2.0.0, PAYLOAD option is deprecated. + */ + @Deprecated public Query setPayload(byte[] payload) { _payload = payload; return this; @@ -435,8 +426,9 @@ public boolean getWithPayloads() { * Set the query to return object payloads, if any were given * * @return the query object itself - * + * @deprecated Since RediSearch 2.0.0, WITHPAYLOADS option is deprecated. */ + @Deprecated public Query setWithPayload() { this._withPayloads = true; return this; diff --git a/src/main/java/redis/clients/jedis/search/RediSearchCommands.java b/src/main/java/redis/clients/jedis/search/RediSearchCommands.java index cf22fbdbfb..8b785ca811 100644 --- a/src/main/java/redis/clients/jedis/search/RediSearchCommands.java +++ b/src/main/java/redis/clients/jedis/search/RediSearchCommands.java @@ -40,6 +40,14 @@ default String ftAlter(String indexName, SchemaField... schemaFields) { String ftAlter(String indexName, Iterable schemaFields); + default SearchResult ftSearch(String indexName) { + return ftSearch(indexName, "*"); + } + + SearchResult ftSearch(String indexName, String query); + + SearchResult ftSearch(String indexName, String query, FTSearchParams params); + SearchResult ftSearch(String indexName, Query query); SearchResult ftSearch(byte[] indexName, Query query); diff --git a/src/main/java/redis/clients/jedis/search/RediSearchPipelineCommands.java b/src/main/java/redis/clients/jedis/search/RediSearchPipelineCommands.java index 2339fb3acc..355cb01efb 100644 --- a/src/main/java/redis/clients/jedis/search/RediSearchPipelineCommands.java +++ b/src/main/java/redis/clients/jedis/search/RediSearchPipelineCommands.java @@ -41,6 +41,14 @@ default Response ftAlter(String indexName, SchemaField... schemaFields) Response ftAlter(String indexName, Iterable schemaFields); + default Response ftSearch(String indexName) { + return ftSearch(indexName, "*"); + } + + Response ftSearch(String indexName, String query); + + Response ftSearch(String indexName, String query, FTSearchParams searchParams); + Response ftSearch(String indexName, Query query); Response ftSearch(byte[] indexName, Query query); diff --git a/src/main/java/redis/clients/jedis/search/SearchProtocol.java b/src/main/java/redis/clients/jedis/search/SearchProtocol.java index 8754b9afc8..bc05582e6d 100644 --- a/src/main/java/redis/clients/jedis/search/SearchProtocol.java +++ b/src/main/java/redis/clients/jedis/search/SearchProtocol.java @@ -46,13 +46,15 @@ public byte[] getRaw() { public enum SearchKeyword implements Rawable { - SCHEMA, TEXT, TAG, NUMERIC, GEO, VECTOR, VERBATIM, NOCONTENT, NOSTOPWORDS, WITHSCORES, - WITHPAYLOADS, LANGUAGE, INFIELDS, SORTBY, ASC, DESC, PAYLOAD, LIMIT, HIGHLIGHT, FIELDS, TAGS, - SUMMARIZE, FRAGS, LEN, SEPARATOR, INKEYS, RETURN, FILTER, GEOFILTER, INCR, MAX, FUZZY, DD, DEL, - READ, COUNT, ADD, TEMPORARY, STOPWORDS, NOFREQS, NOFIELDS, NOOFFSETS, NOHL, SET, GET, ON, - @Deprecated ASYNC, PREFIX, LANGUAGE_FIELD, SCORE, SCORE_FIELD, SCORER, PAYLOAD_FIELD, PARAMS, - DIALECT, SLOP, TIMEOUT, INORDER, EXPANDER, MAXTEXTFIELDS, SKIPINITIALSCAN, SORTABLE, UNF, - NOSTEM, NOINDEX, PHONETIC, WEIGHT, CASESENSITIVE, WITHSUFFIXTRIE; + SCHEMA, TEXT, TAG, NUMERIC, GEO, VECTOR, VERBATIM, NOCONTENT, NOSTOPWORDS, WITHSCORES, LANGUAGE, + INFIELDS, SORTBY, ASC, DESC, LIMIT, HIGHLIGHT, FIELDS, TAGS, SUMMARIZE, FRAGS, LEN, SEPARATOR, + INKEYS, RETURN, FILTER, GEOFILTER, ADD, INCR, MAX, FUZZY, READ, DEL, DD, TEMPORARY, STOPWORDS, + NOFREQS, NOFIELDS, NOOFFSETS, NOHL, SET, GET, ON, SORTABLE, UNF, PREFIX, LANGUAGE_FIELD, SCORE, + SCORE_FIELD, SCORER, PARAMS, AS, DIALECT, SLOP, TIMEOUT, INORDER, EXPANDER, MAXTEXTFIELDS, + SKIPINITIALSCAN, WITHSUFFIXTRIE, NOSTEM, NOINDEX, PHONETIC, WEIGHT, CASESENSITIVE, /*EXPLAINSCORE,*/ + LOAD, APPLY, GROUPBY, MAXIDLE, WITHCURSOR, + @Deprecated ASYNC, @Deprecated PAYLOAD_FIELD, @Deprecated WITHPAYLOADS, @Deprecated PAYLOAD, + @Deprecated COUNT; private final byte[] raw; diff --git a/src/main/java/redis/clients/jedis/search/aggr/AggregationResult.java b/src/main/java/redis/clients/jedis/search/aggr/AggregationResult.java index cda4d6a4a1..a7541cfc3e 100644 --- a/src/main/java/redis/clients/jedis/search/aggr/AggregationResult.java +++ b/src/main/java/redis/clients/jedis/search/aggr/AggregationResult.java @@ -12,11 +12,16 @@ */ public class AggregationResult { + /** + * @deprecated Use {@link AggregationResult#getTotalResults()}. + */ + @Deprecated public final long totalResults; - private long cursorId = -1; private final List> results = new ArrayList<>(); + private long cursorId = -1; + public AggregationResult(Object resp, long cursorId) { this(resp); this.cursorId = cursorId; @@ -41,6 +46,10 @@ public AggregationResult(Object resp) { } } + public long getTotalResults() { + return totalResults; + } + public List> getResults() { return results; } diff --git a/src/main/java/redis/clients/jedis/search/schemafields/TagField.java b/src/main/java/redis/clients/jedis/search/schemafields/TagField.java index bf0e9361e7..044f9d75f6 100644 --- a/src/main/java/redis/clients/jedis/search/schemafields/TagField.java +++ b/src/main/java/redis/clients/jedis/search/schemafields/TagField.java @@ -102,6 +102,10 @@ public void addParams(CommandArguments args) { args.addParams(fieldName); args.add(TAG); + if (separator != null) { + args.add(SEPARATOR).add(separator); + } + if (sortableUNF) { args.add(SORTABLE).add(UNF); } else if (sortable) { @@ -112,10 +116,6 @@ public void addParams(CommandArguments args) { args.add(NOINDEX); } - if (separator != null) { - args.add(SEPARATOR).add(separator); - } - if (caseSensitive) { args.add(CASESENSITIVE); } diff --git a/src/main/java/redis/clients/jedis/util/KeyValue.java b/src/main/java/redis/clients/jedis/util/KeyValue.java index 27561b46fc..8ff7bffe2c 100644 --- a/src/main/java/redis/clients/jedis/util/KeyValue.java +++ b/src/main/java/redis/clients/jedis/util/KeyValue.java @@ -7,4 +7,8 @@ public class KeyValue extends SimpleImmutableEntry { public KeyValue(K key, V value) { super(key, value); } + + public static KeyValue of(K key, V value) { + return new KeyValue<>(key, value); + } } diff --git a/src/test/java/redis/clients/jedis/modules/search/AggregationBuilderTest.java b/src/test/java/redis/clients/jedis/modules/search/AggregationBuilderTest.java index f8a9ad99b3..ed324959a7 100644 --- a/src/test/java/redis/clients/jedis/modules/search/AggregationBuilderTest.java +++ b/src/test/java/redis/clients/jedis/modules/search/AggregationBuilderTest.java @@ -217,7 +217,6 @@ public void loadAll() { sc.addSortableNumericField("subj1"); sc.addSortableNumericField("subj2"); client.ftCreate(index, IndexOptions.defaultOptions(), sc); - addDocument(new Document("data1").set("name", "abc").set("subj1", 20).set("subj2", 70)); addDocument(new Document("data2").set("name", "def").set("subj1", 60).set("subj2", 40)); diff --git a/src/test/java/redis/clients/jedis/modules/search/SearchTest.java b/src/test/java/redis/clients/jedis/modules/search/SearchTest.java index a0fe340001..b260299bb6 100644 --- a/src/test/java/redis/clients/jedis/modules/search/SearchTest.java +++ b/src/test/java/redis/clients/jedis/modules/search/SearchTest.java @@ -141,7 +141,6 @@ public void alterAdd() throws Exception { for (int i = 0; i < 100; i++) { addDocument(String.format("doc%d", i), fields); } - SearchResult res = client.ftSearch(index, new Query("hello world")); assertEquals(100, res.getTotalResults()); @@ -330,6 +329,7 @@ public void geoFilterAndGeoCoordinateObject() throws Exception { fields.put("loc", new redis.clients.jedis.GeoCoordinate(-0.441, 51.458)); // assertTrue(client.addDocument("doc1", fields)); addDocument("doc1", fields); + fields.put("loc", new redis.clients.jedis.GeoCoordinate(-0.1, 51.2)); // assertTrue(client.addDocument("doc2", fields)); addDocument("doc2", fields); @@ -569,15 +569,6 @@ public void dropIndexDD() throws Exception { assertEquals(0, client.dbSize()); } - @Test - public void testDropMissing() throws Exception { - try { - client.ftDropIndex(index); - fail(); - } catch (JedisDataException ex) { - } - } - @Test public void noStem() throws Exception { Schema sc = new Schema().addTextField("stemmed", 1.0).addField(new Schema.TextField("notStemmed", 1.0, false, true)); @@ -651,7 +642,6 @@ public void info() throws Exception { Map info = client.ftInfo(index); assertEquals(index, info.get("index_name")); - assertEquals(6, ((List) info.get("attributes")).size()); assertEquals("global_idle", ((List) info.get("cursor_stats")).get(0)); assertEquals(0L, ((List) info.get("cursor_stats")).get(1)); @@ -730,7 +720,6 @@ public void testHighlightSummarize() throws Exception { assertEquals("is often referred as a data structures server. What this means is that Redis provides... What this means is that Redis provides access to mutable data structures via a set of commands, which are sent using a... So different processes can query and modify the same data structures in a shared... ", res.getDocuments().get(0).get("text")); - } @Test @@ -948,6 +937,7 @@ public void configOnTimeout() throws Exception { public void alias() throws Exception { Schema sc = new Schema().addTextField("field1", 1.0); assertEquals("OK", client.ftCreate(index, IndexOptions.defaultOptions(), sc)); + Map doc = new HashMap<>(); doc.put("field1", "value"); // assertTrue(client.addDocument("doc1", doc)); @@ -1023,7 +1013,6 @@ public void slop() { Map doc = new HashMap<>(); doc.put("field1", "ok hi jedis"); - addDocument("doc1", doc); SearchResult res = client.ftSearch(index, new Query("ok jedis").slop(0)); @@ -1043,7 +1032,6 @@ public void timeout() { Map doc = new HashMap<>(); doc.put("field1", "value"); doc.put("field2", "not"); - addDocument("doc1", doc); SearchResult res = client.ftSearch(index, new Query("value").timeout(1000)); @@ -1061,7 +1049,6 @@ public void inOrder() { Map doc = new HashMap<>(); doc.put("field1", "value"); doc.put("field2", "not"); - addDocument("doc2", doc); addDocument("doc1", doc); diff --git a/src/test/java/redis/clients/jedis/modules/search/SearchWithParamsTest.java b/src/test/java/redis/clients/jedis/modules/search/SearchWithParamsTest.java index 780104c790..635586e86b 100644 --- a/src/test/java/redis/clients/jedis/modules/search/SearchWithParamsTest.java +++ b/src/test/java/redis/clients/jedis/modules/search/SearchWithParamsTest.java @@ -7,7 +7,11 @@ import org.junit.BeforeClass; import org.junit.Test; +import redis.clients.jedis.GeoCoordinate; +import redis.clients.jedis.args.GeoUnit; +import redis.clients.jedis.args.SortingOrder; import redis.clients.jedis.exceptions.JedisDataException; +import redis.clients.jedis.json.Path; import redis.clients.jedis.search.*; import redis.clients.jedis.search.schemafields.*; import redis.clients.jedis.modules.RedisModuleCommandsTestBase; @@ -62,16 +66,16 @@ public void create() { client.hset("student:5555", toMap("first", "Joen", "last", "Ko", "age", "20")); client.hset("teacher:6666", toMap("first", "Pat", "last", "Rod", "age", "20")); - SearchResult noFilters = client.ftSearch(index, new Query()); + SearchResult noFilters = client.ftSearch(index); assertEquals(4, noFilters.getTotalResults()); - SearchResult res1 = client.ftSearch(index, new Query("@first:Jo*")); + SearchResult res1 = client.ftSearch(index, "@first:Jo*"); assertEquals(2, res1.getTotalResults()); - SearchResult res2 = client.ftSearch(index, new Query("@first:Pat")); + SearchResult res2 = client.ftSearch(index, "@first:Pat"); assertEquals(1, res2.getTotalResults()); - SearchResult res3 = client.ftSearch(index, new Query("@last:Rod")); + SearchResult res3 = client.ftSearch(index, "@last:Rod"); assertEquals(0, res3.getTotalResults()); } @@ -87,16 +91,16 @@ public void createNoParams() { addDocument("pupil:4444", toMap("first", "Pat", "last", "Shu", "age", 21)); addDocument("student:5555", toMap("first", "Joen", "last", "Ko", "age", 20)); - SearchResult noFilters = client.ftSearch(index, new Query()); + SearchResult noFilters = client.ftSearch(index); assertEquals(4, noFilters.getTotalResults()); - SearchResult res1 = client.ftSearch(index, new Query("@first:Jo*")); + SearchResult res1 = client.ftSearch(index, "@first:Jo*"); assertEquals(2, res1.getTotalResults()); - SearchResult res2 = client.ftSearch(index, new Query("@first:Pat")); + SearchResult res2 = client.ftSearch(index, "@first:Pat"); assertEquals(1, res2.getTotalResults()); - SearchResult res3 = client.ftSearch(index, new Query("@last:Rod")); + SearchResult res3 = client.ftSearch(index, "@last:Rod"); assertEquals(0, res3.getTotalResults()); } @@ -116,19 +120,19 @@ public void createWithFieldNames() { client.hset("student:5555", toMap("first", "Joen", "last", "Ko", "age", "20")); client.hset("teacher:6666", toMap("first", "Pat", "last", "Rod", "age", "20")); - SearchResult noFilters = client.ftSearch(index, new Query()); + SearchResult noFilters = client.ftSearch(index); assertEquals(5, noFilters.getTotalResults()); - SearchResult asFirst = client.ftSearch(index, new Query("@first:Jo*")); + SearchResult asFirst = client.ftSearch(index, "@first:Jo*"); assertEquals(0, asFirst.getTotalResults()); - SearchResult asGiven = client.ftSearch(index, new Query("@given:Jo*")); + SearchResult asGiven = client.ftSearch(index, "@given:Jo*"); assertEquals(2, asGiven.getTotalResults()); - SearchResult nonLast = client.ftSearch(index, new Query("@last:Rod")); + SearchResult nonLast = client.ftSearch(index, "@last:Rod"); assertEquals(0, nonLast.getTotalResults()); - SearchResult asFamily = client.ftSearch(index, new Query("@family:Rod")); + SearchResult asFamily = client.ftSearch(index, "@family:Rod"); assertEquals(1, asFamily.getTotalResults()); } @@ -141,7 +145,7 @@ public void alterAdd() { for (int i = 0; i < 100; i++) { addDocument(String.format("doc%d", i), fields); } - SearchResult res = client.ftSearch(index, new Query("hello world")); + SearchResult res = client.ftSearch(index, "hello world"); assertEquals(100, res.getTotalResults()); assertOK(client.ftAlter(index, @@ -154,7 +158,7 @@ public void alterAdd() { fields2.put("tags", String.format("tagA,tagB,tag%d", i)); addDocument(String.format("doc%d", i), fields2); } - SearchResult res2 = client.ftSearch(index, new Query("@tags:{tagA}")); + SearchResult res2 = client.ftSearch(index, "@tags:{tagA}"); assertEquals(100, res2.getTotalResults()); Map info = client.ftInfo(index); @@ -175,7 +179,8 @@ public void search() { addDocument(String.format("doc%d", i), fields); } - SearchResult res = client.ftSearch(index, new Query("hello world").limit(0, 5).setWithScores()); + SearchResult res = client.ftSearch(index, "hello world", + FTSearchParams.searchParams().limit(0, 5).withScores()); assertEquals(100, res.getTotalResults()); assertEquals(5, res.getDocuments().size()); for (Document d : res.getDocuments()) { @@ -185,12 +190,12 @@ public void search() { client.del("doc0"); - res = client.ftSearch(index, new Query("hello world")); + res = client.ftSearch(index, "hello world"); assertEquals(99, res.getTotalResults()); assertEquals("OK", client.ftDropIndex(index)); try { - client.ftSearch(index, new Query("hello world")); + client.ftSearch(index, "hello world"); fail(); } catch (JedisDataException e) { } @@ -208,8 +213,8 @@ public void numericFilter() { addDocument(String.format("doc%d", i), fields); } - SearchResult res = client.ftSearch(index, new Query("hello world"). - addFilter(new Query.NumericFilter("price", 0, 49))); + SearchResult res = client.ftSearch(index, "hello world", + FTSearchParams.searchParams().filter("price", 0, 49)); assertEquals(50, res.getTotalResults()); assertEquals(10, res.getDocuments().size()); for (Document d : res.getDocuments()) { @@ -218,8 +223,8 @@ public void numericFilter() { assertTrue(price <= 49); } - res = client.ftSearch(index, new Query("hello world"). - addFilter(new Query.NumericFilter("price", 0, true, 49, true))); + res = client.ftSearch(index, "hello world", + FTSearchParams.searchParams().filter("price", 0, true, 49, true)); assertEquals(48, res.getTotalResults()); assertEquals(10, res.getDocuments().size()); for (Document d : res.getDocuments()) { @@ -227,8 +232,8 @@ public void numericFilter() { assertTrue(price > 0); assertTrue(price < 49); } - res = client.ftSearch(index, new Query("hello world"). - addFilter(new Query.NumericFilter("price", 50, 100))); + res = client.ftSearch(index, "hello world", + FTSearchParams.searchParams().filter("price", 50, 100)); assertEquals(50, res.getTotalResults()); assertEquals(10, res.getDocuments().size()); for (Document d : res.getDocuments()) { @@ -237,13 +242,15 @@ public void numericFilter() { assertTrue(price <= 100); } - res = client.ftSearch(index, new Query("hello world"). - addFilter(new Query.NumericFilter("price", 20, Double.POSITIVE_INFINITY))); + res = client.ftSearch(index, "hello world", + FTSearchParams.searchParams() + .filter("price", 20, Double.POSITIVE_INFINITY)); assertEquals(80, res.getTotalResults()); assertEquals(10, res.getDocuments().size()); - res = client.ftSearch(index, new Query("hello world"). - addFilter(new Query.NumericFilter("price", Double.NEGATIVE_INFINITY, 10))); + res = client.ftSearch(index, "hello world", + FTSearchParams.searchParams() + .filter("price", Double.NEGATIVE_INFINITY, 10)); assertEquals(11, res.getTotalResults()); assertEquals(10, res.getDocuments().size()); @@ -259,9 +266,9 @@ public void stopwords() { Map fields = new HashMap<>(); fields.put("title", "hello world foo bar"); addDocument("doc1", fields); - SearchResult res = client.ftSearch(index, new Query("hello world")); + SearchResult res = client.ftSearch(index, "hello world"); assertEquals(1, res.getTotalResults()); - res = client.ftSearch(index, new Query("foo bar")); + res = client.ftSearch(index, "foo bar"); assertEquals(0, res.getTotalResults()); } @@ -276,9 +283,9 @@ public void noStopwords() { fields.put("title", "hello world foo bar to be or not to be"); addDocument("doc1", fields); - assertEquals(1, client.ftSearch(index, new Query("hello world")).getTotalResults()); - assertEquals(1, client.ftSearch(index, new Query("foo bar")).getTotalResults()); - assertEquals(1, client.ftSearch(index, new Query("to be or not to be")).getTotalResults()); + assertEquals(1, client.ftSearch(index, "hello world").getTotalResults()); + assertEquals(1, client.ftSearch(index, "foo bar").getTotalResults()); + assertEquals(1, client.ftSearch(index, "to be or not to be").getTotalResults()); } @Test @@ -293,18 +300,14 @@ public void geoFilter() { fields.put("loc", "-0.1,51.2"); addDocument("doc2", fields); - SearchResult res = client.ftSearch(index, new Query("hello world"). - addFilter( - new Query.GeoFilter("loc", -0.44, 51.45, - 10, Query.GeoFilter.KILOMETERS) - )); - + SearchResult res = client.ftSearch(index, "hello world", + FTSearchParams.searchParams(). + geoFilter("loc", -0.44, 51.45, 10, GeoUnit.KM)); assertEquals(1, res.getTotalResults()); - res = client.ftSearch(index, new Query("hello world"). - addFilter( - new Query.GeoFilter("loc", -0.44, 51.45, - 100, Query.GeoFilter.KILOMETERS) - )); + + res = client.ftSearch(index, "hello world", + FTSearchParams.searchParams(). + geoFilter("loc", -0.44, 51.45, 100, GeoUnit.KM)); assertEquals(2, res.getTotalResults()); } @@ -314,19 +317,315 @@ public void geoFilterAndGeoCoordinateObject() { Map fields = new HashMap<>(); fields.put("title", "hello world"); - fields.put("loc", new redis.clients.jedis.GeoCoordinate(-0.441, 51.458)); + fields.put("loc", new GeoCoordinate(-0.441, 51.458)); addDocument("doc1", fields); - fields.put("loc", new redis.clients.jedis.GeoCoordinate(-0.1, 51.2)); + fields.put("loc", new GeoCoordinate(-0.1, 51.2)); addDocument("doc2", fields); - SearchResult res = client.ftSearch(index, new Query("hello world").addFilter( - new Query.GeoFilter("loc", -0.44, 51.45, 10, Query.GeoFilter.KILOMETERS))); + SearchResult res = client.ftSearch(index, "hello world", + FTSearchParams.searchParams() + .geoFilter(new FTSearchParams.GeoFilter("loc", -0.44, 51.45, 10, GeoUnit.KM))); + assertEquals(1, res.getTotalResults()); + + res = client.ftSearch(index, "hello world", + FTSearchParams.searchParams() + .geoFilter(new FTSearchParams.GeoFilter("loc", -0.44, 51.45, 100, GeoUnit.KM))); + assertEquals(2, res.getTotalResults()); + } + + @Test + public void testQueryFlags() { + assertOK(client.ftCreate(index, TextField.of("title"))); + + Map fields = new HashMap<>(); + for (int i = 0; i < 100; i++) { + fields.put("title", i % 2 != 0 ? "hello worlds" : "hello world"); + addDocument(String.format("doc%d", i), fields); + } + + SearchResult res = client.ftSearch(index, "hello", + FTSearchParams.searchParams().withScores()); + assertEquals(100, res.getTotalResults()); + assertEquals(10, res.getDocuments().size()); + + for (Document d : res.getDocuments()) { + assertTrue(d.getId().startsWith("doc")); + assertTrue(((String) d.get("title")).startsWith("hello world")); + } +// +// res = client.ftSearch(index, "hello", +// FTSearchParams.searchParams().withScores().explainScore()); +// assertEquals(100, res.getTotalResults()); +// assertEquals(10, res.getDocuments().size()); +// +// for (Document d : res.getDocuments()) { +// assertTrue(d.getId().startsWith("doc")); +// assertTrue(((String) d.get("title")).startsWith("hello world")); +// } + + res = client.ftSearch(index, "hello", + FTSearchParams.searchParams().noContent()); + for (Document d : res.getDocuments()) { + assertTrue(d.getId().startsWith("doc")); + assertEquals(1.0, d.getScore(), 0); + assertNull(d.get("title")); + } + + // test verbatim vs. stemming + res = client.ftSearch(index, "hello worlds"); + assertEquals(100, res.getTotalResults()); + res = client.ftSearch(index, "hello worlds", FTSearchParams.searchParams().verbatim()); + assertEquals(50, res.getTotalResults()); + res = client.ftSearch(index, "hello a world", FTSearchParams.searchParams().verbatim()); + assertEquals(50, res.getTotalResults()); + res = client.ftSearch(index, "hello a worlds", FTSearchParams.searchParams().verbatim()); + assertEquals(50, res.getTotalResults()); + res = client.ftSearch(index, "hello a world", FTSearchParams.searchParams().verbatim().noStopwords()); + assertEquals(0, res.getTotalResults()); + } + + @Test + public void testQueryParams() { + assertOK(client.ftCreate(index, NumericField.of("numval"))); + + client.hset("1", "numval", "1"); + client.hset("2", "numval", "2"); + client.hset("3", "numval", "3"); + + assertEquals(2, client.ftSearch(index, "@numval:[$min $max]", + FTSearchParams.searchParams().addParam("min", 1).addParam("max", 2) + .dialect(2)).getTotalResults()); + + Map paramValues = new HashMap<>(); + paramValues.put("min", 1); + paramValues.put("max", 2); + assertEquals(2, client.ftSearch(index, "@numval:[$min $max]", + FTSearchParams.searchParams().params(paramValues) + .dialect(2)).getTotalResults()); + } + + @Test + public void testSortQueryFlags() { + assertOK(client.ftCreate(index, TextField.of("title").sortable())); + + Map fields = new HashMap<>(); + + fields.put("title", "b title"); + addDocument("doc1", fields); + + fields.put("title", "a title"); + addDocument("doc2", fields); + + fields.put("title", "c title"); + addDocument("doc3", fields); + + SearchResult res = client.ftSearch(index, "title", + FTSearchParams.searchParams().sortBy("title", SortingOrder.ASC)); + + assertEquals(3, res.getTotalResults()); + Document doc1 = res.getDocuments().get(0); + assertEquals("a title", doc1.get("title")); + + doc1 = res.getDocuments().get(1); + assertEquals("b title", doc1.get("title")); + + doc1 = res.getDocuments().get(2); + assertEquals("c title", doc1.get("title")); + } + + @Test + public void testJsonWithAlias() { + assertOK(client.ftCreate(index, + FTCreateParams.createParams() + .on(IndexDataType.JSON) + .prefix("king:"), + TextField.of("$.name").as("name"), + NumericField.of("$.num").as("num"))); + + Map king1 = new HashMap<>(); + king1.put("name", "henry"); + king1.put("num", 42); + client.jsonSet("king:1", Path.ROOT_PATH, king1); + + Map king2 = new HashMap<>(); + king2.put("name", "james"); + king2.put("num", 3.14); + client.jsonSet("king:2", Path.ROOT_PATH, king2); + + SearchResult res = client.ftSearch(index, "@name:henry"); + assertEquals(1, res.getTotalResults()); + assertEquals("king:1", res.getDocuments().get(0).getId()); + + res = client.ftSearch(index, "@num:[0 10]"); + assertEquals(1, res.getTotalResults()); + assertEquals("king:2", res.getDocuments().get(0).getId()); + } + + @Test + public void dropIndex() { + assertOK(client.ftCreate(index, TextField.of("title"))); + + Map fields = new HashMap<>(); + fields.put("title", "hello world"); + for (int i = 0; i < 100; i++) { + addDocument(String.format("doc%d", i), fields); + } + + SearchResult res = client.ftSearch(index, "hello world"); + assertEquals(100, res.getTotalResults()); + + assertEquals("OK", client.ftDropIndex(index)); + + try { + client.ftSearch(index, "hello world"); + fail("Index should not exist."); + } catch (JedisDataException de) { + assertTrue(de.getMessage().contains("no such index")); + } + assertEquals(100, client.dbSize()); + } + + @Test + public void dropIndexDD() { + assertOK(client.ftCreate(index, TextField.of("title"))); + + Map fields = new HashMap<>(); + fields.put("title", "hello world"); + for (int i = 0; i < 100; i++) { + addDocument(String.format("doc%d", i), fields); + } + + SearchResult res = client.ftSearch(index, "hello world"); + assertEquals(100, res.getTotalResults()); + + assertEquals("OK", client.ftDropIndexDD(index)); + + Set keys = client.keys("*"); + assertTrue(keys.isEmpty()); + assertEquals(0, client.dbSize()); + } + + @Test + public void noStem() { + assertOK(client.ftCreate(index, new TextField("stemmed").weight(1.0), + new TextField("notStemmed").weight(1.0).noStem())); + + Map doc = new HashMap<>(); + doc.put("stemmed", "located"); + doc.put("notStemmed", "located"); + addDocument("doc", doc); + + // Query + SearchResult res = client.ftSearch(index, "@stemmed:location"); + assertEquals(1, res.getTotalResults()); + + res = client.ftSearch(index, "@notStemmed:location"); + assertEquals(0, res.getTotalResults()); + } + + @Test + public void phoneticMatch() { + assertOK(client.ftCreate(index, new TextField("noPhonetic").weight(1.0), + new TextField("withPhonetic").weight(1.0).phonetic("dm:en"))); + + Map doc = new HashMap<>(); + doc.put("noPhonetic", "morfix"); + doc.put("withPhonetic", "morfix"); + addDocument("doc", doc); + + // Query + SearchResult res = client.ftSearch(index, "@withPhonetic:morphix=>{$phonetic:true}"); assertEquals(1, res.getTotalResults()); - res = client.ftSearch(index, new Query("hello world").addFilter( - new Query.GeoFilter("loc", -0.44, 51.45, 100, Query.GeoFilter.KILOMETERS))); + try { + client.ftSearch(index, "@noPhonetic:morphix=>{$phonetic:true}"); + fail(); + } catch (JedisDataException e) {/*field does not support phonetics*/ + } + + SearchResult res3 = client.ftSearch(index, "@withPhonetic:morphix=>{$phonetic:false}"); + assertEquals(0, res3.getTotalResults()); + } + + @Test + public void info() { + Collection sc = new ArrayList<>(); + sc.add(TextField.of("title").weight(5)); + sc.add(TextField.of("plot").sortable()); + sc.add(TagField.of("genre").separator(',').sortable()); + sc.add(NumericField.of("release_year").sortable()); + sc.add(NumericField.of("rating").sortable()); + sc.add(NumericField.of("votes").sortable()); + + assertOK(client.ftCreate(index, sc)); + + Map info = client.ftInfo(index); + assertEquals(index, info.get("index_name")); + assertEquals(6, ((List) info.get("attributes")).size()); + assertEquals("global_idle", ((List) info.get("cursor_stats")).get(0)); + assertEquals(0L, ((List) info.get("cursor_stats")).get(1)); + } + + @Test + public void noIndexAndSortBy() { + assertOK(client.ftCreate(index, TextField.of("f1").sortable().noIndex(), TextField.of("f2"))); + + Map mm = new HashMap<>(); + + mm.put("f1", "MarkZZ"); + mm.put("f2", "MarkZZ"); + addDocument("doc1", mm); + + mm.clear(); + mm.put("f1", "MarkAA"); + mm.put("f2", "MarkBB"); + addDocument("doc2", mm); + + SearchResult res = client.ftSearch(index, "@f1:Mark*"); + assertEquals(0, res.getTotalResults()); + + res = client.ftSearch(index, "@f2:Mark*"); assertEquals(2, res.getTotalResults()); + + res = client.ftSearch(index, "@f2:Mark*", + FTSearchParams.searchParams().sortBy("f1", SortingOrder.DESC)); + assertEquals(2, res.getTotalResults()); + + assertEquals("doc1", res.getDocuments().get(0).getId()); + + res = client.ftSearch(index, "@f2:Mark*", + FTSearchParams.searchParams().sortBy("f1", SortingOrder.ASC)); + assertEquals("doc2", res.getDocuments().get(0).getId()); + } + + @Test + public void testHighlightSummarize() { + assertOK(client.ftCreate(index, TextField.of("text").weight(1))); + + Map doc = new HashMap<>(); + doc.put("text", "Redis is often referred as a data structures server. What this means is that " + + "Redis provides access to mutable data structures via a set of commands, which are sent " + + "using a server-client model with TCP sockets and a simple protocol. So different " + + "processes can query and modify the same data structures in a shared way"); + // Add a document + addDocument("foo", doc); + + SearchResult res = client.ftSearch(index, "data", FTSearchParams.searchParams().highlight().summarize()); + assertEquals("is often referred as a data structures server. What this means is that " + + "Redis provides... What this means is that Redis provides access to mutable data " + + "structures via a set of commands, which are sent using a... So different processes can " + + "query and modify the same data structures in a shared... ", + res.getDocuments().get(0).get("text")); + + res = client.ftSearch(index, "data", FTSearchParams.searchParams() + .highlight(FTSearchParams.highlightParams().tags("", "")) + .summarize()); + assertEquals("is often referred as a data structures server. What this means is that " + + "Redis provides... What this means is that Redis provides access to mutable data " + + "structures via a set of commands, which are sent using a... So different processes can " + + "query and modify the same data structures in a shared... ", + res.getDocuments().get(0).get("text")); } @Test @@ -353,14 +652,14 @@ public void getTagField() { fields4.put("category", "orange;purple"); addDocument("qux", fields4); - assertEquals(1, client.ftSearch(index, new Query("@category:{red}")).getTotalResults()); - assertEquals(1, client.ftSearch(index, new Query("@category:{blue}")).getTotalResults()); - assertEquals(1, client.ftSearch(index, new Query("hello @category:{red}")).getTotalResults()); - assertEquals(1, client.ftSearch(index, new Query("hello @category:{blue}")).getTotalResults()); - assertEquals(1, client.ftSearch(index, new Query("@category:{yellow}")).getTotalResults()); - assertEquals(0, client.ftSearch(index, new Query("@category:{purple}")).getTotalResults()); - assertEquals(1, client.ftSearch(index, new Query("@category:{orange\\;purple}")).getTotalResults()); - assertEquals(4, client.ftSearch(index, new Query("hello")).getTotalResults()); + assertEquals(1, client.ftSearch(index, "@category:{red}").getTotalResults()); + assertEquals(1, client.ftSearch(index, "@category:{blue}").getTotalResults()); + assertEquals(1, client.ftSearch(index, "hello @category:{red}").getTotalResults()); + assertEquals(1, client.ftSearch(index, "hello @category:{blue}").getTotalResults()); + assertEquals(1, client.ftSearch(index, "@category:{yellow}").getTotalResults()); + assertEquals(0, client.ftSearch(index, "@category:{purple}").getTotalResults()); + assertEquals(1, client.ftSearch(index, "@category:{orange\\;purple}").getTotalResults()); + assertEquals(4, client.ftSearch(index, "hello").getTotalResults()); assertEquals(new HashSet<>(Arrays.asList("red", "blue", "green", "yellow", "orange;purple")), client.ftTagVals(index, "category")); @@ -392,14 +691,14 @@ public void testGetTagFieldWithNonDefaultSeparator() { fields4.put("category", "orange,purple"); addDocument("qux", fields4); - assertEquals(1, client.ftSearch(index, new Query("@category:{red}")).getTotalResults()); - assertEquals(1, client.ftSearch(index, new Query("@category:{blue}")).getTotalResults()); - assertEquals(1, client.ftSearch(index, new Query("hello @category:{red}")).getTotalResults()); - assertEquals(1, client.ftSearch(index, new Query("hello @category:{blue}")).getTotalResults()); - assertEquals(1, client.ftSearch(index, new Query("hello @category:{yellow}")).getTotalResults()); - assertEquals(0, client.ftSearch(index, new Query("@category:{purple}")).getTotalResults()); - assertEquals(1, client.ftSearch(index, new Query("@category:{orange\\,purple}")).getTotalResults()); - assertEquals(4, client.ftSearch(index, new Query("hello")).getTotalResults()); + assertEquals(1, client.ftSearch(index, "@category:{red}").getTotalResults()); + assertEquals(1, client.ftSearch(index, "@category:{blue}").getTotalResults()); + assertEquals(1, client.ftSearch(index, "hello @category:{red}").getTotalResults()); + assertEquals(1, client.ftSearch(index, "hello @category:{blue}").getTotalResults()); + assertEquals(1, client.ftSearch(index, "hello @category:{yellow}").getTotalResults()); + assertEquals(0, client.ftSearch(index, "@category:{purple}").getTotalResults()); + assertEquals(1, client.ftSearch(index, "@category:{orange\\,purple}").getTotalResults()); + assertEquals(4, client.ftSearch(index, "hello").getTotalResults()); assertEquals(new HashSet<>(Arrays.asList("red", "blue", "green", "yellow", "orange,purple")), client.ftTagVals(index, "category")); @@ -416,11 +715,177 @@ public void caseSensitiveTagField() { fields.put("category", "RedX"); addDocument("foo", fields); - assertEquals(0, client.ftSearch(index, new Query("@category:{redx}")).getTotalResults()); - assertEquals(0, client.ftSearch(index, new Query("@category:{redX}")).getTotalResults()); - assertEquals(0, client.ftSearch(index, new Query("@category:{Redx}")).getTotalResults()); - assertEquals(1, client.ftSearch(index, new Query("@category:{RedX}")).getTotalResults()); - assertEquals(1, client.ftSearch(index, new Query("hello")).getTotalResults()); + assertEquals(0, client.ftSearch(index, "@category:{redx}").getTotalResults()); + assertEquals(0, client.ftSearch(index, "@category:{redX}").getTotalResults()); + assertEquals(0, client.ftSearch(index, "@category:{Redx}").getTotalResults()); + assertEquals(1, client.ftSearch(index, "@category:{RedX}").getTotalResults()); + assertEquals(1, client.ftSearch(index, "hello").getTotalResults()); + } + + @Test + public void testReturnFields() { + assertOK(client.ftCreate(index, TextField.of("field1"), TextField.of("field2"))); + + Map doc = new HashMap<>(); + doc.put("field1", "value1"); + doc.put("field2", "value2"); + addDocument("doc", doc); + + // Query + SearchResult res = client.ftSearch(index, "*", + FTSearchParams.searchParams().returnFields("field1")); + assertEquals(1, res.getTotalResults()); + Document ret = res.getDocuments().get(0); + assertEquals("value1", ret.get("field1")); + assertNull(ret.get("field2")); + } + + @Test + public void returnWithFieldNames() { + assertOK(client.ftCreate(index, TextField.of("a"), TextField.of("b"), TextField.of("c"))); + + Map map = new HashMap<>(); + map.put("a", "value1"); + map.put("b", "value2"); + map.put("c", "value3"); + addDocument("doc", map); + + // Query + SearchResult res = client.ftSearch(index, "*", + FTSearchParams.searchParams().returnFields( + FieldName.of("a"), FieldName.of("b").as("d"))); + assertEquals(1, res.getTotalResults()); + Document doc = res.getDocuments().get(0); + assertEquals("value1", doc.get("a")); + assertNull(doc.get("b")); + assertEquals("value2", doc.get("d")); + assertNull(doc.get("c")); + } + + @Test + public void inKeys() { + assertOK(client.ftCreate(index, TextField.of("field1"), TextField.of("field2"))); + + Map doc = new HashMap<>(); + doc.put("field1", "value"); + doc.put("field2", "not"); + // Store it + addDocument("doc1", doc); + addDocument("doc2", doc); + + // Query + SearchResult res = client.ftSearch(index, "value", + FTSearchParams.searchParams().inKeys("doc1")); + assertEquals(1, res.getTotalResults()); + assertEquals("doc1", res.getDocuments().get(0).getId()); + assertEquals("value", res.getDocuments().get(0).get("field1")); + assertEquals(null, res.getDocuments().get(0).get("value")); + } + + @Test + public void alias() { + assertOK(client.ftCreate(index, TextField.of("field1"))); + + Map doc = new HashMap<>(); + doc.put("field1", "value"); + addDocument("doc1", doc); + + assertEquals("OK", client.ftAliasAdd("ALIAS1", index)); + SearchResult res1 = client.ftSearch("ALIAS1", "*", + FTSearchParams.searchParams().returnFields("field1")); + assertEquals(1, res1.getTotalResults()); + assertEquals("value", res1.getDocuments().get(0).get("field1")); + + assertEquals("OK", client.ftAliasUpdate("ALIAS2", index)); + SearchResult res2 = client.ftSearch("ALIAS2", "*", + FTSearchParams.searchParams().returnFields("field1")); + assertEquals(1, res2.getTotalResults()); + assertEquals("value", res2.getDocuments().get(0).get("field1")); + + try { + client.ftAliasDel("ALIAS3"); + fail("Should throw JedisDataException"); + } catch (JedisDataException e) { + // Alias does not exist + } + assertEquals("OK", client.ftAliasDel("ALIAS2")); + try { + client.ftAliasDel("ALIAS2"); + fail("Should throw JedisDataException"); + } catch (JedisDataException e) { + // Alias does not exist + } + } + + @Test + public void synonym() { + assertOK(client.ftCreate(index, TextField.of("name").weight(1), TextField.of("addr").weight(1))); + + long group1 = 345L; + long group2 = 789L; + String group1_str = Long.toString(group1); + String group2_str = Long.toString(group2); + assertEquals("OK", client.ftSynUpdate(index, group1_str, "girl", "baby")); + assertEquals("OK", client.ftSynUpdate(index, group1_str, "child")); + assertEquals("OK", client.ftSynUpdate(index, group2_str, "child")); + + Map> dump = client.ftSynDump(index); + + Map> expected = new HashMap<>(); + expected.put("girl", Arrays.asList(group1_str)); + expected.put("baby", Arrays.asList(group1_str)); + expected.put("child", Arrays.asList(group1_str, group2_str)); + assertEquals(expected, dump); + } + + @Test + public void slop() { + assertOK(client.ftCreate(index, TextField.of("field1"), TextField.of("field2"))); + + Map doc = new HashMap<>(); + doc.put("field1", "ok hi jedis"); + addDocument("doc1", doc); + + SearchResult res = client.ftSearch(index, "ok jedis", FTSearchParams.searchParams().slop(0)); + assertEquals(0, res.getTotalResults()); + + res = client.ftSearch(index, "ok jedis", FTSearchParams.searchParams().slop(1)); + assertEquals(1, res.getTotalResults()); + assertEquals("doc1", res.getDocuments().get(0).getId()); + assertEquals("ok hi jedis", res.getDocuments().get(0).get("field1")); + } + + @Test + public void timeout() { + assertOK(client.ftCreate(index, TextField.of("field1"), TextField.of("field2"))); + + Map doc = new HashMap<>(); + doc.put("field1", "value"); + doc.put("field2", "not"); + addDocument("doc1", doc); + + SearchResult res = client.ftSearch(index, "value", FTSearchParams.searchParams().timeout(1000)); + assertEquals(1, res.getTotalResults()); + assertEquals("doc1", res.getDocuments().get(0).getId()); + assertEquals("value", res.getDocuments().get(0).get("field1")); + assertEquals("not", res.getDocuments().get(0).get("field2")); + } + + @Test + public void inOrder() { + assertOK(client.ftCreate(index, TextField.of("field1"), TextField.of("field2"))); + + Map doc = new HashMap<>(); + doc.put("field1", "value"); + doc.put("field2", "not"); + addDocument("doc2", doc); + addDocument("doc1", doc); + + SearchResult res = client.ftSearch(index, "value", FTSearchParams.searchParams().inOrder()); + assertEquals(2, res.getTotalResults()); + assertEquals("doc2", res.getDocuments().get(0).getId()); + assertEquals("value", res.getDocuments().get(0).get("field1")); + assertEquals("not", res.getDocuments().get(0).get("field2")); } @Test @@ -437,23 +902,18 @@ public void testHNSWVVectorSimilarity() { client.hset("b", "v", "aaaabaaa"); client.hset("c", "v", "aaaaabaa"); - Query query = new Query("*=>[KNN 2 @v $vec]") + FTSearchParams searchParams = FTSearchParams.searchParams() .addParam("vec", "aaaaaaaa") - .setSortBy("__v_score", true) + .sortBy("__v_score", SortingOrder.ASC) .returnFields("__v_score") .dialect(2); - Document doc1 = client.ftSearch(index, query).getDocuments().get(0); + Document doc1 = client.ftSearch(index, "*=>[KNN 2 @v $vec]", searchParams).getDocuments().get(0); assertEquals("a", doc1.getId()); assertEquals("0", doc1.get("__v_score")); } @Test public void testFlatVectorSimilarity() { - Map attr = new HashMap<>(); - attr.put("TYPE", "FLOAT32"); - attr.put("DIM", 2); - attr.put("DISTANCE_METRIC", "L2"); - assertOK(client.ftCreate(index, VectorField.builder().fieldName("v") .algorithm(VectorField.VectorAlgorithm.FLAT) @@ -467,12 +927,13 @@ public void testFlatVectorSimilarity() { client.hset("b", "v", "aaaabaaa"); client.hset("c", "v", "aaaaabaa"); - Query query = new Query("*=>[KNN 2 @v $vec]") + FTSearchParams searchParams = FTSearchParams.searchParams() .addParam("vec", "aaaaaaaa") - .setSortBy("__v_score", true) + .sortBy("__v_score", SortingOrder.ASC) .returnFields("__v_score") .dialect(2); - Document doc1 = client.ftSearch(index, query).getDocuments().get(0); + + Document doc1 = client.ftSearch(index, "*=>[KNN 2 @v $vec]", searchParams).getDocuments().get(0); assertEquals("a", doc1.getId()); assertEquals("0", doc1.get("__v_score")); }