2021年12月21日更新搜索结果以下剩余部分,除数据同步模块
2021年12月22日更新数据同步模块
Elasticsearch is the distributed search and analytics engine at the heart of the Elastic Stack. Logstash and Beats facilitate collecting, aggregating, and enriching your data and storing it in Elasticsearch. Kibana enables you to interactively explore, visualize, and share insights into your data and manage and monitor the stack. Elasticsearch is where the indexing, search, and analysis magic happens.
Elasticsearch provides near real-time search and analytics for all types of data. Whether you have structured or unstructured text, numerical data, or geospatial data, Elasticsearch can efficiently store and index it in a way that supports fast searches. You can go far beyond simple data retrieval and aggregate information to discover trends and patterns in your data. And as your data and query volume grows, the distributed nature of Elasticsearch enables your deployment to grow seamlessly right along with it.
https://www.elastic.co/guide/en/elasticsearch/reference/current/elasticsearch-intro.html
上面是官网的说明,翻译一下
Elasticsearch 是位于 Elastic Stack 核心的分布式搜索和分析引擎。Logstash 和 Beats 有助于收集、聚合和丰富您的数据并将其存储在 Elasticsearch 中。Kibana 使您能够以交互方式探索、可视化和共享对数据的洞察,并管理和监控堆栈。Elasticsearch 是索引、搜索和分析魔法发生的地方。
Elasticsearch 为所有类型的数据提供近乎实时的搜索和分析。无论您拥有结构化或非结构化文本、数值数据还是地理空间数据,Elasticsearch 都可以以支持快速搜索的方式高效地存储和索引它。您可以超越简单的数据检索和聚合信息来发现数据中的趋势和模式。随着您的数据和查询量的增长,Elasticsearch 的分布式特性使您的部署能够随之无缝增长。
结合目前(2021年11月30日)搜索指数,学习人数,可见ES在搜索领域的火热程度。
原理
倒排索引
倒排索引中有两个非常重要的概念:
- 文档(
Document
):用来搜索的数据,其中的每一条数据就是一个文档。例如一个网页、一个商品信息 - 词条(
Term
):对文档数据或用户搜索数据,利用某种算法分词,得到的具备含义的词语就是词条。例如:我是中国人,就可以分为:我、是、中国人、中国、国人这样的几个词条
创建倒排索引是对正向索引的一种特殊处理,流程如下:
- 将每一个文档的数据利用算法分词,得到一个个词条
- 创建表,每行数据包括词条、词条所在文档 id、位置等信息
- 因为词条唯一性,可以给词条创建索引,例如 hash 表结构索引
倒排索引的搜索流程如下(以搜索"华为手机"为例)
- 用户输入条件
"华为手机"
进行搜索 - 对用户输入内容分词,得到词条:
华为
、手机
- 拿着词条在倒排索引中查找,可以得到包含词条的文档 id 有 1、2、3
- 拿着文档 id 到正向索引中查找具体文档
虽然要先查询倒排索引,再查询倒排索引,但是无论是词条、还是文档 id 都建立了索引,查询速度非常快!无需全表扫描。
然后为什么一个叫做正向索引,一个叫做倒排索引呢?
- 正向索引是最传统的,根据 id 索引的方式。但根据词条查询时,必须先逐条获取每个文档,然后判断文档中是否包含所需要的词条,是根据文档找词条的过程。
- 而倒排索引则相反,是先找到用户要搜索的词条,根据得到的文档 id 获取该文档。是根据词条找文档的过程。
文档和字段
elasticsearch 是面向文档(Document)存储的,可以是数据库中的一条商品数据,一个订单信息。文档数据会被序列化为 json 格式后存储在 elasticsearch,而 JSON 文档中往往包含很多的字段(Field),类似于数据库中的列。
索引(Index),就是相同类型的文档的集合。
例如:
- 所有用户文档,就可以组织在一起,称为用户的索引;
- 所有商品的文档,可以组织在一起,称为商品的索引;
- 所有订单的文档,可以组织在一起,称为订单的索引;
因此,我们可以把索引当做是数据库中的表。
数据库的表会有约束信息,用来定义表的结构、字段的名称、类型等信息。因此,索引库中就有映射(mapping),是索引中文档的字段约束信息,类似表的结构约束。
mysql 与 elasticsearch
MySQL | Elasticsearch | 说明 |
---|---|---|
Table | Index | 索引index,就是文档的集合,类似数据库的表table |
Row | Document | 文档(Document),就是一条条的数据,类似数据库中的行(Row),文档都是JSON格式 |
Column | Field | 字段(Field),就是JSON文档中的字段,类似数据库中的列(Column) |
Schema | Mapping | Mapping(映射)是索引中文档的约束,例如字段类型约束。类似数据库的表结构(Schema) |
SQL | DSL | DSL是elasticsearch提供的JSON风格的请求语句,用来操作elasticsearch,实现CRUD |
- Mysql:擅长事务类型操作,可以确保数据的安全和一致性
- Elasticsearch:擅长海量数据的搜索、分析、计算
因此在企业中,往往是两者结合使用:
- 对安全性要求较高的写操作,使用 MySQL 实现
- 对查询性能要求较高的搜索需求,使用 ELasticsearch 实现
- 两者再基于某种方式,实现数据的同步,保证一致性
安装
ElasticSearch
一般与ElasticSearch一起搭配使用的有kibana,这是一个数据可视化管理工具,用于可视化管理ElasticSearch。
这里就说一下ES,kibana有兴趣的可以自己安装测试
官网链接:https://www.elastic.co/cn/downloads/elasticsearch
Start Elasticsearch
Run bin/elasticsearch
(or bin\elasticsearch.bat
on Windows)
docker安装:https://www.elastic.co/guide/en/elasticsearch/reference/current/docker.html
IK分词器
中文分词器,原生es对中文的分词做的很不好,基本都是一字一分,在中文搜索中需要国人的分词插件
官方地址:https://github.com/medcl/elasticsearch-analysis-ik
安装方式:
1.download or compile
- optional 1 - download pre-build package from here: https://github.com/medcl/elasticsearch-analysis-ik/releasescreate plugin folder
cd your-es-root/plugins/ && mkdir ik
unzip plugin to folderyour-es-root/plugins/ik
- optional 2 - use elasticsearch-plugin to install ( supported from version v5.5.1 ):
./bin/elasticsearch-plugin install https://github.com/medcl/elasticsearch-analysis-ik/releases/download/v6.3.0/elasticsearch-analysis-ik-6.3.0.zip
NOTE: replace6.3.0
to your own elasticsearch version
2.restart elasticsearch
IK分词器包含两种模式:
ik_smart
:智能切分,粗粒度ik_max_word
:最细切分,细粒度
扩展词词典
打开IK分词器 config 目录是 IKAnalyzer.cfg.xml
,添加一个文件名,我们以 ext.dic
文件名为例。
我们去创建 ext.dic
,在其中添加热点词就好了,一个词一行。
<?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"></entry>
<!--用户可以在这里配置自己的扩展停止词字典-->
<entry key="ext_stopwords"></entry>
<!--用户可以在这里配置远程扩展字典 -->
<!-- <entry key="remote_ext_dict">words_location</entry> -->
<!--用户可以在这里配置远程扩展停止词字典-->
<!-- <entry key="remote_ext_stopwords">words_location</entry> -->
</properties>
数据相关操作
序列和文档操作,主要学习DSL语法和RestClient客户端的使用方法,DSL为客户端服务。
RestClient初始化
@Component
public class EsUtil {
private RestHighLevelClient client;
public RestHighLevelClient getRestHighLevelClient() {
return client;
}
@PostConstruct
public void config(){
this.client = new RestHighLevelClient(RestClient.builder(
HttpHost.create("http://127.0.0.1:9200")
));
}
public void releaseEs(){
try {
client.close();
} catch (IOException e) {
System.out.println("释放链接失败...");
}
}
}
索引库操作
Mapping属性映射
索引库就类似数据库表,mapping 映射就类似表的结构
我们要向 es 中存储数据,必须先创建“库”和“表”
mapping 是对索引库中文档的约束,常见的 mapping 属性包括:
- type:字段数据类型,常见的简单类型有:
- 字符串:text(可分词的文本)、keyword(精确值,例如:品牌、国家、ip地址)
- 数值:long、integer、short、byte、double、float、
- 布尔:boolean
- 日期:date
- 对象:object
- index:是否创建索引,默认为 true
- analyzer:使用哪种分词器
- properties:该字段的子字段
DSL,geo_point为地理坐标,all可以整合其他字段,其他字段使用"copy_to": "all"即可
ES中支持两种地理坐标数据类型:
- geo_point:由纬度(latitude)和经度(longitude)确定的一个点。例如:"32.8752345, 120.2981576"
- geo_shape:有多个 geo_point 组成的复杂几何图形。例如一条直线,"LINESTRING −77.0365338.897676,−77.00905138.889939"
PUT /hotel
{
"mappings": {
"properties": {
"id": {
"type": "keyword"
},
"name":{
"type": "text",
"analyzer": "ik_max_word",
"copy_to": "all"
},
"address":{
"type": "keyword",
"index": false
},
"price":{
"type": "integer"
},
"score":{
"type": "integer"
},
"brand":{
"type": "keyword",
"copy_to": "all"
},
"city":{
"type": "keyword",
"copy_to": "all"
},
"starName":{
"type": "keyword"
},
"business":{
"type": "keyword"
},
"location":{
"type": "geo_point"
},
"pic":{
"type": "keyword",
"index": false
},
"all":{
"type": "text",
"analyzer": "ik_max_word"
}
}
}
}
修改索引库
倒排索引结构虽然不复杂,但是一旦数据结构改变(比如改变了分词器),就需要重新创建倒排索引,这简直是灾难。因此索引库一旦创建,无法修改 mapping
虽然无法修改 mapping 中已有的字段,但是却允许添加新的字段到 mapping 中,不会对倒排索引产生影响。
PUT /索引库名/_mapping
{
"properties": {
"新字段名":{
"type": "integer"
}
}
}
删除索引库
DELETE /索引库名
查询索引库
GET /数据库名
不带格式复制到IDEA快捷键是 ctrl+shift+v
@Autowired
EsUtil esUtil;
public void indexCreatTest() throws IOException {
RestHighLevelClient client = esUtil.getRestHighLevelClient();
//指定索引库名
CreateIndexRequest hotel = new CreateIndexRequest("hotel");
//写入JSON数据,这里是Mapping映射
hotel.source("{\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" +
"}", XContentType.JSON);
//创建索引库
client.indices().create(hotel, RequestOptions.DEFAULT);
esUtil.releaseEs();
}
public void getIndexTest() throws IOException {
GetIndexRequest indexRequest = new GetIndexRequest("hotel");
RestHighLevelClient client = esUtil.getRestHighLevelClient();
boolean exists = client.indices().exists(indexRequest, RequestOptions.DEFAULT);
System.out.println("exists = " + exists);
esUtil.releaseEs();
}
文档库操作
DSL和客户端文档操作
新增文档
POST /索引库名/_doc/文档id
{
"字段1": "值1",
"字段2": "值2",
"字段3": {
"子属性1": "值3",
"子属性2": "值4"
}
// ...
}
POST /xn2001/_doc/1
{
"info": "我不会Java",
"email": "jialna@qq.com",
"name": {
"firstName": "钟",
"lastName": "弟弟"
}
}
public void createHotelDoc() throws IOException {
RestHighLevelClient client = esUtil.getRestHighLevelClient();
IndexRequest indexRequest = new IndexRequest("hotel").id(String.valueOf(1L));
indexRequest.source("{\n" +
" \"name\": \"测试名字\",\n" +
" \"address\": \"测试地址\"\n" +
"}",XContentType.JSON);
client.index(indexRequest,RequestOptions.DEFAULT);
esUtil.releaseEs();
}
public void getHotelDoc() throws IOException {
RestHighLevelClient client = esUtil.getRestHighLevelClient();
GetRequest getRequest = new GetRequest("hotel",String.valueOf(1L));
GetResponse response = client.get(getRequest, RequestOptions.DEFAULT);
System.out.println("response = " + response);
}
修改文档
修改文档有两种方式:
- 全量修改:直接覆盖原来的文档
- 增量修改:修改文档中的部分字段
全量修改是覆盖原来的文档,其本质是:
- 根据指定的 id 删除文档
- 新增一个相同 id 的文档
注意:如果根据 id 删除时,id 不存在,第二步的新增也会执行,也就是变成了新增操作
PUT /{索引库名}/_doc/id
{
"字段1": "值1",
"字段2": "值2",
// ... 略
}
PUT /xn2001/_doc/1
{
"info": "我也不会敲代码",
"email": "3300123589@qq.com",
"name": {
"firstName": "弟弟",
"lastName": "钟"
}
}
增量修改是只修改指定 id 匹配的文档中的部分字段
POST /{索引库名}/_update/文档id
{
"doc": {
"字段名": "新的值",
}
}
POST /heima/_update/1
{
"doc": {
"email": "update@qq.com"
}
}
@Test
void testUpdateDocument() throws IOException {
// 1.准备Request
UpdateRequest request = new UpdateRequest("hotel", "61083");
// 2.准备请求参数
request.doc(
"price", "952",
"starName", "四钻"
);
// 3.发送请求
restHighLevelClient.update(request, RequestOptions.DEFAULT);
}
查询文档
GET /{索引库名称}/_doc/{id}
public void getHotelDoc() throws IOException {
RestHighLevelClient client = esUtil.getRestHighLevelClient();
GetRequest getRequest = new GetRequest("hotel",String.valueOf(1L));
GetResponse response = client.get(getRequest, RequestOptions.DEFAULT);
System.out.println("response = " + response);
}
删除文档
DELETE /{索引库名}/_doc/{id}
@Test
void testDeleteDocumentById() throws IOException {
DeleteRequest hotel = new DeleteRequest("hotel", "61083");
restHighLevelClient.delete(hotel,RequestOptions.DEFAULT);
}
搜索结果处理
排序
elasticsearch 默认是根据相关度算分(_score)来排序,但是也支持自定义方式对搜索结果排序。可以排序字段类型有:keyword 类型、数值类型、地理坐标类型、日期类型等
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" // 排序的距离单位
}
}
]
}
GET /hotel/_search
{
"query": {
"match_all": {}
},
"sort": [
{
"_geo_distance" : {
"location": "31.034661,121.612282",
"order" : "asc",
"unit" : "km"
}
}
]
}
获取你的位置的经纬度的方式:https://lbs.amap.com/demo/jsapi-v2/example/map/click-to-get-lnglat
分页
elasticsearch 默认情况下只返回 top10 的数据。而如果要查询更多数据就需要修改分页参数了。
elasticsearch 通过修改 from、size 参数来控制要返回的分页结果:
- from:从第几个文档开始
- size:总共查询几个文档
类似于mysql中的limit ?, ?
GET /hotel/_search
{
"query": {
"match_all": {}
},
"from": 0, // 分页开始的位置,默认为0
"size": 10, // 期望获取的文档总数
"sort": [
{"price": "asc"}
]
}
深度分页问题
现在,我要查询990~1000的数据,查询逻辑要这么写
GET /hotel/_search
{
"query": {
"match_all": {}
},
"from": 990, // 分页开始的位置,默认为0
"size": 10, // 期望获取的文档总数
"sort": [
{"price": "asc"}
]
}
这里是查询990开始的数据,也就是 第990~第1000条 数据。
注意:elasticsearch 内部分页时,必须先查询 0~1000条,然后截取其中的 990 ~ 1000 的这10条

