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:
committed by
Mark Paluch
parent
5d09272876
commit
07d0b82100
@@ -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);
|
||||
|
||||
@@ -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()));
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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 {
|
||||
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user