diff --git a/pom.xml b/pom.xml index 8657b1992a..77764a826a 100644 --- a/pom.xml +++ b/pom.xml @@ -1,11 +1,13 @@ - + 4.0.0 org.springframework.data spring-data-redis - 2.6.0-SNAPSHOT + 2.6.0-2043-SNAPSHOT Spring Data Redis diff --git a/src/main/asciidoc/appendix/appendix-command-reference.adoc b/src/main/asciidoc/appendix/appendix-command-reference.adoc index 4d8faadc07..0b571a3b04 100644 --- a/src/main/asciidoc/appendix/appendix-command-reference.adoc +++ b/src/main/asciidoc/appendix/appendix-command-reference.adoc @@ -48,6 +48,14 @@ |EXPIREAT |X |FLUSHALL |X |FLUSHDB |X +|GEOADD |X +|GEODIST |X +|GEOHASH |X +|GEOPOS |X +|GEORADIUS |X +|GEORADIUSBYMEMBER |X +|GEOSEARCH |X +|GEOSEARCHSTORE |X |GET |X |GETBIT |X |GETRANGE |X diff --git a/src/main/asciidoc/new-features.adoc b/src/main/asciidoc/new-features.adoc index f35f2b2a1e..428aa0dc62 100644 --- a/src/main/asciidoc/new-features.adoc +++ b/src/main/asciidoc/new-features.adoc @@ -7,7 +7,7 @@ This section briefly covers items that are new and noteworthy in the latest rele == New in Spring Data Redis 2.6 * Support for `SubscriptionListener` when using `MessageListener` for subscription confirmation callbacks. `ReactiveRedisMessageListenerContainer` and `ReactiveRedisOperations` provide `receiveLater(…)` and `listenToLater(…)` methods to await until Redis acknowledges the subscription. -* Support Redis 6.2 commands (`LPOP`/`RPOP` with `count`, `LMOVE`/`BLMOVE`, `COPY`, `GETEX`, `GETDEL`, `ZPOPMIN`, `BZPOPMIN`, `ZPOPMAX`, `BZPOPMAX`, `ZMSCORE`, `ZDIFF`, `ZDIFFSTORE`, `ZINTER`, `ZUNION`, `HRANDFIELD`, `ZRANDMEMBER`, `SMISMEMBER`). +* Support Redis 6.2 commands (`LPOP`/`RPOP` with `count`, `LMOVE`/`BLMOVE`, `COPY`, `GETEX`, `GETDEL`, `GEOSEARCH`, `GEOSEARCHSTORE`, `ZPOPMIN`, `BZPOPMIN`, `ZPOPMAX`, `BZPOPMAX`, `ZMSCORE`, `ZDIFF`, `ZDIFFSTORE`, `ZINTER`, `ZUNION`, `HRANDFIELD`, `ZRANDMEMBER`, `SMISMEMBER`). [[new-in-2.5.0]] == New in Spring Data Redis 2.5 diff --git a/src/main/java/org/springframework/data/redis/connection/DefaultStringRedisConnection.java b/src/main/java/org/springframework/data/redis/connection/DefaultStringRedisConnection.java index d9419e8ffc..22d8f8fc2b 100644 --- a/src/main/java/org/springframework/data/redis/connection/DefaultStringRedisConnection.java +++ b/src/main/java/org/springframework/data/redis/connection/DefaultStringRedisConnection.java @@ -53,6 +53,7 @@ import org.springframework.data.redis.core.ScanOptions; import org.springframework.data.redis.core.types.Expiration; import org.springframework.data.redis.core.types.RedisClientInfo; +import org.springframework.data.redis.domain.geo.GeoShape; import org.springframework.data.redis.serializer.RedisSerializer; import org.springframework.data.redis.serializer.StringRedisSerializer; import org.springframework.lang.Nullable; @@ -2120,6 +2121,14 @@ private byte[] serialize(String data) { return serializer.serialize(data); } + @SuppressWarnings("unchecked") + private GeoReference serialize(GeoReference data) { + return data instanceof GeoReference.GeoSearchMemberReference + ? GeoReference + .fromMember(serializer.serialize(((GeoReference.GeoSearchMemberReference) data).getMember())) + : (GeoReference) data; + } + @SuppressWarnings("unchecked") private StreamOffset[] serialize(StreamOffset[] offsets) { @@ -3886,6 +3895,50 @@ public Long geoRemove(String key, String... members) { return geoRemove(serialize(key), serializeMulti(members)); } + /* + * (non-Javadoc) + * @see org.springframework.data.redis.connection.RedisGeoCommands#geoSearch(byte[], byte[], org.springframework.data.redis.connection.RedisGeoCommands.GeoShape, org.springframework.data.redis.connection.RedisGeoCommands.GeoSearchCommandArgs) + */ + @Override + public GeoResults> geoSearch(byte[] key, GeoReference reference, GeoShape predicate, + GeoSearchCommandArgs args) { + return convertAndReturn(delegate.geoSearch(key, reference, predicate, args), Converters.identityConverter()); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.redis.connection.RedisGeoCommands#geoSearchStore(byte[], byte[], byte[], org.springframework.data.redis.connection.RedisGeoCommands.GeoShape, org.springframework.data.redis.connection.RedisGeoCommands.GeoSearchStoreCommandArgs) + */ + @Override + public Long geoSearchStore(byte[] destKey, byte[] key, GeoReference reference, GeoShape predicate, + GeoSearchStoreCommandArgs args) { + return convertAndReturn(delegate.geoSearchStore(destKey, key, reference, predicate, args), + Converters.identityConverter()); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.redis.connection.StringRedisConnection#geoSearch(java.lang.String, java.lang.String, org.springframework.data.redis.connection.RedisGeoCommands.GeoShape, org.springframework.data.redis.connection.RedisGeoCommands.GeoSearchCommandArgs) + */ + @Override + public GeoResults> geoSearch(String key, GeoReference reference, GeoShape predicate, + GeoSearchCommandArgs args) { + return convertAndReturn(delegate.geoSearch(serialize(key), serialize(reference), predicate, args), + byteGeoResultsToStringGeoResults); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.redis.connection.StringRedisConnection#geoSearchStore(java.lang.String, java.lang.String, java.lang.String, org.springframework.data.redis.connection.RedisGeoCommands.GeoShape, org.springframework.data.redis.connection.RedisGeoCommands.GeoSearchStoreCommandArgs) + */ + @Override + public Long geoSearchStore(String destKey, String key, GeoReference reference, GeoShape predicate, + GeoSearchStoreCommandArgs args) { + return convertAndReturn( + delegate.geoSearchStore(serialize(destKey), serialize(key), serialize(reference), predicate, args), + Converters.identityConverter()); + } + /* * (non-Javadoc) * @see org.springframework.data.redis.connection.RedisConnection#closePipeline() diff --git a/src/main/java/org/springframework/data/redis/connection/DefaultedRedisConnection.java b/src/main/java/org/springframework/data/redis/connection/DefaultedRedisConnection.java index 9035009f95..c8a9c9db82 100644 --- a/src/main/java/org/springframework/data/redis/connection/DefaultedRedisConnection.java +++ b/src/main/java/org/springframework/data/redis/connection/DefaultedRedisConnection.java @@ -44,6 +44,7 @@ import org.springframework.data.redis.core.ScanOptions; import org.springframework.data.redis.core.types.Expiration; import org.springframework.data.redis.core.types.RedisClientInfo; +import org.springframework.data.redis.domain.geo.GeoShape; import org.springframework.lang.Nullable; /** @@ -1540,6 +1541,22 @@ default Long geoRemove(byte[] key, byte[]... members) { return geoCommands().geoRemove(key, members); } + /** @deprecated in favor of {@link RedisConnection#geoCommands()}}. */ + @Override + @Deprecated + default GeoResults> geoSearch(byte[] key, GeoReference reference, GeoShape predicate, + GeoSearchCommandArgs args) { + return geoCommands().geoSearch(key, reference, predicate, args); + } + + /** @deprecated in favor of {@link RedisConnection#geoCommands()}}. */ + @Override + @Deprecated + default Long geoSearchStore(byte[] destKey, byte[] key, GeoReference reference, GeoShape predicate, + GeoSearchStoreCommandArgs args) { + return geoCommands().geoSearchStore(destKey, key, reference, predicate, args); + } + // HLL COMMANDS /** @deprecated in favor of {@link RedisConnection#hyperLogLogCommands()}. */ diff --git a/src/main/java/org/springframework/data/redis/connection/ReactiveGeoCommands.java b/src/main/java/org/springframework/data/redis/connection/ReactiveGeoCommands.java index 3fe3dc6f2e..245996cdbe 100644 --- a/src/main/java/org/springframework/data/redis/connection/ReactiveGeoCommands.java +++ b/src/main/java/org/springframework/data/redis/connection/ReactiveGeoCommands.java @@ -15,6 +15,9 @@ */ package org.springframework.data.redis.connection; +import static org.springframework.data.redis.connection.RedisGeoCommands.*; + +import org.springframework.data.redis.domain.geo.GeoShape; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; @@ -27,6 +30,7 @@ import java.util.Set; import org.reactivestreams.Publisher; + import org.springframework.data.domain.Sort.Direction; import org.springframework.data.geo.Circle; import org.springframework.data.geo.Distance; @@ -37,9 +41,6 @@ import org.springframework.data.redis.connection.ReactiveRedisConnection.KeyCommand; import org.springframework.data.redis.connection.ReactiveRedisConnection.MultiValueResponse; import org.springframework.data.redis.connection.ReactiveRedisConnection.NumericResponse; -import org.springframework.data.redis.connection.RedisGeoCommands.DistanceUnit; -import org.springframework.data.redis.connection.RedisGeoCommands.GeoLocation; -import org.springframework.data.redis.connection.RedisGeoCommands.GeoRadiusCommandArgs; import org.springframework.data.redis.connection.RedisGeoCommands.GeoRadiusCommandArgs.Flag; import org.springframework.lang.Nullable; import org.springframework.util.Assert; @@ -672,7 +673,7 @@ public GeoRadiusCommand withFlag(Flag flag) { Assert.notNull(flag, "Flag must not be null!"); GeoRadiusCommandArgs args = cloneArgs(); - args.flags.add(flag); + args.getFlags().add(flag); return new GeoRadiusCommand(getKey(), point, distance, args, store, storeDist); } @@ -735,7 +736,7 @@ public GeoRadiusCommand sort(Direction direction) { Assert.notNull(direction, "Direction must not be null!"); GeoRadiusCommandArgs args = cloneArgs(); - args.sortDirection = direction; + args.sort(direction); return new GeoRadiusCommand(getKey(), point, distance, args, store, storeDist); } @@ -1007,7 +1008,7 @@ public GeoRadiusByMemberCommand withFlag(Flag flag) { Assert.notNull(flag, "Flag must not be null!"); GeoRadiusCommandArgs args = cloneArgs(); - args.flags.add(flag); + args.getFlags().add(flag); return new GeoRadiusByMemberCommand(getKey(), member, distance, args, store, storeDist); } @@ -1069,7 +1070,7 @@ public GeoRadiusByMemberCommand sort(Direction direction) { Assert.notNull(direction, "Direction must not be null!"); GeoRadiusCommandArgs args = cloneArgs(); - args.sortDirection = direction; + args.sort(direction); return new GeoRadiusByMemberCommand(getKey(), member, distance, args, store, storeDist); } @@ -1239,4 +1240,261 @@ default Flux>> geoRadiusByMember(ByteBuffer ke */ Flux>>>> geoRadiusByMember( Publisher commands); + + /** + * {@code GEOSEARCH} command parameters. + * + * @author Mark Paluch + * @since 2.6 + * @see Redis Documentation: GEOSEARCH + */ + class GeoSearchCommand extends KeyCommand { + + private final @Nullable GeoReference reference; + private final @Nullable GeoShape shape; + private final @Nullable GeoSearchCommandArgs args; + + private GeoSearchCommand(@Nullable ByteBuffer key, @Nullable GeoReference reference, + @Nullable GeoShape shape, @Nullable GeoSearchCommandArgs args) { + super(key); + this.reference = reference; + this.shape = shape; + this.args = args; + } + + /** + * Creates a new {@link GeoSearchCommand} given a {@link GeoShape}. + * + * @param shape must not be {@literal null}. + * @return a new {@link GeoSearchCommand} for a {@link GeoShape}. + */ + public static GeoSearchCommand within(GeoShape shape) { + + Assert.notNull(shape, "GeoShape must not be null!"); + + return new GeoSearchCommand(null, null, shape, null); + } + + /** + * Sets the geoset {@literal key}. Constructs a new command instance with all previously configured properties. + * + * @param member must not be {@literal null}. + * @return a new {@link GeoSearchCommand} with {@literal key} applied. + */ + public GeoSearchCommand at(GeoReference reference) { + + Assert.notNull(reference, "GeoReference must not be null!"); + + return new GeoSearchCommand(getKey(), reference, getShape(), args); + } + + /** + * Sets the geoset {@literal key}. Constructs a new command instance with all previously configured properties. + * + * @param member must not be {@literal null}. + * @return a new {@link GeoSearchCommand} with {@literal key} applied. + */ + public GeoSearchCommand in(ByteBuffer key) { + + Assert.notNull(key, "Key must not be null!"); + + return new GeoSearchCommand(key, getReference(), getShape(), args); + } + + /** + * Sets the command {@literal args}. Constructs a new command instance with all previously configured properties. + * + * @param args must not be {@literal null}. + * @return a new {@link GeoSearchCommand} with {@literal args} applied. + */ + public GeoSearchCommand with(GeoSearchCommandArgs args) { + + Assert.notNull(args, "Args must not be null!"); + + return new GeoSearchCommand(getKey(), getReference(), getShape(), args); + } + + public Optional getArgs() { + return Optional.ofNullable(args); + } + + @Nullable + public GeoReference getReference() { + return reference; + } + + @Nullable + public GeoShape getShape() { + return shape; + } + } + + /** + * {@code GEOSEARCHSTORE} command parameters. + * + * @author Mark Paluch + * @since 2.6 + * @see Redis Documentation: GEOSEARCHSTORE + */ + class GeoSearchStoreCommand extends KeyCommand { + + private final @Nullable ByteBuffer destKey; + private final @Nullable GeoReference reference; + private final @Nullable GeoShape shape; + private final @Nullable GeoSearchStoreCommandArgs args; + + private GeoSearchStoreCommand(@Nullable ByteBuffer key, @Nullable ByteBuffer destKey, + @Nullable GeoReference reference, @Nullable GeoShape shape, + @Nullable GeoSearchStoreCommandArgs args) { + super(key); + this.destKey = destKey; + this.reference = reference; + this.shape = shape; + this.args = args; + } + + /** + * Creates a new {@link GeoSearchStoreCommand} given a {@link GeoShape}. + * + * @param shape must not be {@literal null}. + * @return a new {@link GeoSearchStoreCommand} for a {@link GeoShape}. + */ + public static GeoSearchStoreCommand within(GeoShape shape) { + + Assert.notNull(shape, "GeoShape must not be null!"); + + return new GeoSearchStoreCommand(null, null, null, shape, null); + } + + /** + * Sets the geoset {@literal key}. Constructs a new command instance with all previously configured properties. + * + * @param member must not be {@literal null}. + * @return a new {@link GeoSearchStoreCommand} with {@literal key} applied. + */ + public GeoSearchStoreCommand at(GeoReference reference) { + + Assert.notNull(reference, "GeoReference must not be null!"); + + return new GeoSearchStoreCommand(getKey(), getDestKey(), reference, getShape(), args); + } + + /** + * Sets the geoset {@literal key}. Constructs a new command instance with all previously configured properties. + * + * @param member must not be {@literal null}. + * @return a new {@link GeoSearchStoreCommand} with {@literal key} applied. + */ + public GeoSearchStoreCommand in(ByteBuffer key) { + + Assert.notNull(key, "Key must not be null!"); + + return new GeoSearchStoreCommand(key, getDestKey(), getReference(), getShape(), args); + } + + /** + * Sets the geoset {@literal destKey}. Constructs a new command instance with all previously configured properties. + * + * @param member must not be {@literal null}. + * @return a new {@link GeoSearchStoreCommand} with {@literal destKey} applied. + */ + public GeoSearchStoreCommand storeAt(ByteBuffer destKey) { + + Assert.notNull(destKey, "Destination key must not be null!"); + + return new GeoSearchStoreCommand(getKey(), destKey, getReference(), getShape(), args); + } + + /** + * Sets the command {@literal args}. Constructs a new command instance with all previously configured properties. + * + * @param args must not be {@literal null}. + * @return a new {@link GeoSearchStoreCommand} with {@literal args} applied. + */ + public GeoSearchStoreCommand with(GeoSearchStoreCommandArgs args) { + + Assert.notNull(args, "Args must not be null!"); + + return new GeoSearchStoreCommand(getKey(), getDestKey(), getReference(), getShape(), args); + } + + @Nullable + public ByteBuffer getDestKey() { + return destKey; + } + + public Optional getArgs() { + return Optional.ofNullable(args); + } + + @Nullable + public GeoReference getReference() { + return reference; + } + + @Nullable + public GeoShape getShape() { + return shape; + } + } + + /** + * Return the members of a geo set which are within the borders of the area specified by a given {@link GeoShape + * shape}. The query's center point is provided by {@link GeoReference}. + * + * @param key must not be {@literal null}. + * @param reference must not be {@literal null}. + * @param shape must not be {@literal null}. + * @param args must not be {@literal null}. + * @return + * @since 2.6 + * @see Redis Documentation: GEOSEARCH + */ + default Flux>> geoSearch(ByteBuffer key, GeoReference reference, + GeoShape shape, GeoSearchCommandArgs args) { + return geoSearch(Mono.just(GeoSearchCommand.within(shape).in(key).at(reference).with(args))) + .flatMap(CommandResponse::getOutput); + } + + /** + * Get the {@literal member}s within given {@link GeoShape} from {@link GeoReference} applying given parameters. + * + * @param commands must not be {@literal null}. + * @return + * @since 2.6 + * @see Redis Documentation: GEOSEARCH + */ + Flux>>>> geoSearch( + Publisher commands); + + /** + * Query the members of a geo set which are within the borders of the area specified by a given {@link GeoShape shape} + * and store the result at {@code destKey}. The query's center point is provided by {@link GeoReference}. + * + * @param key must not be {@literal null}. + * @param reference must not be {@literal null}. + * @param shape must not be {@literal null}. + * @param args must not be {@literal null}. + * @return + * @since 2.6 + * @see Redis Documentation: GEOSEARCH + */ + default Mono geoSearchStore(ByteBuffer destKey, ByteBuffer key, GeoReference reference, + GeoShape shape, GeoSearchStoreCommandArgs args) { + return geoSearchStore( + Mono.just(GeoSearchStoreCommand.within(shape).in(key).storeAt(destKey).at(reference).with(args))).next() + .map(CommandResponse::getOutput); + } + + /** + * Store the {@literal member}s within given {@link GeoShape} from {@link GeoReference} applying given parameters in a + * new geo set. + * + * @param commands must not be {@literal null}. + * @return + * @since 2.6 + * @see Redis Documentation: GEOSEARCHSTORE + */ + Flux> geoSearchStore(Publisher commands); + } diff --git a/src/main/java/org/springframework/data/redis/connection/RedisGeoCommands.java b/src/main/java/org/springframework/data/redis/connection/RedisGeoCommands.java index b250d0d4fb..cacac91e3e 100644 --- a/src/main/java/org/springframework/data/redis/connection/RedisGeoCommands.java +++ b/src/main/java/org/springframework/data/redis/connection/RedisGeoCommands.java @@ -15,10 +15,13 @@ */ package org.springframework.data.redis.connection; +import static org.springframework.data.redis.connection.RedisGeoCommands.GeoRadiusCommandArgs.*; + import java.util.LinkedHashSet; import java.util.List; import java.util.Map; import java.util.Set; +import java.util.stream.Collectors; import org.springframework.data.domain.Sort.Direction; import org.springframework.data.geo.Circle; @@ -26,6 +29,7 @@ import org.springframework.data.geo.GeoResults; import org.springframework.data.geo.Metric; import org.springframework.data.geo.Point; +import org.springframework.data.redis.domain.geo.GeoShape; import org.springframework.lang.Nullable; import org.springframework.util.Assert; import org.springframework.util.ObjectUtils; @@ -215,28 +219,131 @@ GeoResults> geoRadiusByMember(byte[] key, byte[] member, Dis @Nullable Long geoRemove(byte[] key, byte[]... members); + /** + * Return the members of a geo set which are within the borders of the area specified by a given {@link GeoShape + * shape}. The query's center point is provided by {@link GeoReference}. + * + * @param key must not be {@literal null}. + * @param reference must not be {@literal null}. + * @param predicate must not be {@literal null}. + * @param args must not be {@literal null}. + * @return {@literal null} when used in pipeline / transaction. + * @since 2.6 + * @see Redis Documentation: GEOSEARCH + */ + @Nullable + GeoResults> geoSearch(byte[] key, GeoReference reference, GeoShape predicate, + GeoSearchCommandArgs args); + + /** + * Query the members of a geo set which are within the borders of the area specified by a given {@link GeoShape shape} + * and store the result at {@code destKey}. The query's center point is provided by {@link GeoReference}. + * + * @param key must not be {@literal null}. + * @param reference must not be {@literal null}. + * @param predicate must not be {@literal null}. + * @param args must not be {@literal null}. + * @return {@literal null} when used in pipeline / transaction. + * @since 2.6 + * @see Redis Documentation: GEOSEARCH + */ + @Nullable + Long geoSearchStore(byte[] destKey, byte[] key, GeoReference reference, GeoShape predicate, + GeoSearchStoreCommandArgs args); + + /** + * Arguments to be used with {@link RedisGeoCommands}. + * + * @author Christoph Strobl + * @since 2.6 + */ + interface GeoCommandArgs { + + /** + * @return can be {@literal null}. + */ + @Nullable + public Direction getSortDirection(); + + /** + * @return can be {@literal null}. + */ + @Nullable + Long getLimit(); + + /** + * @return never {@literal null}. + */ + Set getFlags(); + + /** + * @return {@literal true} if {@literal limit} has been set. + */ + default boolean hasLimit() { + return getLimit() != null; + } + + /** + * @return {@literal true} if {@literal sort} has been set. + */ + default boolean hasSortDirection() { + return getSortDirection() != null; + } + + /** + * @return {@literal true} if {@literal flags} is not empty. + */ + default boolean hasFlags() { + return !getFlags().isEmpty(); + } + + /** + * A flag to be used. + */ + interface GeoCommandFlag { + + static GeoCommandFlag any() { + return Flag.ANY; + } + + static GeoCommandFlag withCord() { + return Flag.WITHCOORD; + } + + static GeoCommandFlag withDist() { + return Flag.WITHDIST; + } + + static GeoCommandFlag storeDist() { + return Flag.STOREDIST; + } + } + } + /** * Additional arguments (like count/sort/...) to be used with {@link RedisGeoCommands}. * - * @author Ninad Divadkar + * @author Mark Paluch * @author Christoph Strobl - * @since 1.8 + * @since 2.6 */ - class GeoRadiusCommandArgs implements Cloneable { + class GeoSearchCommandArgs implements GeoCommandArgs, Cloneable { - Set flags = new LinkedHashSet<>(2, 1); - @Nullable Long limit; - @Nullable Direction sortDirection; + protected final Set flags = new LinkedHashSet<>(2, 1); - private GeoRadiusCommandArgs() {} + @Nullable protected Long limit; + + @Nullable protected Direction sortDirection; + + private GeoSearchCommandArgs() {} /** - * Create new {@link GeoRadiusCommandArgs}. + * Create new {@link GeoSearchCommandArgs}. * * @return never {@literal null}. */ - public static GeoRadiusCommandArgs newGeoRadiusArgs() { - return new GeoRadiusCommandArgs(); + public static GeoSearchCommandArgs newGeoSearchArgs() { + return new GeoSearchCommandArgs(); } /** @@ -244,9 +351,9 @@ public static GeoRadiusCommandArgs newGeoRadiusArgs() { * * @return */ - public GeoRadiusCommandArgs includeCoordinates() { + public GeoSearchCommandArgs includeCoordinates() { - flags.add(Flag.WITHCOORD); + flags.add(GeoCommandFlag.withCord()); return this; } @@ -255,51 +362,74 @@ public GeoRadiusCommandArgs includeCoordinates() { * * @return never {@literal null}. */ - public GeoRadiusCommandArgs includeDistance() { + public GeoSearchCommandArgs includeDistance() { - flags.add(Flag.WITHDIST); + flags.add(GeoCommandFlag.withDist()); return this; } /** - * Sort returned items from the nearest to the furthest, relative to the center. + * Apply a sort direction. * * @return never {@literal null}. */ - public GeoRadiusCommandArgs sortAscending() { + public GeoSearchCommandArgs sort(Direction direction) { - sortDirection = Direction.ASC; + Assert.notNull(direction, "Sort direction must not be null"); + + this.sortDirection = direction; return this; } + /** + * Sort returned items from the nearest to the furthest, relative to the center. + * + * @return never {@literal null}. + */ + public GeoSearchCommandArgs sortAscending() { + return sort(Direction.ASC); + } + /** * Sort returned items from the furthest to the nearest, relative to the center. * * @return never {@literal null}. */ - public GeoRadiusCommandArgs sortDescending() { + public GeoSearchCommandArgs sortDescending() { + return sort(Direction.DESC); + } - sortDirection = Direction.DESC; - return this; + /** + * Limit the results to the first N matching items. + * + * @param count + * @return never {@literal null}. + */ + public GeoSearchCommandArgs limit(long count) { + return limit(count, false); } /** * Limit the results to the first N matching items. * * @param count + * @param any * @return never {@literal null}. */ - public GeoRadiusCommandArgs limit(long count) { + public GeoSearchCommandArgs limit(long count, boolean any) { Assert.isTrue(count > 0, "Count has to positive value."); limit = count; + if (any) { + flags.add(GeoCommandFlag.any()); + } return this; } /** * @return never {@literal null}. */ - public Set getFlags() { + public Set getFlags() { return flags; } @@ -319,87 +449,456 @@ public Direction getSortDirection() { return sortDirection; } - public boolean hasFlags() { - return !flags.isEmpty(); + public boolean hasAnyLimit() { + return hasLimit() && flags.contains(GeoCommandFlag.any()); + } + + @Override + protected GeoSearchCommandArgs clone() { + + GeoSearchCommandArgs that = new GeoSearchCommandArgs(); + that.flags.addAll(this.flags); + that.limit = this.limit; + that.sortDirection = this.sortDirection; + return that; + } + } + + /** + * Additional arguments (like count/sort/...) to be used with {@link RedisGeoCommands}. + * + * @author Mark Paluch + * @since 2.6 + */ + class GeoSearchStoreCommandArgs implements GeoCommandArgs, Cloneable { + + private final Set flags = new LinkedHashSet<>(2, 1); + + @Nullable private Long limit; + + @Nullable private Direction sortDirection; + + private GeoSearchStoreCommandArgs() {} + + /** + * Create new {@link GeoSearchStoreCommandArgs}. + * + * @return never {@literal null}. + */ + public static GeoSearchStoreCommandArgs newGeoSearchStoreArgs() { + return new GeoSearchStoreCommandArgs(); + } + + /** + * Sets the {@link Flag#STOREDIST} flag to also store the distance of the returned items from the specified center. + * + * @return never {@literal null}. + */ + public GeoSearchStoreCommandArgs storeDistance() { + + flags.add(Flag.STOREDIST); + return this; + } + + /** + * Apply a sort direction. + * + * @return never {@literal null}. + */ + public GeoSearchStoreCommandArgs sort(Direction direction) { + + Assert.notNull(direction, "Sort direction must not be null"); + + sortDirection = Direction.ASC; + return this; + } + + /** + * Sort returned items from the nearest to the furthest, relative to the center. + * + * @return never {@literal null}. + */ + public GeoSearchStoreCommandArgs sortAscending() { + return sort(Direction.ASC); + } + + /** + * Sort returned items from the furthest to the nearest, relative to the center. + * + * @return never {@literal null}. + */ + public GeoSearchStoreCommandArgs sortDescending() { + return sort(Direction.DESC); } - public boolean hasSortDirection() { - return sortDirection != null; + /** + * Limit the results to the first N matching items. + * + * @param count + * @return never {@literal null}. + */ + public GeoSearchStoreCommandArgs limit(long count) { + return limit(count, false); } - public boolean hasLimit() { - return limit != null; + /** + * Limit the results to the first N matching items. + * + * @param count + * @param any + * @return never {@literal null}. + */ + public GeoSearchStoreCommandArgs limit(long count, boolean any) { + + Assert.isTrue(count > 0, "Count has to positive value."); + this.limit = count; + if (any) { + flags.add(GeoCommandFlag.any()); + } + return this; } - public enum Flag { - WITHCOORD, WITHDIST + /** + * @return never {@literal null}. + */ + public Set getFlags() { + return flags; + } + + /** + * @return can be {@literal null}. + */ + @Nullable + public Long getLimit() { + return limit; + } + + /** + * @return can be {@literal null}. + */ + @Nullable + public Direction getSortDirection() { + return sortDirection; + } + + public boolean isStoreDistance() { + return flags.contains(GeoCommandFlag.storeDist()); + } + + public boolean hasAnyLimit() { + return hasLimit() && flags.contains(GeoCommandFlag.any()); } @Override - protected GeoRadiusCommandArgs clone() { + protected GeoSearchStoreCommandArgs clone() { - GeoRadiusCommandArgs tmp = new GeoRadiusCommandArgs(); - tmp.flags = this.flags != null ? new LinkedHashSet<>(this.flags) : new LinkedHashSet<>(2); - tmp.limit = this.limit; - tmp.sortDirection = this.sortDirection; - return tmp; + GeoSearchStoreCommandArgs that = new GeoSearchStoreCommandArgs(); + that.flags.addAll(this.flags); + that.limit = this.limit; + that.sortDirection = this.sortDirection; + return that; } } /** - * {@link GeoLocation} representing a {@link Point} associated with a {@literal name}. + * Additional arguments (like count/sort/...) to be used with {@link RedisGeoCommands}. * + * @author Ninad Divadkar * @author Christoph Strobl - * @param * @since 1.8 */ - class GeoLocation { + class GeoRadiusCommandArgs extends GeoSearchCommandArgs implements Cloneable { - private final T name; - private final Point point; + private GeoRadiusCommandArgs() {} - public GeoLocation(T name, Point point) { - this.name = name; - this.point = point; + /** + * Create new {@link GeoRadiusCommandArgs}. + * + * @return never {@literal null}. + */ + public static GeoRadiusCommandArgs newGeoRadiusArgs() { + return new GeoRadiusCommandArgs(); + } + + /** + * Sets the {@link Flag#WITHCOORD} flag to also return the longitude, latitude coordinates of the matching items. + * + * @return + */ + public GeoRadiusCommandArgs includeCoordinates() { + super.includeCoordinates(); + return this; + } + + /** + * Sets the {@link Flag#WITHDIST} flag to also return the distance of the returned items from the specified center. + * + * @return never {@literal null}. + */ + public GeoRadiusCommandArgs includeDistance() { + super.includeDistance(); + return this; + } + + /** + * Apply a sort direction. + * + * @return never {@literal null}. + * @since 2.6 + */ + public GeoRadiusCommandArgs sort(Direction direction) { + super.sort(direction); + return this; + } + + /** + * Sort returned items from the nearest to the furthest, relative to the center. + * + * @return never {@literal null}. + */ + public GeoRadiusCommandArgs sortAscending() { + super.sortAscending(); + return this; + } + + /** + * Sort returned items from the furthest to the nearest, relative to the center. + * + * @return never {@literal null}. + */ + public GeoRadiusCommandArgs sortDescending() { + super.sortDescending(); + return this; + } + + /** + * Limit the results to the first N matching items. + * + * @param count + * @return never {@literal null}. + */ + public GeoRadiusCommandArgs limit(long count) { + super.limit(count); + return this; } - public T getName() { - return this.name; + public Set getFlags() { + return flags.stream().map(it -> (Flag) it).collect(Collectors.toSet()); } - public Point getPoint() { - return this.point; + public enum Flag implements GeoCommandFlag { + WITHCOORD, WITHDIST, ANY, STOREDIST } @Override - public boolean equals(Object o) { + protected GeoRadiusCommandArgs clone() { + + GeoRadiusCommandArgs that = new GeoRadiusCommandArgs(); + that.flags.addAll(this.flags); + that.limit = this.limit; + that.sortDirection = this.sortDirection; + return that; + } + } + + /** + * Reference point for {@code GEOSEARCH} and {@code GEOSEARCHSTORE} commands. Provides factory methods to create + * {@link GeoReference} from geo-set members or reference points. + * + * @param + * @since 2.6 + */ + class GeoReference { + + /** + * Creates a {@link GeoReference} from a geoset member. + * + * @param member must not be {@literal null}. + * @param + * @return + */ + public static GeoReference fromMember(T member) { + + Assert.notNull(member, "Geoset member must not be null"); + + return new GeoSearchMemberReference<>(member); + } + + /** + * Creates a {@link GeoReference} from a {@link GeoLocation geoset member}. + * + * @param member must not be {@literal null}. + * @param + * @return + */ + public static GeoReference fromMember(GeoLocation member) { + + Assert.notNull(member, "GeoLocation must not be null"); + + return new GeoSearchMemberReference<>(member.getName()); + } + + /** + * Creates a {@link GeoReference} from a {@link Circle#getCenter() circle center point} . + * + * @param within must not be {@literal null}. + * @param + * @return + */ + public static GeoReference fromCircle(Circle within) { + + Assert.notNull(within, "Circle must not be null"); + + return fromCoordinate(within.getCenter()); + } + + /** + * Creates a {@link GeoReference} from a WGS84 longitude/latitude coordinate. + * + * @param longitude + * @param latitude + * @param + * @return + */ + public static GeoReference fromCoordinate(double longitude, double latitude) { + return new GeoSearchCoordinateReference<>(longitude, latitude); + } + + /** + * Creates a {@link GeoReference} from a WGS84 longitude/latitude coordinate. + * + * @param location must not be {@literal null}. + * @param + * @return + */ + public static GeoReference fromCoordinate(GeoLocation location) { + + Assert.notNull(location, "GeoLocation must not be null"); + Assert.notNull(location.getPoint(), "GeoLocation point must not be null"); + + return fromCoordinate(location.getPoint()); + } + + /** + * Creates a {@link GeoReference} from a WGS84 longitude/latitude coordinate. + * + * @param point must not be {@literal null}. + * @param + * @return + */ + public static GeoReference fromCoordinate(Point point) { - if (this == o) { - return true; + Assert.notNull(point, "Reference point must not be null"); + + return fromCoordinate(point.getX(), point.getY()); + } + + public static class GeoSearchMemberReference extends GeoReference { + + private final T member; + + public GeoSearchMemberReference(T member) { + this.member = member; } - if (!(o instanceof GeoLocation)) { - return false; + public T getMember() { + return member; } - GeoLocation that = (GeoLocation) o; + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (!(o instanceof GeoSearchMemberReference)) { + return false; + } + GeoSearchMemberReference that = (GeoSearchMemberReference) o; + return ObjectUtils.nullSafeEquals(member, that.member); + } - if (!ObjectUtils.nullSafeEquals(name, that.name)) { - return false; + @Override + public int hashCode() { + return ObjectUtils.nullSafeHashCode(member); } - return ObjectUtils.nullSafeEquals(point, that.point); + @Override + public String toString() { + final StringBuffer sb = new StringBuffer(); + sb.append(getClass().getSimpleName()); + sb.append(" [member=").append(member); + sb.append(']'); + return sb.toString(); + } } - @Override - public int hashCode() { - int result = ObjectUtils.nullSafeHashCode(name); - result = 31 * result + ObjectUtils.nullSafeHashCode(point); - return result; + public static class GeoSearchCoordinateReference extends GeoReference { + + private final double longitude; + private final double latitude; + + public GeoSearchCoordinateReference(double longitude, double latitude) { + this.longitude = longitude; + this.latitude = latitude; + } + + public double getLongitude() { + return longitude; + } + + public double getLatitude() { + return latitude; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (!(o instanceof GeoSearchCoordinateReference)) { + return false; + } + GeoSearchCoordinateReference that = (GeoSearchCoordinateReference) o; + if (longitude != that.longitude) { + return false; + } + return latitude == that.latitude; + } + + @Override + public int hashCode() { + int result; + long temp; + temp = Double.doubleToLongBits(longitude); + result = (int) (temp ^ (temp >>> 32)); + temp = Double.doubleToLongBits(latitude); + result = 31 * result + (int) (temp ^ (temp >>> 32)); + return result; + } + + @Override + public String toString() { + final StringBuffer sb = new StringBuffer(); + sb.append(getClass().getSimpleName()); + sb.append(" [").append(longitude); + sb.append(",").append(latitude); + sb.append(']'); + return sb.toString(); + } } + } + + /** + * {@link GeoLocation} representing a {@link Point} associated with a {@literal name}. + * + * @author Christoph Strobl + * @param + * @since 1.8 + */ + class GeoLocation extends org.springframework.data.redis.domain.geo.GeoLocation { - protected boolean canEqual(Object other) { - return other instanceof GeoLocation; + public GeoLocation(T name, Point point) { + super(name, point); } public String toString() { diff --git a/src/main/java/org/springframework/data/redis/connection/StringRedisConnection.java b/src/main/java/org/springframework/data/redis/connection/StringRedisConnection.java index 3d62afac57..3f51e1b20e 100644 --- a/src/main/java/org/springframework/data/redis/connection/StringRedisConnection.java +++ b/src/main/java/org/springframework/data/redis/connection/StringRedisConnection.java @@ -47,6 +47,7 @@ import org.springframework.data.redis.core.StringRedisTemplate; import org.springframework.data.redis.core.types.Expiration; import org.springframework.data.redis.core.types.RedisClientInfo; +import org.springframework.data.redis.domain.geo.GeoShape; import org.springframework.data.redis.serializer.RedisSerializer; import org.springframework.lang.Nullable; import org.springframework.util.CollectionUtils; @@ -2425,6 +2426,40 @@ GeoResults> geoRadiusByMember(String key, String member, Dis */ Long geoRemove(String key, String... members); + /** + * Return the members of a geo set which are within the borders of the area specified by a given {@link GeoShape + * shape}. The query's center point is provided by + * {@link org.springframework.data.redis.connection.RedisGeoCommands.GeoReference}. + * + * @param key must not be {@literal null}. + * @param reference must not be {@literal null}. + * @param predicate must not be {@literal null}. + * @param args must not be {@literal null}. + * @return {@literal null} when used in pipeline / transaction. + * @since 2.6 + * @see Redis Documentation: GEOSEARCH + */ + @Nullable + GeoResults> geoSearch(String key, GeoReference reference, GeoShape predicate, + GeoSearchCommandArgs args); + + /** + * Query the members of a geo set which are within the borders of the area specified by a given {@link GeoShape shape} + * and store the result at {@code destKey}. The query's center point is provided by + * {@link org.springframework.data.redis.connection.RedisGeoCommands.GeoReference}. + * + * @param key must not be {@literal null}. + * @param reference must not be {@literal null}. + * @param predicate must not be {@literal null}. + * @param args must not be {@literal null}. + * @return {@literal null} when used in pipeline / transaction. + * @since 2.6 + * @see Redis Documentation: GEOSEARCH + */ + @Nullable + Long geoSearchStore(String destKey, String key, GeoReference reference, GeoShape predicate, + GeoSearchStoreCommandArgs args); + // ------------------------------------------------------------------------- // Methods dealing with Redis Pub/Sub // ------------------------------------------------------------------------- diff --git a/src/main/java/org/springframework/data/redis/connection/jedis/JedisClusterGeoCommands.java b/src/main/java/org/springframework/data/redis/connection/jedis/JedisClusterGeoCommands.java index 94cbfa8cb1..57420ec734 100644 --- a/src/main/java/org/springframework/data/redis/connection/jedis/JedisClusterGeoCommands.java +++ b/src/main/java/org/springframework/data/redis/connection/jedis/JedisClusterGeoCommands.java @@ -30,6 +30,7 @@ import org.springframework.data.geo.Metric; import org.springframework.data.geo.Point; import org.springframework.data.redis.connection.RedisGeoCommands; +import org.springframework.data.redis.domain.geo.GeoShape; import org.springframework.util.Assert; /** @@ -281,6 +282,26 @@ public Long geoRemove(byte[] key, byte[]... members) { return connection.zRem(key, members); } + /* + * (non-Javadoc) + * @see org.springframework.data.redis.connection.RedisGeoCommands#geoSearch(byte[], org.springframework.data.redis.connection.RedisGeoCommands.GeoReference, org.springframework.data.redis.connection.RedisGeoCommands.GeoShape, org.springframework.data.redis.connection.RedisGeoCommands.GeoSearchCommandArgs) + */ + @Override + public GeoResults> geoSearch(byte[] key, GeoReference reference, GeoShape predicate, + GeoSearchCommandArgs args) { + throw new UnsupportedOperationException("GEOSEARCH not supported through Jedis"); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.redis.connection.RedisGeoCommands#geoSearchStore(byte[], byte[], org.springframework.data.redis.connection.RedisGeoCommands.GeoReference, org.springframework.data.redis.connection.RedisGeoCommands.GeoShape, org.springframework.data.redis.connection.RedisGeoCommands.GeoSearchStoreCommandArgs) + */ + @Override + public Long geoSearchStore(byte[] destKey, byte[] key, GeoReference reference, GeoShape predicate, + GeoSearchStoreCommandArgs args) { + throw new UnsupportedOperationException("GEOSEARCHSTORE not supported through Jedis"); + } + private DataAccessException convertJedisAccessException(Exception ex) { return connection.convertJedisAccessException(ex); } diff --git a/src/main/java/org/springframework/data/redis/connection/jedis/JedisGeoCommands.java b/src/main/java/org/springframework/data/redis/connection/jedis/JedisGeoCommands.java index fdcee7690d..3e048f8eac 100644 --- a/src/main/java/org/springframework/data/redis/connection/jedis/JedisGeoCommands.java +++ b/src/main/java/org/springframework/data/redis/connection/jedis/JedisGeoCommands.java @@ -15,6 +15,7 @@ */ package org.springframework.data.redis.connection.jedis; +import org.springframework.data.redis.domain.geo.GeoShape; import redis.clients.jedis.BinaryJedis; import redis.clients.jedis.GeoCoordinate; import redis.clients.jedis.GeoUnit; @@ -161,7 +162,6 @@ public List geoPos(byte[] key, byte[]... members) { Assert.notNull(members, "Members must not be null!"); Assert.noNullElements(members, "Members must not contain null!"); - return connection.invoke().fromMany(BinaryJedis::geopos, MultiKeyPipelineBase::geopos, key, members) .toList(JedisConverters::toPoint); } @@ -257,4 +257,24 @@ public GeoResults> geoRadiusByMember(byte[] key, byte[] memb public Long geoRemove(byte[] key, byte[]... members) { return connection.zSetCommands().zRem(key, members); } + + /* + * (non-Javadoc) + * @see org.springframework.data.redis.connection.RedisGeoCommands#geoSearch(byte[], org.springframework.data.redis.connection.RedisGeoCommands.GeoReference, org.springframework.data.redis.connection.RedisGeoCommands.GeoShape, org.springframework.data.redis.connection.RedisGeoCommands.GeoSearchCommandArgs) + */ + @Override + public GeoResults> geoSearch(byte[] key, GeoReference reference, GeoShape predicate, + GeoSearchCommandArgs args) { + throw new UnsupportedOperationException("GEOSEARCH not supported through Jedis"); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.redis.connection.RedisGeoCommands#geoSearchStore(byte[], byte[], org.springframework.data.redis.connection.RedisGeoCommands.GeoReference, org.springframework.data.redis.connection.RedisGeoCommands.GeoShape, org.springframework.data.redis.connection.RedisGeoCommands.GeoSearchStoreCommandArgs) + */ + @Override + public Long geoSearchStore(byte[] destKey, byte[] key, GeoReference reference, GeoShape predicate, + GeoSearchStoreCommandArgs args) { + throw new UnsupportedOperationException("GEOSEARCHSTORE not supported through Jedis"); + } } diff --git a/src/main/java/org/springframework/data/redis/connection/lettuce/LettuceConverters.java b/src/main/java/org/springframework/data/redis/connection/lettuce/LettuceConverters.java index 811893d7c6..364ac3a3f6 100644 --- a/src/main/java/org/springframework/data/redis/connection/lettuce/LettuceConverters.java +++ b/src/main/java/org/springframework/data/redis/connection/lettuce/LettuceConverters.java @@ -15,6 +15,9 @@ */ package org.springframework.data.redis.connection.lettuce; +import static org.springframework.data.redis.connection.RedisGeoCommands.*; +import static org.springframework.data.redis.connection.RedisGeoCommands.GeoReference.*; + import io.lettuce.core.*; import io.lettuce.core.cluster.models.partitions.Partitions; import io.lettuce.core.cluster.models.partitions.RedisClusterNode.NodeFlag; @@ -44,9 +47,6 @@ import org.springframework.data.redis.connection.RedisClusterNode.Flag; import org.springframework.data.redis.connection.RedisClusterNode.LinkState; import org.springframework.data.redis.connection.RedisClusterNode.SlotRange; -import org.springframework.data.redis.connection.RedisGeoCommands.DistanceUnit; -import org.springframework.data.redis.connection.RedisGeoCommands.GeoLocation; -import org.springframework.data.redis.connection.RedisGeoCommands.GeoRadiusCommandArgs; import org.springframework.data.redis.connection.RedisListCommands.Direction; import org.springframework.data.redis.connection.RedisListCommands.Position; import org.springframework.data.redis.connection.RedisNode; @@ -69,6 +69,10 @@ import org.springframework.data.redis.core.ScanOptions; import org.springframework.data.redis.core.types.Expiration; import org.springframework.data.redis.core.types.RedisClientInfo; +import org.springframework.data.redis.domain.geo.BoundingBox; +import org.springframework.data.redis.domain.geo.BoxShape; +import org.springframework.data.redis.domain.geo.GeoShape; +import org.springframework.data.redis.domain.geo.RadiusShape; import org.springframework.data.redis.util.ByteUtils; import org.springframework.lang.Nullable; import org.springframework.util.Assert; @@ -800,18 +804,27 @@ public static GeoArgs.Unit toGeoArgsUnit(Metric metric) { * @since 1.8 */ public static GeoArgs toGeoArgs(GeoRadiusCommandArgs args) { + return toGeoArgs((GeoCommandArgs) args); + } + + /** + * Convert {@link GeoCommandArgs} into {@link GeoArgs}. + * + * @param args + * @return + * @since 2.6 + */ + public static GeoArgs toGeoArgs(GeoCommandArgs args) { GeoArgs geoArgs = new GeoArgs(); if (args.hasFlags()) { - for (GeoRadiusCommandArgs.Flag flag : args.getFlags()) { - switch (flag) { - case WITHCOORD: - geoArgs.withCoordinates(); - break; - case WITHDIST: - geoArgs.withDistance(); - break; + for (GeoRadiusCommandArgs.GeoCommandFlag flag : args.getFlags()) { + if(flag.equals(GeoRadiusCommandArgs.Flag.WITHCOORD)) { + geoArgs.withCoordinates(); + } + else if(flag.equals(GeoRadiusCommandArgs.Flag.WITHDIST)) { + geoArgs.withDistance(); } } } @@ -828,8 +841,9 @@ public static GeoArgs toGeoArgs(GeoRadiusCommandArgs args) { } if (args.hasLimit()) { - geoArgs.withCount(args.getLimit()); + geoArgs.withCount(args.getLimit(), args.getFlags().contains(GeoRadiusCommandArgs.Flag.ANY)); } + return geoArgs; } @@ -1046,6 +1060,41 @@ static LMoveArgs toLmoveArgs(Enum from, Enum to) { return LMoveArgs.Builder.rightRight(); } + static GeoSearch.GeoPredicate toGeoPredicate(GeoShape predicate) { + + if (predicate instanceof RadiusShape) { + + Distance radius = ((RadiusShape) predicate).getRadius(); + + return GeoSearch.byRadius(radius.getValue(), toGeoArgsUnit(radius.getMetric())); + } + + if (predicate instanceof BoxShape) { + + BoxShape boxPredicate = (BoxShape) predicate; + BoundingBox boundingBox = boxPredicate.getBoundingBox(); + return GeoSearch.byBox(boundingBox.getWidth().getValue(), boundingBox.getHeight().getValue(), + toGeoArgsUnit(boxPredicate.getMetric())); + } + + throw new IllegalArgumentException(String.format("Cannot convert %s to Lettuce GeoPredicate", predicate)); + } + + static GeoSearch.GeoRef toGeoRef(GeoReference reference) { + + if (reference instanceof GeoSearchMemberReference) { + return GeoSearch.fromMember(((GeoSearchMemberReference) reference).getMember()); + } + + if (reference instanceof GeoSearchCoordinateReference) { + + GeoSearchCoordinateReference coordinates = (GeoSearchCoordinateReference) reference; + return GeoSearch.fromCoordinates(coordinates.getLongitude(), coordinates.getLatitude()); + } + + throw new IllegalArgumentException(String.format("Cannot convert %s to Lettuce GeoRef", reference)); + } + /** * @author Christoph Strobl * @since 1.8 diff --git a/src/main/java/org/springframework/data/redis/connection/lettuce/LettuceGeoCommands.java b/src/main/java/org/springframework/data/redis/connection/lettuce/LettuceGeoCommands.java index 9b8c3b0f0d..6d662aac7d 100644 --- a/src/main/java/org/springframework/data/redis/connection/lettuce/LettuceGeoCommands.java +++ b/src/main/java/org/springframework/data/redis/connection/lettuce/LettuceGeoCommands.java @@ -16,6 +16,7 @@ package org.springframework.data.redis.connection.lettuce; import io.lettuce.core.GeoArgs; +import io.lettuce.core.GeoSearch; import io.lettuce.core.GeoWithin; import io.lettuce.core.api.async.RedisGeoAsyncCommands; @@ -33,6 +34,7 @@ import org.springframework.data.geo.Metric; import org.springframework.data.geo.Point; import org.springframework.data.redis.connection.RedisGeoCommands; +import org.springframework.data.redis.domain.geo.GeoShape; import org.springframework.lang.Nullable; import org.springframework.util.Assert; @@ -268,4 +270,46 @@ public Long geoRemove(byte[] key, byte[]... values) { return connection.zSetCommands().zRem(key, values); } + /* + * (non-Javadoc) + * @see org.springframework.data.redis.connection.RedisGeoCommands#geoSearch(byte[], org.springframework.data.redis.connection.RedisGeoCommands.GeoReference, org.springframework.data.redis.connection.RedisGeoCommands.GeoShape, org.springframework.data.redis.connection.RedisGeoCommands.GeoSearchCommandArgs) + */ + @Override + public GeoResults> geoSearch(byte[] key, GeoReference reference, GeoShape predicate, + GeoSearchCommandArgs args) { + + Assert.notNull(key, "Key must not be null!"); + Assert.notNull(reference, "Reference must not be null!"); + Assert.notNull(predicate, "GeoPredicate must not be null!"); + Assert.notNull(args, "GeoSearchCommandArgs must not be null!"); + + GeoSearch.GeoRef ref = LettuceConverters.toGeoRef(reference); + GeoSearch.GeoPredicate lettucePredicate = LettuceConverters.toGeoPredicate(predicate); + GeoArgs geoArgs = LettuceConverters.toGeoArgs(args); + + return connection.invoke().from(RedisGeoAsyncCommands::geosearch, key, ref, lettucePredicate, geoArgs) + .get(LettuceConverters.geoRadiusResponseToGeoResultsConverter(predicate.getMetric())); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.redis.connection.RedisGeoCommands#geoSearchStore(byte[], byte[], org.springframework.data.redis.connection.RedisGeoCommands.GeoReference, org.springframework.data.redis.connection.RedisGeoCommands.GeoShape, org.springframework.data.redis.connection.RedisGeoCommands.GeoSearchStoreCommandArgs) + */ + @Override + public Long geoSearchStore(byte[] destKey, byte[] key, GeoReference reference, GeoShape predicate, + GeoSearchStoreCommandArgs args) { + + Assert.notNull(key, "Key must not be null!"); + Assert.notNull(reference, "Reference must not be null!"); + Assert.notNull(predicate, "GeoPredicate must not be null!"); + Assert.notNull(args, "GeoSearchCommandArgs must not be null!"); + + GeoSearch.GeoRef ref = LettuceConverters.toGeoRef(reference); + GeoSearch.GeoPredicate lettucePredicate = LettuceConverters.toGeoPredicate(predicate); + GeoArgs geoArgs = LettuceConverters.toGeoArgs(args); + + return connection.invoke().just( + connection -> connection.geosearchstore(destKey, key, ref, lettucePredicate, geoArgs, args.isStoreDistance())); + } + } diff --git a/src/main/java/org/springframework/data/redis/connection/lettuce/LettuceReactiveGeoCommands.java b/src/main/java/org/springframework/data/redis/connection/lettuce/LettuceReactiveGeoCommands.java index 47e525d79a..f59d1f0775 100644 --- a/src/main/java/org/springframework/data/redis/connection/lettuce/LettuceReactiveGeoCommands.java +++ b/src/main/java/org/springframework/data/redis/connection/lettuce/LettuceReactiveGeoCommands.java @@ -17,6 +17,7 @@ import io.lettuce.core.GeoArgs; import io.lettuce.core.GeoCoordinates; +import io.lettuce.core.GeoSearch; import io.lettuce.core.GeoWithin; import io.lettuce.core.Value; import reactor.core.publisher.Flux; @@ -205,6 +206,60 @@ public Flux>>>> geoSearch( + Publisher commands) { + + return connection.execute(cmd -> Flux.from(commands).map(command -> { + + Assert.notNull(command.getKey(), "Key must not be null!"); + Assert.notNull(command.getReference(), "GeoReference must not be null!"); + Assert.notNull(command.getShape(), "GeoShape must not be null!"); + Assert.notNull(command.getArgs(), "Command args must not be null!"); + + GeoArgs geoArgs = command.getArgs().map(LettuceConverters::toGeoArgs).orElseGet(GeoArgs::new); + GeoSearch.GeoRef ref = LettuceConverters.toGeoRef(command.getReference()); + GeoSearch.GeoPredicate predicate = LettuceConverters.toGeoPredicate(command.getShape()); + + Flux>> result = cmd.geosearch(command.getKey(), ref, predicate, geoArgs) + .map(converter(command.getShape().getMetric())::convert); + + return new CommandResponse<>(command, result); + })); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.redis.connection.ReactiveGeoCommands#geoSearchStore(Publisher) + */ + @Override + public Flux> geoSearchStore(Publisher commands) { + + return connection.execute(cmd -> Flux.from(commands).flatMap(command -> { + + Assert.notNull(command.getKey(), "Key must not be null!"); + Assert.notNull(command.getDestKey(), "Destination key must not be null!"); + Assert.notNull(command.getReference(), "GeoReference must not be null!"); + Assert.notNull(command.getShape(), "GeoShape must not be null!"); + Assert.notNull(command.getArgs(), "Command args must not be null!"); + + GeoArgs geoArgs = command.getArgs().map(LettuceConverters::toGeoArgs).orElseGet(GeoArgs::new); + Boolean storeDist = command.getArgs().map(RedisGeoCommands.GeoSearchStoreCommandArgs::isStoreDistance) + .orElse(false); + GeoSearch.GeoRef ref = LettuceConverters.toGeoRef(command.getReference()); + GeoSearch.GeoPredicate predicate = LettuceConverters.toGeoPredicate(command.getShape()); + + Mono result = cmd.geosearchstore(command.getDestKey(), command.getKey(), ref, predicate, geoArgs, + storeDist); + + return result.map(it -> new NumericResponse<>(command, it)); + })); + } + private Converter, GeoResult>> converter(Metric metric) { return (source) -> { diff --git a/src/main/java/org/springframework/data/redis/core/BoundGeoOperations.java b/src/main/java/org/springframework/data/redis/core/BoundGeoOperations.java index df87b20d4c..d8bfd58d1b 100644 --- a/src/main/java/org/springframework/data/redis/core/BoundGeoOperations.java +++ b/src/main/java/org/springframework/data/redis/core/BoundGeoOperations.java @@ -15,6 +15,8 @@ */ package org.springframework.data.redis.core; +import static org.springframework.data.redis.connection.RedisGeoCommands.*; + import java.util.List; import java.util.Map; @@ -23,8 +25,9 @@ import org.springframework.data.geo.GeoResults; import org.springframework.data.geo.Metric; import org.springframework.data.geo.Point; -import org.springframework.data.redis.connection.RedisGeoCommands.GeoLocation; -import org.springframework.data.redis.connection.RedisGeoCommands.GeoRadiusCommandArgs; +import org.springframework.data.redis.connection.RedisGeoCommands; +import org.springframework.data.redis.domain.geo.BoundingBox; +import org.springframework.data.redis.domain.geo.GeoShape; import org.springframework.lang.Nullable; /** @@ -297,6 +300,8 @@ default GeoResults> geoRadius(Circle within, GeoRadiusCommandArgs return radius(within, args); } + // TODO: Bound ops should not accept K key + /** * Get the {@literal member}s within the circle defined by the {@literal members} coordinates and given * {@literal radius}. @@ -408,4 +413,191 @@ default GeoResults> geoRadiusByMember(M member, Distance distance default Long geoRemove(M... members) { return remove(members); } + + /** + * Get the {@literal member}s within the boundaries of a given {@link Circle}. + * + * @param within must not be {@literal null}. + * @return never {@literal null} unless used in pipeline / transaction. + * @since 2.6 + * @see Redis Documentation: GEOSEARCH + */ + @Nullable + default GeoResults> search(Circle within) { + return search(GeoReference.fromCircle(within), GeoShape.byRadius(within.getRadius()), + GeoSearchCommandArgs.newGeoSearchArgs()); + } + + /** + * Get the {@literal member}s using {@link GeoReference} as center of the query within the boundaries of a given + * {@link Distance radius}. + * + * @param reference must not be {@literal null}. + * @param radius must not be {@literal null}. + * @return never {@literal null} unless used in pipeline / transaction. + * @since 2.6 + * @see Redis Documentation: GEOSEARCH + */ + @Nullable + default GeoResults> search(RedisGeoCommands.GeoReference reference, Distance radius) { + return search(reference, radius, GeoSearchCommandArgs.newGeoSearchArgs()); + } + + /** + * Get the {@literal member}s using {@link GeoReference} as center of the query within the boundaries of a given + * {@link Distance radius} applying {@link GeoRadiusCommandArgs}. + * + * @param reference must not be {@literal null}. + * @param radius must not be {@literal null}. + * @param args must not be {@literal null}. + * @return never {@literal null} unless used in pipeline / transaction. + * @since 2.6 + * @see Redis Documentation: GEOSEARCH + */ + @Nullable + default GeoResults> search(RedisGeoCommands.GeoReference reference, Distance radius, + GeoSearchCommandArgs args) { + return search(reference, GeoShape.byRadius(radius), args); + } + + /** + * Get the {@literal member}s using {@link GeoReference} as center of the query within the boundaries of a given + * bounding box. + * + * @param reference must not be {@literal null}. + * @param boundingBox must not be {@literal null}. + * @return never {@literal null} unless used in pipeline / transaction. + * @since 2.6 + * @see Redis Documentation: GEOSEARCH + */ + @Nullable + default GeoResults> search(RedisGeoCommands.GeoReference reference, BoundingBox boundingBox) { + return search(reference, boundingBox, GeoSearchCommandArgs.newGeoSearchArgs()); + } + + /** + * Get the {@literal member}s using {@link GeoReference} as center of the query within the boundaries of a given + * bounding box applying {@link GeoRadiusCommandArgs}. + * + * @param reference must not be {@literal null}. + * @param boundingBox must not be {@literal null}. + * @param args must not be {@literal null}. + * @return never {@literal null} unless used in pipeline / transaction. + * @since 2.6 + * @see Redis Documentation: GEOSEARCH + */ + @Nullable + default GeoResults> search(RedisGeoCommands.GeoReference reference, BoundingBox boundingBox, + GeoSearchCommandArgs args) { + return search(reference, GeoShape.byBox(boundingBox), args); + } + + /** + * Get the {@literal member}s using {@link GeoReference} as center of the query within the boundaries of a given + * {@link GeoShape predicate} applying {@link GeoRadiusCommandArgs}. + * + * @param reference must not be {@literal null}. + * @param geoPredicate must not be {@literal null}. + * @param args must not be {@literal null}. + * @return never {@literal null} unless used in pipeline / transaction. + * @since 2.6 + * @see Redis Documentation: GEOSEARCH + */ + @Nullable + GeoResults> search(RedisGeoCommands.GeoReference reference, GeoShape geoPredicate, + GeoSearchCommandArgs args); + + /** + * Get the {@literal member}s within the boundaries of a given {@link Circle} and store results at {@code destKey}. + * + * @param within must not be {@literal null}. + * @return never {@literal null} unless used in pipeline / transaction. + * @since 2.6 + * @see Redis Documentation: GEOSEARCHSTORE + */ + @Nullable + default Long searchAndStore(K destKey, Circle within) { + return searchAndStore(destKey, GeoReference.fromCircle(within), GeoShape.byRadius(within.getRadius()), + GeoSearchStoreCommandArgs.newGeoSearchStoreArgs()); + } + + /** + * Get the {@literal member}s using {@link GeoReference} as center of the query within the boundaries of a given + * {@link Distance radius} and store results at {@code destKey}. + * + * @param reference must not be {@literal null}. + * @param radius must not be {@literal null}. + * @return never {@literal null} unless used in pipeline / transaction. + * @since 2.6 + * @see Redis Documentation: GEOSEARCHSTORE + */ + @Nullable + default Long searchAndStore(K destKey, RedisGeoCommands.GeoReference reference, Distance radius) { + return searchAndStore(destKey, reference, radius, GeoSearchStoreCommandArgs.newGeoSearchStoreArgs()); + } + + /** + * Get the {@literal member}s using {@link GeoReference} as center of the query within the boundaries of a given + * {@link Distance radius} applying {@link GeoRadiusCommandArgs} and store results at {@code destKey}. + * + * @param reference must not be {@literal null}. + * @param radius must not be {@literal null}. + * @param args must not be {@literal null}. + * @return never {@literal null} unless used in pipeline / transaction. + * @since 2.6 + * @see Redis Documentation: GEOSEARCHSTORE + */ + @Nullable + default Long searchAndStore(K destKey, RedisGeoCommands.GeoReference reference, Distance radius, + GeoSearchStoreCommandArgs args) { + return searchAndStore(destKey, reference, GeoShape.byRadius(radius), args); + } + + /** + * Get the {@literal member}s using {@link GeoReference} as center of the query within the boundaries of a given + * bounding box and store results at {@code destKey}. + * + * @param reference must not be {@literal null}. + * @param boundingBox must not be {@literal null}. + * @return never {@literal null} unless used in pipeline / transaction. + * @since 2.6 + * @see Redis Documentation: GEOSEARCHSTORE + */ + @Nullable + default Long searchAndStore(K destKey, RedisGeoCommands.GeoReference reference, BoundingBox boundingBox) { + return searchAndStore(destKey, reference, boundingBox, GeoSearchStoreCommandArgs.newGeoSearchStoreArgs()); + } + + /** + * Get the {@literal member}s using {@link GeoReference} as center of the query within the boundaries of a given + * bounding box applying {@link GeoRadiusCommandArgs} and store results at {@code destKey}. + * + * @param reference must not be {@literal null}. + * @param boundingBox must not be {@literal null}. + * @param args must not be {@literal null}. + * @return never {@literal null} unless used in pipeline / transaction. + * @since 2.6 + * @see Redis Documentation: GEOSEARCHSTORE + */ + @Nullable + default Long searchAndStore(K destKey, RedisGeoCommands.GeoReference reference, BoundingBox boundingBox, + GeoSearchStoreCommandArgs args) { + return searchAndStore(destKey, reference, GeoShape.byBox(boundingBox), args); + } + + /** + * Get the {@literal member}s using {@link GeoReference} as center of the query within the boundaries of a given + * {@link GeoShape predicate} applying {@link GeoRadiusCommandArgs} and store results at {@code destKey}. + * + * @param reference must not be {@literal null}. + * @param geoPredicate must not be {@literal null}. + * @param args must not be {@literal null}. + * @return never {@literal null} unless used in pipeline / transaction. + * @since 2.6 + * @see Redis Documentation: GEOSEARCHSTORE + */ + @Nullable + Long searchAndStore(K destKey, RedisGeoCommands.GeoReference reference, GeoShape geoPredicate, + GeoSearchStoreCommandArgs args); + } diff --git a/src/main/java/org/springframework/data/redis/core/DefaultBoundGeoOperations.java b/src/main/java/org/springframework/data/redis/core/DefaultBoundGeoOperations.java index becefa72b9..6e1ea071a7 100644 --- a/src/main/java/org/springframework/data/redis/core/DefaultBoundGeoOperations.java +++ b/src/main/java/org/springframework/data/redis/core/DefaultBoundGeoOperations.java @@ -24,8 +24,10 @@ import org.springframework.data.geo.Metric; import org.springframework.data.geo.Point; import org.springframework.data.redis.connection.DataType; +import org.springframework.data.redis.connection.RedisGeoCommands; import org.springframework.data.redis.connection.RedisGeoCommands.GeoLocation; import org.springframework.data.redis.connection.RedisGeoCommands.GeoRadiusCommandArgs; +import org.springframework.data.redis.domain.geo.GeoShape; /** * Default implementation of {@link BoundGeoOperations}. @@ -177,6 +179,26 @@ public Long remove(M... members) { return ops.remove(getKey(), members); } + /* + * (non-Javadoc) + * @see org.springframework.data.redis.core.BoundGeoOperations#search(org.springframework.data.redis.connection.RedisGeoCommands.GeoReference, org.springframework.data.redis.connection.RedisGeoCommands.GeoShape, org.springframework.data.redis.connection.RedisGeoCommands.GeoSearchCommandArgs) + */ + @Override + public GeoResults> search(RedisGeoCommands.GeoReference reference, + GeoShape geoPredicate, RedisGeoCommands.GeoSearchCommandArgs args) { + return ops.search(getKey(), reference, geoPredicate, args); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.redis.core.BoundGeoOperations#searchAndStore(java.lang.Object, org.springframework.data.redis.connection.RedisGeoCommands.GeoReference, org.springframework.data.redis.connection.RedisGeoCommands.GeoShape, org.springframework.data.redis.connection.RedisGeoCommands.GeoSearchStoreCommandArgs) + */ + @Override + public Long searchAndStore(K destKey, RedisGeoCommands.GeoReference reference, + GeoShape geoPredicate, RedisGeoCommands.GeoSearchStoreCommandArgs args) { + return ops.searchAndStore(getKey(), destKey, reference, geoPredicate, args); + } + /* * (non-Javadoc) * @see org.springframework.data.redis.core.BoundKeyOperations#getType() diff --git a/src/main/java/org/springframework/data/redis/core/DefaultGeoOperations.java b/src/main/java/org/springframework/data/redis/core/DefaultGeoOperations.java index c0c930b7e4..508c08d8e7 100644 --- a/src/main/java/org/springframework/data/redis/core/DefaultGeoOperations.java +++ b/src/main/java/org/springframework/data/redis/core/DefaultGeoOperations.java @@ -25,8 +25,10 @@ import org.springframework.data.geo.GeoResults; import org.springframework.data.geo.Metric; import org.springframework.data.geo.Point; +import org.springframework.data.redis.connection.RedisGeoCommands; import org.springframework.data.redis.connection.RedisGeoCommands.GeoLocation; import org.springframework.data.redis.connection.RedisGeoCommands.GeoRadiusCommandArgs; +import org.springframework.data.redis.domain.geo.GeoShape; /** * Default implementation of {@link GeoOperations}. @@ -241,4 +243,44 @@ public Long remove(K key, M... members) { byte[][] rawMembers = rawValues(members); return execute(connection -> connection.zRem(rawKey, rawMembers), true); } + + /* + * (non-Javadoc) + * @see org.springframework.data.redis.core.GeoOperations#search(java.lang.Object, org.springframework.data.redis.connection.RedisGeoCommands.GeoReference, org.springframework.data.redis.connection.RedisGeoCommands.GeoShape, org.springframework.data.redis.connection.RedisGeoCommands.GeoSearchCommandArgs) + */ + @Override + public GeoResults> search(K key, RedisGeoCommands.GeoReference reference, + GeoShape geoPredicate, RedisGeoCommands.GeoSearchCommandArgs args) { + + byte[] rawKey = rawKey(key); + RedisGeoCommands.GeoReference rawMember = getGeoReference(reference); + + GeoResults> raw = execute( + connection -> connection.geoSearch(rawKey, rawMember, geoPredicate, args), true); + + return deserializeGeoResults(raw); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.redis.core.GeoOperations#searchAndStore(java.lang.Object, java.lang.Object, org.springframework.data.redis.connection.RedisGeoCommands.GeoReference, org.springframework.data.redis.connection.RedisGeoCommands.GeoShape, org.springframework.data.redis.connection.RedisGeoCommands.GeoSearchStoreCommandArgs) + */ + @Override + public Long searchAndStore(K key, K destKey, RedisGeoCommands.GeoReference reference, + GeoShape geoPredicate, RedisGeoCommands.GeoSearchStoreCommandArgs args) { + + byte[] rawKey = rawKey(key); + byte[] rawDestKey = rawKey(destKey); + RedisGeoCommands.GeoReference rawMember = getGeoReference(reference); + + return execute(connection -> connection.geoSearchStore(rawDestKey, rawKey, rawMember, geoPredicate, args), true); + } + + @SuppressWarnings("unchecked") + private RedisGeoCommands.GeoReference getGeoReference(RedisGeoCommands.GeoReference reference) { + return reference instanceof RedisGeoCommands.GeoReference.GeoSearchMemberReference + ? RedisGeoCommands.GeoReference + .fromMember(rawValue(((RedisGeoCommands.GeoReference.GeoSearchMemberReference) reference).getMember())) + : (RedisGeoCommands.GeoReference) reference; + } } diff --git a/src/main/java/org/springframework/data/redis/core/DefaultReactiveGeoOperations.java b/src/main/java/org/springframework/data/redis/core/DefaultReactiveGeoOperations.java index ad074c222d..b4bd394f8d 100644 --- a/src/main/java/org/springframework/data/redis/core/DefaultReactiveGeoOperations.java +++ b/src/main/java/org/springframework/data/redis/core/DefaultReactiveGeoOperations.java @@ -32,8 +32,10 @@ import org.springframework.data.geo.Metric; import org.springframework.data.geo.Point; import org.springframework.data.redis.connection.ReactiveGeoCommands; +import org.springframework.data.redis.connection.RedisGeoCommands; import org.springframework.data.redis.connection.RedisGeoCommands.GeoLocation; import org.springframework.data.redis.connection.RedisGeoCommands.GeoRadiusCommandArgs; +import org.springframework.data.redis.domain.geo.GeoShape; import org.springframework.data.redis.serializer.RedisSerializationContext; import org.springframework.util.Assert; @@ -334,6 +336,38 @@ public Mono delete(K key) { return template.createMono(connection -> connection.keyCommands().del(rawKey(key))).map(l -> l != 0); } + /* + * (non-Javadoc) + * @see org.springframework.data.redis.core.ReactiveGeoOperations#search(K, RedisGeoCommands.GeoReference, GeoShape, GeoSearchCommandArgs) + */ + @Override + public Flux>> search(K key, RedisGeoCommands.GeoReference reference, + GeoShape geoPredicate, RedisGeoCommands.GeoSearchCommandArgs args) { + + Assert.notNull(key, "Key must not be null!"); + Assert.notNull(reference, "GeoReference must not be null!"); + RedisGeoCommands.GeoReference rawReference = getGeoReference(reference); + + return template.createFlux(connection -> connection.geoCommands() + .geoSearch(rawKey(key), rawReference, geoPredicate, args).map(this::readGeoResult)); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.redis.core.ReactiveGeoOperations#searchAndStore(K, K, RedisGeoCommands.GeoReference, GeoShape, GeoSearchStoreCommandArgs) + */ + @Override + public Mono searchAndStore(K key, K destKey, RedisGeoCommands.GeoReference reference, + GeoShape geoPredicate, RedisGeoCommands.GeoSearchStoreCommandArgs args) { + + Assert.notNull(key, "Key must not be null!"); + Assert.notNull(reference, "GeoReference must not be null!"); + RedisGeoCommands.GeoReference rawReference = getGeoReference(reference); + + return template.createMono(connection -> connection.geoCommands().geoSearchStore(rawKey(destKey), rawKey(key), + rawReference, geoPredicate, args)); + } + private Mono createMono(Function> function) { Assert.notNull(function, "Function must not be null!"); @@ -348,6 +382,14 @@ private Flux createFlux(Function> funct return template.createFlux(connection -> function.apply(connection.geoCommands())); } + @SuppressWarnings("unchecked") + private RedisGeoCommands.GeoReference getGeoReference(RedisGeoCommands.GeoReference reference) { + return reference instanceof RedisGeoCommands.GeoReference.GeoSearchMemberReference + ? RedisGeoCommands.GeoReference + .fromMember(rawValue(((RedisGeoCommands.GeoReference.GeoSearchMemberReference) reference).getMember())) + : (RedisGeoCommands.GeoReference) reference; + } + private ByteBuffer rawKey(K key) { return serializationContext.getKeySerializationPair().write(key); } diff --git a/src/main/java/org/springframework/data/redis/core/GeoOperations.java b/src/main/java/org/springframework/data/redis/core/GeoOperations.java index f43ddeca4f..7ddcfc8317 100644 --- a/src/main/java/org/springframework/data/redis/core/GeoOperations.java +++ b/src/main/java/org/springframework/data/redis/core/GeoOperations.java @@ -23,8 +23,14 @@ import org.springframework.data.geo.GeoResults; import org.springframework.data.geo.Metric; import org.springframework.data.geo.Point; +import org.springframework.data.redis.connection.RedisGeoCommands; import org.springframework.data.redis.connection.RedisGeoCommands.GeoLocation; import org.springframework.data.redis.connection.RedisGeoCommands.GeoRadiusCommandArgs; +import org.springframework.data.redis.connection.RedisGeoCommands.GeoReference; +import org.springframework.data.redis.connection.RedisGeoCommands.GeoSearchCommandArgs; +import org.springframework.data.redis.connection.RedisGeoCommands.GeoSearchStoreCommandArgs; +import org.springframework.data.redis.domain.geo.BoundingBox; +import org.springframework.data.redis.domain.geo.GeoShape; import org.springframework.lang.Nullable; /** @@ -437,4 +443,204 @@ default GeoResults> geoRadiusByMember(K key, M member, Distance d default Long geoRemove(K key, M... members) { return remove(key, members); } + + /** + * Get the {@literal member}s within the boundaries of a given {@link Circle}. + * + * @param key must not be {@literal null}. + * @param within must not be {@literal null}. + * @return never {@literal null} unless used in pipeline / transaction. + * @since 2.6 + * @see Redis Documentation: GEOSEARCH + */ + @Nullable + default GeoResults> search(K key, Circle within) { + return search(key, GeoReference.fromCircle(within), GeoShape.byRadius(within.getRadius()), + GeoSearchCommandArgs.newGeoSearchArgs()); + } + + /** + * Get the {@literal member}s using {@link GeoReference} as center of the query within the boundaries of a given + * {@link Distance radius}. + * + * @param key must not be {@literal null}. + * @param reference must not be {@literal null}. + * @param radius must not be {@literal null}. + * @return never {@literal null} unless used in pipeline / transaction. + * @since 2.6 + * @see Redis Documentation: GEOSEARCH + */ + @Nullable + default GeoResults> search(K key, RedisGeoCommands.GeoReference reference, Distance radius) { + return search(key, reference, radius, GeoSearchCommandArgs.newGeoSearchArgs()); + } + + /** + * Get the {@literal member}s using {@link GeoReference} as center of the query within the boundaries of a given + * {@link Distance radius} applying {@link GeoRadiusCommandArgs}. + * + * @param key must not be {@literal null}. + * @param reference must not be {@literal null}. + * @param radius must not be {@literal null}. + * @param args must not be {@literal null}. + * @return never {@literal null} unless used in pipeline / transaction. + * @since 2.6 + * @see Redis Documentation: GEOSEARCH + */ + @Nullable + default GeoResults> search(K key, RedisGeoCommands.GeoReference reference, Distance radius, + GeoSearchCommandArgs args) { + return search(key, reference, GeoShape.byRadius(radius), args); + } + + /** + * Get the {@literal member}s using {@link GeoReference} as center of the query within the boundaries of a given + * bounding box. + * + * @param key must not be {@literal null}. + * @param reference must not be {@literal null}. + * @param boundingBox must not be {@literal null}. + * @return never {@literal null} unless used in pipeline / transaction. + * @since 2.6 + * @see Redis Documentation: GEOSEARCH + */ + @Nullable + default GeoResults> search(K key, RedisGeoCommands.GeoReference reference, + BoundingBox boundingBox) { + return search(key, reference, boundingBox, GeoSearchCommandArgs.newGeoSearchArgs()); + } + + /** + * Get the {@literal member}s using {@link GeoReference} as center of the query within the boundaries of a given + * bounding box applying {@link GeoRadiusCommandArgs}. + * + * @param key must not be {@literal null}. + * @param reference must not be {@literal null}. + * @param boundingBox must not be {@literal null}. + * @param args must not be {@literal null}. + * @return never {@literal null} unless used in pipeline / transaction. + * @since 2.6 + * @see Redis Documentation: GEOSEARCH + */ + @Nullable + default GeoResults> search(K key, RedisGeoCommands.GeoReference reference, BoundingBox boundingBox, + GeoSearchCommandArgs args) { + return search(key, reference, GeoShape.byBox(boundingBox), args); + } + + /** + * Get the {@literal member}s using {@link GeoReference} as center of the query within the boundaries of a given + * {@link GeoShape predicate} applying {@link GeoRadiusCommandArgs}. + * + * @param key must not be {@literal null}. + * @param reference must not be {@literal null}. + * @param geoPredicate must not be {@literal null}. + * @param args must not be {@literal null}. + * @return never {@literal null} unless used in pipeline / transaction. + * @since 2.6 + * @see Redis Documentation: GEOSEARCH + */ + @Nullable + GeoResults> search(K key, RedisGeoCommands.GeoReference reference, + GeoShape geoPredicate, GeoSearchCommandArgs args); + + /** + * Get the {@literal member}s within the boundaries of a given {@link Circle} and store results at {@code destKey}. + * + * @param key must not be {@literal null}. + * @param within must not be {@literal null}. + * @return never {@literal null} unless used in pipeline / transaction. + * @since 2.6 + * @see Redis Documentation: GEOSEARCHSTORE + */ + @Nullable + default Long searchAndStore(K key, K destKey, Circle within) { + return searchAndStore(key, destKey, GeoReference.fromCircle(within), GeoShape.byRadius(within.getRadius()), + GeoSearchStoreCommandArgs.newGeoSearchStoreArgs()); + } + + /** + * Get the {@literal member}s using {@link GeoReference} as center of the query within the boundaries of a given + * {@link Distance radius} and store results at {@code destKey}. + * + * @param key must not be {@literal null}. + * @param reference must not be {@literal null}. + * @param radius must not be {@literal null}. + * @return never {@literal null} unless used in pipeline / transaction. + * @since 2.6 + * @see Redis Documentation: GEOSEARCHSTORE + */ + @Nullable + default Long searchAndStore(K key, K destKey, RedisGeoCommands.GeoReference reference, Distance radius) { + return searchAndStore(key, destKey, reference, radius, GeoSearchStoreCommandArgs.newGeoSearchStoreArgs()); + } + + /** + * Get the {@literal member}s using {@link GeoReference} as center of the query within the boundaries of a given + * {@link Distance radius} applying {@link GeoRadiusCommandArgs} and store results at {@code destKey}. + * + * @param key must not be {@literal null}. + * @param reference must not be {@literal null}. + * @param radius must not be {@literal null}. + * @param args must not be {@literal null}. + * @return never {@literal null} unless used in pipeline / transaction. + * @since 2.6 + * @see Redis Documentation: GEOSEARCHSTORE + */ + @Nullable + default Long searchAndStore(K key, K destKey, RedisGeoCommands.GeoReference reference, Distance radius, + GeoSearchStoreCommandArgs args) { + return searchAndStore(key, destKey, reference, GeoShape.byRadius(radius), args); + } + + /** + * Get the {@literal member}s using {@link GeoReference} as center of the query within the boundaries of a given + * bounding box and store results at {@code destKey}. + * + * @param key must not be {@literal null}. + * @param reference must not be {@literal null}. + * @param boundingBox must not be {@literal null}. + * @return never {@literal null} unless used in pipeline / transaction. + * @since 2.6 + * @see Redis Documentation: GEOSEARCHSTORE + */ + @Nullable + default Long searchAndStore(K key, K destKey, RedisGeoCommands.GeoReference reference, BoundingBox boundingBox) { + return searchAndStore(key, destKey, reference, boundingBox, GeoSearchStoreCommandArgs.newGeoSearchStoreArgs()); + } + + /** + * Get the {@literal member}s using {@link GeoReference} as center of the query within the boundaries of a given + * bounding box applying {@link GeoRadiusCommandArgs} and store results at {@code destKey}. + * + * @param key must not be {@literal null}. + * @param reference must not be {@literal null}. + * @param boundingBox must not be {@literal null}. + * @param args must not be {@literal null}. + * @return never {@literal null} unless used in pipeline / transaction. + * @since 2.6 + * @see Redis Documentation: GEOSEARCHSTORE + */ + @Nullable + default Long searchAndStore(K key, K destKey, RedisGeoCommands.GeoReference reference, BoundingBox boundingBox, + GeoSearchStoreCommandArgs args) { + return searchAndStore(key, destKey, reference, GeoShape.byBox(boundingBox), args); + } + + /** + * Get the {@literal member}s using {@link GeoReference} as center of the query within the boundaries of a given + * {@link GeoShape predicate} applying {@link GeoRadiusCommandArgs} and store results at {@code destKey}. + * + * @param key must not be {@literal null}. + * @param reference must not be {@literal null}. + * @param geoPredicate must not be {@literal null}. + * @param args must not be {@literal null}. + * @return never {@literal null} unless used in pipeline / transaction. + * @since 2.6 + * @see Redis Documentation: GEOSEARCHSTORE + */ + @Nullable + Long searchAndStore(K key, K destKey, RedisGeoCommands.GeoReference reference, GeoShape geoPredicate, + GeoSearchStoreCommandArgs args); + } diff --git a/src/main/java/org/springframework/data/redis/core/ReactiveGeoOperations.java b/src/main/java/org/springframework/data/redis/core/ReactiveGeoOperations.java index 7abc5bd277..4393b4abab 100644 --- a/src/main/java/org/springframework/data/redis/core/ReactiveGeoOperations.java +++ b/src/main/java/org/springframework/data/redis/core/ReactiveGeoOperations.java @@ -15,6 +15,8 @@ */ package org.springframework.data.redis.core; +import static org.springframework.data.redis.connection.RedisGeoCommands.*; + import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; @@ -28,8 +30,9 @@ import org.springframework.data.geo.GeoResult; import org.springframework.data.geo.Metric; import org.springframework.data.geo.Point; -import org.springframework.data.redis.connection.RedisGeoCommands.GeoLocation; -import org.springframework.data.redis.connection.RedisGeoCommands.GeoRadiusCommandArgs; +import org.springframework.data.redis.connection.RedisGeoCommands; +import org.springframework.data.redis.domain.geo.BoundingBox; +import org.springframework.data.redis.domain.geo.GeoShape; /** * Reactive Redis operations for geo commands. @@ -228,4 +231,193 @@ public interface ReactiveGeoOperations { * @param key must not be {@literal null}. */ Mono delete(K key); + + /** + * Get the {@literal member}s within the boundaries of a given {@link Circle}. + * + * @param key must not be {@literal null}. + * @param within must not be {@literal null}. + * @return + * @since 2.6 + * @see Redis Documentation: GEOSEARCH + */ + default Flux>> search(K key, Circle within) { + return search(key, GeoReference.fromCircle(within), GeoShape.byRadius(within.getRadius()), + GeoSearchCommandArgs.newGeoSearchArgs()); + } + + /** + * Get the {@literal member}s using {@link GeoReference} as center of the query within the boundaries of a given + * {@link Distance radius}. + * + * @param key must not be {@literal null}. + * @param reference must not be {@literal null}. + * @param radius must not be {@literal null}. + * @return + * @since 2.6 + * @see Redis Documentation: GEOSEARCH + */ + default Flux>> search(K key, RedisGeoCommands.GeoReference reference, Distance radius) { + return search(key, reference, radius, GeoSearchCommandArgs.newGeoSearchArgs()); + } + + /** + * Get the {@literal member}s using {@link GeoReference} as center of the query within the boundaries of a given + * {@link Distance radius} applying {@link GeoRadiusCommandArgs}. + * + * @param key must not be {@literal null}. + * @param reference must not be {@literal null}. + * @param radius must not be {@literal null}. + * @param args must not be {@literal null}. + * @return + * @since 2.6 + * @see Redis Documentation: GEOSEARCH + */ + default Flux>> search(K key, RedisGeoCommands.GeoReference reference, Distance radius, + GeoSearchCommandArgs args) { + return search(key, reference, GeoShape.byRadius(radius), args); + } + + /** + * Get the {@literal member}s using {@link GeoReference} as center of the query within the boundaries of a given + * bounding box. + * + * @param key must not be {@literal null}. + * @param reference must not be {@literal null}. + * @param boundingBox must not be {@literal null}. + * @return + * @since 2.6 + * @see Redis Documentation: GEOSEARCH + */ + default Flux>> search(K key, RedisGeoCommands.GeoReference reference, + BoundingBox boundingBox) { + return search(key, reference, boundingBox, GeoSearchCommandArgs.newGeoSearchArgs()); + } + + /** + * Get the {@literal member}s using {@link GeoReference} as center of the query within the boundaries of a given + * bounding box applying {@link GeoRadiusCommandArgs}. + * + * @param key must not be {@literal null}. + * @param reference must not be {@literal null}. + * @param boundingBox must not be {@literal null}. + * @param args must not be {@literal null}. + * @return + * @since 2.6 + * @see Redis Documentation: GEOSEARCH + */ + default Flux>> search(K key, RedisGeoCommands.GeoReference reference, + BoundingBox boundingBox, GeoSearchCommandArgs args) { + return search(key, reference, GeoShape.byBox(boundingBox), args); + } + + /** + * Get the {@literal member}s using {@link GeoReference} as center of the query within the boundaries of a given + * {@link GeoShape predicate} applying {@link GeoRadiusCommandArgs}. + * + * @param key must not be {@literal null}. + * @param reference must not be {@literal null}. + * @param geoPredicate must not be {@literal null}. + * @param args must not be {@literal null}. + * @return + * @since 2.6 + * @see Redis Documentation: GEOSEARCH + */ + Flux>> search(K key, RedisGeoCommands.GeoReference reference, GeoShape geoPredicate, + GeoSearchCommandArgs args); + + /** + * Get the {@literal member}s within the boundaries of a given {@link Circle} and store results at {@code destKey}. + * + * @param key must not be {@literal null}. + * @param within must not be {@literal null}. + * @return + * @since 2.6 + * @see Redis Documentation: GEOSEARCHSTORE + */ + default Mono searchAndStore(K key, K destKey, Circle within) { + return searchAndStore(key, destKey, GeoReference.fromCircle(within), GeoShape.byRadius(within.getRadius()), + GeoSearchStoreCommandArgs.newGeoSearchStoreArgs()); + } + + /** + * Get the {@literal member}s using {@link GeoReference} as center of the query within the boundaries of a given + * {@link Distance radius} and store results at {@code destKey}. + * + * @param key must not be {@literal null}. + * @param reference must not be {@literal null}. + * @param radius must not be {@literal null}. + * @return + * @since 2.6 + * @see Redis Documentation: GEOSEARCHSTORE + */ + default Mono searchAndStore(K key, K destKey, RedisGeoCommands.GeoReference reference, Distance radius) { + return searchAndStore(key, destKey, reference, radius, GeoSearchStoreCommandArgs.newGeoSearchStoreArgs()); + } + + /** + * Get the {@literal member}s using {@link GeoReference} as center of the query within the boundaries of a given + * {@link Distance radius} applying {@link GeoRadiusCommandArgs} and store results at {@code destKey}. + * + * @param key must not be {@literal null}. + * @param reference must not be {@literal null}. + * @param radius must not be {@literal null}. + * @param args must not be {@literal null}. + * @return + * @since 2.6 + * @see Redis Documentation: GEOSEARCHSTORE + */ + default Mono searchAndStore(K key, K destKey, RedisGeoCommands.GeoReference reference, Distance radius, + GeoSearchStoreCommandArgs args) { + return searchAndStore(key, destKey, reference, GeoShape.byRadius(radius), args); + } + + /** + * Get the {@literal member}s using {@link GeoReference} as center of the query within the boundaries of a given + * bounding box and store results at {@code destKey}. + * + * @param key must not be {@literal null}. + * @param reference must not be {@literal null}. + * @param boundingBox must not be {@literal null}. + * @return + * @since 2.6 + * @see Redis Documentation: GEOSEARCHSTORE + */ + default Mono searchAndStore(K key, K destKey, RedisGeoCommands.GeoReference reference, + BoundingBox boundingBox) { + return searchAndStore(key, destKey, reference, boundingBox, GeoSearchStoreCommandArgs.newGeoSearchStoreArgs()); + } + + /** + * Get the {@literal member}s using {@link GeoReference} as center of the query within the boundaries of a given + * bounding box applying {@link GeoRadiusCommandArgs} and store results at {@code destKey}. + * + * @param key must not be {@literal null}. + * @param reference must not be {@literal null}. + * @param boundingBox must not be {@literal null}. + * @param args must not be {@literal null}. + * @return + * @since 2.6 + * @see Redis Documentation: GEOSEARCHSTORE + */ + default Mono searchAndStore(K key, K destKey, RedisGeoCommands.GeoReference reference, + BoundingBox boundingBox, GeoSearchStoreCommandArgs args) { + return searchAndStore(key, destKey, reference, GeoShape.byBox(boundingBox), args); + } + + /** + * Get the {@literal member}s using {@link GeoReference} as center of the query within the boundaries of a given + * {@link GeoShape predicate} applying {@link GeoRadiusCommandArgs} and store results at {@code destKey}. + * + * @param key must not be {@literal null}. + * @param reference must not be {@literal null}. + * @param geoPredicate must not be {@literal null}. + * @param args must not be {@literal null}. + * @return + * @since 2.6 + * @see Redis Documentation: GEOSEARCHSTORE + */ + Mono searchAndStore(K key, K destKey, RedisGeoCommands.GeoReference reference, GeoShape geoPredicate, + GeoSearchStoreCommandArgs args); + } diff --git a/src/main/java/org/springframework/data/redis/domain/geo/BoundingBox.java b/src/main/java/org/springframework/data/redis/domain/geo/BoundingBox.java new file mode 100644 index 0000000000..9c8690f32e --- /dev/null +++ b/src/main/java/org/springframework/data/redis/domain/geo/BoundingBox.java @@ -0,0 +1,118 @@ +/* + * 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 org.springframework.data.redis.domain.geo; + +import org.springframework.data.geo.Distance; +import org.springframework.data.geo.Metric; +import org.springframework.data.geo.Shape; +import org.springframework.data.redis.connection.RedisGeoCommands; +import org.springframework.util.Assert; +import org.springframework.util.ObjectUtils; + +/** + * Represents a geospatial bounding box defined by width and height. + * + * @author Mark Paluch + * @since 2.6 + */ +public class BoundingBox implements Shape { + + private static final long serialVersionUID = 5215611530535947924L; + + private final Distance width; + private final Distance height; + + /** + * Creates a new {@link BoundingBox} from the given width and height. Both distances must use the same + * {@link Metric}. + * + * @param width must not be {@literal null}. + * @param height must not be {@literal null}. + */ + public BoundingBox(Distance width, Distance height) { + + Assert.notNull(width, "Width must not be null!"); + Assert.notNull(height, "Height must not be null!"); + Assert.isTrue(width.getMetric().equals(height.getMetric()), "Metric for width and height must be the same!"); + + this.width = width; + this.height = height; + } + + /** + * Creates a new {@link BoundingBox} from the given width, height and {@link Metric}. + * + * @param width + * @param height + * @param metric must not be {@literal null}. + */ + public BoundingBox(double width, double height, Metric metric) { + this(new Distance(width, metric), new Distance(height, metric)); + } + + /** + * Returns the width of this bounding box. + * + * @return will never be {@literal null}. + */ + public Distance getWidth() { + return height; + } + + /** + * Returns the height of this bounding box. + * + * @return will never be {@literal null}. + */ + public Distance getHeight() { + return height; + } + + /* + * (non-Javadoc) + * @see java.lang.Object#hashCode() + */ + @Override + public int hashCode() { + int result = ObjectUtils.nullSafeHashCode(width); + result = 31 * result + ObjectUtils.nullSafeHashCode(height); + return result; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (!(o instanceof BoundingBox)) { + return false; + } + BoundingBox that = (BoundingBox) o; + if (!ObjectUtils.nullSafeEquals(width, that.width)) { + return false; + } + return ObjectUtils.nullSafeEquals(height, that.height); + } + + /* + * (non-Javadoc) + * @see java.lang.Object#toString() + */ + @Override + public String toString() { + return String.format("Bounding box: [width=%s, height=%s]", width, height); + } +} diff --git a/src/main/java/org/springframework/data/redis/domain/geo/BoxShape.java b/src/main/java/org/springframework/data/redis/domain/geo/BoxShape.java new file mode 100644 index 0000000000..e40c6ecc47 --- /dev/null +++ b/src/main/java/org/springframework/data/redis/domain/geo/BoxShape.java @@ -0,0 +1,47 @@ +/* + * 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 org.springframework.data.redis.domain.geo; + +import org.springframework.data.geo.Metric; +import org.springframework.util.Assert; + +/** + * Bounding box defined by width and height. + * + * @author Mark Paluch + * @since 2.6 + * + */ +public class BoxShape implements GeoShape { + + private final BoundingBox boundingBox; + + public BoxShape(BoundingBox boundingBox) { + + Assert.notNull(boundingBox, "BoundingBox must not be null"); + + this.boundingBox = boundingBox; + } + + public BoundingBox getBoundingBox() { + return boundingBox; + } + + @Override + public Metric getMetric() { + return boundingBox.getHeight().getMetric(); + } +} diff --git a/src/main/java/org/springframework/data/redis/domain/geo/GeoLocation.java b/src/main/java/org/springframework/data/redis/domain/geo/GeoLocation.java new file mode 100644 index 0000000000..59606a3bbc --- /dev/null +++ b/src/main/java/org/springframework/data/redis/domain/geo/GeoLocation.java @@ -0,0 +1,74 @@ +/* + * 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 org.springframework.data.redis.domain.geo; + +import org.springframework.data.geo.Point; +import org.springframework.data.redis.connection.RedisGeoCommands; +import org.springframework.util.ObjectUtils; + +/** + * @author Christoph Strobl + * @since 2.6 + */ +public class GeoLocation { + + private final T name; + private final Point point; + + public GeoLocation(T name, Point point) { + this.name = name; + this.point = point; + } + + public T getName() { + return this.name; + } + + public Point getPoint() { + return this.point; + } + + @Override + public boolean equals(Object o) { + + if (this == o) { + return true; + } + + if (!(o instanceof GeoLocation)) { + return false; + } + + GeoLocation that = (GeoLocation) o; + + if (!ObjectUtils.nullSafeEquals(name, that.name)) { + return false; + } + + return ObjectUtils.nullSafeEquals(point, that.point); + } + + @Override + public int hashCode() { + int result = ObjectUtils.nullSafeHashCode(name); + result = 31 * result + ObjectUtils.nullSafeHashCode(point); + return result; + } + + public String toString() { + return "GeoLocation(name=" + this.getName() + ", point=" + this.getPoint() + ")"; + } +} diff --git a/src/main/java/org/springframework/data/redis/domain/geo/GeoShape.java b/src/main/java/org/springframework/data/redis/domain/geo/GeoShape.java new file mode 100644 index 0000000000..d31e7a894c --- /dev/null +++ b/src/main/java/org/springframework/data/redis/domain/geo/GeoShape.java @@ -0,0 +1,70 @@ +/* + * 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 org.springframework.data.redis.domain.geo; + +import org.springframework.data.geo.Distance; +import org.springframework.data.geo.Metric; +import org.springframework.data.geo.Shape; +import org.springframework.data.redis.connection.RedisGeoCommands; +import org.springframework.data.redis.connection.RedisGeoCommands.DistanceUnit; + +/** + * Search predicate for {@code GEOSEARCH} and {@code GEOSEARCHSTORE} commands. + * + * @since 2.6 + */ +public interface GeoShape extends Shape { + + /** + * Create a shape used as predicate for geo queries from a {@link Distance radius} around the query center point. + * + * @param radius + * @return + */ + static GeoShape byRadius(Distance radius) { + return new RadiusShape(radius); + } + + /** + * Create a shape used as predicate for geo queries from a bounding box with specified by {@code width} and + * {@code height}. + * + * @param width must not be {@literal null}. + * @param height must not be {@literal null}. + * @param distanceUnit must not be {@literal null}. + * @return + */ + static GeoShape byBox(double width, double height, DistanceUnit distanceUnit) { + return byBox(new BoundingBox(width, height, distanceUnit)); + } + + /** + * Create a shape used as predicate for geo queries from a {@link BoundingBox}. + * + * @param boundingBox must not be {@literal null}. + * @return + */ + static GeoShape byBox(BoundingBox boundingBox) { + return new BoxShape(boundingBox); + } + + /** + * The metric used for this geo predicate. + * + * @return + */ + Metric getMetric(); +} diff --git a/src/main/java/org/springframework/data/redis/domain/geo/Metrics.java b/src/main/java/org/springframework/data/redis/domain/geo/Metrics.java new file mode 100644 index 0000000000..1fe9cc90e9 --- /dev/null +++ b/src/main/java/org/springframework/data/redis/domain/geo/Metrics.java @@ -0,0 +1,61 @@ +/* + * 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 org.springframework.data.redis.domain.geo; + +import org.springframework.data.geo.Metric; +import org.springframework.data.redis.connection.RedisGeoCommands; + +/** + * {@link Metric}s supported by Redis. + * + * @author Christoph Strobl + * @since 2.6 + */ +public enum Metrics implements Metric { + + METERS(6378137, "m"), KILOMETERS(6378.137, "km"), MILES(3963.191, "mi"), FEET(20925646.325, "ft"); + + private final double multiplier; + private final String abbreviation; + + /** + * Creates a new {@link RedisGeoCommands.DistanceUnit} using the given muliplier. + * + * @param multiplier the earth radius at equator. + */ + Metrics(double multiplier, String abbreviation) { + + this.multiplier = multiplier; + this.abbreviation = abbreviation; + } + + /* + * (non-Javadoc) + * @see org.springframework.data.geo.Metric#getMultiplier() + */ + public double getMultiplier() { + return multiplier; + } + + /* + * (non-Javadoc) + * @see org.springframework.data.geo.Metric#getAbbreviation() + */ + @Override + public String getAbbreviation() { + return abbreviation; + } +} diff --git a/src/main/java/org/springframework/data/redis/domain/geo/RadiusShape.java b/src/main/java/org/springframework/data/redis/domain/geo/RadiusShape.java new file mode 100644 index 0000000000..da5884698e --- /dev/null +++ b/src/main/java/org/springframework/data/redis/domain/geo/RadiusShape.java @@ -0,0 +1,47 @@ +/* + * 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 org.springframework.data.redis.domain.geo; + +import org.springframework.data.geo.Distance; +import org.springframework.data.geo.Metric; +import org.springframework.util.Assert; + +/** + * Radius defined by {@link Distance}. + * + * @author Mark Paluch + * @since 2.6 + */ +public class RadiusShape implements GeoShape { + + private final Distance radius; + + public RadiusShape(Distance radius) { + + Assert.notNull(radius, "Distance must not be null"); + + this.radius = radius; + } + + public Distance getRadius() { + return radius; + } + + @Override + public Metric getMetric() { + return radius.getMetric(); + } +} diff --git a/src/test/java/org/springframework/data/redis/connection/AbstractConnectionIntegrationTests.java b/src/test/java/org/springframework/data/redis/connection/AbstractConnectionIntegrationTests.java index debe99ae15..9e2fffe9fa 100644 --- a/src/test/java/org/springframework/data/redis/connection/AbstractConnectionIntegrationTests.java +++ b/src/test/java/org/springframework/data/redis/connection/AbstractConnectionIntegrationTests.java @@ -24,6 +24,7 @@ import static org.springframework.data.redis.connection.ClusterTestVariables.*; import static org.springframework.data.redis.connection.RedisGeoCommands.DistanceUnit.*; import static org.springframework.data.redis.connection.RedisGeoCommands.GeoRadiusCommandArgs.*; +import static org.springframework.data.redis.connection.RedisGeoCommands.GeoSearchStoreCommandArgs.*; import static org.springframework.data.redis.core.ScanOptions.*; import java.time.Duration; @@ -45,6 +46,7 @@ import org.springframework.data.domain.Range.Bound; import org.springframework.data.geo.Circle; import org.springframework.data.geo.Distance; +import org.springframework.data.geo.GeoResult; import org.springframework.data.geo.GeoResults; import org.springframework.data.geo.Metrics; import org.springframework.data.geo.Point; @@ -79,6 +81,7 @@ import org.springframework.data.redis.core.StringRedisTemplate; import org.springframework.data.redis.core.types.Expiration; import org.springframework.data.redis.core.types.RedisClientInfo; +import org.springframework.data.redis.domain.geo.GeoShape; import org.springframework.data.redis.serializer.RedisSerializer; import org.springframework.data.redis.test.condition.EnabledOnCommand; import org.springframework.data.redis.test.condition.EnabledOnRedisDriver; @@ -3282,6 +3285,102 @@ void geoRadiusByMemberShouldApplyLimit() { assertThat(((GeoResults>) results.get(1)).getContent()).hasSize(2); } + @Test // GH-2043 + @EnabledOnCommand("GEOSEARCH") + @EnabledOnRedisDriver(RedisDriver.LETTUCE) + void geoSearchByMemberShouldReturnMembersCorrectly() { + + String key = "geo-" + UUID.randomUUID(); + actual.add(connection.geoAdd(key, Arrays.asList(ARIGENTO, CATANIA, PALERMO))); + + actual.add(connection.geoSearch(key, RedisGeoCommands.GeoReference.fromMember(PALERMO), + GeoShape.byRadius(new Distance(200, KILOMETERS)), newGeoSearchArgs().limit(2))); + + List results = getResults(); + List>> content = ((GeoResults>) results.get(1)).getContent(); + assertThat(content).hasSize(2); + assertThat(content.get(0).getDistance()).isEqualTo(new Distance(0, KILOMETERS)); + assertThat(content.get(0).getContent().getPoint()).isNull(); + } + + @Test // GH-2043 + @EnabledOnCommand("GEOSEARCH") + @EnabledOnRedisDriver(RedisDriver.LETTUCE) + void geoSearchByPointShouldReturnMembersCorrectly() { + + String key = "geo-" + UUID.randomUUID(); + actual.add(connection.geoAdd(key, Arrays.asList(ARIGENTO, CATANIA, PALERMO))); + + actual.add(connection.geoSearch(key, RedisGeoCommands.GeoReference.fromCoordinate(PALERMO), + GeoShape.byRadius(new Distance(200, KILOMETERS)), newGeoSearchArgs().limit(2))); + + List results = getResults(); + List>> content = ((GeoResults>) results.get(1)).getContent(); + assertThat(content).hasSize(2); + assertThat(content.get(0).getDistance()).isEqualTo(new Distance(0, KILOMETERS)); + assertThat(content.get(0).getContent().getPoint()).isNull(); + } + + @Test // GH-2043 + @EnabledOnCommand("GEOSEARCH") + @EnabledOnRedisDriver(RedisDriver.LETTUCE) + void geoSearchShouldConsiderDistanceCorrectly() { + + String key = "geo-" + UUID.randomUUID(); + actual.add(connection.geoAdd(key, Arrays.asList(ARIGENTO, CATANIA, PALERMO))); + + actual.add( + connection.geoSearch(key, RedisGeoCommands.GeoReference.fromMember(PALERMO), + GeoShape.byRadius(new Distance(200, KILOMETERS)), + newGeoSearchArgs().limit(2).includeDistance().includeCoordinates())); + + List results = getResults(); + List>> content = ((GeoResults>) results.get(1)).getContent(); + assertThat(content).hasSize(2); + assertThat(content.get(0).getDistance()).isNotNull(); + assertThat(content.get(0).getContent().getPoint()).isNotNull(); + } + + @Test // GH-2043 + @EnabledOnCommand("GEOSEARCHSTORE") + @EnabledOnRedisDriver(RedisDriver.LETTUCE) + void geoSearchStoreByMemberShouldStoreResult() { + + String key = "geo-" + UUID.randomUUID(); + actual.add(connection.geoAdd(key, Arrays.asList(ARIGENTO, CATANIA, PALERMO))); + + actual.add(connection.geoSearchStore("georesults", key, RedisGeoCommands.GeoReference.fromMember(PALERMO), + GeoShape.byRadius(new Distance(200, KILOMETERS)), + newGeoSearchStoreArgs().limit(2).storeDistance())); + actual.add(connection.zScore("georesults", PALERMO.getName())); + actual.add(connection.zScore("georesults", ARIGENTO.getName())); + + List results = getResults(); + assertThat(results.get(1)).isEqualTo(2L); + assertThat((Double) results.get(2)).isLessThan(1); + assertThat((Double) results.get(3)).isGreaterThan(1); + } + + @Test // GH-2043 + @EnabledOnCommand("GEOSEARCHSTORE") + @EnabledOnRedisDriver(RedisDriver.LETTUCE) + void geoSearchStoreByPointShouldStoreResult() { + + String key = "geo-" + UUID.randomUUID(); + actual.add(connection.geoAdd(key, Arrays.asList(ARIGENTO, CATANIA, PALERMO))); + + actual.add(connection.geoSearchStore("georesults", key, RedisGeoCommands.GeoReference.fromCoordinate(PALERMO), + GeoShape.byRadius(new Distance(200, KILOMETERS)), + newGeoSearchStoreArgs().limit(2).storeDistance())); + actual.add(connection.zScore("georesults", PALERMO.getName())); + actual.add(connection.zScore("georesults", ARIGENTO.getName())); + + List results = getResults(); + assertThat(results.get(1)).isEqualTo(2L); + assertThat((Double) results.get(2)).isLessThan(1); + assertThat((Double) results.get(3)).isGreaterThan(1); + } + @Test // DATAREDIS-698 void hStrLenReturnsFieldLength() { diff --git a/src/test/java/org/springframework/data/redis/connection/lettuce/LettuceReactiveGeoCommandsIntegrationTests.java b/src/test/java/org/springframework/data/redis/connection/lettuce/LettuceReactiveGeoCommandsIntegrationTests.java index 9bf0470e01..0c89040d6e 100644 --- a/src/test/java/org/springframework/data/redis/connection/lettuce/LettuceReactiveGeoCommandsIntegrationTests.java +++ b/src/test/java/org/springframework/data/redis/connection/lettuce/LettuceReactiveGeoCommandsIntegrationTests.java @@ -17,9 +17,11 @@ import static org.assertj.core.api.Assertions.*; import static org.assertj.core.data.Offset.offset; +import static org.springframework.data.redis.connection.RedisGeoCommands.*; import static org.springframework.data.redis.connection.RedisGeoCommands.DistanceUnit.*; import static org.springframework.data.redis.connection.RedisGeoCommands.GeoRadiusCommandArgs.*; +import org.springframework.data.redis.domain.geo.GeoShape; import reactor.test.StepVerifier; import java.nio.ByteBuffer; @@ -31,7 +33,7 @@ import org.springframework.data.geo.Distance; import org.springframework.data.geo.Metrics; import org.springframework.data.geo.Point; -import org.springframework.data.redis.connection.RedisGeoCommands.GeoLocation; +import org.springframework.data.redis.test.condition.EnabledOnCommand; import org.springframework.data.redis.test.extension.parametrized.ParameterizedRedisTest; /** @@ -231,7 +233,6 @@ void geoRadiusByMemberShouldReturnDistanceCorrectly() { }) // .expectNextCount(1) // .verifyComplete(); - } @ParameterizedRedisTest // DATAREDIS-525 @@ -248,4 +249,43 @@ void geoRadiusByMemberShouldApplyLimit() { .verifyComplete(); } + @ParameterizedRedisTest // GH-2043 + @EnabledOnCommand("GEOSEARCH") + void geoSearchShouldReturnMembersCorrectly() { + + nativeCommands.geoadd(KEY_1, PALERMO.getPoint().getX(), PALERMO.getPoint().getY(), PALERMO_MEMBER_NAME); + nativeCommands.geoadd(KEY_1, CATANIA.getPoint().getX(), CATANIA.getPoint().getY(), CATANIA_MEMBER_NAME); + nativeCommands.geoadd(KEY_1, ARIGENTO.getPoint().getX(), ARIGENTO.getPoint().getY(), ARIGENTO_MEMBER_NAME); + + connection.geoCommands() + .geoSearch(KEY_1_BBUFFER, GeoReference.fromMember(PALERMO.getName()), + GeoShape.byRadius(new Distance(200, KILOMETERS)), newGeoRadiusArgs().limit(2)) + .as(StepVerifier::create) // + .expectNextCount(2) // + .verifyComplete(); + } + + @ParameterizedRedisTest // GH-2043 + @EnabledOnCommand("GEOSEARCHSTORE") + void geoSearchStoreShouldStoreMembersCorrectly() { + + nativeCommands.geoadd(SAME_SLOT_KEY_1, PALERMO.getPoint().getX(), PALERMO.getPoint().getY(), PALERMO_MEMBER_NAME); + nativeCommands.geoadd(SAME_SLOT_KEY_1, CATANIA.getPoint().getX(), CATANIA.getPoint().getY(), CATANIA_MEMBER_NAME); + nativeCommands.geoadd(SAME_SLOT_KEY_1, ARIGENTO.getPoint().getX(), ARIGENTO.getPoint().getY(), + ARIGENTO_MEMBER_NAME); + + connection.geoCommands() + .geoSearchStore(SAME_SLOT_KEY_2_BBUFFER, SAME_SLOT_KEY_1_BBUFFER, GeoReference.fromMember(PALERMO.getName()), + GeoShape.byRadius(new Distance(200, KILOMETERS)), + GeoSearchStoreCommandArgs.newGeoSearchStoreArgs().limit(2)) + .as(StepVerifier::create) // + .expectNext(2L) // + .verifyComplete(); + + connection.zSetCommands().zCard(SAME_SLOT_KEY_2_BBUFFER) // + .as(StepVerifier::create) // + .expectNext(2L) // + .verifyComplete(); + } + } diff --git a/src/test/java/org/springframework/data/redis/core/DefaultGeoOperationsIntegrationTests.java b/src/test/java/org/springframework/data/redis/core/DefaultGeoOperationsIntegrationTests.java index 37e120db8e..a442a241ea 100644 --- a/src/test/java/org/springframework/data/redis/core/DefaultGeoOperationsIntegrationTests.java +++ b/src/test/java/org/springframework/data/redis/core/DefaultGeoOperationsIntegrationTests.java @@ -16,6 +16,7 @@ package org.springframework.data.redis.core; import static org.assertj.core.api.Assertions.*; +import static org.assertj.core.api.Assumptions.*; import static org.assertj.core.data.Offset.offset; import static org.springframework.data.redis.connection.RedisGeoCommands.DistanceUnit.*; import static org.springframework.data.redis.connection.RedisGeoCommands.GeoRadiusCommandArgs.*; @@ -32,8 +33,11 @@ import org.springframework.data.geo.GeoResults; import org.springframework.data.geo.Point; import org.springframework.data.redis.ObjectFactory; +import org.springframework.data.redis.connection.RedisGeoCommands; import org.springframework.data.redis.connection.RedisGeoCommands.DistanceUnit; import org.springframework.data.redis.connection.RedisGeoCommands.GeoLocation; +import org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory; +import org.springframework.data.redis.domain.geo.BoundingBox; import org.springframework.data.redis.test.condition.EnabledOnCommand; import org.springframework.data.redis.test.extension.parametrized.MethodSource; import org.springframework.data.redis.test.extension.parametrized.ParameterizedRedisTest; @@ -429,4 +433,153 @@ void testGeoRemove() { assertThat(geoOperations.remove(key, member1)).isEqualTo(1L); } + + @ParameterizedRedisTest // GH-2043 + @EnabledOnCommand("GEOSEARCH") + void geoSearchWithinShouldReturnMembers() { + + assumeThat(redisTemplate.getRequiredConnectionFactory()).isInstanceOf(LettuceConnectionFactory.class); + + K key = keyFactory.instance(); + M member1 = valueFactory.instance(); + M member2 = valueFactory.instance(); + M member3 = valueFactory.instance(); + + geoOperations.add(key, POINT_PALERMO, member1); + geoOperations.add(key, POINT_CATANIA, member2); + geoOperations.add(key, POINT_ARIGENTO, member3); + + GeoResults> result = geoOperations.search(key, + RedisGeoCommands.GeoReference.fromCoordinate(POINT_PALERMO), new Distance(150, KILOMETERS), + newGeoSearchArgs().includeCoordinates().sortAscending()); + + assertThat(result.getContent()).hasSize(2); + assertThat(result.getContent().get(0).getContent().getPoint().getX()).isCloseTo(POINT_PALERMO.getX(), offset(0.05)); + assertThat(result.getContent().get(0).getContent().getPoint().getY()).isCloseTo(POINT_PALERMO.getY(), offset(0.05)); + assertThat(result.getContent().get(0).getContent().getName()).isEqualTo(member1); + + assertThat(result.getContent().get(1).getContent().getPoint().getX()).isCloseTo(POINT_ARIGENTO.getX(), + offset(0.05)); + assertThat(result.getContent().get(1).getContent().getPoint().getY()).isCloseTo(POINT_ARIGENTO.getY(), + offset(0.05)); + assertThat(result.getContent().get(1).getContent().getName()).isEqualTo(member3); + } + + @ParameterizedRedisTest // GH-2043 + @EnabledOnCommand("GEOSEARCH") + void geoSearchByMemberShouldReturnResults() { + + assumeThat(redisTemplate.getRequiredConnectionFactory()).isInstanceOf(LettuceConnectionFactory.class); + + K key = keyFactory.instance(); + M member1 = valueFactory.instance(); + M member2 = valueFactory.instance(); + M member3 = valueFactory.instance(); + + geoOperations.add(key, POINT_PALERMO, member1); + geoOperations.add(key, POINT_CATANIA, member2); + geoOperations.add(key, POINT_ARIGENTO, member3); + + GeoResults> result = geoOperations.search(key, RedisGeoCommands.GeoReference.fromMember(member1), + new Distance(150, KILOMETERS), + newGeoSearchArgs().includeCoordinates().sortAscending()); + + assertThat(result.getContent()).hasSize(2); + assertThat(result.getContent().get(0).getContent().getPoint().getX()).isCloseTo(POINT_PALERMO.getX(), offset(0.05)); + assertThat(result.getContent().get(0).getContent().getPoint().getY()).isCloseTo(POINT_PALERMO.getY(), offset(0.05)); + assertThat(result.getContent().get(0).getContent().getName()).isEqualTo(member1); + + assertThat(result.getContent().get(1).getContent().getPoint().getX()).isCloseTo(POINT_ARIGENTO.getX(), + offset(0.05)); + assertThat(result.getContent().get(1).getContent().getPoint().getY()).isCloseTo(POINT_ARIGENTO.getY(), + offset(0.05)); + assertThat(result.getContent().get(1).getContent().getName()).isEqualTo(member3); + } + + @ParameterizedRedisTest // GH-2043 + @EnabledOnCommand("GEOSEARCH") + void geoSearchByPointWithinBoundingBoxShouldReturnMembers() { + + assumeThat(redisTemplate.getRequiredConnectionFactory()).isInstanceOf(LettuceConnectionFactory.class); + + K key = keyFactory.instance(); + M member1 = valueFactory.instance(); + M member2 = valueFactory.instance(); + M member3 = valueFactory.instance(); + + geoOperations.add(key, POINT_PALERMO, member1); + geoOperations.add(key, POINT_CATANIA, member2); + geoOperations.add(key, POINT_ARIGENTO, member3); + + GeoResults> result = geoOperations.search(key, + RedisGeoCommands.GeoReference.fromCoordinate(POINT_PALERMO), + new BoundingBox(180, 180, KILOMETERS), + newGeoSearchArgs().includeCoordinates().sortAscending()); + + assertThat(result.getContent()).hasSize(2); + assertThat(result.getContent().get(0).getContent().getPoint().getX()).isCloseTo(POINT_PALERMO.getX(), offset(0.05)); + assertThat(result.getContent().get(0).getContent().getPoint().getY()).isCloseTo(POINT_PALERMO.getY(), offset(0.05)); + assertThat(result.getContent().get(0).getContent().getName()).isEqualTo(member1); + + assertThat(result.getContent().get(1).getContent().getPoint().getX()).isCloseTo(POINT_ARIGENTO.getX(), + offset(0.05)); + assertThat(result.getContent().get(1).getContent().getPoint().getY()).isCloseTo(POINT_ARIGENTO.getY(), + offset(0.05)); + assertThat(result.getContent().get(1).getContent().getName()).isEqualTo(member3); + } + + @ParameterizedRedisTest // GH-2043 + @EnabledOnCommand("GEOSEARCH") + void geoSearchByMemberWithinBoundingBoxShouldReturnMembers() { + + assumeThat(redisTemplate.getRequiredConnectionFactory()).isInstanceOf(LettuceConnectionFactory.class); + + K key = keyFactory.instance(); + M member1 = valueFactory.instance(); + M member2 = valueFactory.instance(); + M member3 = valueFactory.instance(); + + geoOperations.add(key, POINT_PALERMO, member1); + geoOperations.add(key, POINT_CATANIA, member2); + geoOperations.add(key, POINT_ARIGENTO, member3); + + GeoResults> result = geoOperations.search(key, RedisGeoCommands.GeoReference.fromMember(member1), + new BoundingBox(180, 180, KILOMETERS), + newGeoSearchArgs().includeCoordinates().sortAscending()); + + assertThat(result.getContent()).hasSize(2); + assertThat(result.getContent().get(0).getContent().getPoint().getX()).isCloseTo(POINT_PALERMO.getX(), offset(0.05)); + assertThat(result.getContent().get(0).getContent().getPoint().getY()).isCloseTo(POINT_PALERMO.getY(), offset(0.05)); + assertThat(result.getContent().get(0).getContent().getName()).isEqualTo(member1); + + assertThat(result.getContent().get(1).getContent().getPoint().getX()).isCloseTo(POINT_ARIGENTO.getX(), + offset(0.05)); + assertThat(result.getContent().get(1).getContent().getPoint().getY()).isCloseTo(POINT_ARIGENTO.getY(), + offset(0.05)); + assertThat(result.getContent().get(1).getContent().getName()).isEqualTo(member3); + } + + @ParameterizedRedisTest // GH-2043 + @EnabledOnCommand("GEOSEARCHSTORE") + void geoSearchAndStoreWithinShouldReturnMembers() { + + assumeThat(redisTemplate.getRequiredConnectionFactory()).isInstanceOf(LettuceConnectionFactory.class); + + K key = keyFactory.instance(); + K destKey = keyFactory.instance(); + M member1 = valueFactory.instance(); + M member2 = valueFactory.instance(); + M member3 = valueFactory.instance(); + + geoOperations.add(key, POINT_PALERMO, member1); + geoOperations.add(key, POINT_CATANIA, member2); + geoOperations.add(key, POINT_ARIGENTO, member3); + + Long result = geoOperations.searchAndStore(key, destKey, + RedisGeoCommands.GeoReference.fromCoordinate(POINT_PALERMO), new Distance(150, KILOMETERS), + RedisGeoCommands.GeoSearchStoreCommandArgs.newGeoSearchStoreArgs().sortAscending()); + + assertThat(result).isEqualTo(2); + assertThat(redisTemplate.boundZSetOps(destKey).size()).isEqualTo(2); + } } diff --git a/src/test/java/org/springframework/data/redis/core/DefaultReactiveGeoOperationsIntegrationTests.java b/src/test/java/org/springframework/data/redis/core/DefaultReactiveGeoOperationsIntegrationTests.java index d5194f649c..8bb1d2d43a 100644 --- a/src/test/java/org/springframework/data/redis/core/DefaultReactiveGeoOperationsIntegrationTests.java +++ b/src/test/java/org/springframework/data/redis/core/DefaultReactiveGeoOperationsIntegrationTests.java @@ -16,6 +16,7 @@ package org.springframework.data.redis.core; import static org.assertj.core.api.Assertions.*; +import static org.springframework.data.redis.connection.RedisGeoCommands.*; import static org.springframework.data.redis.connection.RedisGeoCommands.DistanceUnit.*; import static org.springframework.data.redis.connection.RedisGeoCommands.GeoRadiusCommandArgs.*; @@ -29,7 +30,6 @@ import java.util.Map; import org.junit.jupiter.api.BeforeEach; - import org.springframework.data.geo.Circle; import org.springframework.data.geo.Distance; import org.springframework.data.geo.Metrics; @@ -37,8 +37,8 @@ import org.springframework.data.redis.ObjectFactory; import org.springframework.data.redis.connection.RedisConnection; import org.springframework.data.redis.connection.RedisConnectionFactory; -import org.springframework.data.redis.connection.RedisGeoCommands.GeoLocation; import org.springframework.data.redis.core.ReactiveOperationsTestParams.Fixture; +import org.springframework.data.redis.domain.geo.GeoShape; import org.springframework.data.redis.test.condition.EnabledOnCommand; import org.springframework.data.redis.test.extension.parametrized.MethodSource; import org.springframework.data.redis.test.extension.parametrized.ParameterizedRedisTest; @@ -434,4 +434,59 @@ void delete() { .as(StepVerifier::create) // .verifyComplete(); } + + @ParameterizedRedisTest // GH-2043 + @EnabledOnCommand("GEOSEARCH") + void geoSearchShouldReturnLocationsWithDistance() { + + K key = keyFactory.instance(); + V member1 = valueFactory.instance(); + V member2 = valueFactory.instance(); + V member3 = valueFactory.instance(); + + geoOperations.add(key, POINT_PALERMO, member1).block(); + geoOperations.add(key, POINT_CATANIA, member2).block(); + geoOperations.add(key, POINT_ARIGENTO, member3).block(); + + geoOperations + .search(key, GeoReference.fromMember(member3), GeoShape.byRadius(new Distance(100D, KILOMETERS)), + newGeoRadiusArgs().includeDistance().sortDescending()) + .as(StepVerifier::create) // + .consumeNextWith(actual -> { + + assertThat(actual.getDistance().getValue()).isCloseTo(90.9778, offset(0.005)); + assertThat(actual.getContent().getName()).isEqualTo(member1); + }) // + .consumeNextWith(actual -> { + + assertThat(actual.getDistance().getValue()).isCloseTo(0.0, offset(0.005)); + assertThat(actual.getContent().getName()).isEqualTo(member3); + }) // + .verifyComplete(); + } + + @ParameterizedRedisTest // GH-2043 + @EnabledOnCommand("GEOSEARCH") + void geoSearchAndStoreShouldStoreItems() { + + K key = keyFactory.instance(); + K destKey = keyFactory.instance(); + V member1 = valueFactory.instance(); + V member2 = valueFactory.instance(); + V member3 = valueFactory.instance(); + + geoOperations.add(key, POINT_PALERMO, member1).block(); + geoOperations.add(key, POINT_CATANIA, member2).block(); + geoOperations.add(key, POINT_ARIGENTO, member3).block(); + + geoOperations.searchAndStore(key, destKey, GeoReference.fromMember(member3), new Distance(100D, KILOMETERS)) + .as(StepVerifier::create) // + .expectNext(2L) // + .verifyComplete(); + + redisTemplate.opsForZSet().size(destKey) // + .as(StepVerifier::create) // + .expectNext(2L) // + .verifyComplete(); + } } diff --git a/src/test/java/org/springframework/data/redis/test/condition/EnabledOnRedisDriverCondition.java b/src/test/java/org/springframework/data/redis/test/condition/EnabledOnRedisDriverCondition.java index 06047841f7..a92368bc99 100644 --- a/src/test/java/org/springframework/data/redis/test/condition/EnabledOnRedisDriverCondition.java +++ b/src/test/java/org/springframework/data/redis/test/condition/EnabledOnRedisDriverCondition.java @@ -39,7 +39,7 @@ */ class EnabledOnRedisDriverCondition implements ExecutionCondition { - private static final ConditionEvaluationResult ENABLED_BY_DEFAULT = enabled("@WithRedisDriver is not present"); + private static final ConditionEvaluationResult ENABLED_BY_DEFAULT = enabled("@EnabledOnRedisDriver is not present"); @Override public ConditionEvaluationResult evaluateExecutionCondition(ExtensionContext context) { @@ -59,7 +59,7 @@ public ConditionEvaluationResult evaluateExecutionCondition(ExtensionContext con if (annotatedFields.isEmpty()) { throw new IllegalStateException( - "@WithRedisDriver requires a field of type RedisConnectionFactory annotated with @DriverQualifier!"); + "@EnabledOnRedisDriver requires a field of type RedisConnectionFactory annotated with @DriverQualifier!"); } for (Field field : annotatedFields) {