后端——》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去了....