查询TOP1000,如果 es 是单点模式,这并无太大影响。
但是 elasticsearch 将来一定是集群,例如我集群有5个节点,我要查询 TOP1000 的数据,并不是每个节点查询200条就可以了。节点A的 TOP200,在另一个节点可能排到10000名以外了。
因此要想获取整个集群的 TOP1000,必须先查询出每个节点的 TOP1000,汇总结果后,重新排名,重新截取 TOP1000。

当查询分页深度较大时,汇总数据过多,对内存和CPU会产生非常大的压力,因此 elasticsearch 会禁止from+ size 超过10000的请求。
针对深度分页,ES提供了两种解决方案,官方文档:
- search after:分页时需要排序,原理是从上一次的排序值开始,查询下一页数据。官方推荐使用的方式。
- scroll:原理将排序后的文档id形成快照,保存在内存。官方已经不推荐使用。
分页查询的常见实现方案以及优缺点
from + size
- 优点:支持随机翻页
- 缺点:深度分页问题,默认查询上限(from + size)是10000
- 场景:百度、京东、谷歌、淘宝这样的随机翻页搜索
after search
- 优点:没有查询上限(单次查询的size不超过10000)
- 缺点:只能向后逐页查询,不支持随机翻页
- 场景:没有随机翻页需求的搜索,例如手机向下滚动翻页
scroll
- 优点:没有查询上限(单次查询的size不超过10000)
- 缺点:会有额外内存消耗,并且搜索结果是非实时的
- 场景:海量数据的获取和迁移。从ES7.1开始不推荐,建议用 after search方案。
高亮
高亮显示的实现分为两步:
- 1)给文档中的所有关键字都添加一个标签,例如
<em>
标签 - 2)页面给
<em>
标签编写CSS样式
GET /hotel/_search
{
"query": {
"match": {
"FIELD": "TEXT" // 查询条件,高亮一定要使用全文检索查询
}
},
"highlight": {
"fields": { // 指定要高亮的字段
"FIELD": {
"pre_tags": "<em>", // 用来标记高亮字段的前置标签
"post_tags": "</em>" // 用来标记高亮字段的后置标签
}
}
}
}
注意:
- 高亮是对关键字高亮,因此搜索条件必须带有关键字,而不能是范围这样的查询。
- 默认情况下,高亮的字段,必须与搜索指定的字段一致,否则无法高亮
- 如果要对非搜索字段高亮,则需要添加一个属性:
required_field_match=false

