本篇文章为你整理了Elasticsearch全文检索引擎复习笔记(elastic搜索引擎)的详细内容,包含有elastic search 索引 elastic搜索引擎 org.elasticsearch elasticsearch全文检索原理 Elasticsearch全文检索引擎复习笔记,希望能帮助你了解 Elasticsearch全文检索引擎复习笔记。
Elasticsearch是一个基于Lucene的全文检索引擎,其主要用途包括全文搜索、结构化搜索、分析以及数据挖掘等,常用于日志分析、实时分析、搜索引擎、数据仓库等应用场景。图片素材来自互联网,仅作为个人笔记发布,侵删。
Elasticsearch 全文检索引擎复习笔记
Elasticsearch 是一个基于 Lucene 的搜索引擎。它提供了一个分布式、多租户的全文搜索引擎,能够为应用程序提供实时的、结构化和非结构化数据搜索。
ES 是用 Java 开发的,并作为 Apache 许可条款下的开源软件发布。它的特点包括:
分布式架构:Elasticsearch使用分布式架构,可以在多台机器上部署,提供水平扩展能力。
文档导向:Elasticsearch是文档导向的搜索引擎,每一个存储的记录都是一个文档,文档由多个字段组成。
实时搜索:Elasticsearch支持实时搜索,意味着当你添加、修改或删除文档,这些更改将立即反映在搜索结果中。
多租户:Elasticsearch支持多租户,意味着一个Elasticsearch集群可以被多个应用程序使用。
Elasticsearch 的主要用途包括全文搜索、结构化搜索、分析以及数据挖掘等。它常用于日志分析、实时分析、搜索引擎、数据仓库等应用场景。
初识 Elasticsearch
传统数据库使用的是正向索引,即为每个文档建立一个索引,并记录这个文档中包含哪些词条。这样,当用户搜索某个词条时,数据库需要扫描整个文档集合,检查每个文档是否包含这个词条。这种索引查找方式在数据量大时显得效率低下,因为它需要遍历整个文档集合。
相比之下Elasticsearch (ES) 使用倒排索引来支持快速搜索。倒排索引是一种数据结构,它为每个词条(即文档中的单词)建立一个索引,并记录这个词条出现在哪些文档中。这样,当用户搜索某个词条时,搜索引擎可以快速查找包含这个词条的文档。
总的来说,倒排索引的优势在于它可以大大缩短搜索的时间,使得搜索变得更加快速。它还提供了其他一些优势,比如可以支持多种搜索操作(如通配符搜索和布尔搜索),并且可以很方便地执行分析和聚合操作。
倒排索引中包含两部分内容:
词条词典(Term Dictionary):记录所有词条,以及词条与倒排列表(Posting List)之间的关系,会给词条创建索引,提高查询和插入效率
倒排列表(Posting List):记录词条所在的文档id、词条出现频率 、词条在文档中的位置等信息
我们可以统一地把 mysql 与 elasticsearch 的概念做一下对比:
Mapping
Mapping(映射)是索引中文档的约束,例如字段类型约束。类似数据库的表结构(Schema)
Mysql:擅长事务类型操作,可以确保数据的安全和一致性
Elasticsearch:擅长海量数据的搜索、分析、计算
因此在企业中,往往是两者结合使用:
对安全性要求较高的写操作,使用 mysql 实现
对查询性能要求较高的搜索需求,使用 elasticsearch 实现
两者再基于某种方式,实现数据的同步,保证一致性
ES 及其 IK 分词器插件的安装可以参考这篇博客:Elasticsearch、IK分词器安装 (docker)。
一、索引库操作
索引库就类似数据库表,mapping 映射就类似表的结构。我们要向 es 中存储数据,必须先创建“库”和“表”。
Ⅰ、DSL语法操作
mapping 是对索引库中文档的约束,常见的 mapping 属性包括:
type:字段数据类型,常见的简单类型有:
字符串:text(可分词的文本)、keyword(精确值,例如:品牌、国家、ip地址)
数值:long、integer、short、byte、double、float、
布尔:boolean
日期:date
对象:object
age:类型为 integer;参与搜索,因此需要index为true;无需分词器
weight:类型为float;参与搜索,因此需要index为true;无需分词器
isMarried:类型为boolean;参与搜索,因此需要index为true;无需分词器
info:类型为字符串,需要分词,因此是text;参与搜索,因此需要index为true;分词器可以用ik_smart
email:类型为字符串,但是不需要分词,因此是keyword;不参与搜索,因此需要index为false;无需分词器
score:虽然是数组,但是我们只看元素的类型,类型为float;参与搜索,因此需要index为true;无需分词器
name:类型为object,需要定义多个子属性
name.firstName;类型为字符串,但是不需要分词,因此是keyword;参与搜索,因此需要index为true;无需分词器
name.lastName;类型为字符串,但是不需要分词,因此是keyword;参与搜索,因此需要index为true;无需分词器
由于 ES 使用 RESTFul 的接口风格,因此创建一个索引库只需要在 Kibana 中编写 PUT /索引库名称{...} 即可:
PUT/heima
"mappings":{
"properties":{
"info":{
"type":"text",
"analyzer":"ik_smart"
},
"email":{
"type":"keyword",
"index":"falsae"
},
"name":{
"properties":{
"firstName":{
"type":"keyword"
}
}
},
// ... 略
}
}
GET、DELETE
查看索引库语法:
GET /索引库名称
删除索引库的语法:
DELETE /索引库名称
倒排索引结构虽然不复杂,但是一旦数据结构改变(比如改变了分词器),就需要重新创建倒排索引,这简直是灾难。因此索引库一旦创建,无法修改mapping。
虽然无法修改 mapping 中已有的字段,但是却允许添加新的字段到 mapping 中,因为不会对倒排索引产生影响。
语法示例:
PUT/索引库名/_mapping
"properties":{
"新字段名":{
"type":"integer"
}
}
Ⅱ、JavaAPI操作
Elasticsearch 官方提供了各种不同语言的客户端,用来操作 ES。这些客户端的本质就是组装DSL语句,通过 http 请求发送给ES。
在 ES 提供的API中,与 ES 一切交互都封装在一个名为 RestHighLevelClient 的类中,必须先完成这个对象的初始化,建立与 elasticsearch 的连接。
引入 es 的 RestHighLevelClient 依赖:
dependency
groupId org.elasticsearch.client /groupId
artifactId elasticsearch-rest-high-level-client /artifactId
/dependency
因为 SpringBoot 默认的ES版本是7.6.2,所以我们需要覆盖默认的ES版本:
properties
java.version 11 /java.version
elasticsearch.version 7.12.1 /elasticsearch.version
/properties
初始化 RestHighLevelClient,初始化的代码如下:
RestHighLevelClient client = new RestHighLevelClient(RestClient.builder(
HttpHost.create("http://localhost:9200")
创建索引库
使用 RestClient 创建索引库有以下三步骤:
创建 Request 对象。因为是创建索引库的操作,因此 Request 是 CreateIndexRequest。
添加请求参数,其实就是DSL的 JSON 参数部分。
发送请求,client.indices() 方法的返回值是 IndicesClient 类型,封装了所有与索引库操作有关的方法。
使用下列代码可通过 RestClient 创建索引库,其中 HotelConstants.MAPPING_TEMPLATE 是为简化书写定义的一个 mapping 映射静态字符串常量。
@Test
void testCreateHotelIndex() throws IOException {
//创建Request对象
CreateIndexRequest request = new CreateIndexRequest("hotel");
//准备请求参数——DSL语句
request.source(HotelConstants.MAPPING_TEMPLATE, XContentType.JSON);
//发送创建索引库请求
client.indices().create(request, RequestOptions.DEFAULT);
删除索引库
和创建的语法类似,只需要创建 DeleteIndexRequest 对象,发送请求即可。
@Test
void testDeleteHotelIndex() throws IOException {
// 1.创建Request对象
DeleteIndexRequest request = new DeleteIndexRequest("hotel");
// 2.发送请求
client.indices().delete(request, RequestOptions.DEFAULT);
判断索引库是否存在
判断索引库是否存在,本质就是查询,因此与删除的 Java 代码流程是类似的。依然是三步走:
@Test
void testExistsHotelIndex() throws IOException {
// 1.创建Request对象
GetIndexRequest request = new GetIndexRequest("hotel");
// 2.发送请求
boolean exists = client.indices().exists(request, RequestOptions.DEFAULT);
// 3.输出
System.err.println(exists ? "索引库已经存在!" : "索引库不存在!");
RestClient 操作 ES 的流程基本类似。核心是 client.indices() 方法来获取索引库的操作对象。
二、文档操作
Ⅰ、DSL语法操作
新增文档的 DSL 语法如下:
POST/索引库名/_doc/文档id
"字段1":"值1",
"字段2":"值2",
"字段3":{
"子属性1":"值3",
"子属性2":"值4"
},
// ...
GET、DELETE
根据 RESTFul 风格,新增是 post,查询应该是 get,删除是 delete,不过查询一般都需要条件,这里我们把文档id带上。
查看文档语法:
GET /索引库名称/_doc/文档id
删除文档语法:
DELETE /索引库名称/_doc/文档id
修改有两种方式:
全量修改:直接覆盖原来的文档
增量修改:修改文档中的部分字段
全量修改是覆盖原来的文档,其本质是根据指定的 id 删除文档,随后新增一个相同 id 的文档:
PUT/索引库名/_doc/文档id
"字段1":"值1",
"字段2":"值2",
// ... 略
增量修改是只修改指定id匹配的文档中的部分字段,使用 _update:
POST/索引库名/_update/文档id
"doc": {
"字段名":"新的值",
Ⅱ、JavaAPI操作
我们预先在 mysql 数据库中准备了 tb_hotel 的信息表,并在 IHotelService 提供对数据库的 CRUD 操作方法。我们要将数据库的酒店数据查询出来,写入 elasticsearch 中。
数据库查询后的结果是一个 Hotel 类型的对象。结构如下:
@Data
@TableName("tb_hotel")
public class Hotel {
@TableId(type = IdType.INPUT)
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 longitude;
private String latitude;
private String pic;
与我们的索引库结构存在差异:
longitude和latitude需要合并为location
因此,我们需要定义一个新的实体类 HotelDoc,与索引库结构吻合:
package cn.itcast.hotel.pojo;
import lombok.Data;
import lombok.NoArgsConstructor;
@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;
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();
新增文档与创建索引库类似,同样是三步走:
创建Request对象
准备请求参数,也就是DSL中的JSON文档
变化的地方在于,这里直接使用 client.*() 的 API,不再需要 client.indices() 了。另外需要注意的是:
酒店数据来自于数据库,我们需要先查询出来,得到 Hotel 对象
Hotel 对象需要转化为 HotelDoc 对象
HotelDoc 需要序列化为 json 格式
@Test
void testAddDocument() throws IOException {
//获取单条旅馆数据
Hotel hotel = hotelService.getById(61083L);
//转换为文档类型
HotelDoc hotelDoc = new HotelDoc(hotel);
//添加文档至ES
IndexRequest request = new IndexRequest("hotel").id(hotelDoc.getId().toString());
request.source(JSON.toJSONString(hotelDoc), XContentType.JSON);
client.index(request, RequestOptions.DEFAULT);
获取和删除文档
从 ES 获取数据获得的是一个 GetResponse 对象,其 getSourceAsString() 方法将返回一个 json 字符串,而在解析结果的过程中应将其转换为一个 Java 对象。
@Test
void testGetDocumentById() throws IOException {
//通过文档id查询索引数据
GetRequest request = new GetRequest("hotel", "61083");
GetResponse response = client.get(request, RequestOptions.DEFAULT);
//将JSON转化为java对象
String source = response.getSourceAsString();
HotelDoc hotelDoc = JSON.parseObject(source, HotelDoc.class);
System.out.println(hotelDoc);
删除文档操作类似且更为简单,创建 DeleteRequest 发送请求即可。
@Test
void testDeleteDocument() throws IOException {
// 1.准备Request
DeleteRequest request = new DeleteRequest("hotel", "61083");
// 2.发送请求
client.delete(request, RequestOptions.DEFAULT);
在RestClient的API中,全量修改与新增的API完全一致,判断依据是ID是否存在:
如果新增时,ID已经存在,则修改
如果新增时,ID不存在,则新增
这里不再赘述,我们主要关注增量修改。
@Test
void testUpdateDocument() throws IOException {
// 1.准备Request
UpdateRequest request = new UpdateRequest("hotel", "61083");
// 2.准备请求参数
request.doc(
"price", "952",
"starName", "四钻"
// 3.发送请求
client.update(request, RequestOptions.DEFAULT);
request.doc() 方法中的两个参数为一组,分别对应 key 和 value 值。
批量导入文档
我们可以利用 BulkRequest 批量将数据库数据导入到索引库中。批量处理 BulkRequest,其本质就是将多个普通的 CRUD 请求组合在一起发送,因此 Bulk 中添加了多个 IndexRequest,就是批量新增功能了。
我们在导入酒店数据时,将代码改造成 for 循环处理即可。
@Test
void testBulkRequest() throws IOException {
// 批量查询酒店数据
List Hotel hotels = hotelService.list();
// 1.创建Request
BulkRequest request = new BulkRequest();
// 2.准备参数,添加多个新增的Request
for (Hotel hotel : hotels) {
// 2.1.转换为文档类型HotelDoc
HotelDoc hotelDoc = new HotelDoc(hotel);
// 2.2.创建新增文档的Request对象
request.add(new IndexRequest("hotel")
.id(hotelDoc.getId().toString())
.source(JSON.toJSONString(hotelDoc), XContentType.JSON));
// 3.发送请求
client.bulk(request, RequestOptions.DEFAULT);
三、搜索操作
elasticsearch 的查询依然是基于 RESTFul 风格的DSL来实现的。
Ⅰ、DSL语法操作
Elasticsearch 提供了基于JSON的DSL(Domain Specific Language)来定义查询。常见的查询类型包括:
查询所有:查询出所有数据,一般测试用。例如:match_all
全文检索(full text)查询:利用分词器对用户输入内容分词,然后去倒排索引库中匹配。例如:
match_query
multi_match_query
精确查询:根据精确词条值查找数据,一般是查找keyword、数值、日期、boolean等类型字段。例如:
range
因为是拿着词条去匹配,因此参与搜索的字段也必须是可分词的 text 类型的字段。
常见的全文检索查询包括:
match 查询:单字段查询
multi_match 查询:多字段查询,任意一个字段符合条件就算符合查询条件
match 查询语法如下:
GET/indexName/_search
"query":{
"match":{
"FIELD":"TEXT"
}
}
mulit_match 语法如下:
GET/indexName/_search
"query":{
"multi_match":{
"query":"TEXT",
"fields":["FIELD1"," FIELD12"]
}
}
搜索字段越多,对查询性能影响越大,因此建议在创建索引库时采用 copy_to 拷贝到一个新字段,然后使用单字段查询的方式查询这个组合字段。
精确查询一般是查找keyword、数值、日期、boolean等类型字段。所以不会对搜索条件分词。常见的有:
term:根据词条精确值查询
range:根据值的范围查询
因为精确查询的字段搜是不分词的字段,因此查询的条件也必须是不分词的词条。查询时,用户输入的内容跟字段值完全匹配时才认为符合条件。如果用户输入的内容过多,反而搜索不到数据。
//term查询
GET/indexName/_search
"query":{
"term":{
"FIELD":{
"value":"VALUE"
}
}
}
范围查询,一般应用在对数值类型做范围过滤的时候。比如做价格范围过滤。
//range查询
GET/indexName/_search
"query":{
"range":{
"FIELD":{
"gte":10, // 这里的gte代表大于等于,gt则代表大于
"lte":20 // lte代表小于等于,lt则代表小于
}
}
}
地理坐标查询
所谓的地理坐标查询,其实就是根据经纬度查询,官方文档:https://www.elastic.co/guide/en/elasticsearch/reference/current/geo-queries.html
常见的使用场景包括:
携程:搜索我附近的酒店
滴滴:搜索我附近的出租车
微信:搜索我附近的人
查询时,需要指定矩形的左上、右下两个点的坐标,然后画出一个矩形,落在该矩形内的都是符合条件的点。
//geo_bounding_box查询
GET/indexName/_search
"query":{
"geo_bounding_box":{
"FIELD":{
"top_left":{ // 左上点
"lat":31.1,
"lon":121.5
},
"bottom_right":{ // 右下点
"lat":30.9,
"lon":121.7
}
}
}
}
附近查询,也叫做距离查询(geo_distance):查询到指定中心点小于某个距离值的所有文档。
换句话来说,在地图上找一个点作为圆心,以指定距离为半径,画一个圆,落在圆内的坐标都算符合条件:
//geo_distance 查询
GET/indexName/_search
"query":{
"geo_distance":{
"distance":"15km", // 半径
"FIELD":"31.21,121.5" // 圆心
}
}
复合(compound)查询:复合查询可以将其它简单查询组合起来,实现更复杂的搜索逻辑。常见的有两种:
fuction score:算分函数查询,可以控制文档相关性算分,控制文档排名
bool query:布尔查询,利用逻辑关系组合多个其它的查询,实现复杂搜索
1. 文档相关度及算分函数查询:
当我们利用 match 查询时,文档结果会根据与搜索词条的关联度打分(_score),返回结果时按照分值降序排列。
在 elasticsearch 中,早期使用的打分算法是 TF-IDF 算法,在后来的5.1版本升级中, elasticsearch 将算法改进为 BM25 算法。
TF-IDF 算法有一各缺陷,就是词条频率越高,文档得分也会越高,单个词条对文档影响较大。而BM25则会让单个词条的算分有一个上限,曲线更加平滑。
根据相关度打分是比较合理的需求,但合理的不一定是产品经理需要的。
以百度为例,你搜索的结果中,并不是相关度越高排名越靠前,而是谁掏的钱多排名就越靠前。
要想控制相关性算分,就需要利用 ES 中的 function score 查询了。
function score 查询中包含四部分内容:
原始查询条件:query 部分,基于这个条件搜索文档,并且基于BM25算法给文档打分,原始算分(query score)
过滤条件:filter 部分,符合该条件的文档才会重新算分
算分函数:符合 filter 条件的文档要根据这个函数做运算,得到的函数算分(function score),有四种函数
weight:函数结果是常量
field_value_factor:以文档中的某个字段值作为函数结果
random_score:以随机数作为函数结果
script_score:自定义算分函数算法
示例:给“如家”这个品牌的酒店排名靠前一些。翻译一下这个需求,转换为之前说的四个要点:
原始条件:不确定,可以任意变化
过滤条件:brand = "如家"
算分函数:可以简单粗暴,直接给固定的算分结果,weight
运算模式:比如求和
因此最终的DSL语句如下:
GET/hotel/_search
"query":{
"function_score":{
"query":{}, // 原始查询,可以是任意条件
"functions":[//算分函数
{
"filter":{//满足的条件,品牌必须是如家
"term":{
"brand":"如家"
}
},
"weight":2//算分权重为2
}
],
"boost_mode": "sum" // 加权模式,求和
}
}
2. 布尔查询:
布尔查询是一个或多个查询子句的组合,每一个子句就是一个子查询。子查询的组合方式有:
must:必须匹配每个子查询,类似“与”
should:选择性匹配子查询,类似“或”
must_not:必须不匹配,不参与算分,类似“非”
filter:必须匹配,不参与算分
GET/hotel/_search
"query":{
"bool":{
"must":[
{"term":{"city":"上海"}}
],
"should":[
{"term":{"brand":"皇冠假日"}},
{"term":{"brand":"华美达"}}
],
"must_not":[
{"range":{"price":{"lte":500}}}
],
"filter":[
{"range":{"score":{"gte":45}}}
]
}
}
排序、分页与高亮
1. 搜索结果排序:
keyword、数值、日期类型排序的语法基本一致。
GET/indexName/_search
"query":{
"match_all":{}
},
"sort":[
{
"FIELD":"desc"//排序字段、排序方式ASC、DESC
}
]
排序条件是一个数组,也就是可以写多个排序条件。按照声明的顺序,当第一个条件相等时,再按照第二个条件排序,以此类推。
地理坐标排序略有不同。
GET/indexName/_search
"query":{
"match_all":{}
},
"sort":[
{
"_geo_distance":{
"FIELD":"纬度,经度", // 文档中geo_point类型的字段名、目标坐标点
"order":"asc", // 排序方式
"unit":"km" // 排序的距离单位
}
}
]
2. 搜索结果分页:
elasticsearch 默认情况下只返回 top10 的数据。而如果要查询更多数据就需要修改分页参数了。elasticsearch中通过修改from、size参数来控制要返回的分页结果:
from:从第几个文档开始
size:总共查询几个文档
类似于mysql中的limit ?, ?
GET/hotel/_search
"query":{
"match_all":{}
},
"from":0,//分页开始的位置,默认为0
"size":10,//期望获取的文档总数
"sort":[
{"price":"asc"}
]
3. 指定字段高亮:
什么是高亮显示呢?
我们在百度,京东搜索时,关键字会变成红色,比较醒目,这叫高亮显示,高亮显示的实现分为两步:
给文档中的所有关键字都添加一个标签,例如 em 标签
页面给 em 标签编写CSS样式
GET/hotel/_search
"query":{
"match":{
"FIELD":"TEXT" // 查询条件,高亮一定要使用全文检索查询
}
},
"highlight":{
"fields":{//指定要高亮的字段
"FIELD":{
"pre_tags":" em ",//用来标记高亮字段的前置标签
"post_tags":" /em "//用来标记高亮字段的后置标签
}
}
}
注意:
高亮是对关键字高亮,因此搜索条件必须带有关键字,而不能是范围这样的查询。
默认情况下,高亮的字段,必须与搜索指定的字段一致,否则无法高亮
如果要对非搜索字段高亮,则需要添加一个属性:required_field_match=false
聚合(aggregations)可以让我们极其方便的实现对数据的统计、分析、运算。例如:
什么品牌的手机最受欢迎?
这些手机的平均价格、最高价格、最低价格?
这些手机每月的销售情况如何?
实现这些统计功能的比数据库的sql要方便的多,而且查询速度非常快,可以实现近实时搜索效果。
聚合常见的有三类:
桶(Bucket)聚合:用来对文档做分组
TermAggregation:按照文档字段值分组,例如按照品牌值分组、按照国家分组
Date Histogram:按照日期阶梯分组,例如一周为一组,或者一月为一组
1. Bucket 桶聚合语法:
现在,我们要统计所有数据中的酒店品牌有几种,其实就是按照品牌对数据分组。此时可以根据酒店品牌的名称做聚合,也就是 Bucket 聚合。
默认情况下,Bucket 聚合是对索引库的所有文档做聚合,但真实场景下,用户会输入搜索条件,因此聚合必须是对搜索结果聚合。那么聚合必须添加限定条件。
我们可以限定要聚合的文档范围,只要添加 query 条件即可:
GET/hotel/_search
"query":{
"range":{
"price":{
"lte":200 // 只对200元以下的文档聚合
}
}
},
"size":0,//设置size为0,结果中不包含文档,只包含聚合结果
"aggs":{ //定义聚合
"brandAgg":{ //给聚合起个名字
"terms":{ //聚合的类型,按照品牌值聚合,所以选择term
"field":"brand", //参与聚合的字段
"order":{
"_count":"asc" //按照_count升序排列
},
"size":20 //希望获取的聚合结果数量
}
}
}
2. Metric聚合语法:
我们对酒店按照品牌分组,形成了一个个桶。现在我们需要对桶内的酒店做运算,获取每个品牌的用户评分的min、max、avg等值。
这就要用到Metric聚合了,例如stat聚合:就可以获取min、max、avg等结果。
{
"size":0,
"aggs":{
"brandAgg":{
"terms":{
"field":"brand",
"size":20
},
"aggs":{//是brands聚合的子聚合,也就是分组后对每组分别计算
"score_stats":{//聚合名称
"stats":{//聚合类型,这里stats可以计算min、max、avg等
"field":"score"//聚合字段,这里是score
}
}
}
}
}
当用户在搜索框输入字符时,我们应该提示出与该字符有关的搜索项,这种根据用户输入的字母,提示完整词条的功能,就是自动补全了。
如果需要根据拼音字母来推断,因此要用到拼音分词功能。而要实现根据字母做补全,就必须对文档按照拼音分词。在 GitHub 上恰好有 ES 的拼音分词插件。地址:https://github.com/medcl/elasticsearch-analysis-pinyin。
默认的拼音分词器会将每个汉字单独分为拼音,而我们希望的是每个词条形成一组拼音,需要对拼音分词器做个性化定制,形成自定义分词器。
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":{//自定义tokenizerfilter
"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"
}
}
}
//自动补全查询
GET/test/_search
"suggest":{
"title_suggest":{
"text":"s",//关键字
"completion":{
"field":"title",//补全查询的字段
"skip_duplicates":true,//跳过重复的
"size":10//获取前10条结果
}
}
}
Ⅱ、JavaAPI操作
我们以match_all查询为例
第一步,创建 SearchRequest 对象,指定索引库名
第二步,利用 request.source() 构建DSL,DSL中可以包含查询、分页、排序、高亮等
query():代表查询条件,利用 QueryBuilders.matchAllQuery() 构建一个match_all查询的DSL
client.search() 方法将返回一个 SearchResponse 对象,我们还需要对响应数据进行结果解析。我们将解析结果的测试代码封装为 handleResponse() 方法,方便重复调用。
@Test
void testMatchAll() throws IOException {
// 1.准备Request
SearchRequest request = new SearchRequest("hotel");
// 2.准备DSL
request.source().query(QueryBuilders.matchAllQuery());
// 3.发送请求
SearchResponse response = client.search(request, RequestOptions.DEFAULT);
// 4.解析响应
handleResponse(response);
private void handleResponse(SearchResponse response) {
// 4.解析响应
SearchHits searchHits = response.getHits();
// 4.1.获取总条数
long total = searchHits.getTotalHits().value;
System.out.println("共搜索到" + total + "条数据");
// 4.2.文档数组
SearchHit[] hits = searchHits.getHits();
// 4.3.遍历
for (SearchHit hit : hits) {
// 获取文档source
String json = hit.getSourceAsString();
// 反序列化
HotelDoc hotelDoc = JSON.parseObject(json, HotelDoc.class);
System.out.println("hotelDoc = " + hotelDoc);
其他的查询步骤基本类似,在构建查询条件对象时使用不同的方法构建即可:
match:QueryBuilders.matchQuery(FILED, TEXT)
term:QueryBuilders.termQuery(FILED, TEXT)
range:QueryBuilders.rangeQuery(FILED).lte(number)
布尔复合查询
布尔查询是用must、must_not、filter等方式组合其它查询。
与其它查询的差别同样是在查询条件的构建——QueryBuilders,结果解析等其他代码完全不变。完整代码如下:
@Test
void testBool() throws IOException {
// 1.准备Request
SearchRequest request = new SearchRequest("hotel");
// 2.准备DSL
// 2.1.准备BooleanQuery
BoolQueryBuilder boolQuery = QueryBuilders.boolQuery();
// 2.2.添加term
boolQuery.must(QueryBuilders.termQuery("city", "杭州"));
// 2.3.添加range
boolQuery.filter(QueryBuilders.rangeQuery("price").lte(250));
request.source().query(boolQuery);
// 3.发送请求
SearchResponse response = client.search(request, RequestOptions.DEFAULT);
// 4.解析响应
handleResponse(response);
排序与分页
搜索结果的排序与分页是与 query 同级的参数,因此同样是使用 request.source() 来设置。
@Test
void testPageAndSort() throws IOException {
//创建SearchRequest对象获取数据并封装到SearchResponse对象
SearchRequest request = new SearchRequest("hotel");
//创建BoolQuery对象并添加bool条件
request.source().query(QueryBuilders.matchAllQuery());
//排序API,按价格升序
request.source().sort("price", SortOrder.ASC);
//分页API,查询大小为5条的第0页
request.source().from(0).size(5);
//发送请求
SearchResponse response = client.search(request, RequestOptions.DEFAULT);
//获取解析结果对象
SearchHits searchHits = response.getHits();
//处理查询数据
handleResponse(searchHits);
搜索结果高亮
高亮的代码与之前代码差异较大,有两点:
查询的DSL:其中除了查询条件,还需要添加高亮条件,同样是与query同级。
结果解析:结果除了要解析_source文档数据,还要解析高亮结果
@Test
void testHighlighter() throws IOException {
//创建SearchRequest对象获取数据并封装到SearchResponse对象
SearchRequest request = new SearchRequest("hotel");
//创建BoolQuery对象并添加bool条件
request.source().query(QueryBuilders.matchQuery("all", "如家"));
//高亮显示关键词
request.source().highlighter(new HighlightBuilder().field("name").requireFieldMatch(false));
//发送请求
SearchResponse response = client.search(request, RequestOptions.DEFAULT);
//获取解析结果对象
SearchHits searchHits = response.getHits();
//处理查询数据
SearchHit[] hits = searchHits.getHits();
for (SearchHit hit : hits) {
//封装HotelDoc对象
String json = hit.getSourceAsString();
HotelDoc hotelDoc = JSON.parseObject(json, HotelDoc.class);
//获取高亮部分并封装
Map String, HighlightField highlightFields = hit.getHighlightFields();
if (!CollectionUtils.isEmpty(highlightFields)) {
HighlightField highlightField = highlightFields.get("name");
if (highlightField != null) {
String name = highlightField.getFragments()[0].string();
hotelDoc.setName(name);
System.out.println("hotelDoc=" + hotelDoc);
聚合条件与query条件同级别,因此需要使用 request.source() 来指定聚合条件。
需求:搜索页面的品牌、城市等信息不应该是在页面写死,而是通过聚合索引库中的酒店数据得来的。
使用聚合功能,利用 Bucket 聚合,对搜索结果中的文档基于品牌分组、基于城市分组,就能得知包含哪些品牌、哪些城市了。
因为是对搜索结果聚合,因此聚合是限定范围的聚合,也就是说聚合的限定条件跟搜索文档的条件一致。
@Override
public Map String, List String filters(RequestParams params) {
try {
// 1.准备Request
SearchRequest request = new SearchRequest("hotel");
// 2.准备DSL
// 2.1.query
buildBasicQuery(params, request);
// 2.2.设置size
request.source().size(0);
// 2.3.聚合
buildAggregation(request);
// 3.发出请求
SearchResponse response = client.search(request, RequestOptions.DEFAULT);
// 4.解析结果
Map String, List String result = new HashMap ();
Aggregations aggregations = response.getAggregations();
// 4.1.根据品牌名称,获取品牌结果
List String brandList = getAggByName(aggregations, "brandAgg");
result.put("品牌", brandList);
// 4.2.根据品牌名称,获取品牌结果
List String cityList = getAggByName(aggregations, "cityAgg");
result.put("城市", cityList);
// 4.3.根据品牌名称,获取品牌结果
List String starList = getAggByName(aggregations, "starAgg");
result.put("星级", starList);
return result;
} catch (IOException e) {
throw new RuntimeException(e);
private void buildAggregation(SearchRequest request) {
request.source().aggregation(AggregationBuilders
.terms("brandAgg")
.field("brand")
.size(100)
request.source().aggregation(AggregationBuilders
.terms("cityAgg")
.field("city")
.size(100)
request.source().aggregation(AggregationBuilders
.terms("starAgg")
.field("starName")
.size(100)
private List String getAggByName(Aggregations aggregations, String aggName) {
// 4.1.根据聚合名称获取聚合结果
Terms brandTerms = aggregations.get(aggName);
// 4.2.获取buckets
List ? extends Terms.Bucket buckets = brandTerms.getBuckets();
// 4.3.遍历
List String brandList = new ArrayList ();
for (Terms.Bucket bucket : buckets) {
// 4.4.获取key
String key = bucket.getKeyAsString();
brandList.add(key);
return brandList;
现在,我们的 hotel 索引库还没有设置拼音分词器,需要修改索引库中的配置。但是我们知道索引库是无法修改的,只能删除然后重新创建。
另外,我们需要添加一个字段,用来做自动补全,将brand、suggestion、city等都放进去,作为自动补全的提示。
在完成修改酒店映射结构、修改 HotelDoc 实体类属性后,以下代码可以实现依照拼音提示自动补全:
@Override
public List String getSuggestions(String prefix) {
try {
// 1.准备Request
SearchRequest request = new SearchRequest("hotel");
// 2.准备DSL
request.source().suggest(new SuggestBuilder().addSuggestion(
"suggestions",
SuggestBuilders.completionSuggestion("suggestion")
.prefix(prefix)
.skipDuplicates(true)
.size(10)
// 3.发起请求
SearchResponse response = client.search(request, RequestOptions.DEFAULT);
// 4.解析结果
Suggest suggest = response.getSuggest();
// 4.1.根据补全查询名称,获取补全结果
CompletionSuggestion suggestions = suggest.getSuggestion("suggestions");
// 4.2.获取options
List CompletionSuggestion.Entry.Option options = suggestions.getOptions();
// 4.3.遍历
List String list = new ArrayList (options.size());
for (CompletionSuggestion.Entry.Option option : options) {
String text = option.getText().toString();
list.add(text);
return list;
} catch (IOException e) {
throw new RuntimeException(e);
四、数据同步
elasticsearch 中的酒店数据来自于 mysql 数据库,因此 mysql 数据发生改变时,elasticsearch 也必须跟着改变,这个就是 elasticsearch 与 mysql 之间的数据同步。
Ⅰ、思路分析
常见的数据同步方案有三种:
监听binlog
1. 同步调用:
基本步骤如下:
hotel-demo对外提供接口,用来修改elasticsearch中的数据
酒店管理服务在完成数据库操作后,直接调用hotel-demo提供的接口,
优点:实现简单,粗暴
缺点:业务耦合度高
2. 异步通知:
流程如下:
hotel-admin 对 mysql 数据库数据完成增、删、改后,发送MQ消息
hotel-demo 监听MQ,接收到消息后完成 elasticsearch 数据修改
优点:低耦合,实现难度一般
缺点:依赖mq的可靠性
3. 监听binlog:
流程如下:
给 mysql 开启 binlog 功能
mysql完成增、删、改操作都会记录在 binlog 中
hotel-demo 基于 canal 监听 binlog 变。
郑重声明:本文由网友发布,不代表盛行IT的观点,版权归原作者所有,仅为传播更多信息之目的,如有侵权请联系,我们将第一时间修改或删除,多谢。