自定义分词器, lexicon_analyzer是自定义的分词器的名字

Elasticsearch实现电商词库提示搜索

一. 自定义analyzer

  1. 数据索引到ES中的时候:例如 “美的电饭锅” -> mddfg, 美的电饭锅, meididianfanguo
  2. 在用户搜索的时候,将其中的html标签排除掉,如果是拼音转小写,其他原封不动到ES中搜索。
  3. 在数据索引到ES中和搜索的时候使用的 analyzer是 不同的。
  1. # 自定义分词器, lexicon_analyzer是自定义的分词器的名字
  2. PUT lexicon
  3. {
  4. "settings": {
  5. "analysis": {
  6. "analyzer": {
  7. "lexicon_analyzer" : {
  8. "char_filter": ["html_strip"],
  9. "tokenizer": "keyword",
  10. "filter": [
  11. "my_lexicon_filter"
  12. ]
  13. },
  14. "lexicon_search_analyzer": {
  15. "char_filter": ["html_strip"],
  16. "tokenizer": "keyword",
  17. "filter": ["lowercase"]
  18. }
  19. },
  20. "filter": {
  21. "my_lexicon_filter": {
  22. "type": "pinyin",
  23. "keep_first_letter": true,
  24. "keep_full_pinyin": false,
  25. "keep_none_chinese": false,
  26. "keep_separate_first_letter": false,
  27. "keep_joined_full_pinyin": true,
  28. "keep_none_chinese_in_joined_full_pinyin": true,
  29. "none_chinese_pinyin_tokenize": false,
  30. "limit_first_letter_length": 16,
  31. "keep_original": true
  32. }
  33. }
  34. }
  35. }
  36. }

lexicon_analyzer 是数据索引到ES的时候用户的分词器。 lexicon_search_analyzer: 是用户搜索的时候用的分词器。

二. 自定义mappings

# 设置mapping
PUT lexicon/_mapping
{
  "dynamic": false,
  "properties": {
    "words": {
      "type": "completion",
      "analyzer": "lexicon_analyzer",
      "search_analyzer": "lexicon_search_analyzer"
    },
    "id": {
      "type": "long"
    }
  }
}

analyzer是数据索引到ES的时候用的分词器;search_analyzer是用户搜索的时候用的分词器。

三. 数据的测试

3.1 添加测试数据

PUT lexicon/_bulk
{"index": {"_id": 1}}
{"words": "lamer精粹水"}
{"index": {"_id": 2}}
{"words": "lv 女包"}
{"index": {"_id": 3}}
{"words": "macbook pro保护壳"}
{"index": {"_id": 4}}
{"words": "mac口红"}
{"index": {"_id": 5}}
{"words": "jack琼斯"}
{"index": {"_id": 6}}
{"words": "jeep外套男"}
{"index": {"_id": 7}}
{"words": "k20"}

3.2 测试

类型必须是:completion skip_duplicates: 忽略掉重复。 field: 查询那个字段

GET lexicon/_search
{
  "_source": "id", 
  "suggest": {
    "lexicon_suggest": {
      "prefix": "jackqiongsi",
      "completion": {
         "field": "words",
         "skip_duplicates": true,
         "size": 10
      }
    }
  }
}

3.3 导入数据

创建名称为 es 的数据库,然后执行 lexicon.sql 导入数据

创建 logstash-mysql.conf文件,文件内容如下,放到\logstash-7.4.2\config\目录下

input {
  jdbc {
    jdbc_driver_class => "com.mysql.jdbc.Driver"
    jdbc_connection_string => "jdbc:mysql://localhost:3306/es?useSSL=false&serverTimezone=UTC"
    jdbc_user => root
    jdbc_password => "root"
    #启用追踪,如果为true,则需要指定tracking_column
    use_column_value => false
    #指定追踪的字段,
    tracking_column => "id"
    #追踪字段的类型,目前只有数字(numeric)和时间类型(timestamp),默认是数字类型
    tracking_column_type => "numeric"
    #记录最后一次运行的结果
    record_last_run => true
    #上面运行结果的保存位置
    last_run_metadata_path => "mysql-position.txt"
    statement => "SELECT * FROM lexicon"
     #设置对应的时间
    schedule => "0/40 * * * * *"
  }
}
filter {

}
output {
  elasticsearch {
    document_id => "%{id}"
    document_type => "_doc"
    index => "lexicon"
    hosts => ["http://localhost:9200"]
  }
  stdout{
    codec => rubydebug
  }
}

