Springboot integrates ElasticSearch 7.14.1. What is ElasticsearchRestTemplate?

Keywords: ElasticSearch Spring Boot RESTful

Start with a case

pom

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.5.4</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>
    <groupId>com.dongua</groupId>
    <artifactId>es-springboot-demo</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <name>es-springboot-demo</name>

    <properties>
        <java.version>11</java.version>
        <elasticsearch.version>7.14.1</elasticsearch.version>
    </properties>
    <dependencies>
     <!-- That's it -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-elasticsearch</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-configuration-processor</artifactId>
        </dependency>
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>fastjson</artifactId>
            <version>1.2.75</version>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>

</project>

applicantion.yml

spring:
  elasticsearch:
    rest:
    # uri of es
      uris: http://localhost:9200
      # Connection timeout
      connection-timeout: 1
      # Read timeout
      read-timeout: 30

Of course, it can also be defined according to its own configuration class

@EqualsAndHashCode(callSuper = true)
@ConfigurationProperties(prefix = "elasticsearch")
@Data
@Configuration
public class RestHighClientConfig extends AbstractElasticsearchConfiguration {

    private String host;
    private int port;

    @Override
    @Bean
    public RestHighLevelClient elasticsearchClient() {
        return new RestHighLevelClient
                (RestClient.builder(new HttpHost(host, port, "http")));
    }


// This will do
    @Override
    @Bean
    public RestHighLevelClient elasticsearchClient() {

        final ClientConfiguration clientConfiguration = ClientConfiguration.builder()
                .connectedTo("localhost:9200")
                .build();

        return RestClients.create(clientConfiguration).rest();
    }
}

elasticsearch:
  host: 127.0.0.1
  port: 9200

pojo

@Data
@NoArgsConstructor
@AllArgsConstructor
@ToString
// Alias, if not set_ The value of class is its full path, such as com.xxx.pojo.Product
@TypeAlias("product")
// Name of index
@Document(indexName = "product")
// Set the number of slices to 3
@Setting(shards = 3)
public class Product {
	// Unique identification, duplicate will be overwritten
    @Id
    private Long id;
    // text type, which can be queried by word segmentation
    @Field(type = FieldType.Text)
    private String title;
    // Keyword type, query according to the whole word
    @Field(type = FieldType.Keyword)
    private String category;,
    // Double type
    @Field(type = FieldType.Double)
    private Double price;
    // This field is not used as an index. It is an index by default. It is set to false here
    @Field(type = FieldType.Keyword, index = false)
    private String images;
}

Document annotation

Overview of mapping notes (from the official website)

In the mappingelastic searchconverter, use the mapping file of metadata driven objects. Metadata is taken from entity attributes that can be annotated.

The following notes are available:

@Document: applied at the class level, indicating that the class is a candidate mapped to the database. The most important attributes are:

indexName: the index name that stores the entity. This can include a SpEL template expression, such as "log-#{T(java.time.LocalDate).now().toString()}"

Type: mapping type. If not set, the lowercase simple name of the class is used. (deprecated since version 4.0)

createIndex: marks whether an index is created when the repository boots. The default value is true. See automatically creating an index using the appropriate mapping

versionType: configuration of version management. The default value is EXTERNAL.

@Id: applied at the field level to mark the field for identification purposes.

@Transient: by default, all fields are mapped to the document when stored or retrieved. This comment does not include this field.

@PersistenceConstructor: marks that the given constructor - even a package protected constructor - is used when instantiating objects from the database. Constructor parameters map by name to key values in the retrieved document.

@Field: it is applied to the field level and defines the attributes of the field. Most attributes are mapped to their respective Elasticsearch Mapping definitions

Name: field name, because it will be represented in the Elasticsearch document. If it is not set, the Java field name will be used.

Type: field type, which can be Text, Keyword, long, integer, Short, Byte, double, float, Half_Float,Scaled_Float,Date,Date_Nanos,Boolean,Binary,Integer_Range,Float_Range,Long_Range,Double_Range,Date_Range,Ip_ One of range and Object, nested, Ip, TokenCount, Percolator, Flattened, Search_As_You_Type. View Elasticsearch mapping types

format: one or more built-in date formats

pattern: one or more custom date formats

store: Flag whether the original field value should be stored in Elasticsearch. The default value is false.

Index setting (excerpted from the official website)

