文章目录
- Elasticsearch实现电商词库提示搜索
- 前序
- 一. 自定义analyzer
- 二. 自定义mappings
- 三. 数据的测试
- 3.1 添加测试数据
- 3.2 测试
- 3.3 导入数据
- 四. Java代码的编写
- 五. 在Lexicon搜索的基础上实现商品搜索展示
Elasticsearch实现电商词库提示搜索
前序
# 自定义拼音分词器
GET _analyze
{
"text": ["豆腐", "美食", "程序员", "java程序员"],
"tokenizer": "keyword",
"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
}
]
}
一. 自定义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": 4}}
{"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
命令,开始导入数据。
四. Java代码的编写
创建实体类 Lexicon
@Document(indexName = "lexicon", type = "_doc")
public class Lexicon {
private Long id;
private String words;
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public String getWords() {
return words;
}
public void setWords(String words) {
this.words = words;
}
}
创建 LexiconController
package com.qf.controller;
import com.qf.pojo.Movie;
import org.elasticsearch.search.suggest.Suggest;
import org.elasticsearch.search.suggest.SuggestBuilder;
import org.elasticsearch.search.suggest.SuggestionBuilder;
import org.elasticsearch.search.suggest.completion.CompletionSuggestion;
import org.elasticsearch.search.suggest.completion.CompletionSuggestionBuilder;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.data.elasticsearch.core.ElasticsearchTemplate;
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 {
private ElasticsearchTemplate elasticsearchTemplate;
public LexiconController(@Qualifier("elasticsearchTemplate")
ElasticsearchTemplate elasticsearchTemplate) {
this.elasticsearchTemplate = elasticsearchTemplate;
}
@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);
//获取Suggest对象
Suggest suggest = elasticsearchTemplate
.getClient()
.prepareSearch("lexicon")//在哪个索引中搜索
.suggest(suggestBuilder)
.get()
.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
<!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() {
// console.log('================')
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 org.springframework.data.elasticsearch.annotations.Document;
import java.util.Date;
@Document(indexName = "goods", type = "_doc")
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;
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public Long getNo() {
return no;
}
public void setNo(Long no) {
this.no = no;
}
public String getCategory() {
return category;
}
public void setCategory(String category) {
this.category = category;
}
public String getTitle() {
return title;
}
public void setTitle(String title) {
this.title = title;
}
public String getPromowords() {
return promowords;
}
public void setPromowords(String promowords) {
this.promowords = promowords;
}
public String getSamllpic() {
return samllpic;
}
public void setSamllpic(String samllpic) {
this.samllpic = samllpic;
}
public Double getPrice() {
return price;
}
public void setPrice(Double price) {
this.price = price;
}
public String getCreatetime() {
return createtime;
}
public void setCreatetime(String createtime) {
this.createtime = createtime;
}
public Long getStocknum() {
return stocknum;
}
public void setStocknum(Long stocknum) {
this.stocknum = 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 org.elasticsearch.index.query.BoolQueryBuilder;
import org.elasticsearch.index.query.MatchAllQueryBuilder;
import org.elasticsearch.index.query.MultiMatchQueryBuilder;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.elasticsearch.core.ElasticsearchTemplate;
import org.springframework.data.elasticsearch.core.query.NativeSearchQuery;
import org.springframework.data.elasticsearch.core.query.NativeSearchQueryBuilder;
import org.springframework.data.elasticsearch.core.query.SearchQuery;
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.List;
@RestController
@RequestMapping("/goods")
public class GoodsSearchController {
private ElasticsearchTemplate elasticsearchTemplate;
public GoodsSearchController(@Qualifier("elasticsearchTemplate")
ElasticsearchTemplate elasticsearchTemplate) {
this.elasticsearchTemplate = elasticsearchTemplate;
}
@GetMapping
public PageInfo<Goods> indexShow(@RequestParam(defaultValue = "0") Integer currentPage) {
PageInfo<Goods> pageInfo = new PageInfo<>();
PageRequest pageRequest = PageRequest.of(currentPage, 10);
SearchQuery searchQuery = new NativeSearchQueryBuilder()
.withPageable(pageRequest)
.withQuery(new MatchAllQueryBuilder())
.build();
List<Goods> list = elasticsearchTemplate.queryForList(searchQuery, Goods.class);
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);
SearchQuery searchQuery = new NativeSearchQueryBuilder()
.withIndices("goods")
.withPageable(pageRequest)
.withQuery(new MultiMatchQueryBuilder(text, "title", "promowords", "category"))
.build();
List<Goods> list = elasticsearchTemplate.queryForList(searchQuery, Goods.class);
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>