高亮搜索案例
一. 定义分词器
PUT news
{
"settings": {
"analysis": {
"analyzer": {
"news_tag_analyzer": {
"char_filter": "html_strip",
"tokenizer": "keyword",
"filter": "news_tag_filter"
}
},
"filter": {
"news_tag_filter": {
"type": "pinyin",
"keep_separate_first_letter" : false,
"keep_full_pinyin" : false,
"keep_original" : true,
"lowercase" : true,
"keep_joined_full_pinyin": true,
"remove_duplicated_term": true
}
}
}
}
}
二. 定义mapping
PUT news/_mapping
{
"properties" : {
"content" : {
"type" : "text",
"analyzer": "ik_max_word",
"search_analyzer": "ik_smart"
},
"id" : {
"type" : "long"
},
"tags" : {
"type": "completion",
"analyzer": "news_tag_analyzer",
"search_analyzer": "keyword"
},
"title" : {
"type" : "text",
// 在数据索引进ES的时候,让词分的细腻点
"analyzer": "ik_max_word",
// 在查询的时候,尽量不要去分
"search_analyzer": "ik_smart"
},
"url" : {
// url地址栏不进行分词
"type" : "keyword"
}
}
}
三. 自定义网络词库
在某个服务器中(Tomcat或者Nginx)中放两个文件
customer.dic
和stopword.dic
文件(文件可以是txt文件)。customize.dic
文件中放的是业务词库,stopword.dic
中存放的是停用词。IK的一个bug问题,就是第一行不要写内容
, 文件内容一次如下:customize.dic
梅长苏
拜登
柳岩
stopword.dic
腰
的
将两个文件放到
NGINX_HOME/html
目录下,然后启动nginx, 那么我们就在浏览器地址栏访问到两个文件的内容。
四. 配置IK的动态词库
修改
ES_HOME/plugins/ik/config
目录下的IKAnalyzer.cfg.xml
, 文件的内容如下:
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE properties SYSTEM "http://java.sun.com/dtd/properties.dtd">
<properties>
<comment>IK Analyzer 扩展配置</comment>
<!--用户可以在这里配置自己的扩展字典 -->
<!-- <entry key="ext_dict">customer/customize.dic</entry> -->
<entry key="ext_dict"></entry>
<!--用户可以在这里配置自己的扩展停止词字典-->
<!-- <entry key="ext_stopwords">stopword/keys.dir</entry> -->
<entry key="ext_stopwords"></entry>
<!--用户可以在这里配置远程扩展字典 -->
<!-- <entry key="remote_ext_dict"></entry> -->
<entry key="remote_ext_dict">http://localhost/customize.dic</entry>
<!--用户可以在这里配置远程扩展停止词字典-->
<entry key="remote_ext_stopwords">http://localhost/stopword.dic</entry>
<!-- <entry key="remote_ext_stopwords">words_location</entry> -->
</properties>
五. 导入数据库数据
一. 重启ES
二. 将提供的
mysql-news.conf
这个文件放到LOGSTASH_HOME/config
目录下(依情况作一些修改)三. 将mysql的驱动包放到
LOGSTASH_HOME/logstash-core/lib/jars
目录下。四.进入
LOGSTASH_HOME/bin
目录下打开dos命令行,执行如下命令:
logstash -f ../config/mysql-news.conf
六. 书写测试DSL
前缀搜索的DSL
GET news/_search
{
"_source": false,
"suggest": {
"news_tag_suggest": {
// jnl 就是需要前缀提示的内容
"prefix": "jnl",
"completion": {
"field": "tags",
"size": 10,
"skip_duplicates":true
}
}
}
}
高亮搜索的 DSL
GET news/_search
{
"_source": ["id", "title", "content", "url"],
"query": {
"multi_match": {
// 用户在搜索框中输入的内容,然后进行搜索
"query": "梅长苏",
"fields": ["title", "content"]
}
},
"highlight": {
"pre_tags": "<span class='hl'>",
"post_tags": "</span>",
"fields": {
"title": {},
"content": {}
}
}
}
七. 服务端代码的实现
@RestController
@RequestMapping("/news")
public class NewsController {
private RestHighLevelClient restHighLevelClient;
/*
* @Qualifier("elasticsearchClient") 主要是针对容器中有多个 bean, 可以通过 @Qualifier 将那个bean注入进来
* @param elasticsearchClient
*/
public NewsController(@Qualifier("elasticsearchClient") RestHighLevelClient restHighLevelClient) {
this.restHighLevelClient = restHighLevelClient;
}
@GetMapping("/tips")
public List<String> prefixTip(String tips) throws IOException {
Request request = new Request("GET", "news/_search");
String suggestJson = String.format("{" +
" \"_source\": false, " +
" \"suggest\": {" +
" \"news_tag_suggest\": {" +
" \"prefix\": \"%s\"," +
" \"completion\": {" +
" \"field\": \"tags\"," +
" \"size\": 10," +
" \"skip_duplicates\":true" +
" }" +
" }" +
" }" +
"}", tips);
request.setJsonEntity(suggestJson);
// 发送请求, 返回结果
Response response = restHighLevelClient.getLowLevelClient().performRequest(request);
// 返回的json字符串, 就是于 kibana中得到的json数据
String responseJson = EntityUtils.toString(response.getEntity());
JSONObject jsonObject = JSONObject.parseObject(responseJson);
// 获取 kibana 中的 "suggest" 对应的json对象
JSONObject suggest = jsonObject.getJSONObject("suggest");
// 就是搜索的信息与结构
JSONObject resultInfo = suggest.getJSONArray("news_tag_suggest").getJSONObject(0);
JSONArray options = resultInfo.getJSONArray("options");
// 给定长度, 是一种优化的策略
List<String> results = new ArrayList<>(options.size());
for(int i = 0; i < options.size(); i++) {
// opt 就是我们需要的数据
JSONObject opt = options.getJSONObject(i);
results.add(opt.getString("text"));
}
return results;
}
@GetMapping("/search")
public List<NewsModel> search(String keyword) throws IOException {
Request request = new Request("GET", "news/_search");
String suggestJson = String.format("{" +
" \"_source\": [\"title\", \"content\", \"url\", \"id\"], " +
" \"query\": {" +
" \"multi_match\": {" +
" \"query\": \"%s\"," +
" \"fields\": [\"title\", \"content\"]" +
" }" +
" }," +
" \"highlight\": {" +
" \"pre_tags\": \"<span class='hl'>\", " +
" \"post_tags\": \"</span>\", " +
" \"fields\": {" +
" \"title\": {}," +
" \"content\": {}" +
" }" +
" }" +
"}", keyword);
request.setJsonEntity(suggestJson);
// 发送请求, 返回结果
Response response = restHighLevelClient.getLowLevelClient().performRequest(request);
// 返回的json字符串, 就是于 kibana中得到的json数据
String responseJson = EntityUtils.toString(response.getEntity());
JSONObject jsonObject = JSONObject.parseObject(responseJson);
JSONArray hits = jsonObject.getJSONObject("hits").getJSONArray("hits");
List<NewsModel> results = new ArrayList<>();
if(hits.size() > 0) {
for(int i = 0; i < hits.size(); i++) {
// 这是最终查询的每一条数据
JSONObject data = hits.getJSONObject(i);
NewsModel nm = new NewsModel();
// 原始数据
JSONObject originData = data.getJSONObject("_source");
nm.setId(originData.getInteger("id"));
nm.setUrl(originData.getString("url"));
// 高亮数据
JSONObject highLightData = data.getJSONObject("highlight");
// 获取title中高亮的文本碎片, 返回的是一个数组
JSONArray titles = highLightData.getJSONArray("title");
// 如果title中没有查询的高亮文本, 就从原始数据中取
if(null == titles) {
nm.setTitle(originData.getString("title"));
}else {
StringBuffer titleSb = new StringBuffer();
for(int j = 0; j < titles.size(); j++) {
titleSb.append(titles.getString(j));
}
nm.setTitle(titleSb.toString());
}
JSONArray contents = highLightData.getJSONArray("content");
// 如果title中没有查询的高亮文本, 就从原始数据中取
if(null == contents) {
nm.setContent(originData.getString("content"));
}else {
StringBuffer contentSb = new StringBuffer();
for(int j = 0; j < contents.size(); j++) {
contentSb.append(contents.getString(j));
}
nm.setContent(contentSb.toString());
}
results.add(nm);
}
return results;
}else {
return results;
}
}
}
八. 前端页面的实现
<!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-2.1.1.min.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.js"></script>
<style>
.hl {
color: orangered;
}
</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" >
</div>
<button type="submit" class="btn btn-primary col-1" onclick="beginSearch()">搜索一下</button>
</form>
</div>
</div>
<div id="app">
<div class="row mb-3" v-for="nr in newsResults" :key="nr.id">
<div class="col-10">
<h4><a target="_blank" :href="nr.url" v-html="nr.title"></a></h4>
<p v-html="nr.content"></p>
</div>
</div>
</div>
</div>
<script>
const vm = new Vue({
el:'#app',
data() {
return {
newsResults: []
}
}
})
$('#search-text').autocomplete({
delay: 300, // 延迟查询,意思是当在输入框中多输入了一个词,多久往服务器发送请求
max: 20, // 指的是下拉列表中最多出现多少个
source: function(request, cb) {
// console.log(request.term)
$.ajax({
url: '/news/tips?tips=' + request.term,
type: 'get',
dataType: 'json',
success: function(_data) {
// let ds = [];
// for(let i = 0; i < _data.length; i++) {
// ds.push(_data[i]);
// }
// 只需要调用 cb() 方法, 会自动将数组的值展示在输入框的下面
cb(_data);
}
})
},
// minlength: 1 // 最低输入多少个字母就往服务器端发送请求
})
$('#search-text').on('keyup', e => {
console.log(e)
// 当用户点击回车的时候
if(e.keycode == 13) {
beginSearch()
}
})
// 当用户在输入框或者点击搜索按钮的时候,开始执行搜索
function beginSearch() {
let searchText = $('#search-text').val().trim() // 获取输入框的值
if(searchText) { // 有值才搜索
vm.$data.newsResults = []
$.ajax({
url: '/news/search?keyword=' + searchText,
type: 'get',
dataType: 'json'
}).then(res => {
// 给Vue中的 data() {} 中的 newsResults
for(let i = 0; i < res.length; i++) {
vm.$data.newsResults.push(res[i])
}
})
}
}
</script>
</body>
</html>