When using Spring Data Elasticsearch to create an Elasticsearch index, you can use the @ Setting annotation to define different index settings. The following parameters are available:

useServerConfiguration does not send any setting parameters, so Elasticsearch server configuration determines them.

settingPath is a JSON file that defines settings that must be resolved in the classpath

The number of shards to use. The default value is 1

Number of replicas. The default value is 1

Refreshinterval, the default is "1s"

indexStoreType, default to "fs"

You can also define index sorting

@Setting source code

@Persistent
@Inherited
@Retention(RetentionPolicy.RUNTIME)
@Target({ ElementType.TYPE })
public @interface Setting {

	/**
	 * Resource path for a settings configuration
	 * Set the configured resource path. It does not exist by default
	 */
	String settingPath() default "";

	/**
	 * Use server-side settings when creating the index.
	 * Use server-side settings when creating indexes. Default false
	 */
	boolean useServerConfiguration() default false;

	/**
	 * Number of shards for the index. Used for index creation. <br/>
	 * With version 4.0, the default value is changed from 5 to 1 to reflect the change in the default settings of
	 * Elasticsearch which changed to 1 as well in Elasticsearch 7.0.
	 */
	short shards() default 1;

	/**
	 * Number of replicas for the index. Used for index creation.
	 */
	short replicas() default 1;

	/**
	 * Refresh interval for the index. Used for index creation.
	 * Refresh interval for the index. Used to create an index. The so-called refreshing the index is to brush the index into the OS Cache once every 1 second by default
	 */
	String refreshInterval() default "1s";

	/**
	 * Index storage type for the index. Used for index creation.
	 * The index storage type of the index. Used to create an index. The default is fs
	 */
	String indexStoreType() default "fs";

	/**
	 * fields to define an index sorting
	 * Fields that define index sort
	 * @since 4.2
	 */
	String[] sortFields() default {};

	/**
	 * defines the order for {@link #sortFields()}. If present, it must have the same number of elements
	 * Define the order of sortFields(). If it exists, it must have the same number of elements
	 * @since 4.2
	 */
	SortOrder[] sortOrders() default {};

	/**
	 * defines the mode for {@link #sortFields()}. If present, it must have the same number of elements
	 * Defines the schema for sortFields(). If it exists, it must have the same number of elements
	 * @since 4.2
	 */
	SortMode[] sortModes() default {};

	/**
	 * defines the missing value for {@link #sortFields()}. If present, it must have the same number of elements
	 * Defines the missing value for sortFields(). If it exists, it must have the same number of elements
	 * @since 4.2
	 */
	SortMissing[] sortMissingValues() default {};

	// Provides an internal enumeration for use
	enum SortOrder {
		asc, desc
	}

	enum SortMode {
		min, max
	}

	enum SortMissing {
		_last, _first
	}
}

Official case

@Document(indexName = "entities")
@Setting(
  sortFields = { "secondField", "firstField" },                                    // 1 place  
  sortModes = { Setting.SortMode.max, Setting.SortMode.min },                      // 2 places
  sortOrders = { Setting.SortOrder.desc, Setting.SortOrder.asc },
  sortMissingValues = { Setting.SortMissing._last, Setting.SortMissing._first })
class Entity {
    @Nullable
    @Id private String id;

    @Nullable
    @Field(name = "first_field", type = FieldType.Keyword)
    private String firstField;

    @Nullable @Field(name = "second_field", type = FieldType.Keyword)
    private String secondField;

    // getter and setter...
}

1. Place: when defining the sorting field, use the name of the Java attribute (firstField) instead of the name that may be defined for Elasticsearch (first_field)
2. sortModes,sortOrders and sortMissingValues at are optional, but if they are set, the number of entries must match the number of sortFields elements

It means naming, distinguishing between hump and underline. The attribute of java uses hump and the database uses underline

@Filed

Repository

import com.dongua.pojo.Product;
import org.springframework.data.elasticsearch.repository.ElasticsearchRepository;
import org.springframework.stereotype.Repository;

// This is the core of some of our CRUD packages, which will be explored later
@Repository
public interface ProductDao extends ElasticsearchRepository<Product,Long> {

}

Query annotation (from the official website)

Inherit the method from ElasticsearchRepository

Test ElasticsearchRestTemplate

According to the tips on the official website and the code, test several APIs first

@SpringBootTest
public class SpringDataESProductDaoTest {

