後端——》elasticsearch搜尋引擎的分詞搜尋和高亮顯示的應用

語言: CN / TW / HK

簡介

在我的上一篇博文中,詳細寫到了ElasticSearch的日誌服務的應用場景,本文討論的是另一個場景:ElasticSearch作為搜尋引擎在web專案中的使用。ElasticSearch作為搜尋引擎最主要的作用是分詞,即將一個段文字或一個片語分割成小粒度,並將這些經過分割再組合的小粒度的文字來匹配搜尋結果,如有需要,還可以高亮顯示。

效果如下: 在這裡插入圖片描述

我這裡是用ElasticSearch做了一個input的輸入自動填充,自動匹配出來的下拉框就是ElasticSearch將我輸入的詞彙經過分割後在索引中匹配出來的結果。

使用

上面的demo基於以下環境開發: 後端:springboot:2.0.1.RELEASE 前端:layui elasticsearch:6.8.1

demo中的紅色高亮顯示是在後端elasticsearch的程式碼中配置的。

demo中的input自動填充框元件為 autocomplete,可在layui 第三方元件平臺自行下載。如果前端不是用的layui,比如vue的elementUI等,也都有各自適配的input自動填充元件。自動填充元件的匹配條目的資料來源是後端elasticsearch的搜尋結果。

elasticsearch的匹配到的資料最初是存在mysql資料庫中,通過logstash將mysql的資料同步給elasticsearch,並以索引的形式存在。

1,修改logstash的配置

修改 ..\logstash-6.3.0\bin目錄下的logstash.conf檔案。可以通過logstash同步mysql的資料給elasticsearch。如下

```java input { stdin { } }

input { tcp { type => "deliver_log" host => "127.0.0.1" port => 9250 mode => "server" codec => json_lines } jdbc { type => "baoji_company_requirements" jdbc_connection_string => "jdbc:mysql://localhost:3306/baoji-staging?useUnicode=true&characterEncoding=UTF-8&autoReconnect=true&&useSSL=false" jdbc_user => "root" jdbc_password => "123456789" jdbc_driver_library => "D:\Work\Project\elk\logstash-6.3.0\bin\mysql-connector-java-5.1.46.jar" jdbc_driver_class => "com.mysql.jdbc.Driver" #取消小寫 lowercase_column_names => false #要執行的sql語句 statement => "select * from baoji_company_requirements" #這裡可以定製定時操作,比如每10分鐘執行一次同步(分 時 天 月 年) schedule => "/10 * * *" } }

output { stdout { codec => rubydebug } }

output { stdout{codec =>rubydebug} # 這個if判斷容易寫成if[type=="deliver_log"],注意中括號內只有type這個屬性名,不包含條件。 # deliver_log是將收集到的日誌輸出到日誌對應的索引中 if[type]=="deliver_log"{ elasticsearch { hosts => ["localhost:9200"] index => "logback-%{+YYYY.MM.dd}" user => "elastic" password => "gJRr45HLoRVzoqyRaWxO" } } # baoji_company_requirements是將收集到的表資料輸出到表名對應的索引中 # 我們此處直接將要建立的索引名index寫成表名,方便記憶與理解 if[type]=="baoji_company_requirements"{ elasticsearch { hosts => ["localhost:9200"] index => "baoji_company_requirements" user => "elastic" password => "gJRr45HLoRVzoqyRaWxO" }

}

} ``` 以上配置完成後,記得啟動ElasticSearch服務和Logstash服務。啟動logstash服務成功後會立即自動掃描資料庫中的資料;在kibana配置索引模式後也可以看到掃描到的同步後的mysql資料。 logstash服務窗 kibana控制檯

2,修改springboot的配置檔案

先看專案結構: 專案結構 我的專案是多模組專案,本文的demo在圖上的manage-system模組。除了圖上的三個檔案,還有該模組下的pom檔案以及application.properties配置檔案需要改動

1,修改pom檔案,新增依賴(如果專案有多個模組,修改需求所在的當前子模組) pom檔案

```xml

<dependencies>
    <!-- 其他不相關的依賴省略... -->
    <dependency>
        <groupId>org.springframework.data</groupId>
        <artifactId>spring-data-elasticsearch</artifactId>
        <version>3.2.1.RELEASE</version>
    </dependency>
    <dependency>
        <groupId>org.elasticsearch.client</groupId>
        <artifactId>elasticsearch-rest-high-level-client</artifactId>
    </dependency>
 </dependencies>
 <!--版本控制-->
<dependencyManagement>
    <dependencies>
        <dependency>
            <groupId>org.elasticsearch</groupId>
            <artifactId>elasticsearch</artifactId>
            <version>6.8.1</version>
        </dependency>
        <dependency>
            <groupId>org.elasticsearch.client</groupId>
            <artifactId>elasticsearch-rest-high-level-client</artifactId>
            <version>6.8.1</version>
            <exclusions>
                <exclusion>
                    <groupId>org.elasticsearch</groupId>
                    <artifactId>elasticsearch</artifactId>
                </exclusion>
            </exclusions>
        </dependency>
    </dependencies>
</dependencyManagement>

``` (使用dependencyManagement管理版本是為防止啟動時報錯:Caused by: java.lang.ClassNotFoundException: org.elasticsearch.common.xcontent.DeprecationHandler)