DSL 总体结构如下:

RestClient文档查询
发起查询请求

@SpringBootTest
public class HotelSearchTest {
private RestHighLevelClient restHighLevelClient;
@Autowired
private IHotelService hotelService;
@Test
public void match_All() throws IOException {
SearchRequest request = new SearchRequest("hotel");
request.source()
.query(QueryBuilders.matchAllQuery());
SearchResponse response = restHighLevelClient.search(request, RequestOptions.DEFAULT);
}
@BeforeEach
void init() {
this.restHighLevelClient = new RestHighLevelClient(RestClient.builder(
HttpHost.create("http://192.168.211.128:9200")
));
}
@AfterEach
void down() throws IOException {
this.restHighLevelClient.close();
}
}
- 第一步,创建
SearchRequest
对象,指定索引库名 - 第二步,利用
request.source()
构建 DSL,DSL 中可以包含查询、分页、排序、高亮等query()
:代表查询条件,利用QueryBuilders.matchAllQuery()
构建一个 match_all 查询的 DSL
- 第三步,利用
client.search()
发送请求,得到响应
关键的 API 有两个,一个是 request.source()
,其中包含了查询、排序、分页、高亮等所有功能

另一个是 QueryBuilders
,其中包含 match、term、function_score、bool 等各种查询