	// ElasticsearchRestTemplate actually implements the ElasticsearchOperations interface
    @Resource
    ElasticsearchRestTemplate restTemplate;

    @Resource
    private ProductDao productDao;

    /**
     * Add a new document
     */
    @Test
    public void save() {
        Product product = new Product();
        // Globally unique, duplicate will overwrite
        product.setId(1L);
        product.setTitle("Huawei Mobile 4");
        product.setCategory("mobile phone");
        product.setPrice(2999.0);
        product.setImages("http://www.xxx/hw.jpg");
        product.setScore(1);
        productDao.save(product);
    }
    //POSTMAN, GET http://localhost:9200/product/_doc/1

    //Modify a document
    @Test
    public void update() {
        Product product = new Product();
        product.setId(1L);
        product.setTitle("Xiaomi 2 mobile phone");
        product.setCategory("mobile phone");
        product.setPrice(9999.0);
        product.setImages("http://www.xxx/xm.jpg");
        product.setScore(8);
        productDao.save(product);
    }
    //POSTMAN, GET http://localhost:9200/product/_doc/1


    //Query document by id
    @Test
    public void findById() {
        Product product = productDao.findById(2L).get();
        System.out.println(product);
    }

	// Query all documents
    @Test
    public void findAll() {
        Iterable<Product> products = productDao.findAll();
        for (Product product : products) {
            System.out.println(product);
        }
    }


    // Delete index
    @Test
    public void deleteIndex() {
    	// The indexOps() index operation method of restTemplate is used here
    	// The method provided by dao does not support operation index
        boolean delete = restTemplate.indexOps(Product.class).delete();
        System.out.println("delete = " + delete);
    }

    //Delete a document
    @Test
    public void delete() {
        Product product = new Product();
        product.setId(2L);
        productDao.delete(product);
    }
    //POSTMAN, GET http://localhost:9200/product/_doc/2

    //Batch add documents
    @Test
    public void saveAll() {
        List<Product> productList = new ArrayList<>();
        for (int i = 0; i < 10; i++) {
            Product product = new Product();
            product.setId(Long.valueOf(i));
            product.setTitle("[" + i + "]Mi phones");
            product.setCategory("mobile phone");
            product.setPrice(1999.0 + i);
            product.setImages("http://www.xxx/xm.jpg");
            productList.add(product);
        }
        productDao.saveAll(productList);
    }

    //Paging query
    @Test
    public void findByPageable() {
        // Set sorting (sorting method, positive or reverse order, sorting id)
        Sort sort = Sort.by(Sort.Direction.DESC, "id");
        int currentPage = 0;//Current page, the first page starts from 0, and 1 represents the second page
        int pageSize = 5;//How many are displayed per page
        // Set query paging
        // Even if the sort parameter is not provided, he will make some sort rule by himself
        PageRequest pageRequest = PageRequest.of(currentPage, pageSize, sort);
        //Paging query
        Page<Product> productPage = productDao.findAll(pageRequest);
        System.out.println("Total number of elements = " + productPage.getTotalElements());
        for (Product Product : productPage.getContent()) {
            System.out.println(Product);
        }
    }
}

I won't operate the test one by one. Just pay attention to some places

  1. When the save and update methods are executed, the index has been initialized
  2. Only some api tests are listed here. Others can try them slowly
  3. productDao cannot perform index level operations. It can only operate on document data

Core class

ElasticsearchRestTemplate

ElasticsearchRestTemplate inherits AbstractElasticsearchTemplate

AbstractElasticsearchTemplate implements ElasticsearchOperations interface

ElasticsearchOperations inherits DocumentOperations, SearchOperations

Explanation of Elasticsearch operation from the official website:

  • IndexOperations defines index level operations, such as creating or deleting indexes.
  • DocumentOperations defines operations that store, update, and retrieve entities based on IDS.
  • SearchOperations defines the operation of searching multiple entities using a query
  • ElasticsearchOperations combines the DocumentOperations and SearchOperations interfaces.

ElasticsearchOperations

Write a TestController to test a wave of ElasticsearchOperations

// Test ElasticsearchOperations
@RestController
@RequestMapping("/")
public class TestController {

    private final ElasticsearchOperations elasticsearchOperations;

    public TestController(ElasticsearchOperations elasticsearchOperations) {
        this.elasticsearchOperations = elasticsearchOperations;
    }

