基於 ElasticSearch 實現站內全文搜尋,寫得太好了!

語言: CN / TW / HK
"http://www.thymeleaf.org">

來源:blog.csdn.net/weixin_44671737/article/details/114456257

摘要

對於一家公司而言,資料量越來越多,如果快速去查詢這些資訊是一個很難的問題,在計算機領域有一個專門的領域IR(Information Retrival)研究如果獲取資訊,做資訊檢索。

在國內的如百度這樣的搜尋引擎也屬於這個領域,要自己實現一個搜尋引擎是非常難的,不過資訊查詢對每一個公司都非常重要,對於開發人員也可以選則一些市場上的開源專案來構建自己的站內搜尋引擎,本文將通過ElasticSearch來構建一個這樣的資訊檢索專案。

1 技術選型

  • 搜尋引擎服務使用ElasticSearch
  • 提供的對外web服務選則springboot web

1.1 ElasticSearch

Elasticsearch是一個基於Lucene的搜尋伺服器。它提供了一個分散式多使用者能力的全文搜尋引擎,基於RESTful web介面。Elasticsearch是用Java語言開發的,並作為Apache許可條款下的開放原始碼釋出,是一種流行的企業級搜尋引擎。Elasticsearch用於雲端計算中,能夠達到實時搜尋,穩定,可靠,快速,安裝使用方便。

