SpringBoot3集成ElasticSearch

標籤:ElasticSearch8.Kibana8;

一、簡介

Elasticsearch是一個分佈式、RESTful風格的搜索和數據分析引擎,適用於各種數據類型,數字、文本、地理位置、結構化數據、非結構化數據;

在實際的工作中,歷經過Elasticsearch從6.07.0的版本升級,而這次SpringBoot3和ES8.0的集成,雖然腳本的語法變化很小,但是Java客戶端的API語法變化很大;

二、環境搭建

1、下載安裝包

需要注意的是,這些安裝包的版本要選擇對應的,不然容易出問題;

軟件包:elasticsearch-8.8.2-darwin-x86_64.tar.gz
分詞器工具:elasticsearch-analysis-ik-8.8.2.zip
可視化工具:kibana-8.8.2-darwin-x86_64.tar.gz

2、服務啓動

不論是ES還是Kibana,在首次啓動後,會初始化很多配置文件,可以根據自己的需要做相關的配置調整,比如常見的端口調整,資源佔用,安全校驗等;

1、啓動ES
elasticsearch-8.8.2/bin/elasticsearch

本地訪問:localhost:9200

2、啓動Kibana
kibana-8.8.2/bin/kibana

本地訪問:http://localhost:5601

# 3、查看安裝的插件
http://localhost:9200/_cat/plugins  ->  analysis-ik 8.8.2

三、工程搭建

1、工程結構

2、依賴管理

starter-elasticsearch組件中,實際上依賴的是elasticsearch-java組件的8.7.1版本;

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-elasticsearch</artifactId>
    <version>${spring-boot.version}</version>
</dependency>

3、配置文件

在上面環境搭建的過程中,已經禁用了用戶和密碼的登錄驗證,配置ES服務地址即可;

spring:
  # ElasticSearch配置
  elasticsearch:
    uris: localhost:9200

四、基礎用法

1、實體類

通過DocumentField註解描述ES索引結構的實體類,注意這裏JsonIgnoreProperties註解,解決索引中字段和實體類非一一對應的而引起的JSON解析問題;

@JsonIgnoreProperties(ignoreUnknown = true)
@Document(indexName = "contents_index", createIndex = false)
public class ContentsIndex implements Serializable {

    private static final long serialVersionUID=1L;

    @Field(type= FieldType.Integer)
    private Integer id;

    @Field(type= FieldType.Keyword)
    private String title;

    @Field(type= FieldType.Keyword)
    private String intro;

    @Field(type= FieldType.Text)
    private String content;

    @Field(type= FieldType.Integer)
    private Integer createId;

    @Field(type= FieldType.Keyword)
    private String createName;

    @Field(type= FieldType.Date,format = DateFormat.date_hour_minute_second)
    private Date createTime;
}

2、初始化索引

基於ElasticsearchTemplate類和上述實體類,實現索引結構的初始化,並且將tb_contents表中的數據同步到索引中,最後通過ID查詢一條測試數據;

@Service
public class ContentsIndexService {
    private static final Logger log = LoggerFactory.getLogger(ContentsIndexService.class);

    @Resource
    private ContentsService contentsService ;
    @Resource
    private ElasticsearchTemplate template ;

    /**
     * 初始化索引結構和數據
     */
    public void initIndex (){
        // 處理索引結構
        IndexOperations indexOps = template.indexOps(ContentsIndex.class);
        if (indexOps.exists()){
            boolean delFlag = indexOps.delete();
            log.info("contents_index exists,delete:{}",delFlag);
            indexOps.createMapping(ContentsIndex.class);
        } else {
            log.info("contents_index not exists");
            indexOps.createMapping(ContentsIndex.class);
        }
        // 同步數據庫表記錄
        List<Contents> contentsList = contentsService.queryAll();
        if (contentsList.size() > 0){
            List<ContentsIndex> contentsIndexList = new ArrayList<>() ;
            contentsList.forEach(contents -> {
                ContentsIndex contentsIndex = new ContentsIndex() ;
                BeanUtils.copyProperties(contents,contentsIndex);
                contentsIndexList.add(contentsIndex);
            });
            template.save(contentsIndexList);
        }
        // ID查詢
        ContentsIndex contentsIndex = template.get("10",ContentsIndex.class);
        log.info("contents-index-10:{}",contentsIndex);
    }
}

3、倉儲接口

繼承ElasticsearchRepository接口,可以對ES這種特定類型的存儲庫進行通用增刪改查操作;在測試類中對該接口的方法進行測試;

// 1、接口定義
public interface ContentsIndexRepository extends ElasticsearchRepository<ContentsIndex,Long> {
}

// 2、接口測試
public class ContentsIndexRepositoryTest {
    @Autowired
    private ContentsIndexRepository contentsIndexRepository;

    @Test
    public void testAdd (){
        // 單個新增
        contentsIndexRepository.save(buildOne());
        // 批量新增
        contentsIndexRepository.saveAll(buildList()) ;
    }

    @Test
    public void testUpdate (){
        // 根據ID查詢後再更新
        Optional<ContentsIndex> contentsOpt = contentsIndexRepository.findById(14L);
        if (contentsOpt.isPresent()){
            ContentsIndex contentsId = contentsOpt.get();
            System.out.println("id=14:"+contentsId);
            contentsId.setContent("update-content");
            contentsId.setCreateTime(new Date());
            contentsIndexRepository.save(contentsId);
        }
    }

