springboot整合elasticsearch

我的ElasticSearch 版本是5.6.16

一. 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 http://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.1.5.RELEASE</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>
    <groupId>com.boot</groupId>
    <artifactId>es</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <name>es</name>
    <description>Demo project for Spring Boot</description>

    <properties>
        <java.version>1.8</java.version>
    </properties>

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

        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <optional>true</optional>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-elasticsearch</artifactId>
        </dependency>
    </dependencies>

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

</project>

二. Spring-data-elasticsearch 都提供了什麼

1. 方便的註解,讓創建索引更簡單

例如下面創建User的索引,創建的索引名是user_index,類型是user,主鍵是no, name和word兩個字段使用ik_max_word分詞器進行分詞。

package com.boot.es.model;

import lombok.Data;
import org.springframework.data.annotation.Id;
import org.springframework.data.elasticsearch.annotations.Document;
import org.springframework.data.elasticsearch.annotations.Field;
import org.springframework.data.elasticsearch.annotations.FieldType;
import org.springframework.data.elasticsearch.annotations.InnerField;
import org.springframework.data.elasticsearch.annotations.MultiField;

/**
 * Author:   susq
 * Date:     2019-06-10 23:44
 */
@Data
@Document(indexName = "user_index", type = "user", replicas = 0)
public class User {

    @Id
    private Long no;

    @Field(type = FieldType.Integer)
    private int age;

    @MultiField(
        mainField = @Field(type = FieldType.Text, analyzer = "standard", searchAnalyzer = "standard", store = true),
        otherFields = @InnerField(type = FieldType.Keyword, suffix = "raw"))
    private String name;

    @Field(type = FieldType.Text, analyzer = "standard", searchAnalyzer = "standard")
    private String word;
}

這樣創建索引的時候,直接根據類來創建就可以:

package com.boot.es;

import com.boot.es.model.User;
import com.boot.es.service.UserService;
import lombok.extern.slf4j.Slf4j;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.data.elasticsearch.core.ElasticsearchTemplate;
import org.springframework.test.context.junit4.SpringRunner;


/**
 * Author:   susq
 * Date:     2019-06-26 00:09
 */
@Slf4j
@RunWith(SpringRunner.class)
@SpringBootTest
public class EsServiceTests {

    @Autowired
    private ElasticsearchTemplate template;

    @Autowired
    private UserService userService;

    @Test
    public void rebuildIndex() {
        template.createIndex(User.class);
        log.info("user索引在嗎" + template.indexExists(User.class));
        template.deleteIndex(User.class);
        log.info("user索引在嗎" + template.indexExists(User.class));
    }
}

對用到的註解做一下補充:

ublic @interface Document {
 
String indexName(); //索引庫的名稱
 
String type() default ""; //類型
 
short shards() default 5; //默認分區數
 
short replicas() default 1; //每個分區默認的備份數,我們上面設置0是因爲是單機的,在同一臺機器上備份沒用,才設置了0, 否則通過elastic-head插件,會出現UNASSIGNED節點。 elasticsearch-head插件使用參考我之前的文章:https://blog.csdn.net/u013041642/article/details/91355916
 
String refreshInterval() default "1s"; //刷新間隔
 
String indexStoreType() default "fs"; //索引文件存儲類型

boolean creatIndex() default true; // 是否給當前使用註解的對象自動創建索引
}
public @interface Field {
 
FieldType type() default FieldType.Auto; //自動檢測屬性的類型,可以根據實際情況自己設置
 
FieldIndex index() default true; //默認情況下分詞,一般默認分詞就好,除非這個字段你確定查詢時不會用到,可以設置成false讓它不分詞
 
DateFormat format() default DateFormat.none; //時間類型的格式化
 
String pattern() default ""; 
 
boolean store() default false; //默認情況下不存儲原文
 
String searchAnalyzer() default ""; //指定字段搜索時使用的分詞器
 
String indexAnalyzer() default ""; //指定字段建立索引時指定的分詞器
 
String[] ignoreFields() default {}; //如果某個字段需要被忽略
 
boolean includeInParent() default false;
}

2. 封裝好的接口,簡單的增刪改查功能都已經實現,如果功能簡單,我們幾乎不需要自己寫什麼代碼

package com.boot.es.repository;

import com.boot.es.model.User;
import org.springframework.data.elasticsearch.repository.ElasticsearchRepository;
import org.springframework.stereotype.Repository;

// 兩個泛型,第一個是索引對應的實體類User,第二個是主鍵的類型
@Repository
public interface UserRepository extends ElasticsearchRepository<User, Long> {
}

