diff --git a/src/main/asciidoc/new-features.adoc b/src/main/asciidoc/new-features.adoc index f35f2b2a1..428aa0dc6 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 d9419e8ff..0d6f10d2f 100644 --- a/src/main/java/org/springframework/data/redis/connection/DefaultStringRedisConnection.java +++ b/src/main/java/org/springframework/data/redis/connection/DefaultStringRedisConnection.java @@ -2120,6 +2120,14 @@ public class DefaultStringRedisConnection implements StringRedisConnection, Deco 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 +3894,50 @@ public class DefaultStringRedisConnection implements StringRedisConnection, Deco 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 9035009f9..27c823e71 100644 --- a/src/main/java/org/springframework/data/redis/connection/DefaultedRedisConnection.java +++ b/src/main/java/org/springframework/data/redis/connection/DefaultedRedisConnection.java @@ -1540,6 +1540,22 @@ public interface DefaultedRedisConnection extends RedisConnection { 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 3fe3dc6f2..e7a194ee3 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,8 @@ */ package org.springframework.data.redis.connection; +import static org.springframework.data.redis.connection.RedisGeoCommands.*; + import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; @@ -27,6 +29,7 @@ import java.util.Optional; 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 +40,6 @@ import org.springframework.data.redis.connection.ReactiveRedisConnection.Command 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; @@ -1239,4 +1239,261 @@ public interface ReactiveGeoCommands { */ 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 b250d0d4f..f61cd07c8 100644 --- a/src/main/java/org/springframework/data/redis/connection/RedisGeoCommands.java +++ b/src/main/java/org/springframework/data/redis/connection/RedisGeoCommands.java @@ -15,6 +15,8 @@ */ 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; @@ -26,6 +28,7 @@ import org.springframework.data.geo.Distance; import org.springframework.data.geo.GeoResults; import org.springframework.data.geo.Metric; import org.springframework.data.geo.Point; +import org.springframework.data.geo.Shape; import org.springframework.lang.Nullable; import org.springframework.util.Assert; import org.springframework.util.ObjectUtils; @@ -215,28 +218,159 @@ public interface RedisGeoCommands { @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); + + /** + * Search predicate for {@code GEOSEARCH} and {@code GEOSEARCHSTORE} commands. + * + * @since 2.6 + */ + interface GeoShape { + + /** + * 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(); + } + + /** + * Radius defined by {@link Distance}. + * + * @since 2.6 + */ + 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(); + } + } + + /** + * Bounding box defined by width and height. + * + * @since 2.6 + */ + 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(); + } + } + /** * Additional arguments (like count/sort/...) to be used with {@link RedisGeoCommands}. * - * @author Ninad Divadkar - * @author Christoph Strobl - * @since 1.8 + * @author Mark Paluch + * @since 2.6 */ - class GeoRadiusCommandArgs implements Cloneable { + class GeoSearchCommandArgs implements Cloneable { Set flags = new LinkedHashSet<>(2, 1); @Nullable Long limit; @Nullable Direction sortDirection; - private GeoRadiusCommandArgs() {} + 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,7 +378,7 @@ public interface RedisGeoCommands { * * @return */ - public GeoRadiusCommandArgs includeCoordinates() { + public GeoSearchCommandArgs includeCoordinates() { flags.add(Flag.WITHCOORD); return this; @@ -255,21 +389,32 @@ public interface RedisGeoCommands { * * @return never {@literal null}. */ - public GeoRadiusCommandArgs includeDistance() { + public GeoSearchCommandArgs includeDistance() { flags.add(Flag.WITHDIST); return this; } + /** + * Apply a sort direction. + * + * @return never {@literal null}. + */ + public GeoSearchCommandArgs sort(Direction direction) { + + 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 GeoRadiusCommandArgs sortAscending() { - - sortDirection = Direction.ASC; - return this; + public GeoSearchCommandArgs sortAscending() { + return sort(Direction.ASC); } /** @@ -277,10 +422,8 @@ public interface RedisGeoCommands { * * @return never {@literal null}. */ - public GeoRadiusCommandArgs sortDescending() { - - sortDirection = Direction.DESC; - return this; + public GeoSearchCommandArgs sortDescending() { + return sort(Direction.DESC); } /** @@ -289,13 +432,28 @@ public interface RedisGeoCommands { * @param count * @return never {@literal null}. */ - public GeoRadiusCommandArgs limit(long count) { + public GeoSearchCommandArgs limit(long count) { Assert.isTrue(count > 0, "Count has to positive value."); limit = count; return this; } + /** + * Limit the results to the first N matching items. + * + * @param count + * @param any + * @return never {@literal null}. + */ + public GeoSearchCommandArgs limit(long count, boolean any) { + + Assert.isTrue(count > 0, "Count has to positive value."); + limit = count; + flags.add(Flag.ANY); + return this; + } + /** * @return never {@literal null}. */ @@ -331,8 +489,248 @@ public interface RedisGeoCommands { return limit != null; } + public boolean hasAnyLimit() { + return hasLimit() && flags.contains(Flag.ANY); + } + + @Override + protected GeoSearchCommandArgs clone() { + + GeoSearchCommandArgs tmp = new GeoSearchCommandArgs(); + tmp.flags = this.flags != null ? new LinkedHashSet<>(this.flags) : new LinkedHashSet<>(2); + tmp.limit = this.limit; + tmp.sortDirection = this.sortDirection; + return tmp; + } + } + + /** + * Additional arguments (like count/sort/...) to be used with {@link RedisGeoCommands}. + * + * @author Mark Paluch + * @since 2.6 + */ + class GeoSearchStoreCommandArgs implements Cloneable { + + Set flags = new LinkedHashSet<>(2, 1); + @Nullable Long limit; + @Nullable 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); + } + + /** + * Limit the results to the first N matching items. + * + * @param count + * @return never {@literal null}. + */ + public GeoSearchStoreCommandArgs limit(long count) { + + Assert.isTrue(count > 0, "Count has to positive value."); + limit = count; + return this; + } + + /** + * 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."); + limit = count; + flags.add(Flag.ANY); + return this; + } + + /** + * @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(Flag.STOREDIST); + } + + public boolean hasSortDirection() { + return sortDirection != null; + } + + public boolean hasLimit() { + return limit != null; + } + + public boolean hasAnyLimit() { + return hasLimit() && flags.contains(Flag.ANY); + } + + @Override + protected GeoSearchStoreCommandArgs clone() { + + GeoSearchStoreCommandArgs tmp = new GeoSearchStoreCommandArgs(); + tmp.flags = this.flags != null ? new LinkedHashSet<>(this.flags) : new LinkedHashSet<>(2); + tmp.limit = this.limit; + tmp.sortDirection = this.sortDirection; + return tmp; + } + } + + /** + * Additional arguments (like count/sort/...) to be used with {@link RedisGeoCommands}. + * + * @author Ninad Divadkar + * @author Christoph Strobl + * @since 1.8 + */ + class GeoRadiusCommandArgs extends GeoSearchCommandArgs implements Cloneable { + + private GeoRadiusCommandArgs() {} + + /** + * 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 enum Flag { - WITHCOORD, WITHDIST + WITHCOORD, WITHDIST, ANY, STOREDIST } @Override @@ -346,6 +744,193 @@ public interface RedisGeoCommands { } } + /** + * 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) { + + 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; + } + + public T getMember() { + return member; + } + + @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); + } + + @Override + public int hashCode() { + return ObjectUtils.nullSafeHashCode(member); + } + + @Override + public String toString() { + final StringBuffer sb = new StringBuffer(); + sb.append(getClass().getSimpleName()); + sb.append(" [member=").append(member); + sb.append(']'); + return sb.toString(); + } + } + + 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}. * @@ -448,4 +1033,100 @@ public interface RedisGeoCommands { return abbreviation; } } + + /** + * Represents a geospatial bounding box defined by width and height. + * + * @author Mark Paluch + * @since 2.6 + */ + 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/connection/StringRedisConnection.java b/src/main/java/org/springframework/data/redis/connection/StringRedisConnection.java index 3d62afac5..d9f6cb12e 100644 --- a/src/main/java/org/springframework/data/redis/connection/StringRedisConnection.java +++ b/src/main/java/org/springframework/data/redis/connection/StringRedisConnection.java @@ -2425,6 +2425,40 @@ public interface StringRedisConnection extends RedisConnection { */ 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 94cbfa8cb..1902ace60 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 @@ -281,6 +281,26 @@ class JedisClusterGeoCommands implements RedisGeoCommands { 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 fdcee7690..0e4d3ed8c 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 @@ -161,7 +161,6 @@ class JedisGeoCommands implements RedisGeoCommands { 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 +256,24 @@ class JedisGeoCommands implements RedisGeoCommands { 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 beb863a10..385aca9e8 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; @@ -42,9 +45,6 @@ import org.springframework.data.redis.connection.BitFieldSubCommands.BitFieldSub 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.NodeType; @@ -868,6 +868,17 @@ public abstract class LettuceConverters extends Converters { * @since 1.8 */ public static GeoArgs toGeoArgs(GeoRadiusCommandArgs args) { + return toGeoArgs((GeoSearchCommandArgs) args); + } + + /** + * Convert {@link GeoRadiusCommandArgs} into {@link GeoArgs}. + * + * @param args + * @return + * @since 2.6 + */ + public static GeoArgs toGeoArgs(GeoSearchCommandArgs args) { GeoArgs geoArgs = new GeoArgs(); @@ -896,7 +907,35 @@ public abstract class LettuceConverters extends Converters { } if (args.hasLimit()) { - geoArgs.withCount(args.getLimit()); + geoArgs.withCount(args.getLimit(), args.hasAnyLimit()); + } + return geoArgs; + } + + /** + * Convert {@link GeoRadiusCommandArgs} into {@link GeoArgs}. + * + * @param args + * @return + * @since 2.6 + */ + static GeoArgs toGeoArgs(GeoSearchStoreCommandArgs args) { + + GeoArgs geoArgs = new GeoArgs(); + + if (args.hasSortDirection()) { + switch (args.getSortDirection()) { + case ASC: + geoArgs.asc(); + break; + case DESC: + geoArgs.desc(); + break; + } + } + + if (args.hasLimit()) { + geoArgs.withCount(args.getLimit(), args.hasLimit()); } return geoArgs; } @@ -1114,6 +1153,41 @@ public abstract class LettuceConverters extends Converters { 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 9b8c3b0f0..4acaf2438 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; @@ -268,4 +269,46 @@ class LettuceGeoCommands implements RedisGeoCommands { 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 47e525d79..f59d1f077 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 @@ package org.springframework.data.redis.connection.lettuce; 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 @@ class LettuceReactiveGeoCommands implements ReactiveGeoCommands { })); } + /* + * (non-Javadoc) + * @see org.springframework.data.redis.connection.ReactiveGeoCommands#geoSearch(Publisher) + */ + @Override + 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 df87b20d4..202e2618d 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,7 @@ import org.springframework.data.geo.Distance; 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.lang.Nullable; /** @@ -297,6 +298,8 @@ public interface BoundGeoOperations extends BoundKeyOperations { 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 +411,191 @@ public interface BoundGeoOperations extends BoundKeyOperations { 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 becefa72b..92533dd83 100644 --- a/src/main/java/org/springframework/data/redis/core/DefaultBoundGeoOperations.java +++ b/src/main/java/org/springframework/data/redis/core/DefaultBoundGeoOperations.java @@ -24,6 +24,7 @@ import org.springframework.data.geo.GeoResults; 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; @@ -177,6 +178,26 @@ class DefaultBoundGeoOperations extends DefaultBoundKeyOperations imple 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, + RedisGeoCommands.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, + RedisGeoCommands.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 c0c930b7e..edd71a95b 100644 --- a/src/main/java/org/springframework/data/redis/core/DefaultGeoOperations.java +++ b/src/main/java/org/springframework/data/redis/core/DefaultGeoOperations.java @@ -25,6 +25,7 @@ import org.springframework.data.geo.Distance; 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; @@ -241,4 +242,44 @@ class DefaultGeoOperations extends AbstractOperations implements Geo 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, + RedisGeoCommands.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, + RedisGeoCommands.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 ad074c222..51eb04896 100644 --- a/src/main/java/org/springframework/data/redis/core/DefaultReactiveGeoOperations.java +++ b/src/main/java/org/springframework/data/redis/core/DefaultReactiveGeoOperations.java @@ -32,6 +32,7 @@ import org.springframework.data.geo.GeoResult; 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.serializer.RedisSerializationContext; @@ -334,6 +335,38 @@ class DefaultReactiveGeoOperations implements ReactiveGeoOperations 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, + RedisGeoCommands.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, + RedisGeoCommands.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 +381,14 @@ class DefaultReactiveGeoOperations implements ReactiveGeoOperations 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 f43ddeca4..2f51b0872 100644 --- a/src/main/java/org/springframework/data/redis/core/GeoOperations.java +++ b/src/main/java/org/springframework/data/redis/core/GeoOperations.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,8 @@ import org.springframework.data.geo.Distance; 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.connection.RedisGeoCommands.*; import org.springframework.lang.Nullable; /** @@ -437,4 +439,204 @@ public interface GeoOperations { 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 7abc5bd27..cfba328bd 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; @@ -23,13 +25,13 @@ import java.util.List; import java.util.Map; import org.reactivestreams.Publisher; + import org.springframework.data.geo.Circle; import org.springframework.data.geo.Distance; 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; /** * Reactive Redis operations for geo commands. @@ -228,4 +230,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/test/java/org/springframework/data/redis/connection/AbstractConnectionIntegrationTests.java b/src/test/java/org/springframework/data/redis/connection/AbstractConnectionIntegrationTests.java index debe99ae1..8d1e58e4f 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.BitFieldSubCommands.BitF 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.dao.DataAccessException; 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; @@ -3282,6 +3284,102 @@ public abstract class AbstractConnectionIntegrationTests { 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), + RedisGeoCommands.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), + RedisGeoCommands.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), + RedisGeoCommands.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), + RedisGeoCommands.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), + RedisGeoCommands.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 9bf0470e0..376a70576 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,6 +17,7 @@ package org.springframework.data.redis.connection.lettuce; 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.*; @@ -31,7 +32,7 @@ import org.springframework.data.geo.Circle; 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 +232,6 @@ public class LettuceReactiveGeoCommandsIntegrationTests extends LettuceReactiveC }) // .expectNextCount(1) // .verifyComplete(); - } @ParameterizedRedisTest // DATAREDIS-525 @@ -248,4 +248,43 @@ public class LettuceReactiveGeoCommandsIntegrationTests extends LettuceReactiveC .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 37e120db8..c365df0d3 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,10 @@ import org.springframework.data.geo.Distance; 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.test.condition.EnabledOnCommand; import org.springframework.data.redis.test.extension.parametrized.MethodSource; import org.springframework.data.redis.test.extension.parametrized.ParameterizedRedisTest; @@ -429,4 +432,153 @@ public class DefaultGeoOperationsIntegrationTests { 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 RedisGeoCommands.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 RedisGeoCommands.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 d5194f649..78f0128b1 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.*; @@ -37,7 +38,6 @@ import org.springframework.data.geo.Point; 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.test.condition.EnabledOnCommand; import org.springframework.data.redis.test.extension.parametrized.MethodSource; @@ -434,4 +434,59 @@ public class DefaultReactiveGeoOperationsIntegrationTests { .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(); + } }