解析查询响应

Elasticsearch 返回的结果是一个 JSON 字符串,结构包含
hits
:命中的结果total
:总条数,其中的value是具体的总条数值max_score
:所有结果中得分最高的文档的相关性算分hits
:搜索结果的文档数组,其中的每个文档都是一个 json 对象_source
:文档中的原始数据,也是 json 对象
因此,我们解析响应结果,就是逐层解析 JSON 字符串,流程如下
SearchHits
:通过response.getHits()
获取,就是 json 中的最外层的 hits,代表命中的结果SearchHits.getTotalHits().value
:获取总条数信息SearchHits.getHits()
:获取 SearchHit 数组,也就是文档数组SearchHit.getSourceAsString()
:获取文档结果中的_source
,也就是原始的 json 文档数据
@SpringBootTest
public class HotelSearchTest {
private RestHighLevelClient restHighLevelClient;
@Autowired
private IHotelService hotelService;
@Test
public void match_All() throws IOException {
SearchRequest request = new SearchRequest("hotel");
request.source()
.query(QueryBuilders.matchAllQuery());
SearchResponse response = restHighLevelClient.search(request, RequestOptions.DEFAULT);
SearchHits searchHits = response.getHits();
System.out.println("hits.getTotalHits().条数 = " + searchHits.getTotalHits().value);
SearchHit[] hits = searchHits.getHits();
for (SearchHit hit : hits) {
String sourceAsString = hit.getSourceAsString();
HotelDoc hotelDoc = JSON.parseObject(sourceAsString, HotelDoc.class);
System.out.println(hotelDoc);
}
}
@BeforeEach
void init() {
this.restHighLevelClient = new RestHighLevelClient(RestClient.builder(
HttpHost.create("http://192.168.211.128:9200")
));
}
@AfterEach
void down() throws IOException {
this.restHighLevelClient.close();
}
}
match查询

