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: #215.
This commit is contained in:
Christoph Strobl
2016-07-14 12:14:32 +02:00
committed by Mark Paluch
parent 5d09272876
commit 07d0b82100
21 changed files with 930 additions and 43 deletions

View File

@@ -2431,7 +2431,7 @@ public class DefaultStringRedisConnection implements StringRedisConnection, Deco
Map<byte[], Point> byteMap = new HashMap<byte[], Point>();
for (Entry<String, Point> entry : memberCoordinateMap.entrySet()) {
byteMap.put(serialize(entry.getKey()), memberCoordinateMap.get(entry.getValue()));
byteMap.put(serialize(entry.getKey()), entry.getValue());
}
return geoAdd(serialize(key), byteMap);

View File

@@ -18,7 +18,9 @@ package org.springframework.data.redis.core;
import java.util.Set;
import org.springframework.dao.InvalidDataAccessApiUsageException;
import org.springframework.data.redis.connection.DataType;
import org.springframework.data.redis.connection.RedisConnection;
import org.springframework.data.redis.core.convert.GeoIndexedPropertyValue;
import org.springframework.data.redis.core.convert.IndexedData;
import org.springframework.data.redis.core.convert.RedisConverter;
import org.springframework.data.redis.core.convert.RemoveIndexedData;
@@ -126,7 +128,13 @@ class IndexWriter {
byte[] indexHelperKey = ByteUtils.concatAll(toBytes(keyspace + ":"), binKey, toBytes(":idx"));
for (byte[] indexKey : connection.sMembers(indexHelperKey)) {
connection.sRem(indexKey, binKey);
DataType type = connection.type(indexKey);
if (DataType.ZSET.equals(type)) {
connection.zRem(indexKey, binKey);
} else {
connection.sRem(indexKey, binKey);
}
}
connection.del(indexHelperKey);
@@ -166,7 +174,12 @@ class IndexWriter {
if (!CollectionUtils.isEmpty(existingKeys)) {
for (byte[] existingKey : existingKeys) {
connection.sRem(existingKey, key);
if (indexedData instanceof GeoIndexedPropertyValue) {
connection.geoRemove(existingKey, key);
} else {
connection.sRem(existingKey, key);
}
}
}
}
@@ -207,7 +220,23 @@ class IndexWriter {
// keep track of indexes used for the object
connection.sAdd(ByteUtils.concatAll(toBytes(indexedData.getKeyspace() + ":"), key, toBytes(":idx")), indexKey);
} else {
} else if (indexedData instanceof GeoIndexedPropertyValue) {
GeoIndexedPropertyValue geoIndexedData = ((GeoIndexedPropertyValue) indexedData);
Object value = geoIndexedData.getValue();
if (value == null) {
return;
}
byte[] indexKey = toBytes(indexedData.getKeyspace() + ":" + indexedData.getIndexName());
connection.geoAdd(indexKey, geoIndexedData.getPoint(), key);
// keep track of indexes used for the object
connection.sAdd(ByteUtils.concatAll(toBytes(indexedData.getKeyspace() + ":"), key, toBytes(":idx")), indexKey);
}
else {
throw new IllegalArgumentException(
String.format("Cannot write index data for unknown index type %s", indexedData.getClass()));
}

View File

@@ -42,12 +42,14 @@ import org.springframework.data.keyvalue.core.AbstractKeyValueAdapter;
import org.springframework.data.keyvalue.core.KeyValueAdapter;
import org.springframework.data.keyvalue.core.mapping.KeyValuePersistentProperty;
import org.springframework.data.mapping.PersistentProperty;
import org.springframework.data.redis.connection.DataType;
import org.springframework.data.redis.connection.Message;
import org.springframework.data.redis.connection.MessageListener;
import org.springframework.data.redis.connection.RedisConnection;
import org.springframework.data.redis.core.PartialUpdate.PropertyUpdate;
import org.springframework.data.redis.core.PartialUpdate.UpdateCommand;
import org.springframework.data.redis.core.convert.CustomConversions;
import org.springframework.data.redis.core.convert.GeoIndexedPropertyValue;
import org.springframework.data.redis.core.convert.KeyspaceConfiguration;
import org.springframework.data.redis.core.convert.MappingRedisConverter;
import org.springframework.data.redis.core.convert.PathIndexResolver;
@@ -461,8 +463,13 @@ public class RedisKeyValueAdapter extends AbstractKeyValueAdapter
redisUpdateObject.fieldsToRemove.toArray(new byte[redisUpdateObject.fieldsToRemove.size()][]));
}
for (byte[] index : redisUpdateObject.indexesToUpdate) {
connection.sRem(index, toBytes(redisUpdateObject.targetId));
for (RedisUpdateObject.Index index : redisUpdateObject.indexesToUpdate) {
if (ObjectUtils.nullSafeEquals(DataType.ZSET, index.type)) {
connection.zRem(index.key, toBytes(redisUpdateObject.targetId));
} else {
connection.sRem(index.key, toBytes(redisUpdateObject.targetId));
}
}
if (!rdo.getBucket().isEmpty()) {
@@ -509,7 +516,8 @@ public class RedisKeyValueAdapter extends AbstractKeyValueAdapter
? ByteUtils.concatAll(toBytes(redisUpdateObject.keyspace), toBytes((":" + path)), toBytes(":"), value) : null;
if (connection.exists(existingValueIndexKey)) {
redisUpdateObject.addIndexToUpdate(existingValueIndexKey);
redisUpdateObject.addIndexToUpdate(new RedisUpdateObject.Index(existingValueIndexKey, DataType.SET));
}
return redisUpdateObject;
}
@@ -530,12 +538,22 @@ public class RedisKeyValueAdapter extends AbstractKeyValueAdapter
: null;
if (connection.exists(existingValueIndexKey)) {
redisUpdateObject.addIndexToUpdate(existingValueIndexKey);
redisUpdateObject.addIndexToUpdate(new RedisUpdateObject.Index(existingValueIndexKey, DataType.SET));
}
}
}
}
String pathToUse = GeoIndexedPropertyValue.geoIndexName(path);
if (connection.zRank(ByteUtils.concatAll(toBytes(redisUpdateObject.keyspace), toBytes(":"), toBytes(pathToUse)),
toBytes(redisUpdateObject.targetId)) != null) {
redisUpdateObject
.addIndexToUpdate(new org.springframework.data.redis.core.RedisKeyValueAdapter.RedisUpdateObject.Index(
ByteUtils.concatAll(toBytes(redisUpdateObject.keyspace), toBytes(":"), toBytes(pathToUse)),
DataType.ZSET));
}
return redisUpdateObject;
}
@@ -855,7 +873,7 @@ public class RedisKeyValueAdapter extends AbstractKeyValueAdapter
private final byte[] targetKey;
private Set<byte[]> fieldsToRemove = new LinkedHashSet<byte[]>();
private Set<byte[]> indexesToUpdate = new LinkedHashSet<byte[]>();
private Set<Index> indexesToUpdate = new LinkedHashSet<Index>();
RedisUpdateObject(byte[] targetKey, String keyspace, Object targetId) {
@@ -868,8 +886,19 @@ public class RedisKeyValueAdapter extends AbstractKeyValueAdapter
fieldsToRemove.add(field);
}
void addIndexToUpdate(byte[] indexName) {
indexesToUpdate.add(indexName);
void addIndexToUpdate(Index index) {
indexesToUpdate.add(index);
}
static class Index {
final DataType type;
final byte[] key;
public Index(byte[] key, DataType type) {
this.key = key;
this.type = type;
}
}
}
}

