自定义分词器, lexicon_analyzer是自定义的分词器的名字
Elasticsearch实现电商词库提示搜索
一. 自定义analyzer
- 数据索引到ES中的时候:例如 “美的电饭锅” -> mddfg, 美的电饭锅, meididianfanguo
- 在用户搜索的时候,将其中的html标签排除掉,如果是拼音转小写,其他原封不动到ES中搜索。
- 在数据索引到ES中和搜索的时候使用的 analyzer是 不同的。
# 自定义分词器, lexicon_analyzer是自定义的分词器的名字
PUT lexicon
{
"settings": {
"analysis": {
"analyzer": {
"lexicon_analyzer" : {
"char_filter": ["html_strip"],
"tokenizer": "keyword",
"filter": [
"my_lexicon_filter"
]
},
"lexicon_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
}
}
}
}
}
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>