@Test
public void matchQuery() throws IOException {
SearchRequest request = new SearchRequest("hotel");
request.source()
.query(QueryBuilders.matchQuery("all","如家"));
SearchResponse response = restHighLevelClient.search(request, RequestOptions.DEFAULT);
SearchHits searchHits = response.getHits();
System.out.println("hits.getTotalHits().条数 = " + searchHits.getTotalHits().value);
SearchHit[] hits = searchHits.getHits();
for (SearchHit hit : hits) {
String sourceAsString = hit.getSourceAsString();
HotelDoc hotelDoc = JSON.parseObject(sourceAsString, HotelDoc.class);
System.out.println(hotelDoc);
}
}
@Test
public void multiMatchQuery() throws IOException {
SearchRequest request = new SearchRequest("hotel");
request.source()
.query(QueryBuilders.multiMatchQuery("如家","name","brand"));
SearchResponse response = restHighLevelClient.search(request, RequestOptions.DEFAULT);
SearchHits searchHits = response.getHits();
System.out.println("hits.getTotalHits().条数 = " + searchHits.getTotalHits().value);
SearchHit[] hits = searchHits.getHits();
for (SearchHit hit : hits) {
String sourceAsString = hit.getSourceAsString();
HotelDoc hotelDoc = JSON.parseObject(sourceAsString, HotelDoc.class);
System.out.println(hotelDoc);
}
}
精确查询
精确查询主要是两者
- term:词条精确匹配
- range:范围查询