繼承層次中,CrudRepository接口定義好了許多接口, 註釋我刪掉了,大家看源碼的話有很多註釋,比較簡單。

@NoRepositoryBean
public interface CrudRepository<T, ID> extends Repository<T, ID> {

	<S extends T> S save(S entity);

	<S extends T> Iterable<S> saveAll(Iterable<S> entities);

	Optional<T> findById(ID id);

	boolean existsById(ID id);

	Iterable<T> findAll();

	Iterable<T> findAllById(Iterable<ID> ids);

	long count();

	void deleteById(ID id);

	void delete(T entity);

	void deleteAll(Iterable<? extends T> entities);

	void deleteAll();
}

繼承層次中,還有個接口, 如果我們要構建複雜的查詢,可以自行拼裝QueryBuilder,

public interface ElasticsearchRepository<T, ID extends Serializable> extends ElasticsearchCrudRepository<T, ID> {

	<S extends T> S index(S entity);

	Iterable<T> search(QueryBuilder query);

	Page<T> search(QueryBuilder query, Pageable pageable);

	Page<T> search(SearchQuery searchQuery);

	Page<T> searchSimilar(T entity, String[] fields, Pageable pageable);

	void refresh();

	Class<T> getEntityClass();
}

3. 配合查詢的QueryBuilder, 提供了豐富的靜態方法,自由組合查詢條件,下面列舉了一些常用的。

這裏使用的標準分詞器,對漢字分詞不友好,所以這裏的name,word, 我都寫成了英文形式,後面會專門整理一篇漢字和拼音分詞器的。

package com.boot.es;

import com.boot.es.model.User;
import com.boot.es.repository.UserRepository;
import lombok.extern.slf4j.Slf4j;
import org.assertj.core.util.Lists;
import org.elasticsearch.index.query.BoolQueryBuilder;
import org.elasticsearch.index.query.QueryBuilder;
import org.elasticsearch.index.query.QueryBuilders;
import org.elasticsearch.search.sort.SortBuilders;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.data.elasticsearch.core.query.NativeSearchQueryBuilder;
import org.springframework.test.context.junit4.SpringRunner;

import java.util.List;

@Slf4j
@RunWith(SpringRunner.class)
@SpringBootTest
public class EsApplicationTests {

    @Autowired
    private UserRepository userRepository;

    @Test
    public void testCurd() {
        // 添加,可以saveAll批量添加
        User user = new User();
        user.setAge(26);
        user.setName("zhangsan");
        user.setNo(1L);
        user.setWord("good good study!");
        userRepository.save(user);

        // 查詢全部
        List<User> users = Lists.newArrayList(userRepository.findAll());
        log.info("users,{}", users);

        // 更新就是重複保存,有變化的字段會覆蓋老的
        user.setWord("day day up!");
        userRepository.save(user);
        user = userRepository.findById(1L).orElse(null);
        log.info("user,{}", user);

        userRepository.deleteById(1L);
        user = userRepository.findById(1L).orElse(null);
        log.info("user,{}", user);

        User user1 = new User();
        user1.setAge(26);
        user1.setName("zhang san");
        user1.setNo(1L);
        user1.setWord("good good study!");

        User user2 = new User();
        user2.setAge(36);
        user2.setName("li si");
        user2.setNo(2L);
        user2.setWord("day day up!");

        userRepository.saveAll(Lists.newArrayList(user1, user2));
        // 查詢全部
        users = Lists.newArrayList(userRepository.findAll());
        log.info("users,{}", users);
    }


