RestClient操作ES
文章目录
一、初始化RestClient
1) 引入依赖
<dependency>
<groupId>org.elasticsearch.client</groupId>
<artifactId>elasticsearch-rest-high-level-client</artifactId>
</dependency>
2)覆盖es版本
因为soringboot默认 7.6.2 ES版本,因此需要覆盖默认版本
<properties>
<java.version>1.8</java.version>
<elasticsearch.version>7.12.1</elasticsearch.version>
</properties>
3)初始化RestClient
//初始化RestHighLevelClient并交由IOC容器管理,需要使用时直接注入
@Bean
public RestHighLevelClient restClient(){
return new RestHighLevelClient(RestClient.builder(HttpHost.create("http://192.168.43.31:9200")));
}
二、基于Java的索引库操作
注入RestHighLevelClient对象使用即可
基于java操作es,基本分为三步
创建XXXRequest对象 指定索引库
将DSL语句封装到XXXRequest对象
发送http请求,如果是查询还需要接收结果集封装
1)创建索引库
使用CreateIndexRequest对象
//创建索引库
@Test
void testCreateIndex() throws IOException {
//创建XXXRequest
CreateIndexRequest request = new CreateIndexRequest("hotel");
//向XXXRequest中封装DSL语句
request.source(MAPPING_TEMPLATE, XContentType.JSON);
//restHighLevelClient.indices()返回对象中包含索引库所有方法
//使用RestHighLevelClient发送http请求,执行es操作
restHighLevelClient.indices().create(request, RequestOptions.DEFAULT);
}
创建索引库的DSL语句
public static final String MAPPING_TEMPLATE = "{\n" +
" \"mappings\": {\n" +
" \"properties\": {\n" +
" \"id\": {\n" +
" \"type\": \"keyword\"\n" +
" },\n" +
" \"name\":{\n" +
" \"type\": \"text\",\n" +
" \"analyzer\": \"ik_max_word\",\n" +
" \"copy_to\": \"all\"\n" +
" },\n" +
" \"address\":{\n" +
" \"type\": \"keyword\",\n" +
" \"index\": false\n" +
" },\n" +
" \"price\":{\n" +
" \"type\": \"integer\"\n" +
" },\n" +
" \"score\":{\n" +
" \"type\": \"integer\"\n" +
" },\n" +
" \"brand\":{\n" +
" \"type\": \"keyword\",\n" +
" \"copy_to\": \"all\"\n" +
" },\n" +
" \"city\":{\n" +
" \"type\": \"keyword\",\n" +
" \"copy_to\": \"all\"\n" +
" },\n" +
" \"starName\":{\n" +
" \"type\": \"keyword\"\n" +
" },\n" +
" \"business\":{\n" +
" \"type\": \"keyword\"\n" +
" },\n" +
" \"location\":{\n" +
" \"type\": \"geo_point\"\n" +
" },\n" +
" \"pic\":{\n" +
" \"type\": \"keyword\",\n" +
" \"index\": false\n" +
" },\n" +
" \"all\":{\n" +
" \"type\": \"text\",\n" +
" \"analyzer\": \"ik_max_word\"\n" +
" }\n" +
" }\n" +
" }\n" +
"}";
2)删除索引库
//删除索引库
@Test
public void testDeleteIndex() throws IOException {
//封装XXXRequest对象 指定索引库
DeleteIndexRequest request = new DeleteIndexRequest("hotel");
//在XXXRequest封装DSL语句
//使用RestHighLevelClient发送http请求,执行es操作
restHighLevelClient.indices().delete(request, RequestOptions.DEFAULT);
}
3)判断索引库是否存在
@Test
public void testExistsHotelIndex() throws IOException {
//1.创建XXXRequest对象
GetIndexRequest request = new GetIndexRequest("hotel");
//封装dsl语句
//发送http请求,接收返回值
boolean exists = restHighLevelClient.indices().exists(request, RequestOptions.DEFAULT);
System.out.println(exists);
}
4)修改索引库
索引库和mapping一旦创建无法修改,但是可以添加新的字段
PUT /索引库名/_mapping
{
"properties": {
"新字段名":{
"type": "integer"
}
}
}
三、基于Java的文档操作
1)新增文档
注入RestHighLevelClient对象使用即可
基于java操作es,基本分为三步
创建XXXRequest对象 指定索引库
将DSL语句封装到XXXRequest对象
发送http请求,如果是查询还需要接收结果集封装
//指定id新增文档
@Test
public void testCreateDoc() throws IOException {
//封装XXXRequest对象 且指定要操作的索引名字和唯一id
IndexRequest request = new IndexRequest("hotel").id("1");
//往XXXRequest中封装DSL语句,添加的数据需要转json
HotelDoc doc = doc();
String json = JSON.toJSONString(doc);
request.source(json,XContentType.JSON);
//使用RestHighLevelClient发送http请求,执行ES操作
restHighLevelClient.index(request,RequestOptions.DEFAULT);
}
2)根据id查询文档
//查询指定文档
@Test
public void testGetDoc() throws IOException {
//封装XXXRequest对象 指定索引库 指定id
GetRequest request = new GetRequest("hotel").id("1");
//往XXXRequest中封装DSL语句
//执行RestHighLevelClient来发送http请求,执行es操作
GetResponse response = restHighLevelClient.get(request, RequestOptions.DEFAULT);
System.out.println(response.getSourceAsString());
}
3)查询指定条数文档
//查询文档 默认只查询十条
@Test
public void testGetAllDoc() throws IOException {
//封装XXXRequest对象 指定索引库
SearchRequest request = new SearchRequest("hotel");
//往XXXRequest中封装DSL语句
request.source().size(100);//默认只查询10条,可以设置size
//执行RestHighLevelClient来发送http请求,执行es操作
SearchResponse search = restHighLevelClient.search(request, RequestOptions.DEFAULT);
for (SearchHit hit : search.getHits().getHits()) {
System.out.println(hit.getSourceAsString());
}
}
4)修改文档
方式一:全量修改,会删除旧文档,添加新文档
方式二:增量修改,修改指定字段值
//修改指定文档 增量修改
@Test
public void testUpdateDoc() throws IOException {
//封装XXXRequest对象 指定索引库 & id
UpdateRequest request = new UpdateRequest("hotel","1");
//XXXRequest对象封装DSL语句
request.doc("name","lmz");
//发送http请求,执行es操作
restHighLevelClient.update(request,RequestOptions.DEFAULT);
}
5)删除文档
//删除指定文档
@Test
public void testDeleteDoc() throws IOException {
//封装XXXRequest对象 索引库 & id
DeleteRequest request = new DeleteRequest("hotel").id("1");
//XXXRequest对象封装DSL语句
//发送http请求,执行es操作
restHighLevelClient.delete(request,RequestOptions.DEFAULT);
}
6)批量导入文档
从数据库中批量导入数据到ES中
@Test
public void testBatchDoc2() throws IOException {
//查询数据库中所有酒店信息
List<Hotel> list = hotelService.list();
//创建BulkRequest对象,该对象相当于一个Request对象的集合,可以存储后执行批量导入
BulkRequest bulkRequest = new BulkRequest();
//将酒店信息封装成hoteldoc对象
for (Hotel hotel : list) {
//hotel -> hotelDoc
HotelDoc doc = new HotelDoc(hotel);
String json = JSON.toJSONString(doc);
IndexRequest request = new IndexRequest("hotel").id(doc.getId().toString());
request.source(json, XContentType.JSON);
bulkRequest.add(request);
}
restHighLevelClient.bulk(bulkRequest,RequestOptions.DEFAULT);
}
四、DSL查询文档
1)常见查询类型
Elasticsearch提供了基于JSON的DSL(Domain Specific Language)来定义查询。常见的查询类型包括:
- 查询所有:查询出所有数据,一般测试用。例如:match_all
- 全文检索(full text)查询:利用分词器对用户输入内容分词,然后去倒排索引库中匹配。例如:
- match_query
- multi_match_query
- 精确查询:根据精确词条值查找数据,一般是查找keyword、数值、日期、boolean等类型字段。例如:
- ids
- range
- term
- 地理(geo)查询:根据经纬度查询。例如:
- geo_distance
- geo_bounding_box
- 复合(compound)查询:复合查询可以将上述各种查询条件组合起来,合并查询条件。例如:
- bool
- function_score
查询的语法基本一致:
GET /indexName/_search
{
"query": {
"查询类型": {
"查询条件": "条件值"
}
}
}
我们以查询所有为例,其中:
- 查询类型为match_all
- 没有查询条件
// 查询所有
GET /indexName/_search
{
"query": {
"match_all": {
}
}
}
/**
* 查询所有 match all 默认查询 10条数据
*/
@Test
public void testMatchAll() throws IOException {
//1.封装XXXRequest对象 指定查询索引库
SearchRequest request = new SearchRequest("hotel");
//2.XXXRequest封装DSL语句
//QueryBuilders构建各种包装对象
request.source().query(
QueryBuilders.matchAllQuery()
);
//3.发送http请求,执行es操作,获取查询结果
SearchResponse response = client.search(request, RequestOptions.DEFAULT);
//解析结果集对象
List<HotelDoc> hotelDocs = parseResult(response);
//打印
hotelDocs.forEach(System.out::println);
}
其它查询无非就是查询类型、查询条件的变化。
2)全文检索查询 & 词条检索 & 范围检索
match查询(单字段查询) 和 multi_match(多字段查询)
/**
* 单字段全文搜索match 多字段全文搜索 multi_match 词条搜索 term 范围搜索 range
* @throws IOException
*/
@Test
public void testMatch() throws IOException {
//1.封装Request对象
SearchRequest request = new SearchRequest("hotel");
//2.Request对象封装DSL语句
//request.source().query(QueryBuilders.matchQuery("name","君"));//全文搜索
// request.source().query(
// QueryBuilders.multiMatchQuery("君悦","name","brand")
// );//多字段搜索
// request.source().query(QueryBuilders.termQuery("brand","君悦"));//词条搜索
request.source().query(QueryBuilders.rangeQuery("price").gte("500").lte("600"));//范围查询 价格在 500-600
//3.发送http请求,执行es操作结果,获取查询结果
SearchResponse response = client.search(request, RequestOptions.DEFAULT);
//结果集解析打印
parseResult(response).forEach(System.out::println);
}
3) 经纬度检索
分为圆形检索 和 长方形检索
4) 复合查询
复合查询可以将其它简单查询组合起来,实现更复杂的搜索逻辑
例如:fuction score:算分函数查询,可以控制文档相关性算分,控制文档排名。例如百度竞价
布尔查询是一个或多个查询子句的组合。子查询的组合方式有:
must:必须匹配每个子查询,类似“与”
should:选择性匹配子查询,类似“或”
must_not:必须不匹配,不参与算分,类似“非”
filter:必须匹配,不参与算分
/**
* 复合查询 boolean query
*/
@Test
public void testBooleanQuery() throws IOException {
//封装Request对象
SearchRequest request = new SearchRequest("hotel");
//封装boolean对象
BoolQueryBuilder boolQuery = QueryBuilders.boolQuery();
//查询品牌 必须是 万怡的 价格在 500-600之间
boolQuery.must(QueryBuilders.termQuery("brand","万怡"));
boolQuery.filter(QueryBuilders.rangeQuery("price").gte(500).lte(600));
//Request对象封装DSL语句 将boolean对象传入即可
request.source().query(boolQuery);
//发送http请求,执行es操作,获取查询结果
SearchResponse response = client.search(request, RequestOptions.DEFAULT);
//解析查询结果
parseResult(response).forEach(System.out::println);
}
5) 分页和排序
5.1) 根据坐标距离排序
//距离排序
if (ObjectUtils.isNotEmpty(params.getLocation())){
request.source().sort(
//指定es中的经纬度字段 location,自己经纬度地址
SortBuilders.geoDistanceSort( "location",new GeoPoint(params.getLocation()))
.order(SortOrder.ASC)//排序方式 升序
.unit(DistanceUnit.KILOMETERS)//距离单位 km
);
}
5.2) 分页
分页方式有三种:
- from + size
- 支持随机翻页,深度分页越深性能越差
- after search
- 没有查询上限(单次查询的size不超过10000),但不支持随机翻页
- scroll
- 没有查询上限(单次查询的size不超过10000),不建议使用
/**
* 分页和排序
*/
@Test
public void testPageSort() throws IOException {
//封装Request对象
SearchRequest request = new SearchRequest("hotel");
//Request对象封装DSL语句
request.source().query(QueryBuilders.termQuery("brand","君悦"));
//指定分页数 & 排序规则
request.source().from(0).size(2).sort("price", SortOrder.ASC);
//发送http请求,执行es操作,接收返回值
SearchResponse response = client.search(request, RequestOptions.DEFAULT);
//返回值解析
parseResult(response).forEach(System.out::println);
}
6) 相关性算分
//相关性加分
FunctionScoreQueryBuilder functionScoreQueryBuilder = QueryBuilders.functionScoreQuery(
boolQuery,//原始查询条件
new FunctionScoreQueryBuilder.FilterFunctionBuilder[]{
new FunctionScoreQueryBuilder.FilterFunctionBuilder(
QueryBuilders.termQuery("name","喜来登"),//筛选条件
ScoreFunctionBuilders.weightFactorFunction(10)//权重加分
)
}
).boostMode(CombineFunction.SUM);//运算方式
7) 高亮
/**
* 高亮 highLight
*/
@Test
public void testHighLight() throws IOException {
//封装Request对象
SearchRequest request = new SearchRequest("hotel");
//Request对象封装DSL语句
request.source().query(QueryBuilders.matchQuery("name","如家"));
//开启高亮字段查询 指定name字段的词条高亮 且 包裹标签为 <ee>高亮</ee>
request.source().highlighter(
new HighlightBuilder().field("name").preTags("<ee>").postTags("</ee>")
);
//发送http请求,执行es,接收返回值
SearchResponse response = client.search(request, RequestOptions.DEFAULT);
//解析结果
parseResultAndHighLight(response).forEach(System.out::println);
}
7.1)高亮结果集封装
//结果集解析 + 高亮解析
public List<HotelDoc> parseResultAndHighLight(SearchResponse response){
//获取hits属性的对象 可以获取查询到的总记录数
SearchHits hits = response.getHits();
System.out.println(hits.getTotalHits().value);//总条数
ArrayList<HotelDoc> docs = new ArrayList<>();
//获取hits.hits属性并遍历 可以获取高亮数据和未高亮的结果数据
for (SearchHit hit : hits.getHits()) {
//获取hits.hits.source 未高亮的数据
String json = hit.getSourceAsString();
//对象 -》 HotelDoc
HotelDoc doc = JSON.parseObject(json, HotelDoc.class);
//获取hits.hits.highlight 高亮的数据
Map<String, HighlightField> highlightFields = hit.getHighlightFields();
//判断是否存在高亮数据,存在就获取并且设置到结果对象中
if (highlightFields != null && highlightFields.get("name") != null){
// //获取name键的值,值为一个 HighlightField对象 对象中fragments属性是结果,是一个数组
// HighlightField highlightField = highlightFields.get("name");
// //获取高亮数组
// Text[] fragments = highlightField.getFragments();
// //拼接高亮数组
// StringBuffer buffer = new StringBuffer();
// for (Text fragment : fragments) {
// buffer.append(fragment);
// }
//
// //高亮数组设置进HotelDoc对象
// doc.setName(buffer.toString());
//拼接高亮数组的结果
String join = StringUtils.join(highlightFields.get("name").getFragments(), "...");
doc.setName(join);
}
docs.add(doc);
}
return docs;
}
五、数据聚合
**聚合(aggregations)**可以让我们极其方便的实现对数据的统计、分析、运算。例如:
- 什么品牌的手机最受欢迎?
- 这些手机的平均价格、最高价格、最低价格?
- 这些手机每月的销售情况如何?
实现这些统计功能的比数据库的sql要方便的多,而且查询速度非常快,可以实现近实时搜索效果。
5.1.聚合的种类
聚合常见的有三类:
- **桶(Bucket)**聚合:用来对文档做分组
- TermAggregation:按照文档字段值分组,例如按照品牌值分组、按照国家分组
- Date Histogram:按照日期阶梯分组,例如一周为一组,或者一月为一组
- **度量(Metric)**聚合:用以计算一些值,比如:最大值、最小值、平均值等
- Avg:求平均值
- Max:求最大值
- Min:求最小值
- Stats:同时求max、min、avg、sum等
- **管道(pipeline)**聚合:其它聚合的结果为基础做聚合
**注意:**参加聚合的字段必须是keyword、日期、数值、布尔类型
5.2.DSL实现聚合
现在,我们要统计所有数据中的酒店品牌有几种,其实就是按照品牌对数据分组。此时可以根据酒店品牌的名称做聚合,也就是Bucket聚合。
5.2.1.Bucket聚合语法
语法如下:
GET /hotel/_search
{
"size": 0, // 设置size为0,结果中不包含文档,只包含聚合结果
"aggs": { // 定义聚合
"brandAgg": { //给聚合起个名字
"terms": { // 聚合的类型,按照品牌值聚合,所以选择term
"field": "brand", // 参与聚合的字段
"size": 20 // 希望获取的聚合结果数量
}
}
}
}
结果如图:
5.2.2.聚合结果排序
默认情况下,Bucket聚合会统计Bucket内的文档数量,记为_count,并且按照_count降序排序。
我们可以指定order属性,自定义聚合的排序方式:
GET /hotel/_search
{
"size": 0,
"aggs": {
"brandAgg": {
"terms": {
"field": "brand",
"order": {
"_count": "asc" // 按照_count升序排列
},
"size": 20
}
}
}
}
5.2.3.限定聚合范围
默认情况下,Bucket聚合是对索引库的所有文档做聚合,但真实场景下,用户会输入搜索条件,因此聚合必须是对搜索结果聚合。那么聚合必须添加限定条件。
我们可以限定要聚合的文档范围,只要添加query条件即可:
GET /hotel/_search
{
"query": {
"range": {
"price": {
"lte": 200 // 只对200元以下的文档聚合
}
}
},
"size": 0,
"aggs": {
"brandAgg": {
"terms": {
"field": "brand",
"size": 20
}
}
}
}
这次,聚合得到的品牌明显变少了:
5.2.4.Metric聚合语法
上节课,我们对酒店按照品牌分组,形成了一个个桶。现在我们需要对桶内的酒店做运算,获取每个品牌的用户评分的min、max、avg等值。
这就要用到Metric聚合了,例如stat聚合:就可以获取min、max、avg等结果。
语法如下:
GET /hotel/_search
{
"size": 0,
"aggs": {
"brandAgg": {
"terms": {
"field": "brand",
"size": 20
},
"aggs": { // 是brands聚合的子聚合,也就是分组后对每组分别计算
"score_stats": { // 聚合名称
"stats": { // 聚合类型,这里stats可以计算min、max、avg等
"field": "score" // 聚合字段,这里是score
}
}
}
}
}
}
这次的score_stats聚合是在brandAgg的聚合内部嵌套的子聚合。因为我们需要在每个桶分别计算。
另外,我们还可以给聚合结果做个排序,例如按照每个桶的酒店平均分做排序:
5.3.RestAPI实现聚合
//Bucket聚合测试 类似与分组
@Test
public void bucketTest() throws IOException {
//创建XXXRequest对象
SearchRequest request = new SearchRequest("hotel");//指定索引库
//XXXRequest对象封装dsl语句
request.source().size(0);//不获取查询到的结果
TermsAggregationBuilder size = AggregationBuilders.terms("brandAgg")//指定聚合查询后结果的字段名
.field("brand")//聚合查询的列
.size(6)//显示条数
.order(BucketOrder.count(false));//结果按照count来进行排序 true升序 false 降序
request.source().aggregation(size);//封装聚合查询
//发送http请求,接收查询结果
SearchResponse response = client.search(request, RequestOptions.DEFAULT);
//聚合查询结果集解析
ParsedStringTerms brandAgg = response.getAggregations().get("brandAgg");//这里为聚合查询的结果字段名
for (Terms.Bucket bucket : brandAgg.getBuckets()) {
System.out.println("key-->" + bucket.getKeyAsString());
System.out.println("count-->" + bucket.getDocCount());
}
}
六、自动补全
当用户在搜索框输入字符时,我们应该提示出与该字符有关的搜索项,如图:
这种根据用户输入的字母,提示完整词条的功能,就是自动补全了。
因为需要根据拼音字母来推断,因此要用到拼音分词功能。
1) 拼音分词器
要实现根据字母做补全,就必须对文档按照拼音分词。在GitHub上恰好有elasticsearch的拼音分词插件。地址:https://github.com/medcl/elasticsearch-analysis-pinyin
课前资料中也提供了拼音分词器的安装包:
安装方式与IK分词器一样,分三步:
①解压
②上传到虚拟机中,elasticsearch的plugin目录
③重启elasticsearch
④测试
详细安装步骤可以参考IK分词器的安装过程。
测试用法如下:
POST /_analyze
{
"text": "如家酒店还不错",
"analyzer": "pinyin"
}
结果:
2) 自定义分词器
默认的拼音分词器会将每个汉字单独分为拼音,而我们希望的是每个词条形成一组拼音,需要对拼音分词器做个性化定制,形成自定义分词器。
elasticsearch中分词器(analyzer)的组成包含三部分:
- character filters:在tokenizer之前对文本进行处理。例如删除字符、替换字符
- tokenizer:将文本按照一定的规则切割成词条(term)。例如keyword,就是不分词;还有ik_smart
- tokenizer filter:将tokenizer输出的词条做进一步处理。例如大小写转换、同义词处理、拼音处理等
文档分词时会依次由这三部分来处理文档:
声明自定义分词器的语法如下:
PUT /test
{
"settings": {
"analysis": {
"analyzer": { // 自定义分词器
"my_analyzer": { // 分词器名称
"tokenizer": "ik_max_word",
"filter": "py"
}
},
"filter": { // 自定义tokenizer filter
"py": { // 过滤器名称
"type": "pinyin", // 过滤器类型,这里是pinyin
"keep_full_pinyin": false,
"keep_joined_full_pinyin": true,
"keep_original": true,
"limit_first_letter_length": 16,
"remove_duplicated_term": true,
"none_chinese_pinyin_tokenize": false
}
}
}
},
"mappings": {
"properties": {
"name": {
"type": "text",
"analyzer": "my_analyzer",
"search_analyzer": "ik_smart"
}
}
}
}
测试:
完整代码;
PUT /test
{
"settings": {
"analysis": {
"analyzer": {
"my_analyzer": {
"tokenizer": "ik_max_word",
"filter": "py"
}
},
"filter": {
"py": {
"type": "pinyin",
"keep_full_pinyin": false,
"keep_joined_full_pinyin": true,
"keep_original": true,
"limit_first_letter_length": 16,
"remove_duplicated_term": true,
"none_chinese_pinyin_tokenize": false
}
}
}
},
"mappings": {
"properties": {
"name": {
"type": "text",
"analyzer": "my_analyzer",
"search_analyzer": "ik_smart"
}
}
}
}
总结:
如何使用拼音分词器?
- ①下载pinyin分词器
- ②解压并放到elasticsearch的plugin目录
- ③重启即可
如何自定义分词器?
- ①创建索引库时,在settings中配置,可以包含三部分
- ②character filter
- ③tokenizer
- ④filter
拼音分词器注意事项?
- 为了避免搜索到同音字,搜索时不要使用拼音分词器
3) 自动补全查询
elasticsearch提供了Completion Suggester查询来实现自动补全功能。这个查询会匹配以用户输入内容开头的词条并返回。为了提高补全查询的效率,对于文档中字段的类型有一些约束:
- 参与补全查询的字段必须是completion类型。
- 字段的内容一般是用来补全的多个词条形成的数组。
比如,一个这样的索引库:
// 创建索引库
PUT /test
{
"mappings": {
"properties": {
"title": {
"type": "completion"
}
}
}
}
然后插入下面的数据:
// 示例数据
POST test/_doc
{
"title": ["Sony", "WH-1000XM3"]
}
POST test/_doc
{
"title": ["SK-II", "PITERA"]
}
POST test/_doc
{
"title": ["Nintendo", "switch"]
}
查询的DSL语句如下:
// 自动补全查询
GET /test/_search
{
"suggest": {
"title_suggest": {
"text": "s", // 关键字
"completion": {
"field": "title", // 补全查询的字段
"skip_duplicates": true, // 跳过重复的
"size": 10 // 获取前10条结果
}
}
}
}
4) 实现酒店搜索框自动补全
现在,我们的hotel索引库还没有设置拼音分词器,需要修改索引库中的配置。但是我们知道索引库是无法修改的,只能删除然后重新创建。
另外,我们需要添加一个字段,用来做自动补全,将brand、suggestion、city等都放进去,作为自动补全的提示。
因此,总结一下,我们需要做的事情包括:
- 修改hotel索引库结构,设置自定义拼音分词器
- 修改索引库的name、all字段,使用自定义分词器
- 索引库添加一个新字段suggestion,类型为completion类型,使用自定义的分词器
- 给HotelDoc类添加suggestion字段,内容包含brand、business
- 重新导入数据到hotel库
4.1) 修改酒店映射结构
代码如下:
// 酒店数据索引库
PUT /hotel
{
"settings": {
"analysis": {
"analyzer": {
"text_anlyzer": {
"tokenizer": "ik_max_word",
"filter": "py"
},
"completion_analyzer": {
"tokenizer": "keyword",
"filter": "py"
}
},
"filter": {
"py": {
"type": "pinyin",
"keep_full_pinyin": false,
"keep_joined_full_pinyin": true,
"keep_original": true,
"limit_first_letter_length": 16,
"remove_duplicated_term": true,
"none_chinese_pinyin_tokenize": false
}
}
}
},
"mappings": {
"properties": {
"id":{
"type": "keyword"
},
"name":{
"type": "text",
"analyzer": "text_anlyzer",
"search_analyzer": "ik_smart",
"copy_to": "all"
},
"address":{
"type": "keyword",
"index": false
},
"price":{
"type": "integer"
},
"score":{
"type": "integer"
},
"brand":{
"type": "keyword",
"copy_to": "all"
},
"city":{
"type": "keyword"
},
"starName":{
"type": "keyword"
},
"business":{
"type": "keyword",
"copy_to": "all"
},
"location":{
"type": "geo_point"
},
"pic":{
"type": "keyword",
"index": false
},
"all":{
"type": "text",
"analyzer": "text_anlyzer",
"search_analyzer": "ik_smart"
},
"suggestion":{
"type": "completion",
"analyzer": "completion_analyzer"
}
}
}
}
4.2) 修改HotelDoc实体
HotelDoc中要添加一个字段,用来做自动补全,内容可以是酒店品牌、城市、商圈等信息。按照自动补全字段的要求,最好是这些字段的数组。
因此我们在HotelDoc中添加一个suggestion字段,类型为List<String>
,然后将brand、city、business等信息放到里面。
代码如下:
package cn.itcast.hotel.pojo;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
@Data
@NoArgsConstructor
public class HotelDoc {
private Long id;
private String name;
private String address;
private Integer price;
private Integer score;
private String brand;
private String city;
private String starName;
private String business;
private String location;
private String pic;
private Object distance;
private Boolean isAD;
private List<String> suggestion;
public HotelDoc(Hotel hotel) {
this.id = hotel.getId();
this.name = hotel.getName();
this.address = hotel.getAddress();
this.price = hotel.getPrice();
this.score = hotel.getScore();
this.brand = hotel.getBrand();
this.city = hotel.getCity();
this.starName = hotel.getStarName();
this.business = hotel.getBusiness();
this.location = hotel.getLatitude() + ", " + hotel.getLongitude();
this.pic = hotel.getPic();
// 组装suggestion
if(this.business.contains("/")){
String[] split = this.business.split("/");
this.suggestion=new ArrayList();
this.suggestion.add(this.brand);
Collections.addAll(this.suggestion,split);
}
if(this.business.contains("、")){
String[] split = this.business.split("、");
this.suggestion=new ArrayList();
this.suggestion.add(this.brand);
Collections.addAll(this.suggestion,split);
}
else{
this.suggestion = Arrays.asList(this.brand, this.business);
}
}
}
4.3) 重新导入
重新执行之前编写的导入数据功能,可以看到新的酒店数据中包含了suggestion:
4.4) 自动补全查询的JavaAPI
//自动补全测试
@Test
public void suggestTest() throws IOException {
//SearchRequest对象
SearchRequest request = new SearchRequest("hotel");
//自动补全查询DSL语句封装
request.source().suggest(new SuggestBuilder().addSuggestion(
"suggestion",//查询后的列名称
SuggestBuilders.completionSuggestion("suggestion") //查询字段
.prefix("sd") //查询的key
.skipDuplicates(true) //跳过重复
.size(10)//获取前10条
));
//发送请求,结果集解析
SearchResponse response = client.search(request, RequestOptions.DEFAULT);
CompletionSuggestion titleSuggest = response.getSuggest().getSuggestion("suggestion");
titleSuggest.getOptions().stream().forEach(text -> System.out.println(text.getText()));
}
七、数据同步
elasticsearch中的酒店数据来自于mysql数据库,因此mysql数据发生改变时,elasticsearch也必须跟着改变,这个就是elasticsearch与mysql之间的数据同步。
1) 思路分析
常见的数据同步方案有三种:
- 同步调用
- 异步通知
- 监听binlog
1.1) 同步调用
方案一:同步调用
基本步骤如下:
- hotel-demo对外提供接口,用来修改elasticsearch中的数据
- 酒店管理服务在完成数据库操作后,直接调用hotel-demo提供的接口,
1.2) 异步通知
方案二:异步通知
流程如下:
- hotel-admin对mysql数据库数据完成增、删、改后,发送MQ消息
- hotel-demo监听MQ,接收到消息后完成elasticsearch数据修改
1.3) 监听binlog
方案三:监听binlog
流程如下:
- 给mysql开启binlog功能
- mysql完成增、删、改操作都会记录在binlog中
- hotel-demo基于canal监听binlog变化,实时更新elasticsearch中的内容
1.4) 选择
方式一:同步调用
- 优点:实现简单,粗暴
- 缺点:业务耦合度高
方式二:异步通知
- 优点:低耦合,实现难度一般
- 缺点:依赖mq的可靠性
方式三:监听binlog
- 优点:完全解除服务间耦合
- 缺点:开启binlog增加数据库负担、实现复杂度高
2) 代码实现(这里实现第二种)
2.1) 后台增删改
@PostMapping
public void saveHotel(@RequestBody Hotel hotel){
hotel.setId((long) (Math.random() * 10000));
hotelService.save(hotel);
//发送消息到mq指定队列中
rabbitTemplate.convertAndSend(MQConstants.HOTEL_EXCHANGE,MQConstants.HOTEL_ADD_KEY, JSON.toJSONString(hotel));
}
@PutMapping()
public void updateById(@RequestBody Hotel hotel){
if (hotel.getId() == null) {
throw new InvalidParameterException("id不能为空");
}
hotelService.updateById(hotel);
//发送消息到mq指定队列中
rabbitTemplate.convertAndSend(MQConstants.HOTEL_EXCHANGE,MQConstants.HOTEL_UPDATE_KEY, JSON.toJSONString(hotel));
}
@DeleteMapping("/{id}")
public void deleteById(@PathVariable("id") Long id) {
hotelService.removeById(id);
//发送消息到mq指定队列中
rabbitTemplate.convertAndSend(MQConstants.HOTEL_EXCHANGE,MQConstants.HOTEL_DELETE_KEY, JSON.toJSONString(id));
}
2.2) es同步
2.2.1) 监听器
@Component
public class HotelMqListen {
@Autowired
private IHotelService service;
//监听添加酒店
@RabbitListener(bindings = @QueueBinding(//绑定队列 和 交换机
value = @Queue(MQConstants.HOTEL_ADD_QUERY),//定义添加时队列
exchange = @Exchange(value = MQConstants.HOTEL_EXCHANGE,type = ExchangeTypes.DIRECT),//定义添加时交换机
key = {MQConstants.HOTEL_ADD_KEY}//定义添加路由标识
))
public void hotelAdd(String msg) throws IOException {
//解析消息为 HotelDoc对象
Hotel hotel = JSON.parseObject(msg, Hotel.class);
HotelDoc doc = new HotelDoc(hotel);
//添加
service.add(doc);
}
//监听修改酒店
@RabbitListener(bindings = @QueueBinding(//绑定队列 和 交换机
value = @Queue(MQConstants.HOTEL_UPDATE_QUERY),
exchange = @Exchange(MQConstants.HOTEL_EXCHANGE),
key = {MQConstants.HOTEL_UPDATE_KEY}
))
public void hoteUpdate(String msg) throws IOException {
//解析消息为 HotelDoc对象
Hotel hotel = JSON.parseObject(msg, Hotel.class);
HotelDoc doc = new HotelDoc(hotel);
//添加
service.updateHotel(doc);
}
//监听删除酒店
@RabbitListener(bindings = @QueueBinding(//绑定队列 和 交换机
value = @Queue(MQConstants.HOTEL_DELETE_QUERY),
exchange = @Exchange(value = MQConstants.HOTEL_EXCHANGE),
key = {MQConstants.HOTEL_DELETE_KEY}
))
public void hotelDelete(String id) throws IOException {
service.deleteHotel(id);
}
}
2.2.2) 服务端
//同步数据库修改操作
@Override
public void updateHotel(HotelDoc doc) throws IOException {
//封装XXXRequest对象 指定索引库 & id
UpdateRequest request = new UpdateRequest("hotel",doc.getId().toString());
//XXXRequest对象封装DSL语句
request.doc(JSON.toJSONString(doc),XContentType.JSON);
//发送http请求,执行es操作
client.update(request,RequestOptions.DEFAULT);
}
//同步数据库删除操作
@Override
public void deleteHotel(String id) throws IOException {
DeleteRequest request = new DeleteRequest("hotel").id(id);
client.delete(request,RequestOptions.DEFAULT);
}
//同步数据库添加操作
@Override
public void add(HotelDoc doc) throws IOException {
//IndexRequest
IndexRequest request = new IndexRequest("hotel").id(doc.getId().toString());
//封装dsl语句
request.source(JSON.toJSONString(doc), XContentType.JSON);
//发送请求
client.index(request,RequestOptions.DEFAULT);
}
2.3) 常量类
//MQ常量类
public class MQConstants {
public final static String HOTEL_EXCHANGE = "hotel.router";
public final static String HOTEL_ADD_QUERY = "hotel.add.query";
public final static String HOTEL_UPDATE_QUERY = "hotel.update.query";
public final static String HOTEL_DELETE_QUERY = "hotel.delete.query";
public final static String HOTEL_ADD_KEY = "hotel.add";
public final static String HOTEL_UPDATE_KEY = "hotel.update";
public final static String HOTEL_DELETE_KEY = "hotel.delete";
}
八、黑马旅游案例
案例
- 实现黑马旅游的酒店搜索功能,完成关键字搜索和分页
- 添加品牌、城市、星级、价格等过滤功能
- 显示我附近的酒店并显示距离
- 让指定的酒店在搜索结果中排名置顶
- 给黑马旅游添加排序功能
- 给黑马旅游添加搜索关键字高亮效果
- 实现酒店搜索页面输入框的自动补全
- 利用MQ实现mysql与elasticsearch数据同步
7.1业务层代码
package cn.itcast.hotel.service.impl;
import cn.itcast.hotel.mapper.HotelMapper;
import cn.itcast.hotel.pojo.Hotel;
import cn.itcast.hotel.pojo.HotelDoc;
import cn.itcast.hotel.pojo.HotelListDto;
import cn.itcast.hotel.pojo.PageResult;
import cn.itcast.hotel.service.IHotelService;
import com.alibaba.fastjson.JSON;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import org.apache.commons.lang3.ObjectUtils;
import org.apache.commons.lang3.StringUtils;
import org.elasticsearch.action.delete.DeleteRequest;
import org.elasticsearch.action.index.IndexRequest;
import org.elasticsearch.action.search.SearchRequest;
import org.elasticsearch.action.search.SearchResponse;
import org.elasticsearch.action.update.UpdateRequest;
import org.elasticsearch.client.RequestOptions;
import org.elasticsearch.client.RestHighLevelClient;
import org.elasticsearch.common.geo.GeoPoint;
import org.elasticsearch.common.lucene.search.function.CombineFunction;
import org.elasticsearch.common.unit.DistanceUnit;
import org.elasticsearch.common.xcontent.XContentType;
import org.elasticsearch.index.query.BoolQueryBuilder;
import org.elasticsearch.index.query.QueryBuilders;
import org.elasticsearch.index.query.functionscore.FunctionScoreQueryBuilder;
import org.elasticsearch.index.query.functionscore.ScoreFunctionBuilders;
import org.elasticsearch.search.SearchHit;
import org.elasticsearch.search.SearchHits;
import org.elasticsearch.search.aggregations.AggregationBuilders;
import org.elasticsearch.search.aggregations.BucketOrder;
import org.elasticsearch.search.aggregations.bucket.terms.ParsedStringTerms;
import org.elasticsearch.search.builder.SearchSourceBuilder;
import org.elasticsearch.search.fetch.subphase.highlight.HighlightBuilder;
import org.elasticsearch.search.sort.SortBuilders;
import org.elasticsearch.search.sort.SortOrder;
import org.elasticsearch.search.suggest.SuggestBuilder;
import org.elasticsearch.search.suggest.SuggestBuilders;
import org.elasticsearch.search.suggest.completion.CompletionSuggestion;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.io.IOException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
@Service
public class HotelService extends ServiceImpl<HotelMapper, Hotel> implements IHotelService {
@Autowired
private RestHighLevelClient client;
//同步数据库修改操作
@Override
public void updateHotel(HotelDoc doc) throws IOException {
//封装XXXRequest对象 指定索引库 & id
UpdateRequest request = new UpdateRequest("hotel",doc.getId().toString());
//XXXRequest对象封装DSL语句
request.doc(JSON.toJSONString(doc),XContentType.JSON);
//发送http请求,执行es操作
client.update(request,RequestOptions.DEFAULT);
}
//同步数据库删除操作
@Override
public void deleteHotel(String id) throws IOException {
DeleteRequest request = new DeleteRequest("hotel").id(id);
client.delete(request,RequestOptions.DEFAULT);
}
//同步数据库添加操作
@Override
public void add(HotelDoc doc) throws IOException {
//IndexRequest
IndexRequest request = new IndexRequest("hotel").id(doc.getId().toString());
//封装dsl语句
request.source(JSON.toJSONString(doc), XContentType.JSON);
//发送请求
client.index(request,RequestOptions.DEFAULT);
}
@Override
public Map<String, List<String>> filters(HotelListDto hotelListDto) throws IOException {
//创建XXXRequest对象
SearchRequest request = new SearchRequest("hotel");
//封装DSL语句
//1.基本查询
basicBuild(hotelListDto,request);
//2.聚合查询
//2.0 不返回查询的结果,只要聚合结果
request.source().size(0);
//2.1 城市查询
SearchSourceBuilder source = request.source();
aggBuild("cityAgg","city",source);
//2.2 品牌查询
aggBuild("brandAgg","brand",source);
//2.3 星级查询
aggBuild("starNameAgg","starName",source);
//发送http请求查询
SearchResponse response = client.search(request, RequestOptions.DEFAULT);
//解析结果集然后返回
Map<String,List<String>> map = new HashMap<>();
map.put("city",parseFilterResult(response,"cityAgg"));
map.put("brand",parseFilterResult(response,"brandAgg"));
map.put("starName",parseFilterResult(response,"starNameAgg"));
return map;
}
@Override
public List<String> suggestion(String key) throws IOException {
//SearchRequest
SearchRequest request = new SearchRequest("hotel");
//自动补全语句封装
request.source().suggest(new SuggestBuilder().addSuggestion(
"titleSuggestion",
SuggestBuilders.completionSuggestion("suggestion")
.prefix(key)
.skipDuplicates(true)
.size(10)
));
//发送http请求
SearchResponse response = client.search(request, RequestOptions.DEFAULT);
//结果集解析
CompletionSuggestion suggestion = response.getSuggest().getSuggestion("titleSuggestion");
List<String> list = suggestion.getOptions().stream()
.map(text -> text.getText().toString())
.collect(Collectors.toList());
return list;
}
//聚合查询构造
private void aggBuild(String aggName,String field,SearchSourceBuilder source){
source.aggregation(AggregationBuilders
.terms(aggName)
.field(field)
.size(6)
.order(BucketOrder.count(false)));
}
//解析filter结果集
private List<String> parseFilterResult(SearchResponse response, String cityAgg){
ParsedStringTerms agg = response.getAggregations().get(cityAgg);
return agg.getBuckets().stream()
.map(k -> k.getKeyAsString())
.collect(Collectors.toList());
}
@Override
public PageResult hotelList(HotelListDto hotelListDto) throws IOException {
//封装Request对象 指定索引库
SearchRequest request = new SearchRequest("hotel");
//Request对象封装DSL语句 match
basicBuild(hotelListDto,request);
//设置分页
//起始页 (page - 1)*size
int page = (hotelListDto.getPage() - 1) * hotelListDto.getSize();
request.source().from(page).size(hotelListDto.getSize());
//排序
//发送http请求 执行es操作,获取结果
SearchResponse response = client.search(request, RequestOptions.DEFAULT);
//解析结果返回
return parseResult(response, hotelListDto.getSize(),hotelListDto);
}
//搜索条件构造
private void basicBuild(HotelListDto params,SearchRequest request){
//复合查询
BoolQueryBuilder boolQuery = QueryBuilders.boolQuery();
//判断用户是否输入
if (ObjectUtils.isEmpty(params.getKey())){
//没有输入就查询所有
// request.source().query(QueryBuilders.matchAllQuery());
boolQuery.must(QueryBuilders.matchAllQuery());
}else{
//输入则根据key查询
// request.source().query(QueryBuilders.matchQuery("name",hotelListDto.getKey()));
boolQuery.must(QueryBuilders.matchQuery("name",params.getKey()));
}
//品牌过滤
if (ObjectUtils.isNotEmpty(params.getBrand())){
boolQuery.filter(QueryBuilders.termQuery("brand",params.getBrand()));
}
//城市过滤
if (ObjectUtils.isNotEmpty(params.getCity())){
boolQuery.filter(QueryBuilders.termQuery("city",params.getCity()));
}
//星级过滤
if (ObjectUtils.isNotEmpty(params.getStarName())){
boolQuery.filter(QueryBuilders.termQuery("starName",params.getStarName()));
}
//价格过滤
if (ObjectUtils.allNotNull(params.getMinPrice(),params.getMaxPrice())){
boolQuery.filter(QueryBuilders.rangeQuery("price")
.gt(Integer.valueOf(params.getMinPrice().toString()))
.lte(Integer.valueOf(params.getMaxPrice().toString()))
);
}
//距离排序
if (ObjectUtils.isNotEmpty(params.getLocation())){
request.source().sort(
//指定es中的经纬度字段 location,自己经纬度地址
SortBuilders.geoDistanceSort( "location",new GeoPoint(params.getLocation()))
.order(SortOrder.ASC)//排序方式 升序
.unit(DistanceUnit.KILOMETERS)//距离单位 km
);
}
//用户评价降序 || 价格升序
if (StringUtils.equals(params.getSortBy(),"score")){
request.source().sort("score",SortOrder.DESC);
}
if (StringUtils.equals(params.getSortBy(),"price")){
request.source().sort("price",SortOrder.ASC);
}
//高亮
request.source().highlighter(
new HighlightBuilder().field("name").preTags("<em>").postTags("</em>")
);
//相关性加分
FunctionScoreQueryBuilder functionScoreQueryBuilder = QueryBuilders.functionScoreQuery(
boolQuery,//原始查询条件
new FunctionScoreQueryBuilder.FilterFunctionBuilder[]{
new FunctionScoreQueryBuilder.FilterFunctionBuilder(
QueryBuilders.termQuery("name","喜来登"),//筛选条件
ScoreFunctionBuilders.weightFactorFunction(10)//权重加分
)
}
).boostMode(CombineFunction.SUM);//运算方式
request.source().query(functionScoreQueryBuilder);
}
//解析分页查询结果
private PageResult parseResult(SearchResponse response,int size,HotelListDto hotelListDto){
//获取hits
SearchHits hits = response.getHits();
long total = hits.getTotalHits().value;//总记录数
//获取hits.hits集合数据
ArrayList<HotelDoc> docs = new ArrayList<>();
for (SearchHit hit : hits.getHits()) {
//获取每个对象的source
String json = hit.getSourceAsString();
HotelDoc doc = JSON.parseObject(json, HotelDoc.class);
//获取距离
//判断前端是否传入了距离,有就获取距离排序
if (ObjectUtils.allNotNull(hotelListDto.getLocation())){
Object[] sortValues = hit.getSortValues();
doc.setDistance(sortValues[0]);
}
//获取高亮数据,判断是否有高亮数据,有就获取
if(ObjectUtils.allNotNull(hit.getHighlightFields(),hit.getHighlightFields().get("name"))){
String highName = StringUtils.join(
hit.getHighlightFields().get("name").getFragments(), "....");
//赋值给HotelDoc对象
doc.setName(highName);
}
docs.add(doc);
}
return new PageResult(total,docs);
}
}
7.2 实体类
package cn.itcast.hotel.pojo;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
@Data
@NoArgsConstructor
public class HotelDoc {
private Long id;
private String name;
private String address;
private Integer price;
private Integer score;
private String brand;
private String city;
private String starName;
private String business;
private String location;
private String pic;
private Object distance;
private Boolean isAD;//该字段用于相关性加分,当为true就加分
//自动补全字段
private List<String> suggestion;
public HotelDoc(Hotel hotel) {
this.id = hotel.getId();
this.name = hotel.getName();
this.address = hotel.getAddress();
this.price = hotel.getPrice();
this.score = hotel.getScore();
this.brand = hotel.getBrand();
this.city = hotel.getCity();
this.starName = hotel.getStarName();
this.business = hotel.getBusiness();
this.location = hotel.getLatitude() + ", " + hotel.getLongitude();
this.pic = hotel.getPic();
//自动补全字段
if (this.business.contains("/")){
String[] split = this.business.split("/");
this.suggestion = new ArrayList<>();
this.suggestion.add(this.brand);
this.suggestion.addAll(Arrays.asList(split));
}
if (this.business.contains("、")){
String[] split = this.business.split("、");
this.suggestion = new ArrayList<>();
this.suggestion.add(this.brand);
this.suggestion.addAll(Arrays.asList(split));
}else{
this.suggestion = Arrays.asList(this.brand,this.business);
}
}
}
package cn.itcast.hotel.pojo;
import lombok.Data;
@Data
public class HotelListDto {
//搜索词条
private String key;
private Integer page;
private Integer size;
private String sortBy;
private String brand;
private String city;
private Integer maxPrice;
private Integer minPrice;
private String starName;
private String location;
}