将mysql的驱动包放到 logstash根目录的:\logstash-7.4.2\logstash-core\lib\jars 目录下

进到logstash的bin目录下,执行: logstash.bat -f D:\elasticsearch\logstash-7.4.2\config\logstash-mysql.conf 命令,开始导入数据。

四. 实现中文推荐搜索Lexicon案例

创建springboot项目并导入依赖

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

创建配置类

package com.qf.config;

import org.apache.http.HttpHost;
import org.elasticsearch.client.RestClient;
import org.elasticsearch.client.RestHighLevelClient;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.elasticsearch.config.AbstractElasticsearchConfiguration;
import org.springframework.data.elasticsearch.core.ElasticsearchRestTemplate;

@Configuration
public class ElasticsearchConfig extends AbstractElasticsearchConfiguration {

    @Bean
    public RestHighLevelClient elasticsearchClient() {

        //spring官网
//        final ClientConfiguration clientConfiguration = ClientConfiguration.builder()
//            .connectedTo("localhost:9200")
//            .build();
//        return RestClients.create(clientConfiguration).rest();

        //elasticsearch官网
        RestHighLevelClient client = new RestHighLevelClient(
                RestClient.builder(
                        new HttpHost("localhost", 9200, "http")));

        return client;
    }

    @Bean
    public ElasticsearchRestTemplate elasticsearchRestTemplate() {
        return new ElasticsearchRestTemplate(elasticsearchClient());
    }
}

创建实体类 Lexicon

package com.qf.pojo;

import lombok.Data;
import org.springframework.data.elasticsearch.annotations.Document;

@Document(indexName = "lexicon")
@Data
public class Lexicon {
    private Long id;
    private String words;
}

创建 LexiconController

package com.qf.controller;

import org.elasticsearch.action.search.SearchResponse;
import org.elasticsearch.search.suggest.Suggest;
import org.elasticsearch.search.suggest.SuggestBuilder;
import org.elasticsearch.search.suggest.completion.CompletionSuggestion;
import org.elasticsearch.search.suggest.completion.CompletionSuggestionBuilder;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.elasticsearch.core.ElasticsearchRestTemplate;
import org.springframework.data.elasticsearch.core.mapping.IndexCoordinates;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import java.util.HashSet;
import java.util.List;

@RestController
@RequestMapping("lexicon-suggest")
public class LexiconController {

    @Autowired
    private ElasticsearchRestTemplate elasticsearchRestTemplate;

    @RequestMapping("findAll")
    public Object findAll(String text){

        CompletionSuggestionBuilder completionSuggestionBuilder =
                new CompletionSuggestionBuilder("words")
                        .prefix(text)
                        .skipDuplicates(true);

        SuggestBuilder suggestBuilder =
                new SuggestBuilder().addSuggestion("words_prefix_suggestion", completionSuggestionBuilder);


        SearchResponse response=elasticsearchRestTemplate.suggest(suggestBuilder, IndexCoordinates.of("lexicon"));

        //获取Suggest对象
        Suggest suggest = response.getSuggest();

        Suggest.Suggestion suggestSuggestion = suggest.getSuggestion("words_prefix_suggestion");

        //声明一个集合获取options中的text
        HashSet<String> set = new HashSet<>();

        List entries = suggestSuggestion.getEntries();

        if(entries.size()>0 && entries!=null){
            Object object = entries.get(0);

            if(object instanceof CompletionSuggestion.Entry){

                CompletionSuggestion.Entry entry = (CompletionSuggestion.Entry)object;

                List<CompletionSuggestion.Entry.Option> options = entry.getOptions();

                if(options.size()>0 && options!=null){
                    for(CompletionSuggestion.Entry.Option option : options){
                        set.add(option.getText().toString());
                    }
                }
            }
        }
        return set;
    }
}

在static目录下创建 lexicon.html 并导入 js

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
    <script src="js/jquery.min.js"></script>
</head>
<body>
<input id="autoCompt" type="text" onkeyup="getPrefixResult()"><button>搜索一下</button>
<div id="callbackValue" style="width: 400px; background-color: antiquewhite;"></div>
</body>
<script>
    function getPrefixResult() {
        var callbackValueBox = $('#callbackValue');
        callbackValueBox.html('');
        var value = $('#autoCompt').val();

        if(value && value.trim() && value.trim().length > 0) {
            $.get("/lexicon-suggest/findAll", {text: value}, function(_data) {
                for(var i = 0; i < _data.length; i++) {
                    callbackValueBox.append('<p>' + _data[i] + "</p>");
                }
            }, "json");
        }
    }