View File

@@ -25,13 +25,19 @@ import java.util.List;
import java.util.Map;
import org.springframework.dao.DataAccessException;
import org.springframework.data.geo.Circle;
import org.springframework.data.geo.GeoResult;
import org.springframework.data.geo.GeoResults;
import org.springframework.data.keyvalue.core.CriteriaAccessor;
import org.springframework.data.keyvalue.core.QueryEngine;
import org.springframework.data.keyvalue.core.SortAccessor;
import org.springframework.data.keyvalue.core.query.KeyValueQuery;
import org.springframework.data.redis.connection.RedisConnection;
import org.springframework.data.redis.connection.RedisGeoCommands.GeoLocation;
import org.springframework.data.redis.core.convert.GeoIndexedPropertyValue;
import org.springframework.data.redis.core.convert.RedisData;
import org.springframework.data.redis.repository.query.RedisOperationChain;
import org.springframework.data.redis.repository.query.RedisOperationChain.NearPath;
import org.springframework.data.redis.repository.query.RedisOperationChain.PathAndValue;
import org.springframework.data.redis.util.ByteUtils;
import org.springframework.util.CollectionUtils;
@@ -74,7 +80,8 @@ class RedisQueryEngine extends QueryEngine<RedisKeyValueAdapter, RedisOperationC
final int rows, final Serializable keyspace, Class<T> type) {
if (criteria == null
|| (CollectionUtils.isEmpty(criteria.getOrSismember()) && CollectionUtils.isEmpty(criteria.getSismember()))) {
|| (CollectionUtils.isEmpty(criteria.getOrSismember()) && CollectionUtils.isEmpty(criteria.getSismember()))
&& criteria.getNear() == null) {
return (Collection<T>) getAdapter().getAllOf(keyspace, offset, rows);
}
@@ -99,6 +106,15 @@ class RedisQueryEngine extends QueryEngine<RedisKeyValueAdapter, RedisOperationC
allKeys.addAll(connection.sUnion(keys(keyspace + ":", criteria.getOrSismember())));
}
if (criteria.getNear() != null) {
GeoResults<GeoLocation<byte[]>> x = connection.geoRadius(geoKey(keyspace + ":", criteria.getNear()),
new Circle(criteria.getNear().getPoint(), criteria.getNear().getDistance()));
for (GeoResult<GeoLocation<byte[]>> y : x) {
allKeys.add(y.getContent().getName());
}
}
byte[] keyspaceBin = getAdapter().getConverter().getConversionService().convert(keyspace + ":", byte[].class);
final Map<byte[], Map<byte[], byte[]>> rawData = new LinkedHashMap<byte[], Map<byte[], byte[]>>();
@@ -195,6 +211,13 @@ class RedisQueryEngine extends QueryEngine<RedisKeyValueAdapter, RedisOperationC
return keys;
}
private byte[] geoKey(String prefix, NearPath source) {
String path = GeoIndexedPropertyValue.geoIndexName(source.getPath());
return getAdapter().getConverter().getConversionService().convert(prefix + path, byte[].class);
}
/**
* @author Christoph Strobl
* @since 1.7

View File

@@ -0,0 +1,67 @@
/*
* Copyright 2016 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.data.redis.core.convert;
import org.springframework.data.geo.Point;
import lombok.Data;
/**
* {@link IndexedData} implementation indicating storage of data within a Redis GEO structure.
*
* @author Christoph Strobl
* @since 1.8
*/
@Data
public class GeoIndexedPropertyValue implements IndexedData {
private final String keyspace;
private final String indexName;
private final Point value;
/*
* (non-Javadoc)
* @see org.springframework.data.redis.core.convert.IndexedData#getIndexName()
*/
@Override
public String getIndexName() {
return GeoIndexedPropertyValue.geoIndexName(indexName);
}
/*
* (non-Javadoc)
* @see org.springframework.data.redis.core.convert.IndexedData#getKeyspace()
*/
@Override
public String getKeyspace() {
return keyspace;
}
public Point getPoint() {
return value;
}
public static String geoIndexName(String path) {
int index = path.lastIndexOf('.');
if (index == -1) {
return path;
}
StringBuilder sb = new StringBuilder(path);
sb.setCharAt(index, ':');
return sb.toString();
}
}