官方客戶端在Java、.NET(C#)、PHP、Python、Apache Groovy、Ruby和許多其他語言中都是可用的。根據DB-Engines的排名顯示,Elasticsearch是最受歡迎的企業搜尋引擎,其次是Apache Solr,也是基於Lucene。1

現在開源的搜尋引擎在市面上最常見的就是ElasticSearch和Solr,二者都是基於Lucene的實現,其中ElasticSearch相對更加重量級,在分散式環境表現也更好,二者的選則需考慮具體的業務場景和資料量級。對於資料量不大的情況下,完全需要使用像Lucene這樣的搜尋引擎服務,通過關係型資料庫檢索即可。

1.2 springBoot

Spring Boot makes it easy to create stand-alone, production-grade Spring based Applications that you can “just run”.2

現在springBoot在做web開發上是絕對的主流,其不僅僅是開發上的優勢,在佈署,運維各個方面都有著非常不錯的表現,並且spring生態圈的影響力太大了,可以找到各種成熟的解決方案。

1.3 ik分詞器

elasticSearch本身不支援中文的分詞,需要安裝中文分詞外掛,如果需要做中文的資訊檢索,中文分詞是基礎,此處選則了ik,下載好後放入elasticSearch的安裝位置的plugin目錄即可。

2 環境準備

需要安裝好elastiSearch以及kibana(可選),並且需要lk分詞外掛。

  • 安裝elasticSearch elasticsearch官網. 筆者使用的是7.5.1。
  • ik外掛下載 ik外掛github地址. 注意下載和你下載elasticsearch版本一樣的ik外掛。
  • 將ik外掛放入elasticsearch安裝目錄下的plugins包下,新建報名ik,將下載好的外掛解壓到該目錄下即可,啟動es的時候會自動載入該外掛。

  • 搭建springboot專案 idea ->new project ->spring initializer

3 專案架構

  • 獲取資料使用ik分詞外掛
  • 將資料儲存在es引擎中
  • 通過es檢索方式對儲存的資料進行檢索
  • 使用es的java客戶端提供外部服務

4 實現效果

4.1 搜尋頁面

簡單實現一個類似百度的搜尋框即可。

4.2 搜尋結果頁面

點選第一個搜尋結果是我個人的某一篇博文,為了避免資料版權問題,筆者在es引擎中存放的全是個人的部落格資料。

5 具體程式碼實現

5.1 全文檢索的實現物件

按照博文的基本資訊定義瞭如下實體類,主要需要知道每一個博文的url,通過檢索出來的文章具體檢視要跳轉到該url。

``` package com.lbh.es.entity;

import com.fasterxml.jackson.annotation.JsonIgnore;

import javax.persistence.*;

/* * PUT articles * { * "mappings": * {"properties":{ * "author":{"type":"text"}, * "content":{"type":"text","analyzer":"ik_max_word","search_analyzer":"ik_smart"}, * "title":{"type":"text","analyzer":"ik_max_word","search_analyzer":"ik_smart"}, * "createDate":{"type":"date","format":"yyyy-MM-dd HH:mm:ss||yyyy-MM-dd"}, * "url":{"type":"text"} * } }, * "settings":{ * "index":{ * "number_of_shards":1, * "number_of_replicas":2 * } * } * } * --------------------------------------------------------------------------------------------------------------------- * Copyright(c)[email protected] * @author liubinhao * @date 2021/3/3 / @Entity @Table(name = "es_article") public class ArticleEntity { @Id @JsonIgnore @GeneratedValue(strategy = GenerationType.IDENTITY) private long id; @Column(name = "author") private String author; @Column(name = "content",columnDefinition="TEXT") private String content; @Column(name = "title") private String title; @Column(name = "createDate") private String createDate; @Column(name = "url") private String url;

public String getAuthor() {
    return author;
}

public void setAuthor(String author) {
    this.author = author;
}

public String getContent() {
    return content;
}

public void setContent(String content) {
    this.content = content;
}

public String getTitle() {
    return title;
}

public void setTitle(String title) {
    this.title = title;
}

public String getCreateDate() {
    return createDate;
}

public void setCreateDate(String createDate) {
    this.createDate = createDate;
}

public String getUrl() {
    return url;
}

public void setUrl(String url) {
    this.url = url;
}

} ```

5.2 客戶端配置

通過java配置es的客戶端。

``` package com.lbh.es.config;

import org.apache.http.HttpHost; import org.elasticsearch.client.RestClient; import org.elasticsearch.client.RestClientBuilder; import org.elasticsearch.client.RestHighLevelClient; import org.springframework.beans.factory.annotation.Value; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration;

import java.util.ArrayList; import java.util.List;

/* * Copyright(c)[email protected] * @author liubinhao * @date 2021/3/3 / @Configuration public class EsConfig {

@Value("${elasticsearch.schema}")
private String schema;
@Value("${elasticsearch.address}")
private String address;
@Value("${elasticsearch.connectTimeout}")
private int connectTimeout;
@Value("${elasticsearch.socketTimeout}")
private int socketTimeout;
@Value("${elasticsearch.connectionRequestTimeout}")
private int tryConnTimeout;
@Value("${elasticsearch.maxConnectNum}")
private int maxConnNum;
@Value("${elasticsearch.maxConnectPerRoute}")
private int maxConnectPerRoute;

@Bean
public RestHighLevelClient restHighLevelClient() {
    // 拆分地址
    List<HttpHost> hostLists = new ArrayList<>();
    String[] hostList = address.split(",");
    for (String addr : hostList) {
        String host = addr.split(":")[0];
        String port = addr.split(":")[1];
        hostLists.add(new HttpHost(host, Integer.parseInt(port), schema));
    }
    // 轉換成 HttpHost 陣列
    HttpHost[] httpHost = hostLists.toArray(new HttpHost[]{});
    // 構建連線物件
    RestClientBuilder builder = RestClient.builder(httpHost);
    // 非同步連線延時配置
    builder.setRequestConfigCallback(requestConfigBuilder -> {
        requestConfigBuilder.setConnectTimeout(connectTimeout);
        requestConfigBuilder.setSocketTimeout(socketTimeout);
        requestConfigBuilder.setConnectionRequestTimeout(tryConnTimeout);
        return requestConfigBuilder;
    });
    // 非同步連線數配置
    builder.setHttpClientConfigCallback(httpClientBuilder -> {
        httpClientBuilder.setMaxConnTotal(maxConnNum);
        httpClientBuilder.setMaxConnPerRoute(maxConnectPerRoute);
        return httpClientBuilder;
    });
    return new RestHighLevelClient(builder);
}

} ```

5.3 業務程式碼編寫

包括一些檢索文章的資訊,可以從文章標題,文章內容以及作者資訊這些維度來檢視相關資訊。

``` package com.lbh.es.service;

import com.google.gson.Gson; import com.lbh.es.entity.ArticleEntity; import com.lbh.es.repository.ArticleRepository; import org.elasticsearch.action.admin.indices.delete.DeleteIndexRequest; import org.elasticsearch.action.get.GetRequest; import org.elasticsearch.action.get.GetResponse; import org.elasticsearch.action.index.IndexRequest; import org.elasticsearch.action.index.IndexResponse; import org.elasticsearch.action.search.SearchRequest; import org.elasticsearch.action.search.SearchResponse; import org.elasticsearch.action.support.master.AcknowledgedResponse; import org.elasticsearch.client.RequestOptions; import org.elasticsearch.client.RestHighLevelClient; import org.elasticsearch.client.indices.CreateIndexRequest; import org.elasticsearch.client.indices.CreateIndexResponse; import org.elasticsearch.common.settings.Settings; import org.elasticsearch.common.xcontent.XContentType; import org.elasticsearch.index.query.QueryBuilders; import org.elasticsearch.search.SearchHit; import org.elasticsearch.search.builder.SearchSourceBuilder; import org.springframework.stereotype.Service;

import javax.annotation.Resource; import java.io.IOException;

import java.util.*;

/* * Copyright(c)[email protected] * @author liubinhao * @date 2021/3/3 / @Service public class ArticleService {

private static final String ARTICLE_INDEX = "article";

@Resource
private RestHighLevelClient client;
@Resource
private ArticleRepository articleRepository;

public boolean createIndexOfArticle(){
    Settings settings = Settings.builder()
            .put("index.number_of_shards", 1)
            .put("index.number_of_replicas", 1)
            .build();

// {"properties":{"author":{"type":"text"}, // "content":{"type":"text","analyzer":"ik_max_word","search_analyzer":"ik_smart"} // ,"title":{"type":"text","analyzer":"ik_max_word","search_analyzer":"ik_smart"}, // ,"createDate":{"type":"date","format":"yyyy-MM-dd HH:mm:ss||yyyy-MM-dd"} // } String mapping = "{\"properties\":{\"author\":{\"type\":\"text\"},\n" + "\"content\":{\"type\":\"text\",\"analyzer\":\"ik_max_word\",\"search_analyzer\":\"ik_smart\"}\n" + ",\"title\":{\"type\":\"text\",\"analyzer\":\"ik_max_word\",\"search_analyzer\":\"ik_smart\"}\n" + ",\"createDate\":{\"type\":\"date\",\"format\":\"yyyy-MM-dd HH:mm:ss||yyyy-MM-dd\"}\n" + "},\"url\":{\"type\":\"text\"}\n" + "}"; CreateIndexRequest indexRequest = new CreateIndexRequest(ARTICLE_INDEX) .settings(settings).mapping(mapping,XContentType.JSON); CreateIndexResponse response = null; try { response = client.indices().create(indexRequest, RequestOptions.DEFAULT); } catch (IOException e) { e.printStackTrace(); } if (response!=null) { System.err.println(response.isAcknowledged() ? "success" : "default"); return response.isAcknowledged(); } else { return false; } }

public boolean deleteArticle(){
    DeleteIndexRequest request = new DeleteIndexRequest(ARTICLE_INDEX);
    try {
        AcknowledgedResponse response = client.indices().delete(request, RequestOptions.DEFAULT);
        return response.isAcknowledged();
    } catch (IOException e) {
        e.printStackTrace();
    }
    return false;
}

public IndexResponse addArticle(ArticleEntity article){
    Gson gson = new Gson();
    String s = gson.toJson(article);
    //建立索引建立物件
    IndexRequest indexRequest = new IndexRequest(ARTICLE_INDEX);
    //文件內容
    indexRequest.source(s,XContentType.JSON);
    //通過client進行http的請求
    IndexResponse re = null;
    try {
        re = client.index(indexRequest, RequestOptions.DEFAULT);
    } catch (IOException e) {
        e.printStackTrace();
    }
    return re;
}

public void transferFromMysql(){
    articleRepository.findAll().forEach(this::addArticle);
}

public List<ArticleEntity> queryByKey(String keyword){
    SearchRequest request = new SearchRequest();
    /*
     * 建立  搜尋內容引數設定物件:SearchSourceBuilder
     * 相對於matchQuery,multiMatchQuery針對的是多個fi eld,也就是說,當multiMatchQuery中,fieldNames引數只有一個時,其作用與matchQuery相當;
     * 而當fieldNames有多個引數時,如field1和field2,那查詢的結果中,要麼field1中包含text,要麼field2中包含text。
     */
    SearchSourceBuilder searchSourceBuilder = new SearchSourceBuilder();

    searchSourceBuilder.query(QueryBuilders
            .multiMatchQuery(keyword, "author","content","title"));
    request.source(searchSourceBuilder);
    List<ArticleEntity> result = new ArrayList<>();
    try {
        SearchResponse search = client.search(request, RequestOptions.DEFAULT);
        for (SearchHit hit:search.getHits()){
            Map<String, Object> map = hit.getSourceAsMap();
            ArticleEntity item = new ArticleEntity();
            item.setAuthor((String) map.get("author"));
            item.setContent((String) map.get("content"));
            item.setTitle((String) map.get("title"));
            item.setUrl((String) map.get("url"));
            result.add(item);
        }
        return result;
    } catch (IOException e) {
        e.printStackTrace();
    }
    return null;
}

public ArticleEntity queryById(String indexId){
    GetRequest request = new GetRequest(ARTICLE_INDEX, indexId);
    GetResponse response = null;
    try {
        response = client.get(request, RequestOptions.DEFAULT);
    } catch (IOException e) {
        e.printStackTrace();
    }
    if (response!=null&&response.isExists()){
        Gson gson = new Gson();
        return gson.fromJson(response.getSourceAsString(),ArticleEntity.class);
    }
    return null;
}

} ```

5.4 對外介面

和使用springboot開發web程式相同。

Spring Boot 基礎就不介紹了,推薦下這個實戰教程: http://github.com/javastacks/spring-boot-best-practice

``` package com.lbh.es.controller;

import com.lbh.es.entity.ArticleEntity; import com.lbh.es.service.ArticleService; import org.elasticsearch.action.index.IndexResponse; import org.springframework.web.bind.annotation.*;

import javax.annotation.Resource; import java.util.List;

/* * Copyright(c)[email protected] * @author liubinhao * @date 2021/3/3 / @RestController @RequestMapping("article") public class ArticleController {

@Resource
private ArticleService articleService;

@GetMapping("/create")
public boolean create(){
    return articleService.createIndexOfArticle();
}

@GetMapping("/delete")
public boolean delete() {
    return articleService.deleteArticle();
}

@PostMapping("/add")
public IndexResponse add(@RequestBody ArticleEntity article){
    return articleService.addArticle(article);
}

@GetMapping("/fransfer")
public String transfer(){
    articleService.transferFromMysql();
    return "successful";
}

@GetMapping("/query")
public List<ArticleEntity> query(String keyword){
    return articleService.queryByKey(keyword);
}

} ```

5.5 頁面

此處頁面使用thymeleaf,主要原因是筆者真滴不會前端,只懂一丟丟簡單的h5,就隨便做了一個可以展示的頁面。

搜尋頁面

```

YiyiDu

一億度

```

搜尋結果頁面

```

xx-manager

```

6 小結

上班擼程式碼,下班繼續擼程式碼寫部落格,花了兩天研究了以下es,其實這個玩意兒還是挺有意思的,現在IR領域最基礎的還是基於統計學的,所以對於es這類搜尋引擎而言在大資料的情況下具有良好的表現。

每一次寫實戰筆者其實都感覺有些無從下手,因為不知道做啥?所以也希望得到一些有意思的點子筆者會將實戰做出來。

近期熱文推薦:

1.1,000+ 道 Java面試題及答案整理(2021最新版)

2.勁爆!Java 協程要來了。。。

3.玩大了!Log4j 2.x 再爆雷。。。

4.Spring Boot 2.6 正式釋出,一大波新特性。。

5.《Java開發手冊(嵩山版)》最新發布,速速下載!

覺得不錯,別忘了隨手點贊+轉發哦!