    @Test
    public void testQuery (){
        // 單個ID查詢
        Optional<ContentsIndex> contentsOpt = contentsIndexRepository.findById(1L);
        if (contentsOpt.isPresent()){
            ContentsIndex contentsId1 = contentsOpt.get();
            System.out.println("id=1:"+contentsId1);
        }
        // 批量ID查詢
        Iterator<ContentsIndex> contentsIterator = contentsIndexRepository
                                        .findAllById(Arrays.asList(10L,12L)).iterator();
        while (contentsIterator.hasNext()){
            ContentsIndex contentsIndex = contentsIterator.next();
            System.out.println("id="+contentsIndex.getId()+":"+contentsIndex);
        }
    }

    @Test
    public void testDelete (){
        contentsIndexRepository.deleteById(15L);
        contentsIndexRepository.deleteById(16L);
    }
}

4、查詢語法

無論是ElasticsearchTemplate類還是ElasticsearchRepository接口,都是對ES常用的簡單功能進行封裝,在實際使用時,複雜的查詢語法還是依賴ElasticsearchClient和原生的API封裝;

這裏主要演示七個查詢方法,主要涉及:ID查詢,字段匹配,組合與範圍查詢,分頁與排序,分組統計,最大值查詢和模糊匹配;更多的查詢API還是要多看文檔中的案例纔行;

public class ElasticsearchClientTest {

    @Autowired
    private ElasticsearchClient client ;

    @Test
    public void testSearch1 () throws IOException {
        // ID查詢
        GetResponse<ContentsIndex> resp = client.get(
                getReq ->getReq.index("contents_index").id("7"), ContentsIndex.class);
        if (resp.found()){
            ContentsIndex contentsIndex = resp.source() ;
            System.out.println("contentsIndex-7:"+contentsIndex);
        }
    }

    @Test
    public void testSearch2 () throws IOException {
        // 指定字段匹配
        SearchResponse<ContentsIndex> resp = client.search(searchReq -> searchReq.index("contents_index")
                        .query(query -> query.match(field -> field
                        .field("createName").query("張三"))),ContentsIndex.class);
        printResp(resp);
    }

    @Test
    public void testSearch3 () throws IOException {
        // 組合查詢:姓名和時間範圍
        Query byName = MatchQuery.of(field -> field.field("createName").query("王五"))._toQuery();
        Query byTime = RangeQuery.of(field -> field.field("createTime")
                        .gte(JsonData.of("2023-07-10T00:00:00"))
                        .lte(JsonData.of("2023-07-12T00:00:00")))._toQuery();
        SearchResponse<ContentsIndex> resp = client.search(searchReq -> searchReq.index("contents_index")
                        .query(query -> query.bool(boolQuery -> boolQuery.must(byName).must(byTime))),ContentsIndex.class);
        printResp(resp);
    }

    @Test
    public void testSearch4 () throws IOException {
        // 排序和分頁,在14條數據中,根據ID倒序排列,從第5條往後取4條數據
        SearchResponse<ContentsIndex> resp = client.search(searchReq -> searchReq.index("contents_index")
                .from(5).size(4)
                .sort(sort -> sort.field(sortField -> sortField.field("id").order(SortOrder.Desc))),ContentsIndex.class);
        printResp(resp);
    }

    @Test
    public void testSearch5 () throws IOException {
        // 根據createId分組統計
        SearchResponse<ContentsIndex> resp = client.search(searchReq -> searchReq.index("contents_index")
                .aggregations("createIdGroup",agg -> agg.terms(term -> term.field("createId"))),ContentsIndex.class);
        Aggregate aggregate = resp.aggregations().get("createIdGroup");
        LongTermsAggregate termsAggregate = aggregate.lterms();
        Buckets<LongTermsBucket> buckets = termsAggregate.buckets();
        for (LongTermsBucket termsBucket : buckets.array()) {
            System.out.println(termsBucket.key() + " : " + termsBucket.docCount());
        }
    }

    @Test
    public void testSearch6 () throws IOException {
        // 查詢最大的ID
        SearchResponse<ContentsIndex> resp = client.search(searchReq -> searchReq.index("contents_index")
                .aggregations("maxId",agg -> agg.max(field -> field.field("id"))),ContentsIndex.class);
        for (Map.Entry<String, Aggregate> entry : resp.aggregations().entrySet()){
            System.out.println(entry.getKey()+":"+entry.getValue().max().value());
        }
    }

    @Test
    public void testSearch7 () throws IOException {
        // 模糊查詢title字段,允許1個誤差
        Query byContent = FuzzyQuery.of(field -> field.field("title").value("設計").fuzziness("1"))._toQuery();
        SearchResponse<ContentsIndex> resp = client.search(
                searchReq -> searchReq.index("contents_index").query(byContent),ContentsIndex.class);
        printResp(resp);
    }

    private void printResp (SearchResponse<ContentsIndex> resp){
        TotalHits total = resp.hits().total();
        System.out.println("total:"+total);
        List<Hit<ContentsIndex>> hits = resp.hits().hits();
        for (Hit<ContentsIndex> hit: hits) {
            ContentsIndex contentsIndex = hit.source();
            System.out.println(hit.id()+":"+contentsIndex);
        }
    }
}

五、參考源碼

文檔倉庫:
https://gitee.com/cicadasmile/butte-java-note

源碼倉庫:
https://gitee.com/cicadasmile/butte-spring-parent
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章