</script>
</html>

五. 在Lexicon搜索的基础上实现商品搜索展示

1.执行goods.sql导入数据到mysql

2.自定义analyzer

# 自定义analyzer
PUT goods
{
  "settings": {
    "analysis": {
      "analyzer": {
        "goods_analyzer": {
          "char_filter": ["html_strip"],
          "tokenizer": "goods_tokenizer"
        }
      },
      "tokenizer": {
        "goods_tokenizer": {
          "type": "hanlp_index",
          "enable_stop_dictionary": true
        }
      }
    }
  }
}

3.定义mapping

PUT goods/_mapping
{
  "properties": {
    "id": {
      "type": "long"
    },
    "no": {
      "type": "long"
    },
    "category": {
      "type": "keyword"
    },
    "title": {
      "type": "text",
      "analyzer": "goods_analyzer"
    },
    "promowords": {
      "type": "text",
      "analyzer": "goods_analyzer",
      "search_analyzer": "goods_analyzer"
    },
    "samllpic": {
      "type": "keyword"
    },
    "price": {
      "type": "double"
    },
    "createtime": {
      "type": "date"
    },
    "stockNum": {
      "type": "long"
    }
  }
}

4.创建logstash-mysql-goods.conf文件,文件内容如下,放到\logstash-7.4.2\config\目录下

input {
  jdbc {
    jdbc_driver_class => "com.mysql.jdbc.Driver"
    jdbc_connection_string => "jdbc:mysql://localhost:3306/es?useSSL=false&serverTimezone=UTC"
    jdbc_user => root
    jdbc_password => "root"
    #启用追踪,如果为true,则需要指定tracking_column
    use_column_value => false
    #指定追踪的字段,
    tracking_column => "id"
    #追踪字段的类型,目前只有数字(numeric)和时间类型(timestamp),默认是数字类型
    tracking_column_type => "numeric"
    #记录最后一次运行的结果
    record_last_run => true
    #上面运行结果的保存位置
    last_run_metadata_path => "mysql-goods-position.txt"
    statement => "SELECT i.name category, i.no, g.id, g.title, g.promo_words promowords, g.small_pic samllpic, g.price, g.create_time createtime, g.stock_num stocknum
  from goods g join items i on g.item_id = i.no"
    schedule => "0/40 * * * * *"
  }
}

filter {

}

output {
  elasticsearch {
    document_id => "%{id}"
    document_type => "_doc"
    index => "goods"
    hosts => ["http://localhost:9200"]
  }
  stdout{
    codec => rubydebug
  }
}

5.将mysql的驱动包放到 logstash根目录的:\logstash-7.4.2\logstash-core\lib\jars 目录下

6.进到logstash的bin目录下,执行: logstash.bat -f D:\elasticsearch\logstash-7.4.2\config\logstash-mysql-goods.conf 命令,开始导入数据。

7.创建实体类

package com.qf.pojo;

import lombok.Data;
import org.springframework.data.elasticsearch.annotations.Document;

@Document(indexName = "goods")
@Data
public class Goods {
    private Long id;
    private Long no;
    private String category;
    private String title;
    private String promowords;
    private String samllpic;
    private Double price;
    private String createtime;
    private Long stocknum;
}

8.创建PageInfo

package com.qf.controller;

import java.util.List;

public class PageInfo<T> {
    private List<T> datas;
    private Integer currentPage;

    public List<T> getDatas() {
        return datas;
    }

    public void setDatas(List<T> datas) {
        this.datas = datas;
    }

    public Integer getCurrentPage() {
        return currentPage;
    }

    public void setCurrentPage(Integer currentPage) {
        this.currentPage = currentPage;
    }
}

9.创建controller

package com.qf.controller;

import com.qf.pojo.Goods;
import com.qf.utils.PageInfo;
import org.elasticsearch.index.query.MatchAllQueryBuilder;
import org.elasticsearch.index.query.MultiMatchQueryBuilder;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.elasticsearch.core.ElasticsearchRestTemplate;
import org.springframework.data.elasticsearch.core.SearchHit;
import org.springframework.data.elasticsearch.core.SearchHits;
import org.springframework.data.elasticsearch.core.mapping.IndexCoordinates;
import org.springframework.data.elasticsearch.core.query.NativeSearchQuery;
import org.springframework.data.elasticsearch.core.query.NativeSearchQueryBuilder;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

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