View File

@@ -0,0 +1,85 @@
/*
* Copyright 2016 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.data.redis.core.convert;
import org.springframework.data.geo.Point;
import org.springframework.data.redis.core.index.GeoIndexDefinition;
import org.springframework.data.redis.core.index.IndexDefinition;
import org.springframework.data.redis.core.index.SimpleIndexDefinition;
/**
* @author Christoph Strobl
* @since 1.8
*/
class IndexedDataFactoryProvider {
/**
* @author Christoph Strobl
* @since 1.8
*/
IndexedDataFactory getIndexedDataFactory(IndexDefinition definition) {
if (definition instanceof SimpleIndexDefinition) {
return new SimpleIndexedPropertyValueFactory((SimpleIndexDefinition) definition);
} else if (definition instanceof GeoIndexDefinition) {
return new GeoIndexedPropertyValueFactory(((GeoIndexDefinition) definition));
}
return null;
}
static interface IndexedDataFactory {
IndexedData createIndexedDataFor(Object value);
}
/**
* @author Christoph Strobl
* @since 1.8
*/
static class SimpleIndexedPropertyValueFactory implements IndexedDataFactory {
final SimpleIndexDefinition indexDefinition;
public SimpleIndexedPropertyValueFactory(SimpleIndexDefinition indexDefinition) {
this.indexDefinition = indexDefinition;
}
public SimpleIndexedPropertyValue createIndexedDataFor(Object value) {
return new SimpleIndexedPropertyValue(indexDefinition.getKeyspace(), indexDefinition.getIndexName(),
indexDefinition.valueTransformer().convert(value));
}
}
/**
* @author Christoph Strobl
* @since 1.8
*/
static class GeoIndexedPropertyValueFactory implements IndexedDataFactory {
final GeoIndexDefinition indexDefinition;
public GeoIndexedPropertyValueFactory(GeoIndexDefinition indexDefinition) {
this.indexDefinition = indexDefinition;
}
public GeoIndexedPropertyValue createIndexedDataFor(Object value) {
return new GeoIndexedPropertyValue(indexDefinition.getKeyspace(), indexDefinition.getPath(),
(Point) indexDefinition.valueTransformer().convert(value));
}
}
}

