1 初识elasticsearch
1.1 了解ES
- elasticsearch的作用:开源的分布式搜索引擎,可以用来实现搜索、日志统计、分析、系统监控等功能
- elastic stack(ELK):以elasticsearch为核心的技术栈,包括beats、Logstash、kibana、elasticsearch
1.2 倒排索引
- 正向索引:根据id索引的方式,但根据词条查询时,必须先逐条获取每个文档,然后判断文档中是否包含所需要的词条,是根据文档找词条的过程;
- 倒排索引:先找到用户要搜索的词条,根据词条得到保护词条的文档的id,然后根据id获取文档。是根据词条找文档的过程。
- 正向索引和倒排索引的优缺点:
- 正向索引:
- 优点:
- 可以给多个字段创建索引
- 根据索引字段搜索、排序速度非常快
- 缺点:
- 根据非索引字段,或者索引字段中的部分词条查找时,只能全表扫描。
- 优点:
- 倒排索引:
- 优点:
- 根据词条搜索、模糊搜索时,速度非常快
- 缺点:
- 只能给词条创建索引,而不是字段
- 无法根据字段做排序
- 优点:
- 正向索引:
1.3 es的一些概念
- 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:擅长海量数据的搜索、分析、计算
- Mysql 与 Elasticsearch 的结合使用:
- 对安全性要求较高的写操作,使用mysql实现
- 对查询性能要求较高的搜索需求,使用elasticsearch实现
- 两者再基于某种方式,实现数据的同步,保证一致性
1.4 安装 es、kibana
- es;
- kibana:提供 elasticsearch 的可视化界面
- IK 分词器插件:
- 分词器作用:
- 创建倒排索引时对文档分词
- 用户搜索时,对输入的内容分词
- IK 分词器模式:
- ik_smart:智能切分,粗粒度
- ik_max_word:最细切分,细粒度
- IK 分词器拓展词条、停用词条:
- 利用config目录的IkAnalyzer.cfg.xml文件添加拓展词典和停用词典
- 在词典中添加拓展词条或者停用词条
- 分词器作用:
2 索引库操作
- 索引库 – 数据库表;
- mapping 映射 – 数据库表的结构约束;
- DSL – 数据库进行 CRUD 进行请求的 sql 语句;
2.1 mapping 映射属性
- mapping 是对索引库中文档的约束;
- 常见 mapping 属性:
- type:字段数据类型,常见的简单类型有:
- 字符串:text(可分词的文本)、keyword(精确值,例如:品牌、国家、ip地址)
- 数值:long、integer、short、byte、double、float、
- 布尔:boolean
- 日期:date
- 对象:object
- index:是否创建索引,默认为true
- analyzer:使用哪种分词器
- properties:该字段的子字段
- type:字段数据类型,常见的简单类型有:
2.2 索引库的 CRUD
- 索引库的 CRUD – 在 Kibana 编写 DSL
2.2.1 创建索引库和映射
- 基本语法:
- 请求方式:PUT
- 请求路径:/索引库名,可以自定义
- 请求参数:mapping映射
- 格式:
PUT /索引库名称
{
"mappings": {
"properties": {
"字段名":{
"type": "text",
"analyzer": "ik_smart"
},
"字段名2":{
"type": "keyword",
"index": "false"
},
"字段名3":{
"properties": {
"子字段": {
"type": "keyword"
}
}
},
// ...略
}
}
}
2.2.2 查询索引库
- 基本语法:
- 请求方式:GET
- 请求路径:/索引库名
- 请求参数:无
- 格式:
GET /索引库名
2.2.3 修改索引库
- 索引库一旦创建,无法修改 mapping – 倒排索引结构本身不复杂,但是一旦改变数据结构(如分词器),整个倒排索引就都需要重新创建!
- 无法修改mapping中已有的字段,但是却允许添加新的字段到mapping中 – 不会对倒排索引产生影响;
- 语法格式:
PUT /索引库名/_mapping
{
"properties": {
"新字段名":{
"type": "integer"
}
}
}
2.2.4 删除索引库
- 语法:
- 请求方式:DELETE
- 请求路径:/索引库名
- 请求参数:无
- 格式:
DELETE /索引库名
2.2.5 总结
- 索引库操作:
- 创建索引库:PUT /索引库名
- 查询索引库:GET /索引库名
- 删除索引库:DELETE /索引库名
- 添加字段:PUT /索引库名/_mapping
3 文档操作
- 文档 – 数据库表中的一行数据
3.1 新增文档
- 语法:
POST /索引库名/_doc/文档id
{
"字段1": "值1",
"字段2": "值2",
"字段3": {
"子属性1": "值3",
"子属性2": "值4"
},
// ...
}
3.2 查询文档
- 语法:
GET /{索引库名称}/_doc/{id}
3.3 删除文档
- 语法:
DELETE /{索引库名}/_doc/id值
3.4 修改文档
- 全量修改:直接覆盖原来的文档,本质是根据指定 id 删除文档然后新增一个相同 id 的文档;
- 语法:
PUT /{索引库名}/_doc/文档id { "字段1": "值1", "字段2": "值2", // ... 略 }
- 增量修改:修改文档中的部分字段:
- 语法:
POST /{索引库名}/_update/文档id { "doc": { "字段名": "新的值", } }
3.5 总结
- 文档操作:
- 创建文档:
POST /{索引库名}/_doc/文档id { json文档 }
- 查询文档:
GET /{索引库名}/_doc/文档id
- 删除文档:
DELETE /{索引库名}/_doc/文档id
- 修改文档:
- 全量修改:
PUT /{索引库名}/_doc/文档id { json文档 }
- 增量修改:
POST /{索引库名}/_update/文档id { "doc": {字段}}
- 全量修改:
- 创建文档:
4 RestAPI – RestClient 操作索引库
- ES 官方提供的用于操作 ES 的各种不同语言的客户端;
- 本质:组装 DSL 语句,通过 http 请求 发送给 ES;
- Java Rest Client 包括两种:
- Java Low Level Rest Client
- Java High Level Rest Client (使用)
4.0 初始化 RestClient
- 初始化 RestHighLevelClient 类对象,建立与elasticsearch的连接:
- 1)引入es的RestHighLevelClient依赖:
<dependency> <groupId>org.elasticsearch.client</groupId> <artifactId>elasticsearch-rest-high-level-client</artifactId> </dependency>
- 2)因为SpringBoot默认的ES版本是7.6.2,需要覆盖默认的ES版本:
<properties> <java.version>1.8</java.version> <elasticsearch.version>7.12.1</elasticsearch.version> </properties>
- 3)初始化RestHighLevelClient:
RestHighLevelClient client = new RestHighLevelClient(RestClient.builder( HttpHost.create("http://ES安装IP:9200") ));
4.1 创建索引库
- 操作步骤:
- 1)创建Request对象。因为是创建索引库的操作,因此Request是CreateIndexRequest。
- 2)添加请求参数,其实就是DSL的JSON参数部分。因为json字符串很长,这里是定义了静态字符串常量MAPPING_TEMPLATE,让代码看起来更加优雅。
- 3)发送请求,client.indices()方法的返回值是IndicesClient类型,封装了所有与索引库操作有关的方法。
@Test
void createHotelIndex() throws IOException {
// 1.创建Request对象
CreateIndexRequest request = new CreateIndexRequest("hotel");
// 2.准备请求的参数:DSL语句
request.source(MAPPING_TEMPLATE, XContentType.JSON);
// 3.发送请求
client.indices().create(request, RequestOptions.DEFAULT);
}
4.2 删除索引库
- 操作步骤:
- 1)创建Request对象。这次是DeleteIndexRequest对象
- 2)准备参数。这里是无参
- 3)发送请求。改用delete方法
@Test
void testDeleteHotelIndex() throws IOException {
// 1.创建Request对象
DeleteIndexRequest request = new DeleteIndexRequest("hotel");
// 2.发送请求
client.indices().delete(request, RequestOptions.DEFAULT);
}
4.3 判断索引库是否存在
- 判断是否存在 – 本质就是查询
- 操作步骤:
- 1)创建Request对象。这次是GetIndexRequest对象
- 2)准备参数。这里是无参
- 3)发送请求。改用exists方法
@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 ? "索引库已经存在!" : "索引库不存在!");
}
4.4 总结
- JavaRestClient操作的核心是用client.indices()方法来获取索引库的操作对象;
- 操作索引库的基本步骤:
- 0.初始化RestHighLevelClient
- 1.创建XxxIndexRequest。XXX是Create、Get、Delete
- 2.准备DSL( Create时需要,其它是无参)
- 3.发送请求。调用RestHighLevelClient#indices().xxx()方法,xxx是create、exists、delete
5 RestClient 操作文档
5.1 新增文档
- 操作步骤:
- 1)创建Request对象
- 2)准备请求参数,也就是DSL中的JSON文档
- 3)发送请求 – 直接使用client.xxx()的API,不再需要client.indices()了
5.2 查询文档
- 操作步骤:
- 1)准备Request对象。这次是查询,所以是GetRequest
- 2)发送请求,得到结果。因为是查询,这里调用client.get()方法
- 3)解析结果,就是对JSON做反序列化
5.3 删除文档
- 操作步骤:
- 1)准备Request对象,因为是删除,这次是DeleteRequest对象。要指定索引库名和id
- 2)准备参数,无参
- 3)发送请求。因为是删除,所以是client.delete()方法
@Test
void testDeleteDocument() throws IOException {
// 1.准备Request
DeleteRequest request = new DeleteRequest("hotel", "61083");
// 2.发送请求
client.delete(request, RequestOptions.DEFAULT);
}
5.4 修改文档
- 全量修改:本质是根据 id 先删除再新增,与新增的 API 完全一样 – 传入的 ID 已存在就是修改,不存在就是新增;
- 增量修改:
- 操作步骤:
- 1)准备Request对象。这次是修改,所以是UpdateRequest
- 2)准备参数。也就是JSON文档,里面包含要修改的字段
- 3)更新文档。这里调用client.update()方法
5.5 批量导入文档
- 批量处理BulkRequest,其本质就是将多个普通的CRUD请求组合在一起发送;
- add 方法,可以添加新增、修改、删除请求,链式添加
- 操作步骤:
- 1)创建Request对象。这里是BulkRequest
- 2)准备参数。批处理的参数,就是其它Request对象,这里就是多个IndexRequest(或者实际需要的操作,批量导入添加就是 IndexRequest)
- 3)发起请求。这里是批处理,调用的方法为client.bulk()方法
5.6 小结
- 文档操作基本步骤:
- 0.初始化RestHighLevelClient
- 1.创建XxxRequest。XXX是Index、Get、Update、Delete、Bulk
- 2.准备参数(Index、Update、Bulk时需要)
- 3.发送请求。调用RestHighLevelClient#.xxx()方法,xxx是index、get、update、delete、bulk
- 4.解析结果(Get时需要)
6 DSL 查询文档
6.1 DSL 查询分类
- 基于 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": {
"查询类型": {
"查询条件": "条件值"
}
}
}
- 查询所有:
// 查询所有
GET /indexName/_search
{
"query": {
"match_all": {
}
}
}
6.2 全文检索查询
- 全文检索查询分类:
- 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 复制到 all 字段,然后直接用单字段查询 all 字段即可。
6.3 精准查询
- 精确查询不会对搜索条件分词,常见精确查询有:
- term:根据词条精确值查询
- range:根据值的范围查询
6.3.1 term 查询
- 语法:
// term查询
GET /indexName/_search
{
"query": {
"term": {
"FIELD": {
"value": "VALUE"
}
}
}
}
6.3.2 range 查询
- 语法:
// range查询
GET /indexName/_search
{
"query": {
"range": {
"FIELD": {
"gte": 10, // 这里的gte代表大于等于,gt则代表大于
"lte": 20 // lte代表小于等于,lt则代表小于
}
}
}
}
6.4 地理坐标查询
- 地理坐标查询:即根据经纬度查询;
6.4.1 矩形范围查询
- 矩形范围查询:geo_bounding_box查询,查询坐标落在某个矩形范围的所有文档;查询时指定矩形的左上、右下两个点的坐标
- 语法:
// 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
}
}
}
}
}
6.4.2 附近查询
- 附近查询:也即距离查询(geo_distance):查询到指定中心点小于某个距离值的所有文档;
- 语法:
// geo_distance 查询
GET /indexName/_search
{
"query": {
"geo_distance": {
"distance": "15km", // 半径
"FIELD": "31.21,121.5" // 圆心
}
}
}
6.5 复合查询
- 复合(compound)查询:将其它简单查询组合起来,实现更复杂的逻辑;
- 常见复合查询分类:
- function score:算分函数查询,可以控制文档相关性算分,控制文档排名;
- bool query:布尔查询,利用逻辑关系组合多个其它的查询,实现复杂搜索
6.5.1 相关性算分
- 打分算法:
- 早期:TF-IDF算法;
- 5.1版本后:BM25算法
6.5.2 算分函数查询
- 语法:
- function score 查询的四部分内容:
- 原始查询条件:query部分,基于这个条件搜索文档,并且基于BM25算法给文档打分,原始算分(query score)
- 过滤条件:filter部分,符合该条件的文档才会重新算分
- 算分函数:符合filter条件的文档要根据这个函数做运算,得到的函数算分(function score),有四种函数
- weight:函数结果是常量
- field_value_factor:以文档中的某个字段值作为函数结果
- random_score:以随机数作为函数结果
- script_score:自定义算分函数算法
- 运算模式:算分函数的结果、原始查询的相关性算分,两者之间的运算方式,包括:
- multiply:相乘
- replace:用function score替换query score
- 其它,例如:sum、avg、max、min
- function score 运行流程
- function score query 定义的三要素:
- 过滤条件:哪些文档要加分
- 算分函数:如何计算function score
- 加权方式:function score 与 query score如何运算
6.5.3 布尔查询
- 布尔查询:一个或多个查询子句的组合;
- 子查询:每一个子句;
- 子查询的组合方式:
- 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 } }}
]
}
}
}
- 搜索时,参与打分的字段越多,查询的性能越差。
7 搜索结果处理
7.1 排序
- es 默认根据相关度算分排序;
- 支持自定义方式对搜索结果排序,支持的字段类型有:keyword 类型、数值类型、地理坐标类型、日期类型
7.1.1 普通字段排序
- 使用于 keyword、数值、日期类型排序
- 语法:
GET /indexName/_search
{
"query": {
"match_all": {}
},
"sort": [
{
"FIELD": "desc" // 排序字段、排序方式ASC、DESC
}
]
}
- 排序条件是数组,可以写多个排序条件这样当前面条件相等时按照后面的条件排序
7.1.2 地理坐标排序
- 含义:指定目标坐标点,计算每一个文档中指定字段(geo_point 类型)的坐标到目标点的距离,根据该距离排序;
- 语法:
GET /indexName/_search
{
"query": {
"match_all": {}
},
"sort": [
{
"_geo_distance" : {
"FIELD" : "纬度,经度", // 文档中geo_point类型的字段名、目标坐标点
"order" : "asc", // 排序方式
"unit" : "km" // 排序的距离单位
}
}
]
}
7.2 分页
7.2.1 基本分页
- 基本语法:
GET /hotel/_search
{
"query": {
"match_all": {}
},
"from": 0, // 分页开始的位置,默认为0
"size": 10, // 期望获取的文档总数
"sort": [
{"price": "asc"}
]
}
7.2.2 深度分页问题
- 深度分页问题:分页深度较大时,集群部署中,需要从每个节点的从开头到 from + size 的所有文档,聚合到一起然后重新排序选择前 from + size 个,最后截取 from 到第 from + size 个文档——这会对内存和 CPU 会产生非常大的压力,es 会禁止 from + size 超过 10000 的请求;
from + size
:- 优点:支持随机翻页
- 缺点:深度分页问题,默认查询上限(from + size)是10000
- 场景:百度、京东、谷歌、淘宝这样的随机翻页搜索
- 解决方案:
- after search:分页时需要排序,原理是从上一次的排序值开始,查询下一页数据。官方推荐使用的方式。
- 优点:没有查询上限(单次查询的size不超过10000)
- 缺点:只能向后逐页查询,不支持随机翻页
- 场景:没有随机翻页需求的搜索,例如手机向下滚动翻页
scroll
:原理是将排序后的文档id形成快照,保存在内存。官方已经不推荐使用- 优点:没有查询上限(单次查询的size不超过10000)
- 缺点:会有额外内存消耗,并且搜索结果是非实时的
- 场景:海量数据的获取和迁移。从ES7.1开始不推荐,建议用 after search方案。
- after search:分页时需要排序,原理是从上一次的排序值开始,查询下一页数据。官方推荐使用的方式。
7.3 高亮
7.3.1 高亮原理
- 高亮显示:搜索时,将搜索的关键字在结果中高亮显示;
- 高亮显示的步骤:
- 1)给文档中的所有关键字都添加一个标签,例如
<em>
标签 - 2)页面给
<em>
标签编写CSS样式
- 1)给文档中的所有关键字都添加一个标签,例如
7.3.2 实现高亮
- 高亮语法:
GET /hotel/_search
{
"query": {
"match": {
"FIELD": "TEXT" // 查询条件,高亮一定要使用全文检索查询
}
},
"highlight": {
"fields": { // 指定要高亮的字段
"FIELD": {
"pre_tags": "<em>", // 用来标记高亮字段的前置标签
"post_tags": "</em>" // 用来标记高亮字段的后置标签
}
}
}
}
- 高亮是对关键字高亮,因此搜索条件必须带有关键字,而不能是范围这样的查询。
- 默认情况下,高亮的字段,必须与搜索指定的字段一致,否则无法高亮
- 如果要对非搜索字段高亮,则需要添加一个属性:required_field_match=false
7.4 总结
- 查询 DSL 是一个包含多个属性的 JSON 对象:
- query:查询条件
- from和size:分页条件
- sort:排序条件
- highlight:高亮条件
8 RestClient 查询文档
- 基本步骤:
- 1)准备 request 对象;
- 2)准备请求参数;
- 3)发起请求;
- 4)解析响应
8.1 快速入门
- 利用 RestClient 实现 match_all 查询
8.1.1 发起查询请求
- 请求发起步骤:
- 第一步,创建
SearchRequest
对象,指定索引库名 - 第二步,利用
request.source()
构建DSL,DSL中可以包含查询、分页、排序、高亮等query()
:代表查询条件,利用QueryBuilders.matchAllQuery()
构建一个match_all查询的DSL
- 第三步,利用client.search()发送请求,得到响应
- 第一步,创建
- 关键 API:
- request.source:
- QueryBuilders:
- request.source:
8.1.2 解析响应
- 返回结果是 JSON 字符串,结构为:
hits
:命中的结果total
:总条数,其中的value是具体的总条数值max_score
:所有结果中得分最高的文档的相关性算分hits
:搜索结果的文档数组,其中的每个文档都是一个json对象_source
:文档中的原始数据,也是json对象
- 解析顺序:
SearchHits
:通过response.getHits()获取,就是JSON中的最外层的hits,代表命中的结果SearchHits -- getTotalHits().value
:获取总条数信息SearchHits -- getHits()
:获取SearchHit数组,也就是文档数组SearchHit -- getSourceAsString()
:获取文档结果中的_source,也就是原始的json文档数据
8.1.3 小结
查询基本步骤:
- 创建SearchRequest对象
- 准备Request.source(),也就是DSL
① QueryBuilders来构建查询条件
② 传入Request.source() 的 query() 方法 - 发送请求,得到结果
- 解析结果(参考JSON结果,从外到内,逐层解析)
8.2 match查询
- 全文检索的 match 和 multi_match 查询与 match_all 的API 基本一致,只是查询条件有差异:
- API :
8.3 精确查询
- 精确查询分类:
- term:词条精确匹配
- range:范围查询
- API :
8.4 布尔查询
8.5 排序、分页
- 搜索结果的排序、分页与 query 是同级的参数,使用
request.source()
设置; - API :
8.6 高亮
- 高亮代码的差异:
- 查询的DSL:其中除了查询条件,还需要添加高亮条件,同样是与query同级。
- 结果解析:结果除了要解析_source文档数据,还要解析高亮结果
8.6.1 高亮请求构建
- 高亮查询查询条件必须使用全文检索查询,并且要有关键字搜索;
- API :
8.6.2 高亮结果解析
- 高亮结果与查询文档结果默认分离:
- 第一步:从结果中获取
source.hit.getSourceAsString()
,这部分是非高亮结果的 json 字符串,需反序列化为对象 - 第二步:获取高亮结果
hit.getHighlightFields()
,是一个Map,key是高亮字段名称,值是HighlightField对象,代表高亮值 - 第三步:从map中根据高亮字段名称,获取高亮字段值对象HighlightField
- 第四步:从HighlightField中获取Fragments,并转为字符串,得到真正的高亮字符串
- 第五步:用高亮的结果替换第一步反序列化对象中的飞高亮结果
- 第一步:从结果中获取