@RestController
@RequestMapping("/goods")
public class GoodsSearchController {

    @Autowired
    private ElasticsearchRestTemplate elasticsearchRestTemplate;

    @GetMapping
    public PageInfo<Goods> indexShow(@RequestParam(defaultValue = "0") Integer currentPage) {
        PageInfo<Goods> pageInfo = new PageInfo<>();

        PageRequest pageRequest = PageRequest.of(currentPage, 10);

        NativeSearchQuery nativeSearchQuery = new NativeSearchQueryBuilder()
                .withPageable(pageRequest)
                .withQuery(new MatchAllQueryBuilder())
                .build();

        SearchHits<Goods> searchHits =
                elasticsearchRestTemplate.search(nativeSearchQuery, Goods.class, IndexCoordinates.of("goods"));

        List<SearchHit<Goods>> searchHitList = searchHits.getSearchHits();

        ArrayList<Goods> list = new ArrayList<Goods>(searchHitList.size());

        for(SearchHit<Goods> movieSearchHit:searchHits){
            Goods goods=movieSearchHit.getContent();
            list.add(goods);
        }

        list.forEach(g -> g.setSamllpic("http://localhost/" + g.getSamllpic()));

        pageInfo.setDatas(list);
        pageInfo.setCurrentPage(currentPage);
        return pageInfo;
    }

    @GetMapping("/search")
    public PageInfo<Goods> searchPageGoodsData(String text, @RequestParam(defaultValue = "0") Integer currentPage) {

        PageInfo<Goods> pageInfo = new PageInfo<>();

        PageRequest pageRequest = PageRequest.of(currentPage, 10);

        NativeSearchQuery nativeSearchQuery = new NativeSearchQueryBuilder()
                .withPageable(pageRequest)
                .withQuery(new MultiMatchQueryBuilder(text, "title", "promowords", "category"))
                .build();

        SearchHits<Goods> searchHits =
                elasticsearchRestTemplate.search(nativeSearchQuery, Goods.class, IndexCoordinates.of("goods"));

        List<SearchHit<Goods>> searchHitList = searchHits.getSearchHits();

        ArrayList<Goods> list = new ArrayList<Goods>(searchHitList.size());

        for(SearchHit<Goods> movieSearchHit:searchHits){
            Goods goods=movieSearchHit.getContent();
            list.add(goods);
        }

        list.forEach(g -> g.setSamllpic("http://localhost/" + g.getSamllpic()));

        pageInfo.setDatas(list);
        pageInfo.setCurrentPage(currentPage);
        return pageInfo;
    }
}

10.引入js以及css文件,创建goods.html页面

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
    <link rel="stylesheet" href="css/bootstrap.min.css">
    <link rel="stylesheet" href="css/jquery-ui.min.css">
    <script src="js/jquery-3.5.0.js"></script>
    <script src="js/jquery-ui.min.js"></script>
    <script src="js/bootstrap.min.js"></script>
    <script src="js/vue.js"></script>
    <script src="js/axios.min.js"></script>
    <style>
        .desc-text {
            height: 50px;
            overflow: hidden;
        }
    </style>
</head>
<body>
<div class="container-fluid">
    <div class="row mt-3 pb-3 mb-3 justify-content-center" style="border-bottom: 1px solid #e2e3e5;">
        <div class="col-10">
            <form class="form-inline justify-content-center" onsubmit="javascript: return false;">
                <div class="form-group col-6 ">
                    <input class="form-control col" id="search-text" onkeyup="searchGood()">
                </div>
                <button type="submit" class="btn btn-primary col-1">搜索一下</button>
            </form>
        </div>
    </div>
</div>
<div class="container" id="app">
    <div class="row row-cols-5 mb-4">
        <div v-for="g in goods" class="col mb-3" :key="g.id">
            <div class="card">
                <img height="196" width="196" alt="暂无图片展示" :src="g.samllpic" class="card-img-top">
                <div class="card-body pb-3">
                    <p class="card-text pb-0 desc-text">{{g.title}} {{g.promowords}}</p>
                    <p class="card-text">¥{{g.price}}</p>
                </div>
            </div>
        </div>
    </div>