2:修改配置檔案,新增屬性 xml elasticsearch.host=127.0.0.1 elasticsearch.port=9200 elasticsearch.search.pool.size=5 elasticsearch.username=elastic elasticsearch.password=gJRr45HLoRVzoqyRaWxO (username和password在我的 上一篇部落格有提到)

3,程式碼檔案

程式碼檔案最基本的要有(參上圖目錄結構):

1.連線elasticSeartch的配置檔案(意義上類似jdbc的配置類)

```java import org.apache.http.HttpHost; import org.apache.http.auth.AuthScope; import org.apache.http.auth.UsernamePasswordCredentials; import org.apache.http.client.CredentialsProvider; import org.apache.http.impl.client.BasicCredentialsProvider; import org.apache.http.impl.nio.client.HttpAsyncClientBuilder; 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 org.springframework.data.elasticsearch.core.ElasticsearchRestTemplate; import org.springframework.data.elasticsearch.repository.config.EnableElasticsearchRepositories;

@Configuration @EnableElasticsearchRepositories(basePackages = "com.xxx.elk.entity") public class ElasticsearchConfig {

@Value("${elasticsearch.host}")
private String esHost;

@Value("${elasticsearch.port}")
private int esPort;

@Value("${elasticsearch.clustername}")
private String esClusterName;

@Value("${elasticsearch.search.pool.size}")
private Integer threadPoolSearchSize;

@Value("${elasticsearch.username}")
private String userName;

@Value("${elasticsearch.password}")
private String password;

@Bean
public RestHighLevelClient client(){
    /*使用者認證物件*/
    final CredentialsProvider credentialsProvider = new BasicCredentialsProvider();
    /*設定賬號密碼*/
    credentialsProvider.setCredentials(AuthScope.ANY,new UsernamePasswordCredentials(userName, password));
    /*建立rest client物件*/
    RestClientBuilder builder = RestClient.builder(new HttpHost(esHost, esPort))
            .setHttpClientConfigCallback(new RestClientBuilder.HttpClientConfigCallback() {
                @Override
                public HttpAsyncClientBuilder customizeHttpClient(HttpAsyncClientBuilder httpAsyncClientBuilder) {
                    return httpAsyncClientBuilder.setDefaultCredentialsProvider(credentialsProvider);
                }
            });
    RestHighLevelClient client = new RestHighLevelClient(builder);
    return client;
}

@Bean(name="elasticsearchTemplate")
public ElasticsearchRestTemplate elasticsearchRestTemplate(){
    return new ElasticsearchRestTemplate(client());
}

}

```

2.索引實體類(意義上類似mysql資料庫表對應的實體類)

```java import org.springframework.data.elasticsearch.annotations.Document; import org.springframework.data.elasticsearch.annotations.Field; import org.springframework.data.elasticsearch.annotations.FieldType;

import javax.persistence.Id; import java.util.Date;

/Document註解中的type為什麼是doc呢,因為elasticsearch7預設不在支援指定索引型別,預設索引型別是doc, createIndex為false即不主動建立索引,因為這個索引在logstash啟動的時候就被建立過了/ @Document(indexName = "baoji_company_requirements",type = "doc",createIndex = false) public class CompanyRequirementsElk {

@Id
private Long id;

@Field(name = "name",type = FieldType.Text,analyzer = "ik_max_word")
private String name;


@Field(name = "create_time",type = FieldType.Date)
private Date createTime;


public Long getId() {
    return id;
}

public void setId(Long id) {
    this.id = id;
}

public String getName() {
    return name;
}

public void setName(String name) {
    this.name = name;
}

public Date getCreateTime() {
    return createTime;
}

public void setCreateTime(Date createTime) {
    this.createTime = createTime;
}

}

``` (索引實體並沒有將elasticSearch索引中所有的欄位都取出來,我這裡只取了三個欄位,因為需要自動填充的name欄位,實際我這裡只寫一個name就夠了。)

3 .repository(意義上類似dao層的資料訪問檔案)

```java import com.baoji.elk.entity.CompanyRequirementsElk; import org.springframework.data.elasticsearch.repository.ElasticsearchRepository; import org.springframework.stereotype.Repository;

@Repository public interface CompanyRequirementsElkRepository extends ElasticsearchRepository{

} ```