布尔查询
布尔查询是用 must、must_not、filter等方式组合其它查询,代码示例如下

@Test
void testBool() throws IOException {
// 1.准备Request
SearchRequest request = new SearchRequest("hotel");
// 2.准备DSL
request.source()
.query(
QueryBuilders.boolQuery()
.must(QueryBuilders.termQuery("city", "上海"))
.filter(QueryBuilders.rangeQuery("price").lte(300))
);
// 3.发送请求
SearchResponse response = restHighLevelClient.search(request, RequestOptions.DEFAULT);
// 4.解析响应
SearchHits searchHits = response.getHits();
System.out.println("hits.getTotalHits().条数 = " + searchHits.getTotalHits().value);
SearchHit[] hits = searchHits.getHits();
for (SearchHit hit : hits) {
String sourceAsString = hit.getSourceAsString();
HotelDoc hotelDoc = JSON.parseObject(sourceAsString, HotelDoc.class);
System.out.println(hotelDoc);
}
}
排序、分页
搜索结果的排序和分页是与 query 同级的参数,因此同样是使用 request.source()
来设置。
对应的API如下

@Test
void testPageAndSort() throws IOException {
// 页码,每页大小
int page = 1, size = 5;
// 1.准备Request
SearchRequest request = new SearchRequest("hotel");
// 2.准备DSL
// 2.1.query
request.source().query(QueryBuilders.matchAllQuery());
// 2.2.排序 sort
request.source().sort("price", SortOrder.ASC);
// 2.3.分页 from、size
request.source().from((page - 1) * size).size(5);
// 3.发送请求
SearchResponse response = restHighLevelClient.search(request, RequestOptions.DEFAULT);
// 4.解析响应
SearchHits searchHits = response.getHits();
System.out.println("hits.getTotalHits().条数 = " + searchHits.getTotalHits().value);
SearchHit[] hits = searchHits.getHits();
for (SearchHit hit : hits) {
String sourceAsString = hit.getSourceAsString();
HotelDoc hotelDoc = JSON.parseObject(sourceAsString, HotelDoc.class);
System.out.println(hotelDoc);
}
}
高亮
- 查询的 DSL:其中除了查询条件,还需要添加高亮条件,同样是与 query 同级。
- 结果解析:结果除了要解析
_source
文档数据,还要解析高亮结果
高亮请求的构建 API