</div>
</body>
<script>
    var vm = new Vue({
        el: '#app',
        data() {
            return {
                goods: []
            }
        },
        mounted: function() {
            axios.get('goods')
                .then(res => {
                this.goods = res.data.datas;
        })
        }
    })
    $('#search-text').autocomplete({
        delay: 300,
        max: 20,
        source: function(request, cb) {
            $.ajax({
                url: 'lexicon-suggest/findAll',
                data: {text: request.term},
                type: 'get',
                dataType: 'json',
                success: function(_data) {
                    let tips = [];
                    for(let i = 0; i < _data.length; i++) {
                        tips.push(_data[i]);
                    }
                    cb(tips);
                }
            })
        },
        minlength: 1
    })

    function searchGood() {
            vm.goods = []
            let searchText = $('#search-text').val();
            axios.get('goods/search?text=' + searchText)
                .then(res => {
                for(let i = 0; i < res.data.datas.length; i++) {
                vm.goods.push(res.data.datas[i])
            }
        })
    }
</script>
</html>

访问浏览进行测试即可

基于新闻的高亮搜索

1. 自定义analyzer

# 自定义分词器, news_analyzer是自定义的分词器的名字
PUT news
{
  "settings": {
    "analysis": {
      "analyzer": {
        "news_analyzer" : {
          "char_filter": ["html_strip"],
          "tokenizer": "keyword",
          "filter": [
             "my_lexicon_filter"
          ]
        },
        "news_search_analyzer": {
          "char_filter": ["html_strip"],
          "tokenizer": "keyword",
          "filter": ["lowercase"]
        }
      },
      "filter": {
        "my_lexicon_filter": {
          "type": "pinyin",
          "keep_first_letter": true,
          "keep_full_pinyin": false,
          "keep_none_chinese": false,
          "keep_separate_first_letter": false,
          "keep_joined_full_pinyin": true,
          "keep_none_chinese_in_joined_full_pinyin": true,
          "none_chinese_pinyin_tokenize": false,
          "limit_first_letter_length": 16,
          "keep_original": true
        } 
      }
    }
  }
}

2. 定义mappings

PUT news/_mapping
{
    "properties": {
      "id": {
        "type": "long"
      },
      "title": {
        "type": "text",
        "analyzer": "hanlp_index"
      },
      "url": {
        "type": "keyword"
      },
      "content": {
        "type": "text",
        "analyzer": "hanlp_index"
      },
      "tags": {
        "type": "completion",
        "analyzer": "news_analyzer",
        "search_analyzer": "news_search_analyzer"
      }
    }
}

设置mappings的时候,可以指定 “dynamic”: false,意思是如果mappings中有些字段并没有指定,那么在新增数据的时候,该字段的数据会存入到es中,但是不会进行分词,但是可以被查出来。

3. 导入mysql的数据集

1.将news.sql导入mysql数据库

2.将mysql驱动包放在D:\elasticsearch\logstash-7.4.2\logstash-core\lib\jars目录下

3.将logstash-mysql-news.conf放在D:\elasticsearch\logstash-7.4.2\config目录下

4.进到logstash的bin目录下,执行:logstash.bat -f D:\elasticsearch\logstash-7.4.2\config\logstash-mysql-news.conf命令,开始导入数据。

4.编写suggestion与query

搜索要使用的suggestion

GET news/_search
{
  "_source": ["id"], 
  "suggest": {
    "tags_suggest": {
      "prefix": "中",
      "completion": {
        "field": "tags",
        "skip_duplicates": true,
        "size": 10
      }
    }
  }
}

注: 在使用suggestion的时候,”skip_duplicates”: true,表示的意思是如果出现相同的建议,那么只会保留一个。

搜索要使用的query

GET news/_search
{
  "_source": ["url"], 
  "query": {
    "multi_match": {
      "query": "中国赴塞尔维亚抗疫专家",
      "fields": ["title", "content"]
    }
  },
  "highlight": {
    "post_tags": "</span>",
    "pre_tags": "<span>",
    "fields": {
      "title": {},
      "content": {}
    }
  }
}

5.导入依赖

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

<dependency>
    <groupId>org.projectlombok</groupId>
    <artifactId>lombok</artifactId>
    <optional>true</optional>
</dependency>

6.编写ElasticsearchConfig

package com.qf.config;

import org.apache.http.HttpHost;
import org.elasticsearch.client.RestClient;
import org.elasticsearch.client.RestHighLevelClient;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.elasticsearch.config.AbstractElasticsearchConfiguration;
import org.springframework.data.elasticsearch.core.ElasticsearchRestTemplate;