    @PostMapping("/user")
    public String save(@RequestBody User user) {
		// Index query object: set query criteria
        IndexQuery indexQuery = new IndexQueryBuilder()
        		// . withVersion(2L) set the version number to 2
                .withId(String.valueOf(user.getId()))// Set the index id to user's id
                .withObject(user) // Tell him which entity object it is
                .build(); // return
                
                // IndexCoordinates.of("user") the index name is user, which is user-defined
                // If there is no such index, he will create the index himself and add / update a User document data
        return elasticsearchOperations.index(indexQuery, IndexCoordinates.of("user"));
    }

    @PostMapping("/product")
    public String save2(@RequestBody Product product) {

        IndexQuery indexQuery = new IndexQueryBuilder()
                .withId(String.valueOf(product.getId()))
                .withObject(product)
                .build();
        return elasticsearchOperations.index(indexQuery, IndexCoordinates.of("product"));
    }

	// Find by id
    @GetMapping("/user/{id}")
    public User findById(@PathVariable("id") Long id) {
        return elasticsearchOperations.get(String.valueOf(id), User.class, IndexCoordinates.of("user"));
    }

	// Query all
    @GetMapping("/user/")
    public List<SearchHit<User>> search() {
        SearchHits<User> hits = elasticsearchOperations.search(Query.findAll(), User.class, IndexCoordinates.of("user"));
        hits.forEach(System.out::println);
        return hits.getSearchHits();
    }

    @GetMapping("/product/")
    public List<SearchHit<Product>> search2() {
        SearchHits<Product> hits = elasticsearchOperations.search(Query.findAll(), Product.class, IndexCoordinates.of("product"));
        hits.forEach(System.out::println);
        return hits.getSearchHits();
    }

}

Some query conditions of indexQuery

  • If the document data corresponding to the query criteria is found, the document will be updated (note the version constraint)
  • Add if not found


IndexOperations part of the source code

package org.springframework.data.elasticsearch.core;
 ·······

/**
 * IndexOperations Bind to an entity class or IndexCoordinate by
 */
public interface IndexOperations {

	boolean create();

	boolean create(Map<String, Object> settings);

	boolean create(Map<String, Object> settings, Document mapping);

	boolean createWithMapping();

	boolean delete();

	boolean exists();

	void refresh();

	Document createMapping();

	Document createMapping(Class<?> clazz);

	default boolean putMapping() {
		return putMapping(createMapping());
	}

	boolean putMapping(Document mapping);

	default boolean putMapping(Class<?> clazz) {
		return putMapping(createMapping(clazz));
	}

	Map<String, Object> getMapping();

	Settings createSettings();

	Settings createSettings(Class<?> clazz);

	Settings getSettings();

	Settings getSettings(boolean includeDefaults);

}

ElasticsearchRepository

ElasticsearchRepository inherits PagingAndSortingRepository

PagingAndSortingRepository inherits from CrudRepository
CrudRepository

Explain CrudRepository on the official website

Relationship between ElasticsearchRestTemplate

  • ElasticsearchRestTemplate is an implementation of ElasticsearchOperations using the interface of the advanced REST client.
  • ElasticsearchRestTemplate still uses RestHighLevelClient

Our custom configuration class inherits the AbstractElasticsearchConfiguration abstract class source code

public abstract class AbstractElasticsearchConfiguration extends ElasticsearchConfigurationSupport {

	/**
	 * Return the {@link RestHighLevelClient} instance used to connect to the cluster. <br />
	 *
	 * @return never {@literal null}.
	 */
	@Bean
	public abstract RestHighLevelClient elasticsearchClient();

	/**
	 * Creates {@link ElasticsearchOperations}.
	 *
	 * @return never {@literal null}.
	 */
	@Bean(name = { "elasticsearchOperations", "elasticsearchTemplate" })
	public ElasticsearchOperations elasticsearchOperations(ElasticsearchConverter elasticsearchConverter,
			RestHighLevelClient elasticsearchClient) {

		ElasticsearchRestTemplate template = new ElasticsearchRestTemplate(elasticsearchClient, elasticsearchConverter);
		template.setRefreshPolicy(refreshPolicy());

		return template;
	}
}

In essence, it is only a layer of encapsulation for RestHighLevelClient

Therefore, it is more comfortable for us to use this ElasticsearchRestTemplate, and there is a lot of redundancy when using RestHighLevelClient.

Posted by ariliquin on Fri, 24 Sep 2021 00:49:01 -0700