上述代码省略了查询条件部分,但是高亮查询必须使用全文检索查询,并且要有搜索关键字,将来才可以对关键字高亮.
@Test
void testHighlight() throws IOException {
// 1.准备Request
SearchRequest request = new SearchRequest("hotel");
// 2.准备DSL
// 2.1.query
request.source().query(QueryBuilders.matchQuery("all", "如家"));
// 2.2.高亮
request.source().highlighter(new HighlightBuilder().field("name").requireFieldMatch(false));
// 3.发送请求
SearchResponse response = client.search(request, RequestOptions.DEFAULT);
// 4.解析响应
handleResponse(response); //代码在下文
}
高亮结果解析
高亮的结果与查询的文档结果默认是分离的,并不在一起。
因此解析高亮的代码需要额外处理:

- 第一步:从结果中获取 source。
hit.getSourceAsString()
,这部分是非高亮结果,json 字符串,需要反序列为 HotelDoc 对象 - 第二步:获取高亮结果。
hit.getHighlightFields()
,返回值是一个 Map,key 是高亮字段名称,值是HighlightField 对象,代表高亮值 - 第三步:从 map 中根据高亮字段名称,获取高亮字段值对象 HighlightField
- 第四步:从 HighlightField 中获取 Fragments,并且转为字符串。这部分是真正的高亮字符串
- 第五步:用高亮的结果替换 HotelDoc 中的非高亮结果
完整代码如下:
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);
// 获取高亮结果
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);
}
}
地理坐标查询

相关性得分
function_score 查询结构如下

对应的 JavaAPI 如下

聚合操作
聚合(aggregations)可以让我们极其方便的实现对数据的统计、分析、运算。
- 什么品牌的手机最受欢迎?
- 这些手机的平均价格、最高价格、最低价格?
- 这些手机每月的销售情况如何?
在 Elasticsearch 实现这些统计功能比数据库的 sql 要方便的多,而且查询速度非常快,可以实现近实时搜索效果。
聚合常见的有三类
- 桶(Bucket)聚合:用来对文档做分组
- TermAggregation:按照文档字段值分组,例如按照品牌值分组、按照国家分组
- Date Histogram:按照日期阶梯分组,例如一周为一组,或者一月为一组
- 度量(Metric)聚合:用以计算一些值,比如:最大值、最小值、平均值等
- Avg:求平均值
- Max:求最大值
- Min:求最小值
- Stats:同时求 max、min、avg、sum 等
- 管道(pipeline)聚合:其它聚合的结果为基础做聚合
注意:参加聚合的字段必须是keyword、日期、数值、布尔类型
Bucket聚合语法
例如:我们要统计所有数据中的酒店品牌有几种,其实就是按照品牌对数据分组。此时可以根据酒店品牌的名称做聚合,也就是 Bucket 聚合。
GET /hotel/_search
{
"size": 0, // 设置size为0,结果中不包含文档,只包含聚合结果
"aggs": { // 定义聚合
"brandAgg": { //给聚合起个名字
"terms": { // 聚合的类型,按照品牌值聚合,所以选择term
"field": "brand", // 参与聚合的字段
"size": 20 // 希望获取的聚合结果数量
}
}
}
}

