
com.redis.om.spring.repository.query.RediSearchQuery Maven / Gradle / Ivy
package com.redis.om.spring.repository.query;
import java.lang.reflect.Field;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.Date;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.stream.Collectors;
import java.util.stream.IntStream;
import java.util.stream.StreamSupport;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.data.domain.PageImpl;
import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.Sort.Order;
import org.springframework.data.geo.Point;
import org.springframework.data.keyvalue.core.KeyValueOperations;
import org.springframework.data.mapping.PropertyPath;
import org.springframework.data.repository.core.RepositoryMetadata;
import org.springframework.data.repository.query.Parameter;
import org.springframework.data.repository.query.QueryMethod;
import org.springframework.data.repository.query.QueryMethodEvaluationContextProvider;
import org.springframework.data.repository.query.RepositoryQuery;
import org.springframework.data.repository.query.parser.AbstractQueryCreator;
import org.springframework.data.repository.query.parser.Part;
import org.springframework.data.repository.query.parser.PartTree;
import org.springframework.data.util.Pair;
import org.springframework.util.ClassUtils;
import com.google.gson.Gson;
import com.redis.om.spring.annotations.Aggregation;
import com.redis.om.spring.annotations.GeoIndexed;
import com.redis.om.spring.annotations.Indexed;
import com.redis.om.spring.annotations.NumericIndexed;
import com.redis.om.spring.annotations.Searchable;
import com.redis.om.spring.annotations.TagIndexed;
import com.redis.om.spring.annotations.TextIndexed;
import com.redis.om.spring.ops.RedisModulesOperations;
import com.redis.om.spring.ops.search.SearchOperations;
import com.redis.om.spring.repository.query.autocomplete.AutoCompleteQueryExecutor;
import com.redis.om.spring.repository.query.bloom.BloomQueryExecutor;
import com.redis.om.spring.repository.query.clause.QueryClause;
import com.redis.om.spring.util.ObjectUtils;
import io.redisearch.AggregationResult;
import io.redisearch.Query;
import io.redisearch.Schema.FieldType;
import io.redisearch.SearchResult;
import io.redisearch.aggregation.AggregationBuilder;
public class RediSearchQuery implements RepositoryQuery {
private static final Log logger = LogFactory.getLog(RediSearchQuery.class);
private final QueryMethod queryMethod;
private final String searchIndex;
private RediSearchQueryType type;
private String value;
@SuppressWarnings("unused")
private RepositoryMetadata metadata;
// query fields
private String[] returnFields;
// aggregation fields
private String[] load;
private Map apply;
private List>> queryOrParts = new ArrayList<>();
// for non @Param annotated dynamic names
private List paramNames = new ArrayList<>();
private Class> domainType;
private RedisModulesOperations modulesOperations;
@SuppressWarnings("unused")
private KeyValueOperations keyValueOperations;
private boolean isANDQuery = false;
private BloomQueryExecutor bloomQueryExecutor;
private AutoCompleteQueryExecutor autoCompleteQueryExecutor;
private Gson gson;
@SuppressWarnings("unchecked")
public RediSearchQuery(//
QueryMethod queryMethod, //
RepositoryMetadata metadata, //
QueryMethodEvaluationContextProvider evaluationContextProvider, //
KeyValueOperations keyValueOperations, //
RedisModulesOperations> rmo, //
Class extends AbstractQueryCreator, ?>> queryCreator, //
Gson gson //
) {
logger.info(String.format("Creating %s query method", queryMethod.getName()));
this.keyValueOperations = keyValueOperations;
this.modulesOperations = (RedisModulesOperations) rmo;
this.queryMethod = queryMethod;
this.searchIndex = this.queryMethod.getEntityInformation().getJavaType().getName() + "Idx";
this.metadata = metadata;
this.domainType = this.queryMethod.getEntityInformation().getJavaType();
this.gson = gson;
bloomQueryExecutor = new BloomQueryExecutor(this, modulesOperations);
autoCompleteQueryExecutor = new AutoCompleteQueryExecutor(this, modulesOperations);
Class> repoClass = metadata.getRepositoryInterface();
@SuppressWarnings("rawtypes")
Class[] params = queryMethod.getParameters().stream().map(Parameter::getType).toArray(Class[]::new);
try {
java.lang.reflect.Method method = repoClass.getMethod(queryMethod.getName(), params);
if (method.isAnnotationPresent(com.redis.om.spring.annotations.Query.class)) {
com.redis.om.spring.annotations.Query queryAnnotation = method
.getAnnotation(com.redis.om.spring.annotations.Query.class);
this.type = RediSearchQueryType.QUERY;
this.value = queryAnnotation.value();
this.returnFields = queryAnnotation.returnFields();
} else if (method.isAnnotationPresent(Aggregation.class)) {
Aggregation aggregation = method.getAnnotation(Aggregation.class);
this.type = RediSearchQueryType.AGGREGATION;
this.value = aggregation.value();
this.load = aggregation.load();
this.apply = splitApplyArguments(aggregation.apply());
} else if (queryMethod.getName().equalsIgnoreCase("search")) {
this.type = RediSearchQueryType.QUERY;
List> orPartParts = new ArrayList<>();
orPartParts.add(Pair.of("__ALL__", QueryClause.FullText_ALL));
queryOrParts.add(orPartParts);
this.returnFields = new String[] {};
} else if (queryMethod.getName().startsWith("getAll")) {
this.type = RediSearchQueryType.TAGVALS;
this.value = ObjectUtils.lcfirst(queryMethod.getName().substring(6, queryMethod.getName().length()));
} else if (queryMethod.getName().startsWith(AutoCompleteQueryExecutor.AUTOCOMPLETE_PREFIX)) {
this.type = RediSearchQueryType.AUTOCOMPLETE;
} else {
isANDQuery = QueryClause.hasContainingAllClause(queryMethod.getName());
String methodName = isANDQuery ? QueryClause.getPostProcessMethodName(queryMethod.getName())
: queryMethod.getName();
PartTree pt = new PartTree(methodName, metadata.getDomainType());
processPartTree(pt);
this.type = RediSearchQueryType.QUERY;
this.returnFields = new String[] {};
}
} catch (NoSuchMethodException | SecurityException e) {
logger.debug(String.format("Could not resolved query method %s(%s): %s", queryMethod.getName(),
Arrays.toString(params), e.getMessage()));
}
}
private void processPartTree(PartTree pt) {
pt.stream().forEach(orPart -> {
List> orPartParts = new ArrayList<>();
orPart.iterator().forEachRemaining(part -> {
PropertyPath propertyPath = part.getProperty();
List path = StreamSupport.stream(propertyPath.spliterator(), false).collect(Collectors.toList());
orPartParts.addAll(extractQueryFields(domainType, part, path));
});
queryOrParts.add(orPartParts);
});
}
private List> extractQueryFields(Class> type, Part part, List path) {
return extractQueryFields(type, part, path, 0);
}
private List> extractQueryFields(Class> type, Part part, List path,
int level) {
List> qf = new ArrayList<>();
String property = path.get(level).getSegment();
String key = part.getProperty().toDotPath().replace(".", "_");
Field field;
try {
field = type.getDeclaredField(property);
if (field.isAnnotationPresent(TextIndexed.class)) {
TextIndexed indexAnnotation = field.getAnnotation(TextIndexed.class);
String actualKey = indexAnnotation.alias().isBlank() ? key : indexAnnotation.alias();
qf.add(Pair.of(actualKey, QueryClause.get(FieldType.FullText, part.getType())));
} else if (field.isAnnotationPresent(Searchable.class)) {
Searchable indexAnnotation = field.getAnnotation(Searchable.class);
String actualKey = indexAnnotation.alias().isBlank() ? key : indexAnnotation.alias();
qf.add(Pair.of(actualKey, QueryClause.get(FieldType.FullText, part.getType())));
} else if (field.isAnnotationPresent(TagIndexed.class)) {
TagIndexed indexAnnotation = field.getAnnotation(TagIndexed.class);
String actualKey = indexAnnotation.alias().isBlank() ? key : indexAnnotation.alias();
qf.add(Pair.of(actualKey, QueryClause.get(FieldType.Tag, part.getType())));
} else if (field.isAnnotationPresent(GeoIndexed.class)) {
GeoIndexed indexAnnotation = field.getAnnotation(GeoIndexed.class);
String actualKey = indexAnnotation.alias().isBlank() ? key : indexAnnotation.alias();
qf.add(Pair.of(actualKey, QueryClause.get(FieldType.Geo, part.getType())));
} else if (field.isAnnotationPresent(NumericIndexed.class)) {
NumericIndexed indexAnnotation = field.getAnnotation(NumericIndexed.class);
String actualKey = indexAnnotation.alias().isBlank() ? key : indexAnnotation.alias();
qf.add(Pair.of(actualKey, QueryClause.get(FieldType.Numeric, part.getType())));
} else if (field.isAnnotationPresent(Indexed.class)) {
Indexed indexAnnotation = field.getAnnotation(Indexed.class);
String actualKey = indexAnnotation.alias().isBlank() ? key : indexAnnotation.alias();
Class> fieldType = ClassUtils.resolvePrimitiveIfNecessary(field.getType());
//
// Any Character class or Boolean -> Tag Search Field
//
if (CharSequence.class.isAssignableFrom(fieldType) || (fieldType == Boolean.class)) {
qf.add(Pair.of(actualKey, QueryClause.get(FieldType.Tag, part.getType())));
}
//
// Any Numeric class -> Numeric Search Field
//
else if (Number.class.isAssignableFrom(fieldType) || (fieldType == LocalDateTime.class)
|| (field.getType() == LocalDate.class) || (field.getType() == Date.class)) {
qf.add(Pair.of(actualKey, QueryClause.get(FieldType.Numeric, part.getType())));
}
//
// Set / List
//
else if (Set.class.isAssignableFrom(fieldType) || List.class.isAssignableFrom(fieldType)) {
if (isANDQuery) {
qf.add(Pair.of(actualKey, QueryClause.Tag_CONTAINING_ALL));
} else {
qf.add(Pair.of(actualKey, QueryClause.get(FieldType.Tag, part.getType())));
}
}
//
// Point
//
else if (fieldType == Point.class) {
qf.add(Pair.of(actualKey, QueryClause.get(FieldType.Geo, part.getType())));
}
//
// Recursively explore the fields for @Indexed annotated fields
//
else {
qf.addAll(extractQueryFields(fieldType, part, path, level + 1));
}
}
} catch (NoSuchFieldException e) {
logger.info(String.format("Did not find a field named %s", key));
}
return qf;
}
@Override
public Object execute(Object[] parameters) {
Optional maybeBloomFilter = bloomQueryExecutor.getBloomFilter();
if (maybeBloomFilter.isPresent()) {
return bloomQueryExecutor.executeBloomQuery(parameters, maybeBloomFilter.get());
} else if (type == RediSearchQueryType.QUERY) {
return executeQuery(parameters);
} else if (type == RediSearchQueryType.AGGREGATION) {
return executeAggregation(parameters);
} else if (type == RediSearchQueryType.TAGVALS) {
return executeFtTagVals();
} else if (type == RediSearchQueryType.AUTOCOMPLETE) {
Optional maybeAutoCompleteDictionaryKey = autoCompleteQueryExecutor.getAutoCompleteDictionaryKey();
return maybeAutoCompleteDictionaryKey.isPresent()
? autoCompleteQueryExecutor.executeAutoCompleteQuery(parameters, maybeAutoCompleteDictionaryKey.get())
: null;
} else {
return null;
}
}
@Override
public QueryMethod getQueryMethod() {
return queryMethod;
}
private Object executeQuery(Object[] parameters) {
SearchOperations ops = modulesOperations.opsForSearch(searchIndex);
String preparedQuery = prepareQuery(parameters);
Query query = new Query(preparedQuery);
query.returnFields(returnFields);
Optional maybePageable = Optional.empty();
if (queryMethod.isPageQuery()) {
maybePageable = Arrays.stream(parameters).filter(Pageable.class::isInstance).map(Pageable.class::cast)
.findFirst();
if (maybePageable.isPresent()) {
Pageable pageable = maybePageable.get();
if (!pageable.isUnpaged()) {
query.limit(Math.toIntExact(pageable.getOffset()), pageable.getPageSize());
if (pageable.getSort() != null) {
for (Order order : pageable.getSort()) {
query.setSortBy(order.getProperty(), order.isAscending());
}
}
}
}
}
// Intercept TAG collection queries with empty parameters and use an
// aggregation
if (queryMethod.isCollectionQuery() && !queryMethod.getParameters().isEmpty()) {
List> emptyCollectionParams = Arrays.asList(parameters).stream() //
.filter(Collection.class::isInstance) //
.map(p -> (Collection>) p) //
.filter(Collection::isEmpty) //
.collect(Collectors.toList());
if (!emptyCollectionParams.isEmpty()) {
return Collections.emptyList();
}
}
SearchResult searchResult = ops.search(query);
// what to return
Object result = null;
if (queryMethod.getReturnedObjectType() == SearchResult.class) {
result = searchResult;
} else if (queryMethod.isPageQuery()) {
List
© 2015 - 2025 Weber Informatics LLC | Privacy Policy