4 .業務程式碼

java @GetMapping("getRequirementNames") public PageResult<Map> getRequirementNames(String keywords){ /*自定義返回結果*/ List<Map> returnList=new ArrayList<>(); /*查詢*/ MatchQueryBuilder matchQueryBuilder = QueryBuilders.matchQuery("name", keywords); Iterable<CompanyRequirementsElk> search = companyRequirementsElkRepository.search(matchQueryBuilder); search.forEach(companyRequirements -> { /*自定義返回結果*/ Map companyRequirementMap=new HashMap(); companyRequirementMap.put("name",companyRequirements.getName()); returnList.add(companyRequirementMap); }); return new PageResult(returnList); } 以上的程式碼就實現了分詞搜尋,MatchQueryBuilder 會預設使用最大分詞粒度進行分詞。但僅僅是分詞搜尋似乎不夠酷,所以我們將分隔的詞彙加上高亮顯示。改造如下: ```java @Autowired ElasticsearchConfig elasticsearchConfig;

@GetMapping("getRequirementNames")
public PageResult<Map> getRequirementNames(String keywords){
    /*自定義返回結果*/
    List<Map> returnList=new ArrayList<>();

    /*高亮的樣式及匹配欄位設定*/
    HighlightBuilder highlightBuilder=new HighlightBuilder()
            .field("name")
            .preTags("<span style='color:red;font-weight:bold;font-size:15px;'>").postTags("</span>");

    /*將高亮的配置加入到查詢中*/
    SearchSourceBuilder searchSourceBuilder = new SearchSourceBuilder();
    searchSourceBuilder.highlighter(highlightBuilder);

    /*查詢條件設定*/
    MatchQueryBuilder matchQueryBuilder = QueryBuilders.matchQuery("name", keywords);
    searchSourceBuilder.query(matchQueryBuilder);

    /*查詢請求,引數為 索引名稱,即要查哪個索引庫*/
    SearchRequest searchRequest = new SearchRequest("baoji_company_requirements");
    searchRequest.source(searchSourceBuilder);

    /*使用RestHighLevelClient連線elasticSearch服務*/
    RestHighLevelClient restHighLevelClient=elasticsearchConfig.client();
    SearchResponse searchResponse=null;
    try {
        /*呼叫使用RestHighLevelClient連線elasticSearch服務的查詢介面*/
        searchResponse = restHighLevelClient.search(searchRequest, RequestOptions.DEFAULT);
    } catch (IOException e) {
        e.printStackTrace();
        LOGGER.error(e.getMessage());
    }
    /*獲取到命中結果的集合*/
    Iterator<SearchHit> searchHitIterator = searchResponse.getHits().iterator();
    /*遍歷命中結果*/
    while(searchHitIterator.hasNext()){
        /*每個查詢命中物件*/
        SearchHit searchHit = searchHitIterator.next();
        Map<String, HighlightField> hightlightFields = searchHit.getHighlightFields();
        /*命中的屬性*/
        HighlightField titleField = hightlightFields.get("name");
        /*提取拼接成String*/
        String hightStr="";
        Text[] text=titleField.getFragments();
        if (text != null) {
            for (Text str : text) {
                hightStr += str.string();
            }
        }
        /*自定義返回結果*/
        Map companyRequirementMap=new HashMap();
        companyRequirementMap.put("name",hightStr);
        returnList.add(companyRequirementMap);
    }
    /*PageResult是對返回結果的一層自定義封裝。這裡可直接返回list*/
    return new PageResult(returnList);
}

``` 上面controller中的程式碼經過debug可以發現,我們提取的結果已經是包含了高亮樣式的結果。 debug

到此為止:elasticSearch搜尋的分詞+高亮的後端程式碼已經完成。至於前端,前端框架各有不同,只要能保證從這個業務介面取到了返回資料就好了。 以layui的前端為例:

jquery部分 javascript autocomplete.render({ elem: $('#requireMentId'),//標籤id cache: false,//不啟用快取 url: base_server +'companyRequirements/getRequirementNames',//介面地址 params:{access_token: admin.getToken()},//介面請求引數,元件內建了輸入框的一個名為keywords的引數 response: {code: 'code', data: 'data'},//介面返回引數格式 template_val: '{{d.name}}',//匹配條目選中的值 template_txt: '{{d.name}}',//匹配條目顯示的值 onselect: function (resp) { console.log(resp); } }); html部分

```html

``` 效果圖:

總結

這個類似淘寶京東的寶貝搜尋框輸入和百度搜索等的輸入,做elasticsearch搜尋的分詞+高亮是一個很有成就感的事情,同時再次對elasticSearch感到震撼,elasticSearch基於Lucene搜尋引擎實現。學無止境,去研究Lucene去了....