默认情况下,Bucket 聚合会统计 Bucket 内的文档数量,记为 _count
,并且按照 _count
降序排序。
我们可以指定 order 属性,自定义聚合的排序方式
GET /hotel/_search
{
"size": 0,
"aggs": {
"brandAgg": {
"terms": {
"field": "brand",
"order": {
"_count": "asc" // 按照_count升序排列
},
"size": 20
}
}
}
}
默认情况下,Bucket 聚合是对索引库的所有文档做聚合,但真实场景下,用户会输入搜索条件,因此聚合必须是对搜索结果聚合。那么聚合必须添加限定条件。
我们可以限定要聚合的文档范围,只要添加 query 条件即可;
GET /hotel/_search
{
"query": {
"range": {
"price": {
"lte": 200 // 只对200元以下的文档聚合
}
}
},
"size": 0,
"aggs": {
"brandAgg": {
"terms": {
"field": "brand",
"size": 20
}
}
}
}
这次,聚合得到的品牌明显变少了

Metric聚合语法
上面,我们对酒店按照品牌分组,形成了一个个桶。现在我们需要对桶内的酒店做运算,获取每个品牌的用户评分的 min、max、avg 等值。
这就要用到 Metric 聚合了,例如 stats 聚合:就可以获取 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 的聚合内部嵌套的子聚合。因为我们需要在每个桶分别计算。
另外,我们还可以给聚合结果做个排序,例如按照每个桶的酒店平均分做排序

RestAPI数据聚合
聚合条件与 query 条件同级别,因此需要使用 request.source()
来指定聚合条件

聚合的结果也与查询结果不同,API 也比较特殊。不过同样是 JSON 逐层解析

@Test
public void testAggregation() throws IOException {
SearchRequest request = new SearchRequest("hotel");
request.source().aggregation(AggregationBuilders.terms("brandAgg").field("brand").size(20));
SearchResponse response = client.search(request, RequestOptions.DEFAULT);
Terms brandAgg = response.getAggregations().get("brandAgg");
List<? extends Terms.Bucket> buckets = brandAgg.getBuckets();
for (Terms.Bucket bucket : buckets) {
String key = bucket.getKeyAsString();
System.out.println("key = " + key);
}
}
自动补全
当用户在搜索框输入字符时,我们应该提示出与该字符有关的搜索项,提示完整词条的功能,就是自动补全了。
拼音分词器
如果我们需要根据拼音字母来推断,因此要用到拼音分词功能。
要实现根据字母做补全,就必须对文档按照拼音分词。插件地址:https://github.com/medcl/elasticsearch-analysis-pinyin

使用 docker volume inspect es-plugins
查看插件目录,将下载的文件解压上传,重启 Elasticsearch
测试用法如下:
POST /_analyze
{
"text": "如家酒店还不错",
"analyzer": "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": { // 自定义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"
}
}
}
}
测试

注意:为了避免搜索到同音字,搜索时不要使用拼音分词器

自动补全查询
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条结果
}
}
}
}
例如一个酒店的索引库完整案例
// 酒店数据索引库
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"
}
}
}
}
JavaAPI

解析响应的代码如下

数据同步
数据同步主要使用到MQ的消息同步机制
elasticsearch 中的数据来自于 mysq l数据库,因此 mysql 数据发生改变时,elasticsearch 也必须跟着改变,这个就是 elasticsearch 与 mysql 之间的数据同步

常见的数据同步方案有三种
- 同步调用
- 异步通知
- 监听 binlog
同步调用
方案一:同步调用

- hotel-demo对外提供接口,用来修改 elasticsearch 中的数据
- 酒店管理服务在完成数据库操作后,直接调用 hotel-demo 提供的接口
异步通知
方案二:异步通知

- hotel-admin 对 mysql 数据库数据完成增、删、改后,发送 MQ 消息
- hotel-demo监听 MQ,接收到消息后完成 elasticsearch 数据修改
监听binlog
方案三:监听binlog

- mysql 开启 binlog 功能
- mysql 完成增、删、改操作都会记录在 binlog 中
- hotel-demo 基于canal 监听 binlog 变化,实时更新 elasticsearch 中的内容
优缺点
方式一:同步调用
- 优点:实现简单,粗暴
- 缺点:业务耦合度高
方式二:异步通知
- 优点:低耦合,实现难度一般
- 缺点:依赖 mq 的可靠性
方式三:监听binlog
- 优点:完全解除服务间耦合
- 缺点:开启 binlog 增加数据库负担、实现复杂度高
Comments | NOTHING