8. Elasticsearch Repositories
This chapter includes details of the implementation of the elastic search repository
8.1.1. Query methods
The elastic search module supports all basic query building features, such as string queries, native search queries, condition based queries, or derived queries from method names
Declared queriesDeriving a Query from a method name is not always sufficient and / or may cause the method name to be unreadable. In this case, you can use the @ Query annotation( See using @ Query annotation ).
8.1.2 Query creation
In general, the query creation mechanism of elastic search is described in the query method. Here is a short example of an elastic search query method
Example 68 Query creation from method names
interface BookRepository extends Repository<Book, String> { List<Book> findByNameAndPrice(String name, Integer price); }
The above method name will be translated into the following Elasticsearch json query
{ "query": { "bool" : { "must" : [ { "query_string" : { "query" : "?", "fields" : [ "name" ] } }, { "query_string" : { "query" : "?", "fields" : [ "price" ] } } ] } } }
The following shows a list of keywords supported by Elasticsearch.
Supported keywords inside method names Keyword Sample instance Elasticsearch Query String And findByNameAndPrice { "query" : { "bool" : { "must" : [ { "query_string" : { "query" : "?", "fields" : [ "name" ] } }, { "query_string" : { "query" : "?", "fields" : [ "price" ] } } ] } }} Or findByNameOrPrice { "query" : { "bool" : { "should" : [ { "query_string" : { "query" : "?", "fields" : [ "name" ] } }, { "query_string" : { "query" : "?", "fields" : [ "price" ] } } ] } }} Is findByName { "query" : { "bool" : { "must" : [ { "query_string" : { "query" : "?", "fields" : [ "name" ] } } ] } }} Not findByNameNot { "query" : { "bool" : { "must_not" : [ { "query_string" : { "query" : "?", "fields" : [ "name" ] } } ] } }} Between findByPriceBetween { "query" : { "bool" : { "must" : [ {"range" : {"price" : {"from" : ?, "to" : ?, "include_lower" : true, "include_upper" : true } } } ] } }} LessThan findByPriceLessThan { "query" : { "bool" : { "must" : [ {"range" : {"price" : {"from" : null, "to" : ?, "include_lower" : true, "include_upper" : false } } } ] } }} LessThanEqual findByPriceLessThanEqual { "query" : { "bool" : { "must" : [ {"range" : {"price" : {"from" : null, "to" : ?, "include_lower" : true, "include_upper" : true } } } ] } }} GreaterThan findByPriceGreaterThan { "query" : { "bool" : { "must" : [ {"range" : {"price" : {"from" : ?, "to" : null, "include_lower" : false, "include_upper" : true } } } ] } }} GreaterThanEqual findByPriceGreaterThan { "query" : { "bool" : { "must" : [ {"range" : {"price" : {"from" : ?, "to" : null, "include_lower" : true, "include_upper" : true } } } ] } }} Before findByPriceBefore { "query" : { "bool" : { "must" : [ {"range" : {"price" : {"from" : null, "to" : ?, "include_lower" : true, "include_upper" : true } } } ] } }} After findByPriceAfter { "query" : { "bool" : { "must" : [ {"range" : {"price" : {"from" : ?, "to" : null, "include_lower" : true, "include_upper" : true } } } ] } }} Like findByNameLike { "query" : { "bool" : { "must" : [ { "query_string" : { "query" : "?*", "fields" : [ "name" ] }, "analyze_wildcard": true } ] } }} StartingWith findByNameStartingWith { "query" : { "bool" : { "must" : [ { "query_string" : { "query" : "?*", "fields" : [ "name" ] }, "analyze_wildcard": true } ] } }} EndingWith findByNameEndingWith { "query" : { "bool" : { "must" : [ { "query_string" : { "query" : "*?", "fields" : [ "name" ] }, "analyze_wildcard": true } ] } }} Contains/Containing findByNameContaining { "query" : { "bool" : { "must" : [ { "query_string" : { "query" : "*?*", "fields" : [ "name" ] }, "analyze_wildcard": true } ] } }} In findByNameIn(Collectionnames) { "query" : { "bool" : { "must" : [ {"bool" : {"must" : [ {"terms" : {"name" : ["?","?"]}} ] } } ] } }} NotIn findByNameNotIn(Collectionnames) { "query" : { "bool" : { "must" : [ {"bool" : {"must_not" : [ {"terms" : {"name" : ["?","?"]}} ] } } ] } }} Near findByStoreNear Not Supported Yet ! True findByAvailableTrue { "query" : { "bool" : { "must" : [ { "query_string" : { "query" : "true", "fields" : [ "available" ] } } ] } }} False findByAvailableFalse { "query" : { "bool" : { "must" : [ { "query_string" : { "query" : "false", "fields" : [ "available" ] } } ] } }} OrderBy findByAvailableTrueOrderByNameDesc { "query" : { "bool" : { "must" : [ { "query_string" : { "query" : "true", "fields" : [ "available" ] } } ] } }, "sort":[{"name":{"order":"desc"}}] }8.1.3. Method return types
The Repository method can be defined with the following return types to return multiple elements:
- List<T>
- Stream<T>
- SearchHits<T>
- List<SearchHit<T>>
- Stream<SearchHit<T>>
- SearchPage<T>
8.1.4. Using @ Query annotation
Example 69 Declare query at the method using the @ Query annotation on the method
interface BookRepository extends ElasticsearchRepository<Book, String> { @Query("{\"match\": {\"name\": {\"query\": \"?0\"}}}") Page<Book> findByName(String name,Pageable pageable); }
The string set as the annotation parameter must be a valid Elasticsearch JSON query. It will be sent to easticearch as the value of the query element; for example, if the function is called with the parameter John, it will generate the following query body
{ "query": { "match": { "name": { "query": "John" } } } }
8.2. Annotation based configuration
You can activate spring data elastic search repository support using annotations through JavaConfig.
Example 70 Spring Data Elasticsearch repositories using JavaConfig spring data elasticsearch repository using JavaConfig@Configuration @EnableElasticsearchRepositories( //1 basePackages = "org.springframework.data.elasticsearch.repositories" ) static class Config { @Bean public ElasticsearchOperations elasticsearchTemplate() { //2 // ... } } class ProductService { private ProductRepository repository; //3 public ProductService(ProductRepository repository) { this.repository = repository; } public Page<Product> findAvailableBookByName(String name, Pageable pageable) { return repository.findByAvailableTrueAndNameStartingWith(name, pageable); } }
- Enable elastic search repositories annotation enables repository support. If the base package is not configured, it uses one of its configuration classes.
- Perform the Elasticsearch operations operation by using one of the configurations shown in the Elasticsearch Operations Section
- Let Spring inject the repository bean into the class.
8.3. Elasticsearch Repositories using CDI
The Spring Data Elasticsearch repository can also be set using the CDI feature.
Example 71 Spring Data Elasticsearch repositories using CDIclass ElasticsearchTemplateProducer { @Produces @ApplicationScoped public ElasticsearchOperations createElasticsearchTemplate() { // ... //1 } } class ProductService { private ProductRepository repository; //2 public Page<Product> findAvailableBookByName(String name, Pageable pageable) { return repository.findByAvailableTrueAndNameStartingWith(name, pageable); } @Inject public void setRepository(ProductRepository repository) { this.repository = repository; } }
- Create the component using the same call as in the elastic search operations section.
- Let the CDI framework inject the repository into the class.
8.4. Spring Namespace
The Spring Data Elasticsearch module contains a custom namespace that allows you to define repository bean s and instantiate elements of the elasticsearch server.
as Create a repository instance as described in , use the repositories element to find the Spring data repository.
Example 72 Setting up Elasticsearch repositories using Namespace<?xml version="1.0" encoding="UTF-8"?> <beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:elasticsearch="http://www.springframework.org/schema/data/elasticsearch" xsi:schemaLocation="http://www.springframework.org/schema/beans https://www.springframework.org/schema/beans/spring-beans-3.1.xsd http://www.springframework.org/schema/data/elasticsearch https://www.springframework.org/schema/data/elasticsearch/spring-elasticsearch-1.0.xsd"> <elasticsearch:repositories base-package="com.acme.repositories" /> </beans>
Register an Elasticsearch Server instance in the context using the transport client or Rest client element.
Example 73 Transport Client using Namespace using name Transport Client<?xml version="1.0" encoding="UTF-8"?> <beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:elasticsearch="http://www.springframework.org/schema/data/elasticsearch" xsi:schemaLocation="http://www.springframework.org/schema/beans https://www.springframework.org/schema/beans/spring-beans-3.1.xsd http://www.springframework.org/schema/data/elasticsearch https://www.springframework.org/schema/data/elasticsearch/spring-elasticsearch-1.0.xsd"> <elasticsearch:transport-client id="client" cluster-nodes="localhost:9300,someip:9300" /> </beans>Example 74 Rest Client using Namespace
<?xml version="1.0" encoding="UTF-8"?> <beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:elasticsearch="http://www.springframework.org/schema/data/elasticsearch" xsi:schemaLocation="http://www.springframework.org/schema/data/elasticsearch https://www.springframework.org/schema/data/elasticsearch/spring-elasticsearch.xsd http://www.springframework.org/schema/beans https://www.springframework.org/schema/beans/spring-beans.xsd"> <elasticsearch:rest-client id="restClient" hosts="http://localhost:9200"> </beans>
8.5. Reactive Elasticsearch Repositories
Responsive Elasticsearch repository support is based on core repository support, which is explained when using Spring data repositories, which use operations provided by responsive Elasticsearch operations performed by responsive clients.
The Spring Data Elasticsearch responsive repository supports the use of Project Reactor as the responsive composite library of its choice.
There are three main interfaces available:
- ReactiveRepository
- ReactiveCrudRepository
- ReactiveSortingRepository
8.5.1. Usage
To use the repository to access domain objects stored in elastic search, you only need to create an interface for them. Before you can actually do this, you need an entity.
Example 75 Person entitypublic class Person { @Id private String id; private String firstname; private String lastname; private Address address; // ... getters and setters omitted }
Note that the id attribute must be of type String.
Example 76 Basic repository interface to persist Person entitiesinterface ReactivePersonRepository extends ReactiveSortingRepository<Person, String> { Flux<Person> findByFirstname(String firstname); //1 Flux<Person> findByFirstname(Publisher<String> firstname); //2 Flux<Person> findByFirstnameOrderByLastname(String firstname); //3 Flux<Person> findByFirstname(String firstname, Sort sort); //4 Flux<Person> findByFirstname(String firstname, Pageable page); //5 Mono<Person> findByFirstnameAndLastname(String firstname, String lastname);//6 Mono<Person> findFirstByLastname(String lastname); //7 @Query("{ \"bool\" : { \"must\" : { \"term\" : { \"lastname\" : \"?0\" } } } }") Flux<Person> findByLastname(String lastname); //8 Mono<Long> countByFirstname(String firstname) //9 Mono<Boolean> existsByFirstname(String firstname) //10 Mono<Long> deleteByFirstname(String firstname) //11 }
- This method shows a query for all people with a given last name.
- The finder method waits for input from the publisher to bind the parameter value of firstname.
- The finder method sorts the matching documents by lastname.
- The finder method sorts the matching documents by an expression defined by the Sort parameter.
- Use Pageable to pass the offset and sort parameters to the database.
- The finder method of determining conditions with and/or keyword
- Find the first matching entity.
- This method shows a Query for all people with a given lastname by running @ Query with annotations
- Calculates all entities with matching firstname.
- Check if there is at least one entity matching firstname
- Delete all entries with matching firstname.
8.5.2. Configutaion configuration
For Java configuration, use the @ EnableReactiveElasticsearchRepositories annotation. If the base package is not configured, the infrastructure scans for packages of annotated configuration classes.
The following listing shows how to use Java configuration for the repository:
Example 77 Java configuration for repositories@Configuration @EnableReactiveElasticsearchRepositories public class Config extends AbstractReactiveElasticsearchConfiguration { @Override public ReactiveElasticsearchClient reactiveElasticsearchClient() { return ReactiveRestClients.create(ClientConfiguration.localhost()); } }
Because the repository in the previous example extends the reactive sorting repository, you can use all CRUD operations as well as the methods for sorting access to entities. Using a repository instance is a dependency issue to inject into the client, as shown in the following example
Example 78 Sorted access to Person entitiespublic class PersonRepositoryTests { @Autowired ReactivePersonRepository repository; @Test public void sortsElementsCorrectly() { Flux<Person> persons = repository.findAll(Sort.by(new Order(ASC, "lastname"))); // ... } }
9. Auditing
9.1. Basics
Spring Data provides sophisticated support to transparently track who created or changed entities and when changes occurred. To benefit from this capability, audit metadata must be provided for entity classes, which can be defined using annotations or through implementation interfaces.
9.1.1. Annotation based audit metadata
We provide @ CreatedBy and @ LastModifiedBy to capture users who create or modify entities, and @ CreatedDate and @ LastModifiedDate to capture users when changes occur
Example 79An audited entityclass Customer { @CreatedBy private User user; @CreatedDate private DateTime createdDate; // ... further properties omitted }
As you can see, annotations can be applied selectively, depending on the information you want to capture. Annotations that capture when changes are made can be used for properties of type joda time, DateTime, legacy Java date and calendar, JDK8 date and time types, and long or long.
Interface based audit metadataIf you don't want to define audit metadata with annotations, you can let the domain class implement the Auditable interface. It exposes setter methods for all audit properties.
There is also a convenient base class AbstractAuditable, which you can extend to avoid implementing interface methods manually. Doing so increases the coupling of domain classes with Spring data, which you may want to avoid. In general, it is best to define audit metadata in an annotation based way, because it is less intrusive and more flexible.
9.1.3. AuditorAware
In the case of @ CreatedBy or @ LastModifiedBy, the audit infrastructure needs to know the current principal. To do this, we provide an auditoraware < T > SPI interface that you must implement to tell the infrastructure who the current user or system is interacting with the application. The generic type T defines what type of attribute must be annotated with @ CreatedBy or @ LastModifiedBy.
The following example shows the interface implementation of an authentication object using Spring Security
Example 80 implementation of auditoraware based on spring securityclass SpringSecurityAuditorAware implements AuditorAware<User> { public Optional<User> getCurrentAuditor() { return Optional.ofNullable(SecurityContextHolder.getContext()) .map(SecurityContext::getAuthentication) .filter(Authentication::isAuthenticated) .map(Authentication::getPrincipal) .map(User.class::cast); } }
This implementation accesses the authentication object provided by Spring Security and finds the custom UserDetails instance that you created in the UserDetailsService implementation. Let's say you're exposing domain users through UserDetails, but depending on the authentication found, you can also find it from anywhere.
9.2. Elasticsearch Audition
9.2.1. Preparing entities
In order for audit code to judge whether an entity instance is a new instance, the entity must implement a persistent < ID > interface, which is defined as follows
package org.springframework.data.domain; import org.springframework.lang.Nullable; public interface Persistable<ID> { @Nullable ID getId(); boolean isNew(); }
Since the existence of Id is not a sufficient criterion to determine whether an enitity is a new elastic search, additional information is necessary. One way is to use the audit fields associated with the creation of the decision
The Person entity might look like this -- omit the getter and setter methods for brevity:
@Document(indexName = "person") public class Person implements Persistable<Long> { @Id private Long id; private String lastName; private String firstName; @Field(type = FieldType.Date, format = DateFormat.basic_date_time) private Instant createdDate; private String createdBy @Field(type = FieldType.Date, format = DateFormat.basic_date_time) private Instant lastModifiedDate; private String lastModifiedBy; public Long getId() { //1 return id; } @Override public boolean isNew() { return id == null || (createdDate == null && createdBy == null); //2 } }
- getter is also the implementation of the interface
- If the object does not have an id, or if the field containing the creation property is not set, the object is new.
9.2.2. Activating auditing
After the entity is set up and AuditorAware is provided, the audit must be activated by setting @ EnableElasticsearchAuditing on the configuration class
@Configuration @EnableElasticsearchRepositories @EnableElasticsearchAuditing class MyConfiguration { // configuration code }
If the code contains multiple auditoraware beans for different types, you must provide the name of the bean as a parameter to the auditorAwareRef parameter of the @ EnableElasticsearchAuditing annotation.
10. Entity Callbacks
The Spring data infrastructure provides hooks for modifying entities before and after certain methods are called. Those so-called EntityCallback instances provide a convenient way to check and modify callback style entities. EntityCallback looks like a dedicated ApplicationListener. Some Spring data modules publish and store specific events (such as BeforeSaveEvent) that allow modification of a given entity. In some cases, such as when using immutable types, these events can cause trouble. In addition, event publishing depends on applicationeventmulticast
Entity callbacks provide integration points with synchronization and reaction APIs to ensure that well-defined checkpoints in the processing chain are executed sequentially, returning an entity or reactor wrapper type that may be modified.
Entity callbacks are usually separated by API types. This separation means that the synchronization API only considers the synchronous entity callback, while the reactive implementation only considers the reactive entity callback.
The entity callback API has been introduced in Spring Data Commons 2.2. This is the recommended method for applying entity modifications. Existing store specific ApplicationEvents are still published before calling the EntityCallback instance that might be registered.
10.1. Implementing Entity Callbacks
EntityCallback is directly associated with its domain type through its generic type parameter. Each Spring data module usually comes with a predefined set of EntityCallback interfaces that cover the entity lifecycle.
Example 81 Anatomy of an EntityCallback@FunctionalInterface public interface BeforeSaveCallback<T> extends EntityCallback<T> { /** * Entity callback method invoked before a domain object is saved. * Can return either the same or a modified instance. * * @return the domain object to be persisted. */ T onBeforeSave(T entity <2>, String collection <3>); //1 }
- The specific method to call before saving the entity. Returns an instance that may be modified.
- Entities before persistence.
- Many store specific parameters are persisted to the collection.
@FunctionalInterface public interface ReactiveBeforeSaveCallback<T> extends EntityCallback<T> { /** * Entity callback method invoked on subscription, before a domain object is saved. * The returned Publisher can emit either the same or a modified instance. * * @return Publisher emitting the domain object to be persisted. */ Publisher<T> onBeforeSave(T entity <2>, String collection <3>); //1 }
- A specific method called at subscription time before the entity is saved. Issue an instance that may be modified.
- Entities before persistence.
- Many store specific parameters are persisted to the collection.
The optional entity callback parameters are defined by the implementation Spring data module, and the EntityCallback.callback Call site inference for ().
Implement an interface that is appropriate for your application, as shown in the following example
Example 83beforesavecallbackclass DefaultingEntityCallback implements BeforeSaveCallback<Person>, Ordered { //2 @Override public Object onBeforeSave(Person entity, String collection) { //1 if(collection == "user") { return // ... } return // ... } @Override public int getOrder() { return 100; //2 } }
- Implement callbacks according to your requirements.
- If there are multiple entity callbacks of the same domain type, you may need to order entity callbacks. The order follows the lowest priority.
10.2. Registering Entity Callbacks
Entitycallback beans are picked up by the store specific implementation in case they are registered in the ApplicationContext. Most template APIs already implement ApplicationContext, so you can access ApplicationContext
The following example explains the set of valid entity callback registrations:
Example 84 EntityCallback Bean registration Bean@Order(1) @Component class First implements BeforeSaveCallback<Person> {//2 @Override public Person onBeforeSave(Person person) {//1 return // ... } } @Component class DefaultingEntityCallback implements BeforeSaveCallback<Person>, Ordered { @Override public Object onBeforeSave(Person entity, String collection) { // ... } @Override public int getOrder() { return 100; //2 } } @Configuration public class EntityCallbackConfiguration { @Bean BeforeSaveCallback<Person> unorderedLambdaReceiverCallback() { return (BeforeSaveCallback<Person>) it -> // ... } } @Component class UserCallbacks implements BeforeConvertCallback<User>, BeforeSaveCallback<User> { @Override public Person onBeforeConvert(User user) { return // ... } @Override public Person onBeforeSave(User user) { return // ... } }
- Receive an Order from the @ Order annotation.
- Receive its orders through an ordered interface.
- Use lambda expressions. By default, it is disordered and finally called. Note that callbacks implemented by lambda expressions do not expose type information, so calling these callbacks with non allocatable entities can affect callback throughput. Use class or enumeration to enable type filtering for callback bean s.
- Combine multiple entity callback interfaces in an implementation class.
10.3. Elasticsearch EntityCallbacks
Spring Data Elasticsearch internally uses the EntityCallback API for audit support and responds to the following callbacks
Supported Entity Callbacks supported entity callbacks
Callback Method method Description Description Order priority Reactive/BeforeConvertCallback onBeforeConvert(T entity, IndexCoordinates index) Invoked before a domain object is converted to org.springframework.data.elasticsearch.core.document.Document. Can return the entity or a modified entity which then will be converted. Ordered.LOWEST_PRECEDENCE Reactive/AfterConvertCallback onAfterConvert(T entity, Document document, IndexCoordinates indexCoordinates) Invoked after a domain object is converted from org.springframework.data.elasticsearch.core.document.Document on reading result data from Elasticsearch. Ordered.LOWEST_PRECEDENCE Reactive/AuditingEntityCallback onBeforeConvert(Object entity, IndexCoordinates index) Marks an auditable entity created or modified 100 Reactive/AfterSaveCallback T onAfterSave(T entity, IndexCoordinates index) Invoked after a domain object is saved. Ordered.LOWEST_PRECEDENCE11. Miscellaneous elastic search operation support
This chapter covers additional support for elastic search operations that cannot be accessed directly through the repository interface. It is recommended that these operations be added as custom implementations, as described in the custom implementation of the Spring data repository.
11.1. Filter Builder
Filter Builder improves query speed.
private ElasticsearchOperations operations; IndexCoordinates index = IndexCoordinates.of("sample-index"); SearchQuery searchQuery = new NativeSearchQueryBuilder() .withQuery(matchAllQuery()) .withFilter(boolFilter().must(termFilter("id", documentId))) .build(); Page<SampleEntity> sampleEntities = operations.searchForPage(searchQuery, SampleEntity.class, index);
11.2. Using Scroll For Big Result Set
There is a scrolling API that can get a large result set. Spring Data Elasticsearch uses it internally to provide < T > searchhitsiterator < T > SearchOperations.searchForStream (query query, class < T > clazz, indexcoordinates index) method.
IndexCoordinates index = IndexCoordinates.of("sample-index"); SearchQuery searchQuery = new NativeSearchQueryBuilder() .withQuery(matchAllQuery()) .withFields("message") .withPageable(PageRequest.of(0, 10)) .build(); SearchHitsIterator<SampleEntity> stream = elasticsearchTemplate.searchForStream(searchQuery, SampleEntity.class, index); List<SampleEntity> sampleEntities = new ArrayList<>(); while (stream.hasNext()) { sampleEntities.add(stream.next()); } stream.close();
There is no method to access the scroll id in the SearchOperations API. If you need to access the scroll id, you can use the following methods of ElasticsearchRestTemplate:
@Autowired ElasticsearchRestTemplate template; IndexCoordinates index = IndexCoordinates.of("sample-index"); SearchQuery searchQuery = new NativeSearchQueryBuilder() .withQuery(matchAllQuery()) .withFields("message") .withPageable(PageRequest.of(0, 10)) .build(); SearchScrollHits<SampleEntity> scroll = template.searchScrollStart(1000, searchQuery, SampleEntity.class, index); String scrollId = scroll.getScrollId(); List<SampleEntity> sampleEntities = new ArrayList<>(); while (scroll.hasSearchHits()) { sampleEntities.addAll(scroll.getSearchHits()); scrollId = scroll.getScrollId(); scroll = template.searchScrollContinue(scrollId, 1000, SampleEntity.class); } template.searchScrollClear(scrollId);
To use the Scroll API with repository methods, the return type must be defined as a Stream in the Elasticsearch Repository. The implementation of this method will then use the scrolling method in ElasticsearchTemplate.
interface SampleEntityRepository extends Repository<SampleEntity, String> { Stream<SampleEntity> findBy(); }
11.3. Sort options
In addition to the described default sorting options for paging and sorting Spring Data Elasticsearch, Elasticsearch also has a GeoDistanceOrder class that can be used to sort the results of search operations based on geographic distance.
If the class to be retrieved has a GeoPoint attribute named location, the following Sort sorts the results based on the distance to the given point:
Sort.by(new GeoDistanceOrder("location", new GeoPoint(48.137154, 11.5761247)))