Skip to content

Commit 07d0b82

Browse files
christophstroblmp911de
authored andcommitted
DATAREDIS-533 - Add support for geo indexes.
We now allow usage of @GeoIndexed to mark GeoLocation or Point properties as candidates for secondary index creation. Non null values will be included in GEOADD command as follows: GEOADD keyspace:property-path point.x point.y entity-id @GeoIndexed can be used on top level as well as on nested properties. class Person { @id String id; String firstname, lastname; Address hometown; } class Address { String city, street, housenumber; @GeoIndexed Point location; } The above allows us to derive geospatial queries from a given method using NEAR and WITHIN keywords like: interface PersonRepository extends CrudRepository<Person, String> { List<Person> findByAddressLocationNear(Point point, Distance distance); List<Person> findByAddressLocationWithin(Circle circle); } Partial updates on the Point itself also trigger an index refresh operation. So it is possible to alter existing entities via: template.save(new PartialUpdate<Person>("1", Person.class).set("address.location", new Point(17, 18)); Original pull request: spring-projects#215.
1 parent 5d09272 commit 07d0b82

21 files changed

+930
-43
lines changed

src/main/asciidoc/reference/redis-repositories.adoc

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -310,6 +310,8 @@ public class ApplicationConfig {
310310
== Secondary Indexes
311311
http://redis.io/topics/indexes[Secondary indexes] are used to enable lookup operations based on native Redis structures. Values are written to the according indexes on every save and are removed when objects are deleted or <<redis.repositories.expirations,expire>>.
312312

313+
=== Simple Property Index
314+
313315
Given the sample `Person` entity we can create an index for _firstname_ by annotating the property with `@Indexed`.
314316

315317
.Annotation driven indexing
@@ -419,6 +421,51 @@ public class ApplicationConfig {
419421
----
420422
====
421423

424+
=== Geospatial Index
425+
426+
Assume the `Address` type contains a property `location` of type `Point` that holds the geo coordinates of the particular address. By annotating the property with `@GeoIndexed` those values will be added using Redis `GEO` commands.
427+
428+
====
429+
[source,java]
430+
----
431+
@RedisHash("persons")
432+
public class Person {
433+
434+
Address address;
435+
436+
// ... other properties omitted
437+
}
438+
439+
public class Address {
440+
441+
@GeoIndexed Point location;
442+
443+
// ... other properties omitted
444+
}
445+
446+
public interface PersonRepository extends CrudRepository<Person, String> {
447+
448+
List<Person> findByAddressLocationNear(Point point, Distance distance); <1>
449+
List<Person> findByAddressLocationWithin(Circle circle); <2>
450+
}
451+
452+
Person rand = new Person("rand", "al'thor");
453+
rand.setAddress(new Address(new Point(13.361389D, 38.115556D)));
454+
455+
repository.save(rand); <3>
456+
457+
repository.findByAddressLocationNear(new Point(15D, 37D), new Distance(200)); <4>
458+
----
459+
<1> finder declaration on nested property using Point and Distance.
460+
<2> finder declaration on nested property using Circle to search within.
461+
<3> `GEOADD persons:address:location 13.361389 38.115556 e2c7dcee-b8cd-4424-883e-736ce564363e`
462+
<4> `GEORADIUS persons:address:location 15.0 37.0 200.0 km`
463+
====
464+
465+
In the above example the lon/lat values are stored using `GEOADD` using the objects `id` as the members name. The finder methods allow usage of `Circle` or `Point, Distance` combinations for querying those values.
466+
467+
NOTE: It is **not** possible to combine `near`/`within` with other criteria.
468+
422469

423470
[[redis.repositories.expirations]]
424471
== Time To Live

src/main/java/org/springframework/data/redis/connection/DefaultStringRedisConnection.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2431,7 +2431,7 @@ public Long geoAdd(String key, Map<String, Point> memberCoordinateMap) {
24312431

24322432
Map<byte[], Point> byteMap = new HashMap<byte[], Point>();
24332433
for (Entry<String, Point> entry : memberCoordinateMap.entrySet()) {
2434-
byteMap.put(serialize(entry.getKey()), memberCoordinateMap.get(entry.getValue()));
2434+
byteMap.put(serialize(entry.getKey()), entry.getValue());
24352435
}
24362436

24372437
return geoAdd(serialize(key), byteMap);

src/main/java/org/springframework/data/redis/core/IndexWriter.java

Lines changed: 32 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,9 @@
1818
import java.util.Set;
1919

2020
import org.springframework.dao.InvalidDataAccessApiUsageException;
21+
import org.springframework.data.redis.connection.DataType;
2122
import org.springframework.data.redis.connection.RedisConnection;
23+
import org.springframework.data.redis.core.convert.GeoIndexedPropertyValue;
2224
import org.springframework.data.redis.core.convert.IndexedData;
2325
import org.springframework.data.redis.core.convert.RedisConverter;
2426
import org.springframework.data.redis.core.convert.RemoveIndexedData;
@@ -126,7 +128,13 @@ public void removeKeyFromIndexes(String keyspace, Object key) {
126128
byte[] indexHelperKey = ByteUtils.concatAll(toBytes(keyspace + ":"), binKey, toBytes(":idx"));
127129

128130
for (byte[] indexKey : connection.sMembers(indexHelperKey)) {
129-
connection.sRem(indexKey, binKey);
131+
132+
DataType type = connection.type(indexKey);
133+
if (DataType.ZSET.equals(type)) {
134+
connection.zRem(indexKey, binKey);
135+
} else {
136+
connection.sRem(indexKey, binKey);
137+
}
130138
}
131139

132140
connection.del(indexHelperKey);
@@ -166,7 +174,12 @@ protected void removeKeyFromExistingIndexes(byte[] key, IndexedData indexedData)
166174

167175
if (!CollectionUtils.isEmpty(existingKeys)) {
168176
for (byte[] existingKey : existingKeys) {
169-
connection.sRem(existingKey, key);
177+
178+
if (indexedData instanceof GeoIndexedPropertyValue) {
179+
connection.geoRemove(existingKey, key);
180+
} else {
181+
connection.sRem(existingKey, key);
182+
}
170183
}
171184
}
172185
}
@@ -207,7 +220,23 @@ else if (indexedData instanceof SimpleIndexedPropertyValue) {
207220

208221
// keep track of indexes used for the object
209222
connection.sAdd(ByteUtils.concatAll(toBytes(indexedData.getKeyspace() + ":"), key, toBytes(":idx")), indexKey);
210-
} else {
223+
} else if (indexedData instanceof GeoIndexedPropertyValue) {
224+
225+
GeoIndexedPropertyValue geoIndexedData = ((GeoIndexedPropertyValue) indexedData);
226+
227+
Object value = geoIndexedData.getValue();
228+
if (value == null) {
229+
return;
230+
}
231+
232+
byte[] indexKey = toBytes(indexedData.getKeyspace() + ":" + indexedData.getIndexName());
233+
connection.geoAdd(indexKey, geoIndexedData.getPoint(), key);
234+
235+
// keep track of indexes used for the object
236+
connection.sAdd(ByteUtils.concatAll(toBytes(indexedData.getKeyspace() + ":"), key, toBytes(":idx")), indexKey);
237+
}
238+
239+
else {
211240
throw new IllegalArgumentException(
212241
String.format("Cannot write index data for unknown index type %s", indexedData.getClass()));
213242
}

src/main/java/org/springframework/data/redis/core/RedisKeyValueAdapter.java

Lines changed: 36 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -42,12 +42,14 @@
4242
import org.springframework.data.keyvalue.core.KeyValueAdapter;
4343
import org.springframework.data.keyvalue.core.mapping.KeyValuePersistentProperty;
4444
import org.springframework.data.mapping.PersistentProperty;
45+
import org.springframework.data.redis.connection.DataType;
4546
import org.springframework.data.redis.connection.Message;
4647
import org.springframework.data.redis.connection.MessageListener;
4748
import org.springframework.data.redis.connection.RedisConnection;
4849
import org.springframework.data.redis.core.PartialUpdate.PropertyUpdate;
4950
import org.springframework.data.redis.core.PartialUpdate.UpdateCommand;
5051
import org.springframework.data.redis.core.convert.CustomConversions;
52+
import org.springframework.data.redis.core.convert.GeoIndexedPropertyValue;
5153
import org.springframework.data.redis.core.convert.KeyspaceConfiguration;
5254
import org.springframework.data.redis.core.convert.MappingRedisConverter;
5355
import org.springframework.data.redis.core.convert.PathIndexResolver;
@@ -461,8 +463,13 @@ public Void doInRedis(RedisConnection connection) throws DataAccessException {
461463
redisUpdateObject.fieldsToRemove.toArray(new byte[redisUpdateObject.fieldsToRemove.size()][]));
462464
}
463465

464-
for (byte[] index : redisUpdateObject.indexesToUpdate) {
465-
connection.sRem(index, toBytes(redisUpdateObject.targetId));
466+
for (RedisUpdateObject.Index index : redisUpdateObject.indexesToUpdate) {
467+
468+
if (ObjectUtils.nullSafeEquals(DataType.ZSET, index.type)) {
469+
connection.zRem(index.key, toBytes(redisUpdateObject.targetId));
470+
} else {
471+
connection.sRem(index.key, toBytes(redisUpdateObject.targetId));
472+
}
466473
}
467474

468475
if (!rdo.getBucket().isEmpty()) {
@@ -509,7 +516,8 @@ private RedisUpdateObject fetchDeletePathsFromHashAndUpdateIndex(RedisUpdateObje
509516
? ByteUtils.concatAll(toBytes(redisUpdateObject.keyspace), toBytes((":" + path)), toBytes(":"), value) : null;
510517

511518
if (connection.exists(existingValueIndexKey)) {
512-
redisUpdateObject.addIndexToUpdate(existingValueIndexKey);
519+
520+
redisUpdateObject.addIndexToUpdate(new RedisUpdateObject.Index(existingValueIndexKey, DataType.SET));
513521
}
514522
return redisUpdateObject;
515523
}
@@ -530,12 +538,22 @@ private RedisUpdateObject fetchDeletePathsFromHashAndUpdateIndex(RedisUpdateObje
530538
: null;
531539

532540
if (connection.exists(existingValueIndexKey)) {
533-
redisUpdateObject.addIndexToUpdate(existingValueIndexKey);
541+
redisUpdateObject.addIndexToUpdate(new RedisUpdateObject.Index(existingValueIndexKey, DataType.SET));
534542
}
535543
}
536544
}
537545
}
538546

547+
String pathToUse = GeoIndexedPropertyValue.geoIndexName(path);
548+
if (connection.zRank(ByteUtils.concatAll(toBytes(redisUpdateObject.keyspace), toBytes(":"), toBytes(pathToUse)),
549+
toBytes(redisUpdateObject.targetId)) != null) {
550+
551+
redisUpdateObject
552+
.addIndexToUpdate(new org.springframework.data.redis.core.RedisKeyValueAdapter.RedisUpdateObject.Index(
553+
ByteUtils.concatAll(toBytes(redisUpdateObject.keyspace), toBytes(":"), toBytes(pathToUse)),
554+
DataType.ZSET));
555+
}
556+
539557
return redisUpdateObject;
540558
}
541559

@@ -855,7 +873,7 @@ private static class RedisUpdateObject {
855873
private final byte[] targetKey;
856874

857875
private Set<byte[]> fieldsToRemove = new LinkedHashSet<byte[]>();
858-
private Set<byte[]> indexesToUpdate = new LinkedHashSet<byte[]>();
876+
private Set<Index> indexesToUpdate = new LinkedHashSet<Index>();
859877

860878
RedisUpdateObject(byte[] targetKey, String keyspace, Object targetId) {
861879

@@ -868,8 +886,19 @@ void addFieldToRemove(byte[] field) {
868886
fieldsToRemove.add(field);
869887
}
870888

871-
void addIndexToUpdate(byte[] indexName) {
872-
indexesToUpdate.add(indexName);
889+
void addIndexToUpdate(Index index) {
890+
indexesToUpdate.add(index);
891+
}
892+
893+
static class Index {
894+
final DataType type;
895+
final byte[] key;
896+
897+
public Index(byte[] key, DataType type) {
898+
this.key = key;
899+
this.type = type;
900+
}
901+
873902
}
874903
}
875904
}

src/main/java/org/springframework/data/redis/core/RedisQueryEngine.java

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,13 +25,19 @@
2525
import java.util.Map;
2626

2727
import org.springframework.dao.DataAccessException;
28+
import org.springframework.data.geo.Circle;
29+
import org.springframework.data.geo.GeoResult;
30+
import org.springframework.data.geo.GeoResults;
2831
import org.springframework.data.keyvalue.core.CriteriaAccessor;
2932
import org.springframework.data.keyvalue.core.QueryEngine;
3033
import org.springframework.data.keyvalue.core.SortAccessor;
3134
import org.springframework.data.keyvalue.core.query.KeyValueQuery;
3235
import org.springframework.data.redis.connection.RedisConnection;
36+
import org.springframework.data.redis.connection.RedisGeoCommands.GeoLocation;
37+
import org.springframework.data.redis.core.convert.GeoIndexedPropertyValue;
3338
import org.springframework.data.redis.core.convert.RedisData;
3439
import org.springframework.data.redis.repository.query.RedisOperationChain;
40+
import org.springframework.data.redis.repository.query.RedisOperationChain.NearPath;
3541
import org.springframework.data.redis.repository.query.RedisOperationChain.PathAndValue;
3642
import org.springframework.data.redis.util.ByteUtils;
3743
import org.springframework.util.CollectionUtils;
@@ -74,7 +80,8 @@ public <T> Collection<T> execute(final RedisOperationChain criteria, final Compa
7480
final int rows, final Serializable keyspace, Class<T> type) {
7581

7682
if (criteria == null
77-
|| (CollectionUtils.isEmpty(criteria.getOrSismember()) && CollectionUtils.isEmpty(criteria.getSismember()))) {
83+
|| (CollectionUtils.isEmpty(criteria.getOrSismember()) && CollectionUtils.isEmpty(criteria.getSismember()))
84+
&& criteria.getNear() == null) {
7885
return (Collection<T>) getAdapter().getAllOf(keyspace, offset, rows);
7986
}
8087

@@ -99,6 +106,15 @@ public Map<byte[], Map<byte[], byte[]>> doInRedis(RedisConnection connection) th
99106
allKeys.addAll(connection.sUnion(keys(keyspace + ":", criteria.getOrSismember())));
100107
}
101108

109+
if (criteria.getNear() != null) {
110+
111+
GeoResults<GeoLocation<byte[]>> x = connection.geoRadius(geoKey(keyspace + ":", criteria.getNear()),
112+
new Circle(criteria.getNear().getPoint(), criteria.getNear().getDistance()));
113+
for (GeoResult<GeoLocation<byte[]>> y : x) {
114+
allKeys.add(y.getContent().getName());
115+
}
116+
}
117+
102118
byte[] keyspaceBin = getAdapter().getConverter().getConversionService().convert(keyspace + ":", byte[].class);
103119

104120
final Map<byte[], Map<byte[], byte[]>> rawData = new LinkedHashMap<byte[], Map<byte[], byte[]>>();
@@ -195,6 +211,13 @@ private byte[][] keys(String prefix, Collection<PathAndValue> source) {
195211
return keys;
196212
}
197213

214+
private byte[] geoKey(String prefix, NearPath source) {
215+
216+
String path = GeoIndexedPropertyValue.geoIndexName(source.getPath());
217+
return getAdapter().getConverter().getConversionService().convert(prefix + path, byte[].class);
218+
219+
}
220+
198221
/**
199222
* @author Christoph Strobl
200223
* @since 1.7
Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
/*
2+
* Copyright 2016 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package org.springframework.data.redis.core.convert;
17+
18+
import org.springframework.data.geo.Point;
19+
20+
import lombok.Data;
21+
22+
/**
23+
* {@link IndexedData} implementation indicating storage of data within a Redis GEO structure.
24+
*
25+
* @author Christoph Strobl
26+
* @since 1.8
27+
*/
28+
@Data
29+
public class GeoIndexedPropertyValue implements IndexedData {
30+
31+
private final String keyspace;
32+
private final String indexName;
33+
private final Point value;
34+
35+
/*
36+
* (non-Javadoc)
37+
* @see org.springframework.data.redis.core.convert.IndexedData#getIndexName()
38+
*/
39+
@Override
40+
public String getIndexName() {
41+
return GeoIndexedPropertyValue.geoIndexName(indexName);
42+
}
43+
44+
/*
45+
* (non-Javadoc)
46+
* @see org.springframework.data.redis.core.convert.IndexedData#getKeyspace()
47+
*/
48+
@Override
49+
public String getKeyspace() {
50+
return keyspace;
51+
}
52+
53+
public Point getPoint() {
54+
return value;
55+
}
56+
57+
public static String geoIndexName(String path) {
58+
59+
int index = path.lastIndexOf('.');
60+
if (index == -1) {
61+
return path;
62+
}
63+
StringBuilder sb = new StringBuilder(path);
64+
sb.setCharAt(index, ':');
65+
return sb.toString();
66+
}
67+
}

0 commit comments

Comments
 (0)