Secondary 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 expire.

Simple Property Index

Given the sample Person entity shown earlier, we can create an index for firstname by annotating the property with @Indexed, as shown in the following example:

Example 1. Annotation driven indexing
@RedisHash("people")
public class Person {

  @Id String id;
  @Indexed String firstname;
  String lastname;
  Address address;
}

Indexes are built up for actual property values. Saving two Persons (for example, "rand" and "aviendha") results in setting up indexes similar to the following:

SADD people:firstname:rand e2c7dcee-b8cd-4424-883e-736ce564363e
SADD people:firstname:aviendha a9d4b3a0-50d3-4538-a2fc-f7fc2581ee56

It is also possible to have indexes on nested elements. Assume Address has a city property that is annotated with @Indexed. In that case, once person.address.city is not null, we have Sets for each city, as shown in the following example:

SADD people:address.city:tear e2c7dcee-b8cd-4424-883e-736ce564363e

Furthermore, the programmatic setup lets you define indexes on map keys and list properties, as shown in the following example:

@RedisHash("people")
public class Person {

  // ... other properties omitted

  Map<String,String> attributes;      (1)
  Map<String Person> relatives;       (2)
  List<Address> addresses;            (3)
}
1 SADD people:attributes.map-key:map-value e2c7dcee-b8cd-4424-883e-736ce564363e
2 SADD people:relatives.map-key.firstname:tam e2c7dcee-b8cd-4424-883e-736ce564363e
3 SADD people:addresses.city:tear e2c7dcee-b8cd-4424-883e-736ce564363e
Indexes cannot be resolved on References.

As with keyspaces, you can configure indexes without needing to annotate the actual domain type, as shown in the following example:

Example 2. Index Setup with @EnableRedisRepositories
@Configuration
@EnableRedisRepositories(indexConfiguration = MyIndexConfiguration.class)
public class ApplicationConfig {

  //... RedisConnectionFactory and RedisTemplate Bean definitions omitted

  public static class MyIndexConfiguration extends IndexConfiguration {

    @Override
    protected Iterable<IndexDefinition> initialConfiguration() {
      return Collections.singleton(new SimpleIndexDefinition("people", "firstname"));
    }
  }
}

Again, as with keyspaces, you can programmatically configure indexes, as shown in the following example:

Example 3. Programmatic Index setup
@Configuration
@EnableRedisRepositories
public class ApplicationConfig {

  //... RedisConnectionFactory and RedisTemplate Bean definitions omitted

  @Bean
  public RedisMappingContext keyValueMappingContext() {
    return new RedisMappingContext(
      new MappingConfiguration(
        new KeyspaceConfiguration(), new MyIndexConfiguration()));
  }

  public static class MyIndexConfiguration extends IndexConfiguration {

    @Override
    protected Iterable<IndexDefinition> initialConfiguration() {
      return Collections.singleton(new SimpleIndexDefinition("people", "firstname"));
    }
  }
}

Geospatial Index

Assume the Address type contains a location property of type Point that holds the geo coordinates of the particular address. By annotating the property with @GeoIndexed, Spring Data Redis adds those values by using Redis GEO commands, as shown in the following example:

@RedisHash("people")
public class Person {

  Address address;

  // ... other properties omitted
}

public class Address {

  @GeoIndexed Point location;

  // ... other properties omitted
}

public interface PersonRepository extends CrudRepository<Person, String> {

  List<Person> findByAddressLocationNear(Point point, Distance distance);     (1)
  List<Person> findByAddressLocationWithin(Circle circle);                    (2)
}

Person rand = new Person("rand", "al'thor");
rand.setAddress(new Address(new Point(13.361389D, 38.115556D)));

repository.save(rand);                                                        (3)

repository.findByAddressLocationNear(new Point(15D, 37D), new Distance(200)); (4)
1 Query method declaration on a nested property, using Point and Distance.
2 Query method declaration on a nested property, using Circle to search within.
3 GEOADD people:address:location 13.361389 38.115556 e2c7dcee-b8cd-4424-883e-736ce564363e
4 GEORADIUS people:address:location 15.0 37.0 200.0 km

In the preceding example the, longitude and latitude values are stored by using GEOADD that use the object’s id as the member’s name. The finder methods allow usage of Circle or Point, Distance combinations for querying those values.

It is not possible to combine near and within with other criteria.

Executing an Example

The following example uses Query by Example against a repository:

Example 4. Query by Example using a Repository
interface PersonRepository extends QueryByExampleExecutor<Person> {
}

class PersonService {

  @Autowired PersonRepository personRepository;

  List<Person> findPeople(Person probe) {
    return personRepository.findAll(Example.of(probe));
  }
}

Redis Repositories support, with their secondary indexes, a subset of Spring Data’s Query by Example features. In particular, only exact, case-sensitive, and non-null values are used to construct a query.

Secondary indexes use set-based operations (Set intersection, Set union) to determine matching keys. Adding a property to the query that is not indexed returns no result, because no index exists. Query by Example support inspects indexing configuration to include only properties in the query that are covered by an index. This is to prevent accidental inclusion of non-indexed properties.

Case-insensitive queries and unsupported StringMatcher instances are rejected at runtime.

The following list shows the supported Query by Example options:

  • Case-sensitive, exact matching of simple and nested properties

  • Any/All match modes

  • Value transformation of the criteria value

  • Exclusion of null values from the criteria

The following list shows properties not supported by Query by Example:

  • Case-insensitive matching

  • Regex, prefix/contains/suffix String-matching

  • Querying of Associations, Collection, and Map-like properties

  • Inclusion of null values from the criteria

  • findAll with sorting