View File

@@ -210,7 +210,7 @@ public class MappingRedisConverter implements RedisConverter, InitializingBean {
Object instance = instantiator.createInstance((RedisPersistentEntity<?>) entity,
new PersistentEntityParameterValueProvider<KeyValuePersistentProperty>(entity,
new ConverterAwareParameterValueProvider(source, conversionService), null));
new ConverterAwareParameterValueProvider(path, source, conversionService), this.conversionService));
final PersistentPropertyAccessor accessor = entity.getPropertyAccessor(instance);
@@ -259,14 +259,14 @@ public class MappingRedisConverter implements RedisConverter, InitializingBean {
Bucket bucket = source.getBucket().extract(currentPath + ".");
RedisData source = new RedisData(bucket);
RedisData newBucket = new RedisData(bucket);
byte[] type = bucket.get(currentPath + "." + TYPE_HINT_ALIAS);
if (type != null && type.length > 0) {
source.getBucket().put(TYPE_HINT_ALIAS, type);
newBucket.getBucket().put(TYPE_HINT_ALIAS, type);
}
accessor.setProperty(persistentProperty, readInternal(currentPath, targetType, source));
accessor.setProperty(persistentProperty, readInternal(currentPath, targetType, newBucket));
} else {
if (persistentProperty.isIdProperty() && StringUtils.isEmpty(path.isEmpty())) {
@@ -602,8 +602,8 @@ public class MappingRedisConverter implements RedisConverter, InitializingBean {
writeAssociation(path, entity, value, sink);
}
private void writeAssociation(final String path, final KeyValuePersistentEntity<?> entity,
final Object value, final RedisData sink) {
private void writeAssociation(final String path, final KeyValuePersistentEntity<?> entity, final Object value,
final RedisData sink) {
if (value == null) {
return;
@@ -992,10 +992,13 @@ public class MappingRedisConverter implements RedisConverter, InitializingBean {
private static class ConverterAwareParameterValueProvider
implements PropertyValueProvider<KeyValuePersistentProperty> {
private final String path;
private final RedisData source;
private final ConversionService conversionService;
public ConverterAwareParameterValueProvider(RedisData source, ConversionService conversionService) {
public ConverterAwareParameterValueProvider(String path, RedisData source, ConversionService conversionService) {
this.path = path;
this.source = source;
this.conversionService = conversionService;
}
@@ -1003,7 +1006,9 @@ public class MappingRedisConverter implements RedisConverter, InitializingBean {
@Override
@SuppressWarnings("unchecked")
public <T> T getPropertyValue(KeyValuePersistentProperty property) {
return (T) conversionService.convert(source.getBucket().get(property.getName()), property.getActualType());
String name = StringUtils.hasText(path) ? path + "." + property.getName() : property.getName();
return (T) conversionService.convert(source.getBucket().get(name), property.getActualType());
}
}

View File

@@ -15,16 +15,22 @@
*/
package org.springframework.data.redis.core.convert;
import java.util.Arrays;
import java.util.HashSet;
import java.util.LinkedHashSet;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
import org.springframework.data.geo.Point;
import org.springframework.data.keyvalue.core.mapping.KeyValuePersistentProperty;
import org.springframework.data.mapping.PersistentProperty;
import org.springframework.data.mapping.PersistentPropertyAccessor;
import org.springframework.data.mapping.PropertyHandler;
import org.springframework.data.redis.connection.RedisGeoCommands.GeoLocation;
import org.springframework.data.redis.core.index.ConfigurableIndexDefinitionProvider;
import org.springframework.data.redis.core.index.GeoIndexDefinition;
import org.springframework.data.redis.core.index.GeoIndexed;
import org.springframework.data.redis.core.index.IndexConfiguration;
import org.springframework.data.redis.core.index.IndexDefinition;
import org.springframework.data.redis.core.index.IndexDefinition.Condition;
@@ -49,8 +55,12 @@ import org.springframework.util.CollectionUtils;
*/
public class PathIndexResolver implements IndexResolver {
private final Set<Class<?>> VALUE_TYPES = new HashSet<Class<?>>(
Arrays.<Class<?>> asList(Point.class, GeoLocation.class));
private ConfigurableIndexDefinitionProvider indexConfiguration;
private RedisMappingContext mappingContext;
private IndexedDataFactoryProvider indexedDataFactoryProvider;
/**
* Creates new {@link PathIndexResolver} with empty {@link IndexConfiguration}.
@@ -69,6 +79,7 @@ public class PathIndexResolver implements IndexResolver {
Assert.notNull(mappingContext, "MappingContext must not be null!");
this.mappingContext = mappingContext;
this.indexConfiguration = mappingContext.getMappingConfiguration().getIndexConfiguration();
this.indexedDataFactoryProvider = new IndexedDataFactoryProvider();
}
/*
@@ -94,7 +105,7 @@ public class PathIndexResolver implements IndexResolver {
RedisPersistentEntity<?> entity = mappingContext.getPersistentEntity(typeInformation);
if (entity == null) {
if (entity == null || (value != null && VALUE_TYPES.contains(value.getClass()))) {
return resolveIndex(keyspace, path, fallback, value);
}
@@ -206,10 +217,11 @@ public class PathIndexResolver implements IndexResolver {
Object transformedValue = indexDefinition.valueTransformer().convert(value);
IndexedData indexedData = new SimpleIndexedPropertyValue(keyspace, indexDefinition.getIndexName(),
transformedValue);
IndexedData indexedData = null;
if (transformedValue == null) {
indexedData = new RemoveIndexedData(indexedData);
} else {
indexedData = indexedDataFactoryProvider.getIndexedDataFactory(indexDefinition).createIndexedDataFor(value);
}
data.add(indexedData);
}
@@ -220,7 +232,13 @@ public class PathIndexResolver implements IndexResolver {
SimpleIndexDefinition indexDefinition = new SimpleIndexDefinition(keyspace, path);
indexConfiguration.addIndexDefinition(indexDefinition);
data.add(new SimpleIndexedPropertyValue(keyspace, path, indexDefinition.valueTransformer().convert(value)));
data.add(indexedDataFactoryProvider.getIndexedDataFactory(indexDefinition).createIndexedDataFor(value));
} else if (property != null && property.isAnnotationPresent(GeoIndexed.class)) {
GeoIndexDefinition indexDefinition = new GeoIndexDefinition(keyspace, path);
indexConfiguration.addIndexDefinition(indexDefinition);
data.add(indexedDataFactoryProvider.getIndexedDataFactory(indexDefinition).createIndexedDataFor(value));
}
return data;

View File

@@ -0,0 +1,72 @@
/*
* Copyright 2016 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.data.redis.core.index;
import org.springframework.data.geo.Point;
import org.springframework.data.redis.connection.RedisGeoCommands.GeoLocation;
/**
* @author Christoph Strobl
* @since 1.8
*/
public class GeoIndexDefinition extends RedisIndexDefinition implements PathBasedRedisIndexDefinition {
/**
* Creates new {@link GeoIndexDefinition}.
*
* @param keyspace must not be {@literal null}.
* @param path
*/
public GeoIndexDefinition(String keyspace, String path) {
this(keyspace, path, path);
}
/**
* Creates new {@link GeoIndexDefinition}.
*
* @param keyspace must not be {@literal null}.
* @param path
* @param name must not be {@literal null}.
*/
public GeoIndexDefinition(String keyspace, String path, String name) {
super(keyspace, path, name);
addCondition(new PathCondition(path));
setValueTransformer(new PointValueTransformer());
}
/**
* @author Christoph Strobl
* @since 1.8
*/
static class PointValueTransformer implements IndexValueTransformer {
@Override
public Point convert(Object source) {
if (source == null || source instanceof Point) {
return (Point) source;
}
if (source instanceof GeoLocation<?>) {
return ((GeoLocation<?>) source).getPoint();
}
throw new IllegalArgumentException(
String.format("Cannot convert %s to %s. GeoIndexed property needs to be of type Point or GeoLocation!",
source.getClass(), Point.class));
}
}
}

View File

@@ -0,0 +1,37 @@
/*
* Copyright 2016 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.data.redis.core.index;
import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* Mark properties value to be included in a secondary index. <br />
* Uses Redis {@literal GEO} structures for storage. <br />
* The value will be part of the key built for the index.
*
* @author Christoph Strobl
* @since 1.8
*/
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target({ ElementType.FIELD, ElementType.ANNOTATION_TYPE })
public @interface GeoIndexed {
}

View File

@@ -23,7 +23,7 @@ import java.lang.annotation.Target;
/**
* Mark properties value to be included in a secondary index. <br />
* Uses Redos {@literal SET} for storage. <br />
* Uses Redis {@literal SET} for storage. <br />
* The value will be part of the key built for the index.
*
* @author Christoph Strobl

View File

@@ -15,11 +15,15 @@
*/
package org.springframework.data.redis.repository.query;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.Iterator;
import java.util.LinkedHashSet;
import java.util.Set;
import org.springframework.data.geo.Distance;
import org.springframework.data.geo.Point;
import org.springframework.util.ObjectUtils;
/**
@@ -33,6 +37,8 @@ public class RedisOperationChain {
private Set<PathAndValue> sismember = new LinkedHashSet<PathAndValue>();
private Set<PathAndValue> orSismember = new LinkedHashSet<PathAndValue>();
private NearPath near;
public void sismember(String path, Object value) {
sismember(new PathAndValue(path, value));
}
@@ -61,6 +67,14 @@ public class RedisOperationChain {
return orSismember;
}
public void near(NearPath near) {
this.near = near;
}
public NearPath getNear() {
return near;
}
public static class PathAndValue {
private final String path;
@@ -128,4 +142,26 @@ public class RedisOperationChain {
}
/**
* @since 1.8
* @author Christoph Strobl
*/
public static class NearPath extends PathAndValue {
public NearPath(String path, Point point, Distance distance) {
super(path, Arrays.<Object> asList(point, distance));
}
public Point getPoint() {
return (Point) getFirstValue();
}
public Distance getDistance() {
Iterator<Object> it = values().iterator();
it.next();
return (Distance) it.next();
}
}
}

View File

@@ -17,8 +17,14 @@ package org.springframework.data.redis.repository.query;
import java.util.Iterator;
import org.springframework.dao.InvalidDataAccessApiUsageException;
import org.springframework.data.domain.Sort;
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.keyvalue.core.query.KeyValueQuery;
import org.springframework.data.redis.repository.query.RedisOperationChain.NearPath;
import org.springframework.data.repository.query.ParameterAccessor;
import org.springframework.data.repository.query.parser.AbstractQueryCreator;
import org.springframework.data.repository.query.parser.Part;
@@ -35,6 +41,7 @@ public class RedisQueryCreator extends AbstractQueryCreator<KeyValueQuery<RedisO
public RedisQueryCreator(PartTree tree, ParameterAccessor parameters) {
super(tree, parameters);
}
/*
@@ -52,6 +59,10 @@ public class RedisQueryCreator extends AbstractQueryCreator<KeyValueQuery<RedisO
case SIMPLE_PROPERTY:
sink.sismember(part.getProperty().toDotPath(), iterator.next());
break;
case WITHIN:
case NEAR:
sink.near(getNearPath(part, iterator));
break;
default:
throw new IllegalArgumentException(part.getType() + "is not supported for redis query derivation");
}
@@ -103,4 +114,41 @@ public class RedisQueryCreator extends AbstractQueryCreator<KeyValueQuery<RedisO
return query;
}
private NearPath getNearPath(Part part, Iterator<Object> iterator) {
Object o = iterator.next();
Point point = null;
Distance distance = null;
if (o instanceof Circle) {
point = ((Circle) o).getCenter();
distance = ((Circle) o).getRadius();
} else if (o instanceof Point) {
point = (Point) o;
if (!iterator.hasNext()) {
throw new InvalidDataAccessApiUsageException(
"Expected to find distance value for geo query. Are you missing a parameter?");
}
Object distObject = iterator.next();
if (distObject instanceof Distance) {
distance = (Distance) distObject;
} else if (distObject instanceof Number) {
distance = new Distance(((Number) distObject).doubleValue(), Metrics.KILOMETERS);
} else {
throw new InvalidDataAccessApiUsageException(String
.format("Expected to find Distance or Numeric value for geo query but was %s.", distObject.getClass()));
}
} else {
throw new InvalidDataAccessApiUsageException(
String.format("Expected to find a Circle or Point/Distance for geo query but was %s.", o.getClass()));
}
return new NearPath(part.getProperty().toDotPath(), point, distance);
}
}