@Configuration
public class ElasticsearchConfig extends AbstractElasticsearchConfiguration {


    @Bean
    public RestHighLevelClient elasticsearchClient() {

        //spring官网
//        final ClientConfiguration clientConfiguration = ClientConfiguration.builder()
//            .connectedTo("localhost:9200")
//            .build();
//        return RestClients.create(clientConfiguration).rest();

        //elasticsearch官网
        RestHighLevelClient client = new RestHighLevelClient(
                RestClient.builder(
                        new HttpHost("localhost", 9200, "http")));

        return client;

    }

    @Bean
    public ElasticsearchRestTemplate elasticsearchRestTemplate() {
        return new ElasticsearchRestTemplate(elasticsearchClient());
    }
}

7.POJO类的编写

package com.qf.pojo;

import lombok.Data;
import org.springframework.data.elasticsearch.annotations.Document;

@Document(indexName = "news")
@Data
public class News {

    private Integer id;
    private String url;
    private String title;
    private String content;

}

8. 编写NewsSuggestController

package com.qf.controller;

import org.elasticsearch.action.search.SearchResponse;
import org.elasticsearch.common.text.Text;
import org.elasticsearch.search.suggest.Suggest;
import org.elasticsearch.search.suggest.SuggestBuilder;
import org.elasticsearch.search.suggest.completion.CompletionSuggestion;
import org.elasticsearch.search.suggest.completion.CompletionSuggestionBuilder;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.elasticsearch.core.ElasticsearchRestTemplate;
import org.springframework.data.elasticsearch.core.mapping.IndexCoordinates;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import java.util.HashSet;
import java.util.List;
import java.util.Set;

// 新闻提示搜索
@RestController
@RequestMapping("news-suggest")
public class NewsSuggestController {

    @Autowired
    private ElasticsearchRestTemplate elasticsearchRestTemplate;


    @GetMapping
    public Set<String> movieSuggest(String text) {
        /**
         * 第一步构建 CompletionSuggestionBuilder
         */
        CompletionSuggestionBuilder titlePrefixSuggest = new CompletionSuggestionBuilder("tags")
                .prefix(text)
                .size(10)   //提示多少个次
                .skipDuplicates(true);  //忽略重复

        /**
         * 第二步在去构建 SuggestBuilder, 封装所有的建议形式
         */
        SuggestBuilder suggestBuilder = new SuggestBuilder()
                .addSuggestion("tag_prefix_suggestion", titlePrefixSuggest);

        SearchResponse response=elasticsearchRestTemplate.suggest(suggestBuilder, IndexCoordinates.of("news"));

        //获取Suggest对象
        Suggest suggest = response.getSuggest();

        // 获取对应的搜索建议的结果
        Suggest.Suggestion suggestion = suggest.getSuggestion("tag_prefix_suggestion");

        Set<String> suggestionResult = new HashSet<>();

        List<Object> list = suggestion.getEntries();
        if(null != list && list.size() > 0){
            Object object = list.get(0);
            if(object instanceof CompletionSuggestion.Entry) {
                CompletionSuggestion.Entry resultEntry = (CompletionSuggestion.Entry)object;
                List<CompletionSuggestion.Entry.Option> options = resultEntry.getOptions();
                if(null != options && options.size() > 0) {
                    for(CompletionSuggestion.Entry.Option opt : options) {
                        Text txt = opt.getText();
                        suggestionResult.add(txt.toString());
                    }
                }
            }
        }

        return suggestionResult;
    }
}

9.编写NewsSearchController

package com.qf.controller;

import com.qf.pojo.News;
import org.elasticsearch.index.query.MultiMatchQueryBuilder;
import org.elasticsearch.search.fetch.subphase.highlight.HighlightBuilder;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.elasticsearch.core.ElasticsearchRestTemplate;
import org.springframework.data.elasticsearch.core.SearchHit;
import org.springframework.data.elasticsearch.core.SearchHits;
import org.springframework.data.elasticsearch.core.query.NativeSearchQuery;
import org.springframework.data.elasticsearch.core.query.NativeSearchQueryBuilder;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

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

// 新闻内容搜索以及前端高亮显示
@RestController
@RequestMapping("/news")
public class NewsSearchController {

    @Autowired
    private ElasticsearchRestTemplate elasticsearchRestTemplate;

