一、DSL 查询文档
1. DSL Query 的分类
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
查询 DSL 的基本语法:
GET /索引库名/_search
{
"query":{
"查询类型":{
"FIELD":"TEXT"
}
}
}
2. 全文检索查询
对用户输入内容分词,常用于搜索框搜索:
① match 查询:全文检索查询的一种,会
对用户输入内容分词,然后去倒排索引
库检索,语法:
② multi_match:与 match 查询类似,只
不过允许同时查询多个字段,语法:
3. 精准查询
精确查询一般是查找 keyword、数值、
日期、boolean 等类型字段,所以不会
对搜索条件分词
常见的有:
① term:根据词条精确值查询
② range:根据值的范围查询
4. 地理坐标查询
① geo_bounding_box:查询 geo_point
值落在某个矩形范围的所有文档
② geo_distance:查询到指定中心点小
于某个距离值的所有文档
5. 复合查询
fuction score:算分函数查询,可以控制
文档相关性算分,控制文档排名
(1) 相关性算分
利用 match 查询时,文档结果会根据与搜
索词条的关联度打分 (_score),返回结果
时按照分值降序排列
TF-IDF:在 elasticsearch5.0 之前,
会随着词频增加而越来越大
BM25:在 elasticsearch5.0 之后,会
随着词频增加而增大,但增长
曲线会趋于水平
(2) Function Score Query
使用 function score query,可以修改文档
的相关性算分 (query score),根据新得到
的算分排序
function score query 定义的三要素:
① 过滤条件:哪些文档要加分
② 算分函数:如何计算 function score
③ 加权方式:function score 与 query
score 如何运算
(3) Boolean Query
布尔查询是一个或多个查询子句的组合
子查询的组合方式有:
must:必须匹配每个子查询,类似 “与”
should:选择性匹配子查询,类似 “或”
must_not:必须不匹配,不参与算分,
类似 “非”
filter:必须匹配,不参与算分
二、搜索结果处理
1. 排序
es 支持对搜索结果排序,默认是根据
相关度算分 (_score) 来排序,可以排
序字段类型有:keyword 类型、数值
类型、地理坐标类型、日期类型等
2. 分页
es 默认情况下只返回 top10 的数据,
而如果要查询更多数据就需要修改分
页参数了,通过修改 from、size 参
数来控制要返回的分页结果
(2) 深度分页问题
ES 是分布式的,所以会面临深度分页问题
例如:按 price 排序后,获取 from = 990,
size =10 的数据:
① 在每个数据分片上都排序并查询前
1000条文档
② 将所有节点的结果聚合,在内存中
重新排序选出前1000条文档
③ 从这1000条中,选取从990开始的
10条文档
如果搜索页数过深,或者结果集 (from
+ size) 越大,对内存和 CPU 的消耗也
越高,因此 ES 设定结果集查询的上限
是10000
深度分页解决方案:
① search after:分页时需要排序,原
理是从上一次的排序值开始,查询
下一页数据 (推荐)
② scroll:原理将排序数据形成快照,
保存在内存 (不推荐使用)
总结:
① from + size
优点:支持随机翻页
缺点:深度分页问题,默认查询上限
(from + size) 是10000
场景:百度、京东、谷歌、淘宝这样
的随机翻页搜索
② after search
优点:没有查询上限 (单次查询的 size
不超过10000)
缺点:只能向后逐页查询,不支持随机
翻页
场景:没有随机翻页需求的搜索,例如
手机向下滚动翻页
③ scroll
优点:没有查询上限 (单次查询的 size
不超过10000)
缺点:会有额外内存消耗,并且搜索结
果是非实时的
场景:海量数据的获取和迁移,从 ES
7.1 开始不推荐
3. 高亮
在搜索结果中把搜索关键字突出显示
原理:
将搜索结果中的关键字用标签标记出
来,在页面中给标签添加 css 样式
搜索结果处理整体语法:
三、RestClient 查询文档
1. 快速入门
(1) 发起查询请求
request.source() 包含了查询、排序、
分页、高亮等所有功能
QueryBuilders 包含 match、term、
function_score、bool 等各种查询
(2) 解析响应
es 返回的结果是一个 JSON 字符串,
结构包含:
hits:命中的结果
total:总条数,其中的 value 是具体的总条数值
max_score:所有结果中得分最高的文档的相关性算分
hits:搜索结果的文档数组,其中的每个文档都是一个json对象
_source:文档中的原始数据,也是json对象
所以解析响应结果,就是逐层解析 JSON 字
符串
@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);
}
}
2. match 查询
与 match_all 差别是查询条件,也就是
query 的部分
Java 代码上的差异主要是 request.source().
query() 中的参数
@Test
void testMatch() throws IOException {
// 1.准备Request
SearchRequest request = new SearchRequest("hotel");
// 2.准备DSL
request.source()
.query(QueryBuilders.matchQuery("all", "如家"));
// 3.发送请求
SearchResponse response = client.search(request, RequestOptions.DEFAULT);
// 4.解析响应
handleResponse(response);
}
3. 精确查询
与之前的查询相比,差异同样在查询条件
4. 复合查询--Boolean Query
@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);
}
5. 排序、分页
搜索结果的排序和分页是与 query 同
级的参数,因此同样是使用 request.
source() 来设置
@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 = client.search(request, RequestOptions.DEFAULT);
// 4.解析响应
handleResponse(response);
}
6. 高亮
与之前代码差异较大:
① 查询的DSL:其中除了查询条件,还
需要添加高亮条件,同样是与 query
同级
② 结果解析:结果除了要解析 _source
文档数据,还要解析高亮结果
(1) 请求的 DSL 构建
@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);
}
(2) 结果解析
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 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);
}
}
四、数据聚合
1. 聚合的种类
聚合 (aggregations) 可以实现对文档
数据的统计、分析、运算,参与聚合
的字段类型必须是:keyword、数值、
日期、布尔
聚合常见的有三类:
① 桶聚合 (Bucket):用来对文档做分组
TermAggregation:按照文档字段值
分组
Date Histogram:按照日期阶梯分组,
例如一周为一组,或者一月为一组
② 度量聚合 (Metric):用以计算一些值,
Avg:求平均值
Max:求最大值
Min:求最小值
Stats:同时求 max、min、avg、sum 等
③ 管道聚合 (pipeline):其它聚合的结果
为基础做聚合
2. DSL 实现聚合
(1) DSL 实现 Bucket 聚合
聚合必须的三要素:
① 聚合名称
② 聚合类型
③ 聚合字段
2) 聚合结果排序
默认情况下,Bucket 聚合会统计 Bucket
内的文档数量,记为 _count,并且按照
_count 降序排序
可以修改结果排序方式:
3) 限定聚合范围
默认情况下,Bucket 聚合是对索引库
的所有文档做聚合,我们可以限定要
聚合的文档范围,只要添加 query 条
件即可
(2) DSL 实现 Metrics 聚合
3. RestAPI 实现聚合
2) 聚合结果解析
@Test
Void testAggregation() throws IOException {
// 1. 准备 Request
SearchRequest request = new SearchRequest("hotel");
// 2. 准备 DSL
// 2.1 设置 size
request.source().size(0);
// 2.2 聚合
request.source().aggregation(AggregationBuilders
.terms("brandAgg")
.field("brand")
.size(10)
);
// 3. 发出请求
SearchResponse response = client.search(request, RequestOptions.DEFAULT);
// 4. 解析结果
Aggregations aggregations = response.getAggregations();
// 4.1 根据聚合名称获取聚合结果
Terms brandTerms = aggregations.get("brandAgg");
// 4.2 获取 buckets
List extends Terms.Bucket> buckets = brandTerms.getBuckets();
// 4.3 遍历
for (Terms.Bucket bucket : buckets) {
//4.4 获取 key
String key = bucket.getKeyAsString();
System.out.println(key);
}
}
五、自动补全
1. 拼音分词器
要实现根据字母做补全,就必须对文档
按照拼音分词,需要安装拼音分词插件
GitHub - medcl/elasticsearch-analysis-pinyin: This Pinyin Analysis plugin is used to do conversion between Chinese characters and Pinyin.
2. 自定义分词器
es 中分词器 (analyzer) 的组成包含三部分:
① character filters:在 tokenizer 之前
对文本进行处理,例如删除字符、替
换字符
② tokenizer:将文本按照一定的规则切
割成词条 (term),例如 keyword,就
是不分词;还有ik_smart
③ tokenizer filter:将 tokenizer 输出的
词条做进一步处理,例如大小写转换、
同义词处理、拼音处理等
在创建索引库时,通过 settings 来配置自定
义的 analyzer(分词器):
// 自定义拼音分词器
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
}
}
}
}
}
① 拼音分词器适合在创建倒排索引
的时候使用,但不能在搜索的时候
使用
② 因此字段在创建倒排索引时应该
用 my_analyzer 分词器,字段在搜
索时应该使用 ik_smart 分词器
3. 自动补全查询
es 中使用 completion suggester 查询
来实现自动补全功能,匹配以用户输
入内容开头的词条并返回
为了提高补全查询的效率,对于文档中字
段的类型有一些约束:
① 参与的字段必须是 completion 类型
② 字段的内容一般是用来补全的多个
词条形成的数组
// 自动补全查询
POST /test/_search
{
"suggest": {
"title_suggest": {
"text": "s", // 关键字
"completion": {
"field": "title", // 补全字段
"skip_duplicates": true, // 跳过重复的
"size": 10 // 获取前10条结果
}
}
}
}
4. RestAPI 实现自动补全
@Test
void testSuggest() throws IOEvxception {
// 1. 准备 Request
SearchRequest request = new SearchRequest("hotel");
// 2. 准备 DSL
request.source().suggest(new SuggestBuilder().addSuggestion(
"suggestions"
SuggestBuilders.completionSuggestion("suggestion")
.prefix("h")
.skipDuplicates(true)
.size(10)
));
// 3. 发出请求
SearchResponse response = client.search(request, RequestOptions.DEFAULT);
// 4. 处理结果
Suggest suggest = response.getSuggest();
// 4.1 根据名称获取补全结果
CompletionSuggestion suggestion = suggest.getSuggestion("hotelSuggestion");
// 4.2 获取 options 并遍历
for (CompletionSuggestion.Entry.Option option : suggestion.getOptions()) {
//4.3 获取一个 option 中的 text,也就是补全的词条
String text = option.getText().string();
System.out.println(text);
}
}
六、数据同步
数据同步:es 中的数据来自于 mysql
数据库,因此 mysql 数据
发生改变时,es 也必须跟
着改变
跨微服务的数据同步问题解决方案
① 同步调用
优点:实现简单,粗暴
缺点:业务耦合度高
② 异步通知
优点:低耦合,实现难度一般
缺点:依赖 mq 的可靠性
③ 监听 binlog
优点:完全解除服务间耦合
缺点:开启 binlog 增加数据库负担、
实现复杂度高
七、集群
单机的 es 做数据存储,必然面临两个问
题,海量数据存储问题和单点故障问题
解决方案:
① 海量数据存储问题:将索引库从逻辑上
拆分为N个分片(shard),存储到多个节
点
② 单点故障问题:将分片数据在不同节点
备份(replica)
1. 搭建 ES 集群
(1) 创建 es 集群
1) 编写一个 docker-compose 文件,内容如下:
version: '2.2'
services:
es01:
image: docker.elastic.co/elasticsearch/elasticsearch:7.12.1
container_name: es01
environment:
- node.name=es01
- cluster.name=es-docker-cluster
- discovery.seed_hosts=es02,es03
- cluster.initial_master_nodes=es01,es02,es03
- bootstrap.memory_lock=true
- "ES_JAVA_OPTS=-Xms512m -Xmx512m"
ulimits:
memlock:
soft: -1
hard: -1
volumes:
- data01:/usr/share/elasticsearch/data
ports:
- 9200:9200
networks:
- elastic
es02:
image: docker.elastic.co/elasticsearch/elasticsearch:7.12.1
container_name: es02
environment:
- node.name=es02
- cluster.name=es-docker-cluster
- discovery.seed_hosts=es01,es03
- cluster.initial_master_nodes=es01,es02,es03
- bootstrap.memory_lock=true
- "ES_JAVA_OPTS=-Xms512m -Xmx512m"
ulimits:
memlock:
soft: -1
hard: -1
volumes:
- data02:/usr/share/elasticsearch/data
networks:
- elastic
es03:
image: docker.elastic.co/elasticsearch/elasticsearch:7.12.1
container_name: es03
environment:
- node.name=es03
- cluster.name=es-docker-cluster
- discovery.seed_hosts=es01,es02
- cluster.initial_master_nodes=es01,es02,es03
- bootstrap.memory_lock=true
- "ES_JAVA_OPTS=-Xms512m -Xmx512m"
ulimits:
memlock:
soft: -1
hard: -1
volumes:
- data03:/usr/share/elasticsearch/data
networks:
- elastic
volumes:
data01:
driver: local
data02:
driver: local
data03:
driver: local
networks:
elastic:
driver: bridge
2) es 运行需要修改一些 linux 系统权限,
修改 /etc/sysctl.conf 文件
① 添加 vm.max_map_count=262144
② 执行命令 sysctl -p
3) 通过 docker-compose 文件启动集群
docker-compose up -d
(2) 集群状态监控
使用 cerebro 来监控 es 集群状态
GitHub - lmenezes/cerebro
(3) 创建索引库
方法一:利用 kibana 的 DevTools 创建索引库
PUT /itcast
{
"settings": {
"number_of_shards": 3, // 分片数量
"number_of_replicas": 1 // 副本数量
},
"mappings": {
"properties": {
// mapping映射定义 ...
}
}
}
方法二:利用 cerebro 创建索引库
4) 查看分片效果
回到首页,即可查看索引库分片效果:
2. 集群脑裂问题
(1) ES 集群的节点角色
(2) ES 集群的分布式查询
es 中的每个节点角色都有自己不同
的职责,因此建议集群部署时,每
个节点都有独立的角色
各节点的作用:
① aster eligible:参与集群选主;主节点可
以管理集群状态、管理分片信息、处理创
建和删除索引库的请求
② data:数据的 CRUD
③ coordinator:路由请求到其它节点;合并
查询到的结果,返回给用户
(3) ES 集群的脑裂
默认情况下,每个节点都是 master eligible
节点,因此一旦 master 节点宕机,其它候
选节点会选举一个成为主节点,当主节点
与其他节点网络故障时,可能发生脑裂问题
为了避免脑裂,需要要求选票超过 (eligible
节点数量 + 1) / 2 才能当选为主,因此
eligible 节点数量最好是奇数,对应配置项
是 discovery.zen.minimum_master_nodes,
在 es7.0 以后,已经成为默认配置,因此一
般不会发生脑裂问题
3. 集群分布式存储
当新增文档时,应该保存到不同分片,保证
数据均衡
coordinating node 通过 hash 算法计算文档应该
存储到哪个分片:
shard = hash(_routing) % number_of_shards
① _routing 默认是文档的 id
② 算法与分片数量有关,因此索引库一旦
创建,分片数量不能修改!
4. 集群分布式查询
es 查询分成两个阶段:
① scatter phase:分散阶段,coordinating
node 会把请求分发到每一个分片
② gather phase:聚集阶段,coordinating
node 汇总 data node 的搜索结果,并处
理为最终结果集返回给用户
5. 集群故障转移
集群的 master 节点会监控集群中的节点状
态,如果发现有节点宕机,会立即将宕机节
点的分片数据迁移到其它节点,确保数据安
全,这个叫做故障转移