    @Test
    public void testSearch() {
        // 空條件查詢,返回全部
        NativeSearchQueryBuilder queryBuilder = new NativeSearchQueryBuilder();
        Iterable<User> users = userRepository.search(queryBuilder.build());
        List<User> userList = Lists.newArrayList(users);
        log.info("userList:{}", userList);

        //=========================精確匹配查詢==========================================================

        // no等於1的,name用標準分詞器分詞後的倒排索引包含"li"的,name不分詞時候等於"li si"的,也可以同時都組合起來,則查詢條件就是要同時滿足的
//        queryBuilder.withQuery(QueryBuilders.termQuery("no", 1L));
//        queryBuilder.withQuery(QueryBuilders.termQuery("name", "li"));
//        queryBuilder.withQuery(QueryBuilders.termQuery("name.raw", "li si"));
        queryBuilder.withQuery(QueryBuilders.termQuery("age", 26));
        users = userRepository.search(queryBuilder.build());
        userList = Lists.newArrayList(users);
        log.info("userList:{}", userList);

        //=============================檢索==============================================================
        queryBuilder.withQuery(QueryBuilders.matchQuery("name", "zhang")); // 檢索name 中有"zhang"的
        queryBuilder.withQuery(QueryBuilders.multiMatchQuery("good", "name", "word")); // 檢索name 或者 word 中有"good"的
        users = userRepository.search(queryBuilder.build());
        userList = Lists.newArrayList(users);
        log.info("userList:{}", userList);


        /**
         * 我們上面創建的索引,name,word兩個字段都設置分詞,name用漢字分詞器分詞,word用拼音分詞器分詞,
         * termQuery() term過濾器會查找我們設定的準確值。如果有分詞的話,term過濾器只能匹配到分詞後的倒排索引。
         * matchQuery()
         *   1.被查字段是分詞的,那麼查詢字符串也要被分詞
         *   2. 類似低級別的term查詢
         *   3. 綜合考慮詞頻,逆文檔頻率,包含關鍵字的字段長度,來計算相關性打分_score
         * multiMatchQuery() 可以多詞查詢,一段文本可以匹配多個字段,或的關係
         * 在本文最後我也補充了例子,比較直觀
         */
        //=============================組合查詢條件=======================================================

        BoolQueryBuilder boolQueryBuilder = QueryBuilders.boolQuery()
                .must(QueryBuilders.matchQuery("name", "zhang"))
                .mustNot(QueryBuilders.matchQuery("name", "li"))
                .should(QueryBuilders.matchQuery("word", "good"))
                .should(QueryBuilders.matchQuery("word", "up"))
                .minimumShouldMatch(1);
        queryBuilder.withQuery(boolQueryBuilder);
        users = userRepository.search(queryBuilder.build());
        userList = Lists.newArrayList(users);
        log.info("userList:{}", userList);

        //=============================組合查詢條件自定義每個條件的權重,按權重排序=============================
        BoolQueryBuilder boolQueryBuilder1 = QueryBuilders.boolQuery()
                .should(QueryBuilders.functionScoreQuery(QueryBuilders.matchQuery("word", "good")).boost(2))
                .should(QueryBuilders.functionScoreQuery(QueryBuilders.matchQuery("word", "up")).boost(1));
        queryBuilder.withQuery(boolQueryBuilder1).withSort(SortBuilders.scoreSort());
        users = userRepository.search(queryBuilder.build());
        userList = Lists.newArrayList(users);
        log.info("userList:{}", userList);

        /**
         * 調整兩個boost的大小,最後打印的順序就會改變
         * userList:[User(no=2, age=26, name=li si, word=day day up!), User(no=1, age=26, name=zhang san, word=good good study!)]
         * userList:[User(no=1, age=26, name=zhang san, word=good good study!), User(no=2, age=26, name=li si, word=day day up!)]
         */

        //======================================= 帶過濾條件的查詢==========================================
        queryBuilder.withFilter(QueryBuilders.rangeQuery("age").lt(35)); // 只要年齡小於35的
        users = userRepository.search(queryBuilder.build());
        userList = Lists.newArrayList(users);
        log.info("userList:{}", userList);
    }
}

補充:

  • should: 所有的must子句必須匹配,所有的must_not 必須不匹配。默認的,不需要should匹配,但是,如果沒有must語句,則至少需要一個should匹配。像我們控制match查詢精度一樣,我們也可以通過minimum_should_match 參數控制多少should子句需要被匹配,這個參數可以是整數,也可以是百分比。

  • match在匹配時會對所查找的關鍵詞進行分詞,然後按分詞匹配查找,而term會直接對關鍵詞進行查找。一般模糊查找的時候,多用match,而精確查找時可以使用term。

    舉個例子說明一下:

    {  
        "match": { "title": "my cat"}  
    }
    {  
      "bool": {  
        "should": [  
          { "term": { "title": "my" }},  
          { "term": { "title": "cat"   }}  
        ]  
      }  
    } 
    

    match 會將關鍵詞進行分詞分成“my”和“cat”,查找時包含其中任一均可被匹配到。

    term結合bool使用,不進行分詞,但是有2個關鍵詞,並且使用“或”匹配,也就是會匹配關鍵字一“my”或關鍵字“cat”,效果和上面的match是相同的。如果要想精確的匹配“my cat”而不匹配“my lovely cat”,則可以如下方式匹配:

    {  
      "bool": {  
        "should": [  
          { "term": { "title": "my cat" }}  
        ]  
      }  
    } 
    
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章