    /**
     * GET news/_search
     * {
     *   "_source": ["url"],
     *   "query": {
     *     "multi_match": {
     *       "query": "中国",
     *       "fields": ["title", "content"]
     *     }
     *   },
     *   "highlight": {
     *     "pre_tags": "<font color='red'>",
     *     "post_tags": "</font>",
     *     "fields": {
     *       "title": {},
     *       "content": {}
     *     }
     *   }
     * }
     */
    @GetMapping("/search")
    public List<News> searchNews(String searchText) {

        MultiMatchQueryBuilder multiMatchQueryBuilder = new MultiMatchQueryBuilder(searchText, "title", "content");

        HighlightBuilder highlightBuilder = new HighlightBuilder()
                .preTags("<font color='red'>")
                .postTags("</font>")
                .field("title")
                .field("content");

        NativeSearchQuery query = new NativeSearchQueryBuilder()
                .withQuery(multiMatchQueryBuilder)
                .withHighlightBuilder(highlightBuilder)
                .build();

        SearchHits<News> search = elasticsearchRestTemplate.search(query,News.class);
        // 得到查询结构返回的内容
        List<SearchHit<News>> searchHits = search.getSearchHits();
        // 设置一个需要返回的实体类集合
        List<News> news = new ArrayList<>();
        for(SearchHit<News> searchHit:searchHits){
            //获取高亮显示内容
            Map<String,List<String>> highLightFields = searchHit.getHighlightFields();
            // 将高亮内容填充到content中
            searchHit.getContent().setTitle(highLightFields.get("title") == null ? searchHit.getContent().getTitle() : highLightFields.get("title").get(0));
            searchHit.getContent().setContent(highLightFields.get("content") == null ? searchHit.getContent().getContent() : highLightFields.get("content").get(0));
            // 放到实体类中
            news.add(searchHit.getContent());
        }

        return news;
    }
}

10.前端的实现

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
    <link rel="stylesheet" href="css/bootstrap.min.css">
    <link rel="stylesheet" href="css/jquery-ui.min.css">
    <script src="js/jquery-3.5.0.js"></script>
    <script src="js/jquery-ui.min.js"></script>
    <script src="js/bootstrap.min.js"></script>
    <script src="js/vue.js"></script>
    <script src="js/axios.min.js"></script>
    <style>
        .desc-text {
            height: 50px;
            overflow: hidden;
        }
        a,a:link, a:visited, a:hover, a:active {
            text-decoration: none;
        }
    </style>
</head>
<body>
<div class="container-fluid">
    <div class="row mt-3 pb-3 mb-3" style="border-bottom: 1px solid #e2e3e5;">
        <div class="col-10">
            <form class="form-inline" onsubmit="javascript: return false;">
                <div class="form-group col-6">
                    <input class="form-control col" id="search-text" onkeyup="searchNews()">
                </div>
                <button type="submit" class="btn btn-primary col-1">搜索一下</button>
            </form>
        </div>
    </div>
    <div id="app">
        <div v-for="n in news" class="row mb-3" :key="n.id">
            <div class="col-10">
                <h4><a target="_blank" :href="n.url"><span v-html="n.title"></span></a></h4>
                <p v-html="n.content">
                </p>
            </div>
        </div>
    </div>
</div>
</body>
<script>
    var vm = new Vue({
        el: '#app',
        data() {
            return {
                news: []
            }
        }
    })

    $('#search-text').autocomplete({
        delay: 300,  // 延迟查询,意思是当在输入框中多输入了一个词,多久往服务器发送请求
        max: 20,  // 指的是下拉列表中最多出现多少个次
        source: function(request, cb) {
            $.ajax({
                url: 'news-suggest',
                data: {text: request.term},
                type: 'get',
                dataType: 'json',
                success: function(_data) {
                    let tips = [];
                    for(let i = 0; i < _data.length; i++) {
                        tips.push(_data[i]);
                    }
                    cb(tips);
                }
            })
        },
        minlength: 1   // 最低输入多少个字母就往服务器端发送请求
    })


    function searchNews() {

        let searchText = $('#search-text').val();  //拿到搜索内容
        if(searchText && searchText.trim()) {
            vm.news = [];
            axios.get('news/search?searchText=' + searchText)
                .then(res => {
                for(let i = 0; i < res.data.length; i++) {
                vm.news.push(res.data[i])
            }
        })
        }
    }

</script>
</html>

11.页面效果