From 2c75bb6fa415acd583d5c12a7bc4608f284e28bf Mon Sep 17 00:00:00 2001 From: Mark Paluch Date: Fri, 22 Jan 2021 11:42:35 +0100 Subject: [PATCH] Add support for GEOSEARCH and GEOSEARCHSTORE #1561 --- .../core/AbstractRedisAsyncCommands.java | 17 ++ .../core/AbstractRedisReactiveCommands.java | 17 ++ src/main/java/io/lettuce/core/GeoArgs.java | 34 +++- src/main/java/io/lettuce/core/GeoSearch.java | 159 ++++++++++++++++++ .../io/lettuce/core/RedisCommandBuilder.java | 56 +++++- .../core/api/async/RedisGeoAsyncCommands.java | 52 +++++- .../reactive/RedisGeoReactiveCommands.java | 49 +++++- .../core/api/sync/RedisGeoCommands.java | 49 +++++- .../async/NodeSelectionGeoAsyncCommands.java | 50 +++++- .../api/sync/NodeSelectionGeoCommands.java | 50 +++++- .../io/lettuce/core/protocol/CommandType.java | 3 +- .../coroutines/RedisGeoCoroutinesCommands.kt | 56 ++++++ .../RedisGeoCoroutinesCommandsImpl.kt | 65 +++++-- .../io/lettuce/core/api/RedisGeoCommands.java | 41 +++++ .../KotlinCompilationUnitFactory.java | 4 +- .../commands/GeoCommandIntegrationTests.java | 68 +++++++- 16 files changed, 743 insertions(+), 27 deletions(-) create mode 100644 src/main/java/io/lettuce/core/GeoSearch.java diff --git a/src/main/java/io/lettuce/core/AbstractRedisAsyncCommands.java b/src/main/java/io/lettuce/core/AbstractRedisAsyncCommands.java index ffd0895ee1..adffbc4b2a 100644 --- a/src/main/java/io/lettuce/core/AbstractRedisAsyncCommands.java +++ b/src/main/java/io/lettuce/core/AbstractRedisAsyncCommands.java @@ -703,6 +703,23 @@ protected RedisFuture>> georadiusbymember_ro(K key, V member, return dispatch(commandBuilder.georadiusbymember(GEORADIUSBYMEMBER_RO, key, member, distance, unit.name(), geoArgs)); } + @Override + public RedisFuture> geosearch(K key, GeoSearch.GeoRef reference, GeoSearch.GeoPredicate predicate) { + return dispatch(commandBuilder.geosearch(key, reference, predicate)); + } + + @Override + public RedisFuture>> geosearch(K key, GeoSearch.GeoRef reference, GeoSearch.GeoPredicate predicate, + GeoArgs geoArgs) { + return dispatch(commandBuilder.geosearch(key, reference, predicate, geoArgs)); + } + + @Override + public RedisFuture geosearchstore(K destination, K key, GeoSearch.GeoRef reference, + GeoSearch.GeoPredicate predicate, GeoArgs geoArgs, boolean storeDist) { + return dispatch(commandBuilder.geosearchstore(destination, key, reference, predicate, geoArgs, storeDist)); + } + @Override public RedisFuture get(K key) { return dispatch(commandBuilder.get(key)); diff --git a/src/main/java/io/lettuce/core/AbstractRedisReactiveCommands.java b/src/main/java/io/lettuce/core/AbstractRedisReactiveCommands.java index f62bafad56..66c2bacaf3 100644 --- a/src/main/java/io/lettuce/core/AbstractRedisReactiveCommands.java +++ b/src/main/java/io/lettuce/core/AbstractRedisReactiveCommands.java @@ -764,6 +764,23 @@ protected Flux> georadiusbymember_ro(K key, V member, double distan () -> commandBuilder.georadiusbymember(GEORADIUSBYMEMBER_RO, key, member, distance, unit.name(), geoArgs)); } + @Override + public Flux geosearch(K key, GeoSearch.GeoRef reference, GeoSearch.GeoPredicate predicate) { + return createDissolvingFlux(() -> commandBuilder.geosearch(key, reference, predicate)); + } + + @Override + public Flux> geosearch(K key, GeoSearch.GeoRef reference, GeoSearch.GeoPredicate predicate, + GeoArgs geoArgs) { + return createDissolvingFlux(() -> commandBuilder.geosearch(key, reference, predicate, geoArgs)); + } + + @Override + public Mono geosearchstore(K destination, K key, GeoSearch.GeoRef reference, GeoSearch.GeoPredicate predicate, + GeoArgs geoArgs, boolean storeDist) { + return createMono(() -> commandBuilder.geosearchstore(destination, key, reference, predicate, geoArgs, storeDist)); + } + @Override public Mono get(K key) { return createMono(() -> commandBuilder.get(key)); diff --git a/src/main/java/io/lettuce/core/GeoArgs.java b/src/main/java/io/lettuce/core/GeoArgs.java index d082cd558f..39ac0b113a 100644 --- a/src/main/java/io/lettuce/core/GeoArgs.java +++ b/src/main/java/io/lettuce/core/GeoArgs.java @@ -18,6 +18,7 @@ import io.lettuce.core.internal.LettuceAssert; import io.lettuce.core.protocol.CommandArgs; import io.lettuce.core.protocol.CommandKeyword; +import io.lettuce.core.protocol.ProtocolKeyword; /** * @@ -38,6 +39,8 @@ public class GeoArgs implements CompositeArgument { private Long count; + private boolean any; + private Sort sort = Sort.none; /** @@ -145,10 +148,24 @@ public GeoArgs withHash() { * @return {@code this} {@link GeoArgs}. */ public GeoArgs withCount(long count) { + return withCount(count, false); + } + + /** + * Limit results to {@code count} entries. + * + * @param count number greater 0. + * @param any whether to complete the command as soon as enough matches are found, so the results may not be the ones + * closest to the specified point. + * @return {@code this} {@link GeoArgs}. + * @since 6.1 + */ + public GeoArgs withCount(long count, boolean any) { LettuceAssert.isTrue(count > 0, "Count must be greater 0"); this.count = count; + this.any = any; return this; } @@ -232,7 +249,7 @@ public enum Sort { /** * Supported geo unit. */ - public enum Unit { + public enum Unit implements ProtocolKeyword { /** * meter. @@ -253,6 +270,17 @@ public enum Unit { * mile. */ mi; + + private final byte[] asBytes; + + Unit() { + asBytes = name().getBytes(); + } + + @Override + public byte[] getBytes() { + return asBytes; + } } public void build(CommandArgs args) { @@ -275,6 +303,10 @@ public void build(CommandArgs args) { if (count != null) { args.add(CommandKeyword.COUNT).add(count); + + if (any) { + args.add("ANY"); + } } } diff --git a/src/main/java/io/lettuce/core/GeoSearch.java b/src/main/java/io/lettuce/core/GeoSearch.java new file mode 100644 index 0000000000..50486c96d5 --- /dev/null +++ b/src/main/java/io/lettuce/core/GeoSearch.java @@ -0,0 +1,159 @@ +/* + * Copyright 2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.lettuce.core; + +import io.lettuce.core.internal.LettuceAssert; +import io.lettuce.core.protocol.CommandArgs; + +/** + * Utility to create {@link GeoPredicate} and {@link GeoRef} objects to be used with {@code GEOSEARCH}. + * + * @author Mark Paluch + * @since 6.1 + */ +public final class GeoSearch { + + /** + * Create a {@link GeoRef} from a Geo set {@code member}. + * + * @param member the Geo set member to use as search reference starting point. + * @return the {@link GeoRef}. + */ + public static GeoRef fromMember(K member) { + LettuceAssert.notNull(member, "Reference member must not be null"); + return new FromMember<>(member); + } + + /** + * Create a {@link GeoRef} from WGS84 coordinates {@code longitude} and {@code latitude}. + * + * @param longitude the longitude coordinate according to WGS84. + * @param latitude the latitude coordinate according to WGS84. + * @return the {@link GeoRef}. + */ + public static GeoRef fromCoordinates(double longitude, double latitude) { + return (GeoRef) new FromCoordinates(longitude, latitude); + } + + /** + * Create a {@link GeoPredicate} by specifying a radius {@code distance} and {@link GeoArgs.Unit}. + * + * @param distance the radius. + * @param unit size unit. + * @return the {@link GeoPredicate} for the specified radius. + */ + public static GeoPredicate byRadius(double distance, GeoArgs.Unit unit) { + return new Radius(distance, unit); + } + + /** + * Create a {@link GeoPredicate} by specifying a box of the size {@code width}, {@code height} and {@link GeoArgs.Unit}. + * + * @param width box width. + * @param height box height. + * @param unit size unit. + * @return the {@link GeoPredicate} for the specified box. + */ + public static GeoPredicate byBox(double width, double height, GeoArgs.Unit unit) { + return new Box(width, height, unit); + } + + /** + * Geo reference specifying a search starting point. + * + * @param + */ + public interface GeoRef extends CompositeArgument { + + } + + static class FromMember implements GeoRef { + + final K member; + + public FromMember(K member) { + this.member = member; + } + + @Override + @SuppressWarnings("unchecked") + public void build(CommandArgs args) { + args.add("FROMMEMBER").addKey((K) member); + } + + } + + static class FromCoordinates implements GeoRef { + + final double longitude, latitude; + + public FromCoordinates(double longitude, double latitude) { + this.longitude = longitude; + this.latitude = latitude; + } + + @Override + public void build(CommandArgs args) { + args.add("FROMLONLAT").add(longitude).add(latitude); + } + + } + + /** + * Geo predicate specifying a search scope. + */ + public interface GeoPredicate extends CompositeArgument { + + } + + static class Radius implements GeoPredicate { + + final double distance; + + final GeoArgs.Unit unit; + + public Radius(double distance, GeoArgs.Unit unit) { + this.distance = distance; + this.unit = unit; + } + + @Override + public void build(CommandArgs args) { + args.add("BYRADIUS").add(distance).add(unit); + } + + } + + static class Box implements GeoPredicate { + + final double width, height; + + final GeoArgs.Unit unit; + + public Box(double width, double height, GeoArgs.Unit unit) { + this.width = width; + this.height = height; + this.unit = unit; + } + + @Override + public void build(CommandArgs args) { + args.add("BYBOX").add(width).add(height).add(unit); + } + + } + +} diff --git a/src/main/java/io/lettuce/core/RedisCommandBuilder.java b/src/main/java/io/lettuce/core/RedisCommandBuilder.java index 9470d17bfa..09257e34a5 100644 --- a/src/main/java/io/lettuce/core/RedisCommandBuilder.java +++ b/src/main/java/io/lettuce/core/RedisCommandBuilder.java @@ -811,7 +811,6 @@ Command> georadius(CommandType commandType, K key, double longitude String unit) { notNullKey(key); LettuceAssert.notNull(unit, "Unit " + MUST_NOT_BE_NULL); - LettuceAssert.notEmpty(unit, "Unit " + MUST_NOT_BE_EMPTY); CommandArgs args = new CommandArgs<>(codec).addKey(key).add(longitude).add(latitude).add(distance).add(unit); return createCommand(commandType, new ValueSetOutput<>(codec), args); @@ -840,7 +839,7 @@ Command georadius(K key, double longitude, double latitude, double d LettuceAssert.notEmpty(unit, "Unit " + MUST_NOT_BE_EMPTY); LettuceAssert.notNull(geoRadiusStoreArgs, "GeoRadiusStoreArgs " + MUST_NOT_BE_NULL); LettuceAssert.isTrue(geoRadiusStoreArgs.getStoreKey() != null || geoRadiusStoreArgs.getStoreDistKey() != null, - "At least STORE key or STORDIST key is required"); + "At least STORE key or STOREDIST key is required"); CommandArgs args = new CommandArgs<>(codec).addKey(key).add(longitude).add(latitude).add(distance).add(unit); geoRadiusStoreArgs.build(args); @@ -882,7 +881,7 @@ Command georadiusbymember(K key, V member, double distance, String u LettuceAssert.notNull(unit, "Unit " + MUST_NOT_BE_NULL); LettuceAssert.notEmpty(unit, "Unit " + MUST_NOT_BE_EMPTY); LettuceAssert.isTrue(geoRadiusStoreArgs.getStoreKey() != null || geoRadiusStoreArgs.getStoreDistKey() != null, - "At least STORE key or STORDIST key is required"); + "At least STORE key or STOREDIST key is required"); CommandArgs args = new CommandArgs<>(codec).addKey(key).addValue(member).add(distance).add(unit); geoRadiusStoreArgs.build(args); @@ -890,6 +889,57 @@ Command georadiusbymember(K key, V member, double distance, String u return createCommand(GEORADIUSBYMEMBER, new IntegerOutput<>(codec), args); } + Command> geosearch(K key, GeoSearch.GeoRef reference, GeoSearch.GeoPredicate predicate) { + notNullKey(key); + LettuceAssert.notNull(reference, "GeoRef " + MUST_NOT_BE_NULL); + LettuceAssert.notNull(predicate, "GeoPredicate " + MUST_NOT_BE_NULL); + + CommandArgs args = new CommandArgs<>(codec).addKey(key); + + reference.build(args); + predicate.build(args); + + return createCommand(GEOSEARCH, new ValueSetOutput<>(codec), args); + } + + Command>> geosearch(K key, GeoSearch.GeoRef reference, GeoSearch.GeoPredicate predicate, + GeoArgs geoArgs) { + notNullKey(key); + LettuceAssert.notNull(reference, "GeoRef " + MUST_NOT_BE_NULL); + LettuceAssert.notNull(predicate, "GeoPredicate " + MUST_NOT_BE_NULL); + + CommandArgs args = new CommandArgs<>(codec).addKey(key); + + reference.build(args); + predicate.build(args); + geoArgs.build(args); + + return createCommand(GEOSEARCH, + new GeoWithinListOutput<>(codec, geoArgs.isWithDistance(), geoArgs.isWithHash(), geoArgs.isWithCoordinates()), + args); + } + + Command geosearchstore(K destination, K key, GeoSearch.GeoRef reference, GeoSearch.GeoPredicate predicate, + GeoArgs geoArgs, boolean storeDist) { + notNullKey(key); + LettuceAssert.notNull(destination, "Destination " + MUST_NOT_BE_NULL); + LettuceAssert.notNull(key, "Key " + MUST_NOT_BE_NULL); + LettuceAssert.notNull(reference, "GeoRef " + MUST_NOT_BE_NULL); + LettuceAssert.notNull(predicate, "GeoPredicate " + MUST_NOT_BE_NULL); + + CommandArgs args = new CommandArgs<>(codec).addKey(destination).addKey(key); + + reference.build(args); + predicate.build(args); + geoArgs.build(args); + + if (storeDist) { + args.add("STOREDIST"); + } + + return createCommand(GEOSEARCHSTORE, new IntegerOutput<>(codec), args); + } + Command get(K key) { notNullKey(key); diff --git a/src/main/java/io/lettuce/core/api/async/RedisGeoAsyncCommands.java b/src/main/java/io/lettuce/core/api/async/RedisGeoAsyncCommands.java index 717ec7c7da..8c305e468e 100644 --- a/src/main/java/io/lettuce/core/api/async/RedisGeoAsyncCommands.java +++ b/src/main/java/io/lettuce/core/api/async/RedisGeoAsyncCommands.java @@ -18,7 +18,13 @@ import java.util.List; import java.util.Set; -import io.lettuce.core.*; +import io.lettuce.core.GeoArgs; +import io.lettuce.core.GeoCoordinates; +import io.lettuce.core.GeoRadiusStoreArgs; +import io.lettuce.core.GeoSearch; +import io.lettuce.core.GeoWithin; +import io.lettuce.core.RedisFuture; +import io.lettuce.core.Value; /** * Asynchronous executed commands for the Geo-API. @@ -160,4 +166,48 @@ public interface RedisGeoAsyncCommands { * returned. */ RedisFuture geodist(K key, V from, V to, GeoArgs.Unit unit); + + /** + * Retrieve members selected by distance with the center of {@code reference} the search {@code predicate}. Use + * {@link GeoSearch} to create reference and predicate objects. + * + * @param key the key of the geo set. + * @param reference the reference member or longitude/latitude coordinates. + * @param predicate the bounding box or radius to search in. + * @return bulk reply. + * @since 6.1 + */ + RedisFuture> geosearch(K key, GeoSearch.GeoRef reference, GeoSearch.GeoPredicate predicate); + + /** + * Retrieve members selected by distance with the center of {@code reference} the search {@code predicate}. Use + * {@link GeoSearch} to create reference and predicate objects. + * + * @param key the key of the geo set. + * @param reference the reference member or longitude/latitude coordinates. + * @param predicate the bounding box or radius to search in. + * @param geoArgs args to control the result. + * @return nested multi-bulk reply. The {@link GeoWithin} contains only fields which were requested by {@link GeoArgs}. + * @since 6.1 + */ + RedisFuture>> geosearch(K key, GeoSearch.GeoRef reference, GeoSearch.GeoPredicate predicate, + GeoArgs geoArgs); + + /** + * Perform a {@link #geosearch(Object, GeoSearch.GeoRef, GeoSearch.GeoPredicate, GeoArgs)} query and store the results in a + * sorted set. + * + * @param destination the destination where to store results. + * @param key the key of the geo set. + * @param reference the reference member or longitude/latitude coordinates. + * @param predicate the bounding box or radius to search in. + * @param geoArgs args to control the result. + * @param storeDist stores the items in a sorted set populated with their distance from the center of the circle or box, as + * a floating-point number, in the same unit specified for that shape. + * @return Long integer-reply the number of elements in the result. + * @since 6.1 + */ + RedisFuture geosearchstore(K destination, K key, GeoSearch.GeoRef reference, GeoSearch.GeoPredicate predicate, + GeoArgs geoArgs, boolean storeDist); + } diff --git a/src/main/java/io/lettuce/core/api/reactive/RedisGeoReactiveCommands.java b/src/main/java/io/lettuce/core/api/reactive/RedisGeoReactiveCommands.java index c5c9cafc57..7d3a4af06f 100644 --- a/src/main/java/io/lettuce/core/api/reactive/RedisGeoReactiveCommands.java +++ b/src/main/java/io/lettuce/core/api/reactive/RedisGeoReactiveCommands.java @@ -17,7 +17,12 @@ import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; -import io.lettuce.core.*; +import io.lettuce.core.GeoArgs; +import io.lettuce.core.GeoCoordinates; +import io.lettuce.core.GeoRadiusStoreArgs; +import io.lettuce.core.GeoSearch; +import io.lettuce.core.GeoWithin; +import io.lettuce.core.Value; /** * Reactive executed commands for the Geo-API. @@ -159,4 +164,46 @@ public interface RedisGeoReactiveCommands { * returned. */ Mono geodist(K key, V from, V to, GeoArgs.Unit unit); + + /** + * Retrieve members selected by distance with the center of {@code reference} the search {@code predicate}. Use + * {@link GeoSearch} to create reference and predicate objects. + * + * @param key the key of the geo set. + * @param reference the reference member or longitude/latitude coordinates. + * @param predicate the bounding box or radius to search in. + * @return bulk reply. + * @since 6.1 + */ + Flux geosearch(K key, GeoSearch.GeoRef reference, GeoSearch.GeoPredicate predicate); + + /** + * Retrieve members selected by distance with the center of {@code reference} the search {@code predicate}. Use + * {@link GeoSearch} to create reference and predicate objects. + * + * @param key the key of the geo set. + * @param reference the reference member or longitude/latitude coordinates. + * @param predicate the bounding box or radius to search in. + * @param geoArgs args to control the result. + * @return nested multi-bulk reply. The {@link GeoWithin} contains only fields which were requested by {@link GeoArgs}. + * @since 6.1 + */ + Flux> geosearch(K key, GeoSearch.GeoRef reference, GeoSearch.GeoPredicate predicate, GeoArgs geoArgs); + + /** + * Perform a {@link #geosearch(Object, GeoSearch.GeoRef, GeoSearch.GeoPredicate, GeoArgs)} query and store the results in a + * sorted set. + * + * @param destination the destination where to store results. + * @param key the key of the geo set. + * @param reference the reference member or longitude/latitude coordinates. + * @param predicate the bounding box or radius to search in. + * @param geoArgs args to control the result. + * @param storeDist stores the items in a sorted set populated with their distance from the center of the circle or box, as + * a floating-point number, in the same unit specified for that shape. + * @return Long integer-reply the number of elements in the result. + * @since 6.1 + */ + Mono geosearchstore(K destination, K key, GeoSearch.GeoRef reference, GeoSearch.GeoPredicate predicate, + GeoArgs geoArgs, boolean storeDist); } diff --git a/src/main/java/io/lettuce/core/api/sync/RedisGeoCommands.java b/src/main/java/io/lettuce/core/api/sync/RedisGeoCommands.java index 208eb104f2..6ccc1307a6 100644 --- a/src/main/java/io/lettuce/core/api/sync/RedisGeoCommands.java +++ b/src/main/java/io/lettuce/core/api/sync/RedisGeoCommands.java @@ -18,7 +18,12 @@ import java.util.List; import java.util.Set; -import io.lettuce.core.*; +import io.lettuce.core.GeoArgs; +import io.lettuce.core.GeoCoordinates; +import io.lettuce.core.GeoRadiusStoreArgs; +import io.lettuce.core.GeoSearch; +import io.lettuce.core.GeoWithin; +import io.lettuce.core.Value; /** * Synchronous executed commands for the Geo-API. @@ -160,4 +165,46 @@ public interface RedisGeoCommands { * returned. */ Double geodist(K key, V from, V to, GeoArgs.Unit unit); + + /** + * Retrieve members selected by distance with the center of {@code reference} the search {@code predicate}. Use + * {@link GeoSearch} to create reference and predicate objects. + * + * @param key the key of the geo set. + * @param reference the reference member or longitude/latitude coordinates. + * @param predicate the bounding box or radius to search in. + * @return bulk reply. + * @since 6.1 + */ + Set geosearch(K key, GeoSearch.GeoRef reference, GeoSearch.GeoPredicate predicate); + + /** + * Retrieve members selected by distance with the center of {@code reference} the search {@code predicate}. Use + * {@link GeoSearch} to create reference and predicate objects. + * + * @param key the key of the geo set. + * @param reference the reference member or longitude/latitude coordinates. + * @param predicate the bounding box or radius to search in. + * @param geoArgs args to control the result. + * @return nested multi-bulk reply. The {@link GeoWithin} contains only fields which were requested by {@link GeoArgs}. + * @since 6.1 + */ + List> geosearch(K key, GeoSearch.GeoRef reference, GeoSearch.GeoPredicate predicate, GeoArgs geoArgs); + + /** + * Perform a {@link #geosearch(Object, GeoSearch.GeoRef, GeoSearch.GeoPredicate, GeoArgs)} query and store the results in a + * sorted set. + * + * @param destination the destination where to store results. + * @param key the key of the geo set. + * @param reference the reference member or longitude/latitude coordinates. + * @param predicate the bounding box or radius to search in. + * @param geoArgs args to control the result. + * @param storeDist stores the items in a sorted set populated with their distance from the center of the circle or box, as + * a floating-point number, in the same unit specified for that shape. + * @return Long integer-reply the number of elements in the result. + * @since 6.1 + */ + Long geosearchstore(K destination, K key, GeoSearch.GeoRef reference, GeoSearch.GeoPredicate predicate, GeoArgs geoArgs, + boolean storeDist); } diff --git a/src/main/java/io/lettuce/core/cluster/api/async/NodeSelectionGeoAsyncCommands.java b/src/main/java/io/lettuce/core/cluster/api/async/NodeSelectionGeoAsyncCommands.java index 58dcfca4e7..b761667966 100644 --- a/src/main/java/io/lettuce/core/cluster/api/async/NodeSelectionGeoAsyncCommands.java +++ b/src/main/java/io/lettuce/core/cluster/api/async/NodeSelectionGeoAsyncCommands.java @@ -18,7 +18,12 @@ import java.util.List; import java.util.Set; -import io.lettuce.core.*; +import io.lettuce.core.GeoArgs; +import io.lettuce.core.GeoCoordinates; +import io.lettuce.core.GeoRadiusStoreArgs; +import io.lettuce.core.GeoSearch; +import io.lettuce.core.GeoWithin; +import io.lettuce.core.Value; /** * Asynchronous executed commands on a node selection for the Geo-API. @@ -160,4 +165,47 @@ public interface NodeSelectionGeoAsyncCommands { * returned. */ AsyncExecutions geodist(K key, V from, V to, GeoArgs.Unit unit); + + /** + * Retrieve members selected by distance with the center of {@code reference} the search {@code predicate}. Use + * {@link GeoSearch} to create reference and predicate objects. + * + * @param key the key of the geo set. + * @param reference the reference member or longitude/latitude coordinates. + * @param predicate the bounding box or radius to search in. + * @return bulk reply. + * @since 6.1 + */ + AsyncExecutions> geosearch(K key, GeoSearch.GeoRef reference, GeoSearch.GeoPredicate predicate); + + /** + * Retrieve members selected by distance with the center of {@code reference} the search {@code predicate}. Use + * {@link GeoSearch} to create reference and predicate objects. + * + * @param key the key of the geo set. + * @param reference the reference member or longitude/latitude coordinates. + * @param predicate the bounding box or radius to search in. + * @param geoArgs args to control the result. + * @return nested multi-bulk reply. The {@link GeoWithin} contains only fields which were requested by {@link GeoArgs}. + * @since 6.1 + */ + AsyncExecutions>> geosearch(K key, GeoSearch.GeoRef reference, GeoSearch.GeoPredicate predicate, + GeoArgs geoArgs); + + /** + * Perform a {@link #geosearch(Object, GeoSearch.GeoRef, GeoSearch.GeoPredicate, GeoArgs)} query and store the results in a + * sorted set. + * + * @param destination the destination where to store results. + * @param key the key of the geo set. + * @param reference the reference member or longitude/latitude coordinates. + * @param predicate the bounding box or radius to search in. + * @param geoArgs args to control the result. + * @param storeDist stores the items in a sorted set populated with their distance from the center of the circle or box, as + * a floating-point number, in the same unit specified for that shape. + * @return Long integer-reply the number of elements in the result. + * @since 6.1 + */ + AsyncExecutions geosearchstore(K destination, K key, GeoSearch.GeoRef reference, GeoSearch.GeoPredicate predicate, + GeoArgs geoArgs, boolean storeDist); } diff --git a/src/main/java/io/lettuce/core/cluster/api/sync/NodeSelectionGeoCommands.java b/src/main/java/io/lettuce/core/cluster/api/sync/NodeSelectionGeoCommands.java index fb322cf12e..39a85e6e04 100644 --- a/src/main/java/io/lettuce/core/cluster/api/sync/NodeSelectionGeoCommands.java +++ b/src/main/java/io/lettuce/core/cluster/api/sync/NodeSelectionGeoCommands.java @@ -18,7 +18,12 @@ import java.util.List; import java.util.Set; -import io.lettuce.core.*; +import io.lettuce.core.GeoArgs; +import io.lettuce.core.GeoCoordinates; +import io.lettuce.core.GeoRadiusStoreArgs; +import io.lettuce.core.GeoSearch; +import io.lettuce.core.GeoWithin; +import io.lettuce.core.Value; /** * Synchronous executed commands on a node selection for the Geo-API. @@ -160,4 +165,47 @@ public interface NodeSelectionGeoCommands { * returned. */ Executions geodist(K key, V from, V to, GeoArgs.Unit unit); + + /** + * Retrieve members selected by distance with the center of {@code reference} the search {@code predicate}. Use + * {@link GeoSearch} to create reference and predicate objects. + * + * @param key the key of the geo set. + * @param reference the reference member or longitude/latitude coordinates. + * @param predicate the bounding box or radius to search in. + * @return bulk reply. + * @since 6.1 + */ + Executions> geosearch(K key, GeoSearch.GeoRef reference, GeoSearch.GeoPredicate predicate); + + /** + * Retrieve members selected by distance with the center of {@code reference} the search {@code predicate}. Use + * {@link GeoSearch} to create reference and predicate objects. + * + * @param key the key of the geo set. + * @param reference the reference member or longitude/latitude coordinates. + * @param predicate the bounding box or radius to search in. + * @param geoArgs args to control the result. + * @return nested multi-bulk reply. The {@link GeoWithin} contains only fields which were requested by {@link GeoArgs}. + * @since 6.1 + */ + Executions>> geosearch(K key, GeoSearch.GeoRef reference, GeoSearch.GeoPredicate predicate, + GeoArgs geoArgs); + + /** + * Perform a {@link #geosearch(Object, GeoSearch.GeoRef, GeoSearch.GeoPredicate, GeoArgs)} query and store the results in a + * sorted set. + * + * @param destination the destination where to store results. + * @param key the key of the geo set. + * @param reference the reference member or longitude/latitude coordinates. + * @param predicate the bounding box or radius to search in. + * @param geoArgs args to control the result. + * @param storeDist stores the items in a sorted set populated with their distance from the center of the circle or box, as + * a floating-point number, in the same unit specified for that shape. + * @return Long integer-reply the number of elements in the result. + * @since 6.1 + */ + Executions geosearchstore(K destination, K key, GeoSearch.GeoRef reference, GeoSearch.GeoPredicate predicate, + GeoArgs geoArgs, boolean storeDist); } diff --git a/src/main/java/io/lettuce/core/protocol/CommandType.java b/src/main/java/io/lettuce/core/protocol/CommandType.java index 50ea48aada..b8b4b75d0b 100644 --- a/src/main/java/io/lettuce/core/protocol/CommandType.java +++ b/src/main/java/io/lettuce/core/protocol/CommandType.java @@ -89,8 +89,7 @@ public enum CommandType implements ProtocolKeyword { BITCOUNT, BITFIELD, BITOP, GETBIT, SETBIT, BITPOS, // Geo - - GEOADD, GEORADIUS, GEORADIUS_RO, GEORADIUSBYMEMBER, GEORADIUSBYMEMBER_RO, GEOENCODE, GEODECODE, GEOPOS, GEODIST, GEOHASH, + GEOADD, GEORADIUS, GEORADIUS_RO, GEORADIUSBYMEMBER, GEORADIUSBYMEMBER_RO, GEOENCODE, GEODECODE, GEOPOS, GEODIST, GEOHASH, GEOSEARCH, GEOSEARCHSTORE, // Stream diff --git a/src/main/kotlin/io/lettuce/core/api/coroutines/RedisGeoCoroutinesCommands.kt b/src/main/kotlin/io/lettuce/core/api/coroutines/RedisGeoCoroutinesCommands.kt index d68d733624..c5bb381f9e 100644 --- a/src/main/kotlin/io/lettuce/core/api/coroutines/RedisGeoCoroutinesCommands.kt +++ b/src/main/kotlin/io/lettuce/core/api/coroutines/RedisGeoCoroutinesCommands.kt @@ -161,5 +161,61 @@ interface RedisGeoCoroutinesCommands { */ suspend fun geodist(key: K, from: V, to: V, unit: GeoArgs.Unit): Double? + /** + * Retrieve members selected by distance with the center of `reference` the search `predicate`. + * Use [GeoSearch] to create reference and predicate objects. + * + * @param key the key of the geo set. + * @param reference the reference member or longitude/latitude coordinates. + * @param predicate the bounding box or radius to search in. + * @return bulk reply. + * @since 6.1 + */ + fun geosearch( + key: K, + reference: GeoSearch.GeoRef, + predicate: GeoSearch.GeoPredicate + ): Flow + + /** + * Retrieve members selected by distance with the center of `reference` the search `predicate`. + * Use [GeoSearch] to create reference and predicate objects. + * + * @param key the key of the geo set. + * @param reference the reference member or longitude/latitude coordinates. + * @param predicate the bounding box or radius to search in. + * @param geoArgs args to control the result. + * @return nested multi-bulk reply. The [GeoWithin] contains only fields which were requested by [GeoArgs]. + * @since 6.1 + */ + fun geosearch( + key: K, + reference: GeoSearch.GeoRef, + predicate: GeoSearch.GeoPredicate, + geoArgs: GeoArgs + ): Flow> + + /** + * Perform a [geosearch(Any, GeoSearch.GeoRef, GeoSearch.GeoPredicate, GeoArgs)] query and store the results in a + * sorted set. + * + * @param destination the destination where to store results. + * @param key the key of the geo set. + * @param reference the reference member or longitude/latitude coordinates. + * @param predicate the bounding box or radius to search in. + * @param geoArgs args to control the result. + * @param storeDist stores the items in a sorted set populated with their distance from the center of the circle or box, as a floating-point number, in the same unit specified for that shape. + * @return Long integer-reply the number of elements in the result. + * @since 6.1 + */ + suspend fun geosearchstore( + destination: K, + key: K, + reference: GeoSearch.GeoRef, + predicate: GeoSearch.GeoPredicate, + geoArgs: GeoArgs, + storeDist: Boolean + ): Long? + } diff --git a/src/main/kotlin/io/lettuce/core/api/coroutines/RedisGeoCoroutinesCommandsImpl.kt b/src/main/kotlin/io/lettuce/core/api/coroutines/RedisGeoCoroutinesCommandsImpl.kt index 1e9a72e3e0..3859d3481d 100644 --- a/src/main/kotlin/io/lettuce/core/api/coroutines/RedisGeoCoroutinesCommandsImpl.kt +++ b/src/main/kotlin/io/lettuce/core/api/coroutines/RedisGeoCoroutinesCommandsImpl.kt @@ -28,6 +28,7 @@ import kotlinx.coroutines.reactive.awaitFirstOrNull * Coroutine executed commands (based on reactive commands) for the Geo-API. * * @author Mikhael Sokolov + * @author Mark Paluch * @since 6.0 */ @ExperimentalLettuceCoroutinesApi @@ -45,15 +46,59 @@ internal class RedisGeoCoroutinesCommandsImpl(internal val ops override suspend fun georadius(key: K, longitude: Double, latitude: Double, distance: Double, unit: GeoArgs.Unit, geoRadiusStoreArgs: GeoRadiusStoreArgs): Long? = ops.georadius(key, longitude, latitude, distance, unit, geoRadiusStoreArgs).awaitFirstOrNull() - override fun georadiusbymember(key: K, member: V, distance: Double, unit: GeoArgs.Unit): Flow = ops.georadiusbymember(key, member, distance, unit).asFlow() - - override fun georadiusbymember(key: K, member: V, distance: Double, unit: GeoArgs.Unit, geoArgs: GeoArgs): Flow> = ops.georadiusbymember(key, member, distance, unit, geoArgs).asFlow() - - override suspend fun georadiusbymember(key: K, member: V, distance: Double, unit: GeoArgs.Unit, geoRadiusStoreArgs: GeoRadiusStoreArgs): Long? = ops.georadiusbymember(key, member, distance, unit, geoRadiusStoreArgs).awaitFirstOrNull() - - override suspend fun geopos(key: K, vararg members: V): List = ops.geopos(key, *members).map { it.value }.asFlow().toList() - - override suspend fun geodist(key: K, from: V, to: V, unit: GeoArgs.Unit): Double? = ops.geodist(key, from, to, unit).awaitFirstOrNull() - + override fun georadiusbymember( + key: K, + member: V, + distance: Double, + unit: GeoArgs.Unit + ): Flow = ops.georadiusbymember(key, member, distance, unit).asFlow() + + override fun georadiusbymember( + key: K, + member: V, + distance: Double, + unit: GeoArgs.Unit, + geoArgs: GeoArgs + ): Flow> = + ops.georadiusbymember(key, member, distance, unit, geoArgs).asFlow() + + override suspend fun georadiusbymember( + key: K, + member: V, + distance: Double, + unit: GeoArgs.Unit, + geoRadiusStoreArgs: GeoRadiusStoreArgs + ): Long? = ops.georadiusbymember(key, member, distance, unit, geoRadiusStoreArgs) + .awaitFirstOrNull() + + override suspend fun geopos(key: K, vararg members: V): List = + ops.geopos(key, *members).map { it.value }.asFlow().toList() + + override suspend fun geodist(key: K, from: V, to: V, unit: GeoArgs.Unit): Double? = + ops.geodist(key, from, to, unit).awaitFirstOrNull() + + override fun geosearch( + key: K, + reference: GeoSearch.GeoRef, + predicate: GeoSearch.GeoPredicate + ): Flow = ops.geosearch(key, reference, predicate).asFlow() + + override fun geosearch( + key: K, + reference: GeoSearch.GeoRef, + predicate: GeoSearch.GeoPredicate, + geoArgs: GeoArgs + ): Flow> = ops.geosearch(key, reference, predicate, geoArgs).asFlow() + + override suspend fun geosearchstore( + destination: K, + key: K, + reference: GeoSearch.GeoRef, + predicate: GeoSearch.GeoPredicate, + geoArgs: GeoArgs, + storeDist: Boolean + ): Long? = + ops.geosearchstore(destination, key, reference, predicate, geoArgs, storeDist) + .awaitFirstOrNull() } diff --git a/src/main/templates/io/lettuce/core/api/RedisGeoCommands.java b/src/main/templates/io/lettuce/core/api/RedisGeoCommands.java index de945c773d..4edd317cf5 100644 --- a/src/main/templates/io/lettuce/core/api/RedisGeoCommands.java +++ b/src/main/templates/io/lettuce/core/api/RedisGeoCommands.java @@ -160,4 +160,45 @@ Long georadius(K key, double longitude, double latitude, double distance, GeoArg */ Double geodist(K key, V from, V to, GeoArgs.Unit unit); + /** + * Retrieve members selected by distance with the center of {@code reference} the search {@code predicate}. + * Use {@link GeoSearch} to create reference and predicate objects. + * + * @param key the key of the geo set. + * @param reference the reference member or longitude/latitude coordinates. + * @param predicate the bounding box or radius to search in. + * @return bulk reply. + * @since 6.1 + */ + Set geosearch(K key, GeoSearch.GeoRef reference, GeoSearch.GeoPredicate predicate); + + /** + * Retrieve members selected by distance with the center of {@code reference} the search {@code predicate}. + * Use {@link GeoSearch} to create reference and predicate objects. + * + * @param key the key of the geo set. + * @param reference the reference member or longitude/latitude coordinates. + * @param predicate the bounding box or radius to search in. + * @param geoArgs args to control the result. + * @return nested multi-bulk reply. The {@link GeoWithin} contains only fields which were requested by {@link GeoArgs}. + * @since 6.1 + */ + List> geosearch(K key, GeoSearch.GeoRef reference, GeoSearch.GeoPredicate predicate, GeoArgs geoArgs); + + /** + * Perform a {@link #geosearch(Object, GeoSearch.GeoRef, GeoSearch.GeoPredicate, GeoArgs)} query and store the results in a + * sorted set. + * + * @param destination the destination where to store results. + * @param key the key of the geo set. + * @param reference the reference member or longitude/latitude coordinates. + * @param predicate the bounding box or radius to search in. + * @param geoArgs args to control the result. + * @param storeDist stores the items in a sorted set populated with their distance from the center of the circle or box, as a floating-point number, in the same unit specified for that shape. + * @return Long integer-reply the number of elements in the result. + * @since 6.1 + */ + Long geosearchstore(K destination, K key, GeoSearch.GeoRef reference, GeoSearch.GeoPredicate predicate, GeoArgs geoArgs, + boolean storeDist); + } diff --git a/src/test/java/io/lettuce/apigenerator/KotlinCompilationUnitFactory.java b/src/test/java/io/lettuce/apigenerator/KotlinCompilationUnitFactory.java index eb895dcb48..f2c4830931 100644 --- a/src/test/java/io/lettuce/apigenerator/KotlinCompilationUnitFactory.java +++ b/src/test/java/io/lettuce/apigenerator/KotlinCompilationUnitFactory.java @@ -59,7 +59,9 @@ class KotlinCompilationUnitFactory { private static final Set SKIP_IMPORTS = LettuceSets.unmodifiableSet("java.util.List", "java.util.Set", "java.util.Map"); private static final Set NON_SUSPENDABLE_METHODS = LettuceSets.unmodifiableSet("isOpen", "flushCommands", "setAutoFlushCommands"); private static final Set SKIP_METHODS = LettuceSets.unmodifiableSet("BaseRedisCommands.reset", "getStatefulConnection"); - private static final Set FLOW_METHODS = LettuceSets.unmodifiableSet("dispatch", "geohash", "georadius", "georadiusbymember", + + private static final Set FLOW_METHODS = LettuceSets.unmodifiableSet("dispatch", "geohash", "georadius", + "georadiusbymember", "geosearch", "hgetall", "hkeys", "hmget", "hvals", "keys", "mget", "sdiff", "sinter", "smembers", "smismember", "sort", "srandmember", "sunion", "xclaim", "xpending", "xrange", "xread", "xreadgroup", "xrevrange", "zdiff", "zdiffWithScores", "zinter", "zinterWithScores", "zpopmax", "zpopmin", "zrange", "zrangeWithScores", "zrangebylex", "zrangebyscore", "zrangebyscoreWithScores", "zrevrange", "zrevrangeWithScores", "zrevrangebylex", diff --git a/src/test/java/io/lettuce/core/commands/GeoCommandIntegrationTests.java b/src/test/java/io/lettuce/core/commands/GeoCommandIntegrationTests.java index 0892f77273..6b3aee37a6 100644 --- a/src/test/java/io/lettuce/core/commands/GeoCommandIntegrationTests.java +++ b/src/test/java/io/lettuce/core/commands/GeoCommandIntegrationTests.java @@ -15,10 +15,8 @@ */ package io.lettuce.core.commands; -import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.assertThatThrownBy; -import static org.assertj.core.api.Assertions.offset; -import static org.junit.jupiter.api.Assumptions.assumeTrue; +import static org.assertj.core.api.Assertions.*; +import static org.junit.jupiter.api.Assumptions.*; import java.util.List; import java.util.Set; @@ -30,7 +28,15 @@ import org.junit.jupiter.api.TestInstance; import org.junit.jupiter.api.extension.ExtendWith; -import io.lettuce.core.*; +import io.lettuce.core.GeoArgs; +import io.lettuce.core.GeoCoordinates; +import io.lettuce.core.GeoRadiusStoreArgs; +import io.lettuce.core.GeoSearch; +import io.lettuce.core.GeoWithin; +import io.lettuce.core.ScoredValue; +import io.lettuce.core.TestSupport; +import io.lettuce.core.TransactionResult; +import io.lettuce.core.Value; import io.lettuce.core.api.sync.RedisCommands; import io.lettuce.test.LettuceExtension; import io.lettuce.test.condition.EnabledOnCommand; @@ -516,6 +522,58 @@ void georadiusStorebymemberWithNullArgs() { .isInstanceOf(IllegalArgumentException.class); } + @Test + @EnabledOnCommand("GEOSEARCH") + void geosearchWithCountAndSort() { + + prepareGeo(); + + Set empty = redis.geosearch(key, GeoSearch.fromMember("Bahn"), GeoSearch.byRadius(1, GeoArgs.Unit.km)); + assertThat(empty).hasSize(1).contains("Bahn"); + + Set radius = redis.geosearch(key, GeoSearch.fromMember("Bahn"), GeoSearch.byRadius(5, GeoArgs.Unit.km)); + assertThat(radius).hasSize(2).contains("Bahn", "Weinheim"); + + Set box = redis.geosearch(key, GeoSearch.fromMember("Bahn"), GeoSearch.byBox(6, 3, GeoArgs.Unit.km)); + assertThat(box).hasSize(2).contains("Bahn", "Weinheim"); + } + + @Test + @EnabledOnCommand("GEOSEARCH") + void geosearchWithArgs() { + + prepareGeo(); + + List> empty = redis.geosearch(key, GeoSearch.fromMember("Bahn"), + GeoSearch.byRadius(1, GeoArgs.Unit.km), new GeoArgs().withHash().withCoordinates().withDistance().desc()); + assertThat(empty).isNotEmpty(); + + List> withDistanceAndCoordinates = redis.geosearch(key, GeoSearch.fromMember("Bahn"), + GeoSearch.byRadius(5, GeoArgs.Unit.km), new GeoArgs().withCoordinates().withDistance().desc()); + assertThat(withDistanceAndCoordinates).hasSize(2); + + GeoWithin weinheim = withDistanceAndCoordinates.get(0); + assertThat(weinheim.getMember()).isEqualTo("Weinheim"); + assertThat(weinheim.getGeohash()).isNull(); + assertThat(weinheim.getDistance()).isNotNull(); + assertThat(weinheim.getCoordinates()).isNotNull(); + } + + @Test + @EnabledOnCommand("GEOSEARCHSTORE") + void geosearchStoreWithCountAndSort() { + + prepareGeo(); + + String resultKey = "38o54"; // yields in same slot as "key" + Long result = redis.geosearchstore(resultKey, key, GeoSearch.fromMember("Bahn"), GeoSearch.byRadius(5, GeoArgs.Unit.km), + new GeoArgs(), true); + assertThat(result).isEqualTo(2); + + List> dist = redis.zrangeWithScores(resultKey, 0, -1); + assertThat(dist).hasSize(2); + } + protected void prepareGeo() { redis.geoadd(key, 8.6638775, 49.5282537, "Weinheim"); redis.geoadd(key, 8.3796281, 48.9978127, "EFS9", 8.665351, 49.553302, "Bahn");