ElasticSearch入门及简单DSL
转自 ElasticSearch | Kyle’s Blog (cyborg2077.github.io)
初识ElasticSearch
了解ES
ElasticSearch的作用
ElasticSearch是一个分布式的搜索引擎
海量数据中快速寻找有相关关键字的条目
搜索引擎中常用
ELK技术栈(elastic stack)
ElasticSearch
结合kibana
、Logstash
、Beats
,也就是elastic stack
(ELK)。被广泛应用在日志数据分析、实时监控等领域- 日志信息帮助快速排查错误
- 而
ElasticSearch
是elastic stack
的核心,负责存储、搜索、分析数据 ElasticSearch
底层又由Lucene
实现
ElasticSearch和Lucene
- ElasticSearch底层是基于Lucene来实现的
- Lucene是一个Java语言的搜索引擎类库,是Apache公司的顶级项目,由DougCutting于1999年研发,官网地址:https://lucene.apache.org/
- Lucene的优势
- 易扩展
- 高性能(基于倒排索引)
- Lucene的缺点
- 只限于Java语言开发
- 学习曲线陡峭
- 不支持水平扩展
- 后来 ElasticSearch 重写了 Lucene
- 相比于Lucene,ElasticSearch具备以下优势
- 支持分布式,可水平扩展
- 提供Restful接口(与语言无关),可以被任意语言调用
总结
- 什么是ElasticSearch?
- 一个开源的分布式搜索引擎,可以用来实现搜索、日志统计、分析、系统监控等功能
- 什么是Elastic Stack(ELK)?
- 它是以ElasticSearch为核心的技术栈,包括beats、Logstash、kibana、elasticsearch
- 什么是Lucene?
- 是Apache的开源搜索引擎类库,提供了搜索引擎的核心API, 他可以说是ElasticSearch的底层实现
倒排索引
- 倒排索引的概念是基于MySQL这样的正向索引而言的
正向索引
什么是倒排索引?先看看什么是正向索引。
例如MySQL中有这样一个数据库表,一般以id作为主键建立聚簇索引,通过id 搜索数据会非常快这种索引就叫正向索引
id | title | price |
---|---|---|
1 | 小米手机 | 3499 |
2 | 华为手机 | 4999 |
3 | 华为小米充电器 | 49 |
4 | 小米手环 | 49 |
而当我们对title进行模糊匹配 like %小米% 即便字段有索引索引也会失效,他会采用逐条扫描的方式进行匹配,如果匹配上才会加入到结果集当中。性能会非常差
倒排索引
倒排索引的解决方案,
倒排索引(倒排数据库表)有两个重要的字段:文档(document) 词条 (term)
- 文档(Document):(对标于MySQL的一行数据)用来搜索的数据,其中的每一条数据就是一个文档。例如一个网页、一个商品,或理解为一个对象
- 词条(Term):(相当于文本的词汇)对文档中的数据或用户搜索数据,利用某种算法分词,得到的具备含义的词语就是词条。
词条(term) | 文档id |
---|---|
小米 | 1,3,4 |
手机 | 1,2 |
华为 | 2,3 |
充电器 | 3 |
手环 | 4 |
倒排索引为词条再创建索引 ,将来查找的时候按照词条来查找,再来定位文档id 返回文档,这也是为什么叫倒排索引的原因(正向索引:根据文档找词条, 倒排索引:根据词条找文档)
正向和倒排
- 那么为什么一个叫做正向索引,一个叫做倒排索引呢?
正向索引
是最传统的,根据id索引的方式。但是根据词条查询是,必须先逐条获取每个文档,然后判断文档中是否包含所需要的词条,是根据文档查找词条的过程
- 而
倒排索引
则相反,是先找到用户要搜索的词条,然后根据词条得到包含词条的文档id,然后根据文档id获取文档,是根据词条查找文档的过程
- 那么二者的优缺点各是什么呢?
- 正向索引
- 优点:可以给多个字段创建索引,根据索引字段搜索、排序速度非常快
- 缺点:根据非索引字段,或者索引字段中的部分词条查找时,只能全表扫描
- 倒排索引
- 优点:根据词条搜索、模糊搜索时,速度非常快
- 缺点:只能给词条创建索引,而不是字段,无法根据字段做排序
- 正向索引
ES的一些概念
ElasticSearch中有很多独有的概念,与MySQL中略有差别,但也有相似之处
文档和字段
ElasticSearch是面向文档(Document)存储的,可以是数据库中的一行数据 ,如一条商品数据,一个订单信息。这一点和MySQL相似,因为你可以将文档理解为一行一行的数据,文档数据会被序列化为json格式后存储在ElasticSearch中,而Json文档中往往包含很多的字段(Field),类似于数据库中的列
如下,
索引和映射
- 索引(Index),就是相同类型的文档的集合 : ES中的索引就相当于MySQL中的数据表,那么ES的索引库相当于MySQL的数据库
- 映射(mapping) 是索引中文档的字段约束信息 , ES中的映射就 类似于表的结构约束
MySQL与ElasticSearch
- 我们统一的把MySQL和ElasticSearch的概念做一下对比
MySQL | Elasticsearch | 说明 |
---|---|---|
数据库 | 好像没有对应的只有一个库 | |
Table(表) | Index(索引/索引库) | 索引(index),就是文档的集合,类似数据库的表(Table) |
Schema(表结构) | Mapping(映射) | Mapping(映射)是索引中文档的约束,例如字段类型约束。类似数据库的表结构(Schema) |
Row(行) | Document(文档) | 文档(Document),就是一条条的数据,类似数据库中的行(Row),文档都是JSON格式 |
Column(列) | Field(字段) | 字段(Field),就是JSON文档中的字段,类似数据库中的列(Column) |
SQL | DSL | DSL是elasticsearch提供的JSON风格的请求语句,用来操作elasticsearch,实现CRUD |
- 二者各有自己擅长之处
MySQL
:产长事务类型操作,可以保证数据的安全和一致性ElasticSearch
:擅长海量数据的搜索、分析、计算
因此在企业中,往往是这二者结合使用
对安全性要求较高的写操作,使用MySQL实现
对查询性能个较高的搜索需求,使用ElasticSearch实现
- 二者再基于某种方式,实现数据的同步,保证一致性
安装ES、Kibana
部署单点ES
- 因为我们还需要部署Kibana容器,因此需要让es和kibana容器互联,这里先创建一个网络(使用docker compose部署可以一键互联,那不需要这个步骤,但是将来有可能不需要kbiana,只需要es,所以先这里手动部署单点es)
docker network create es-net
- 拉取镜像,这里采用的是ElasticSearch的7.12.1版本镜像
docker pull elasticsearch:7.12.1
- 运行docker命令,部署单点ES
docker run -d \
--name es \
-e "ES_JAVA_OPTS=-Xms512m -Xmx512m" \ # 由于ES基于java写的 堆内存大小
-e "discovery.type=single-node" \ # 单点模式启动
-v es-data:/usr/share/elasticsearch/data \
-v es-plugins:/usr/share/elasticsearch/plugins \
--privileged \ # 授予逻辑卷访问权
--network es-net \ # es容器加入到 这个网络当中
-p 9200:9200 \ #HTTP 协议端口 用户用
-p 9300:9300 \ # 集群模式下各个节点互联的接口
elasticsearch:7.12.1
安装好之后,访问虚拟机ip的 9200端口 我的是 http://192.168.101.65:9200/
可以看到以下结果
部署kibana
同样是先拉取镜像,注意版本需要与ES保持一致
docker pull kibana:7.12.1
运行docker命令,部署kibana
docker run -d \
--name kibana \ # 让kibana加入es-net这个网络,与ES在同一个网络中
-e ELASTICSEARCH_HOSTS=http://es:9200 \ # 设置ES的地址,因为kibana和ES在同一个网络,因此可以直接用容器名访问ES
--network=es-net \
-p 5601:5601 \ # 端口映射配置
kibana:7.12.1
成功启动后,打开浏览器访问:http://192.168.101.65:5601/ ,即可以看到结果
kibana中提供了一个DevTools界面,在这个界面中我们可以编写DSL来操作ElasticSearch,并且有对DSL语句的自动补全功能
GET \
等价于上面部署ES后直接访问http://192.168.101.65:9200/ 看,得到的结果是一样的
安装IK分词器
- 默认的分词对中文的支持不是很好,所以这里我们需要安装IK插件
- 在线安装IK插件
# 进入容器内部
docker exec -it es /bin/bash
# 在线下载并安装
./bin/elasticsearch-plugin install https://github.com/medcl/elasticsearch-analysis-ik/releases/download/v7.12.1/elasticsearch-analysis-ik-7.12.1.zip
#退出
exit
#重启容器
docker restart elasticsearch
# 如果在线下载慢,用离线方式: 仅需将离线安装包放到 上面设置好的es数据卷上即可
# 查看数据卷所在位置
docker volume inspect es-plugins
# 如我的是 /var/lib/docker/volumes/es-plugins/_data
# 将课程资料 elasticsearch-analysis-ik-7.12.1 解压后放到查到的目录
#重启容器
docker restart es
- IK分词器包含两种模式
ik_smart
:最少切分ik_max_word
:最细切分
我们来对比一下
标准(默认分词器)
GET /_analyze
{
"analyzer": "ik_smart",
"text": "青春猪头G7人马文不会梦到JK黑丝兔女郎铁驭艾许"
}
搜索结果
发现中文支持不是很好,基本上是一个词一个词的分。。。
{
"tokens" : [
{
"token" : "青",
"start_offset" : 0,
"end_offset" : 1,
"type" : "<IDEOGRAPHIC>",
"position" : 0
},
{
"token" : "春",
"start_offset" : 1,
"end_offset" : 2,
"type" : "<IDEOGRAPHIC>",
"position" : 1
},
{
"token" : "猪",
"start_offset" : 2,
"end_offset" : 3,
"type" : "<IDEOGRAPHIC>",
"position" : 2
},
{
"token" : "头",
"start_offset" : 3,
"end_offset" : 4,
"type" : "<IDEOGRAPHIC>",
"position" : 3
},
{
"token" : "g7",
"start_offset" : 4,
"end_offset" : 6,
"type" : "<ALPHANUM>",
"position" : 4
},
。。。。}
搜索内容
GET /_analyze
{
"analyzer": "ik_smart",
"text": "青春猪头G7人马文不会梦到JK黑丝兔女郎铁驭艾许"
}
结果:
{
"tokens" : [
{
"token" : "青春",
"start_offset" : 0,
"end_offset" : 2,
"type" : "CN_WORD",
"position" : 0
},
{
"token" : "猪头",
"start_offset" : 2,
"end_offset" : 4,
"type" : "CN_WORD",
"position" : 1
},
{
"token" : "G7",
"start_offset" : 4,
"end_offset" : 6,
"type" : "LETTER",
"position" : 2
},
{
"token" : "人",
"start_offset" : 6,
"end_offset" : 7,
"type" : "COUNT",
"position" : 3
},
...}
GET /_analyze
{
"analyzer": "ik_max_word",
"text": "青春猪头G7人马文不会梦到JK黑丝兔女郎铁驭艾许"
}
结果
{
"tokens" : [
{
"token" : "青春",
"start_offset" : 0,
"end_offset" : 2,
"type" : "CN_WORD",
"position" : 0
},
{
"token" : "猪头",
"start_offset" : 2,
"end_offset" : 4,
"type" : "CN_WORD",
"position" : 1
},
{
"token" : "G7",
"start_offset" : 4,
"end_offset" : 6,
"type" : "LETTER",
"position" : 2
},
{
"token" : "G",
"start_offset" : 4,
"end_offset" : 5,
"type" : "ENGLISH",
"position" : 3
},
{
"token" : "7",
"start_offset" : 5,
"end_offset" : 6,
"type" : "ARABIC",
"position" : 4
},
...
}
对比发现 ik_max_word 相比于 ik_smart划分力度更细一些,相对应的 这样 前者内存占用更高,但是更多的匹配项
随着互联网的发展,造词运动
也愈发频繁。出现了许多新词汇,但是在原有的词汇表中并不存在,例如白给
、白嫖
等,所以我们的词汇也需要不断的更新,IK分词器提供了扩展词汇的功能
打开IK分词器的config目录
找到IKAnalyzer.cfg.xml文件,并添加如下内容
<?xml version="1.0" encoding="UTF-8"?> <!DOCTYPE properties SYSTEM "http://java.sun.com/dtd/properties.dtd"> <properties> <comment>IK Analyzer 扩展配置</comment> <!--用户可以在这里(当前文件夹中ext.dic)配置自己的扩展字典 --> <entry key="ext_dict">ext.dic</entry> <!--用户可以在这里(当前文件夹中stopword.dic)配置自己的扩展停止词字典--> <entry key="ext_stopwords">stopword.dic</entry> </properties>
3.在IKAnalyzer.cfg.xml同级目录下新建ext.dic和stopword.dic,添加扩展词和停止词
记得编码设置为utf-8编码
4.重启es
5.再次检索
会发现原本识别不了“黑丝”如果你添加了 则会加入 有“黑丝”这个词作为词条出现,不会再将其一字一分词,如果在停止词典中加了“的” “了”等词,则展示结果中不会再出现语气词
总结
- 分词器的作用是什么?
- 创建倒排索引时对文档分词
- 用户搜索时,对输入的内容分词
- IK分词有几种模式?
- ik_smart:智能切分,粗粒度
- ik_max_word:最细切分,细粒度
- IK分词器如何拓展词条?如何停用词条?
- 利用config目录的IKAnalyzer.cfg.xml文件添加拓展词典和停用词典
- 在词典中添加拓展词条或者停用词条
索引库操作
- 索引库就类似于数据库表,mapping映射就类似表的结构
- 我们要向es中存储数据,必须先创建
库
和表
索引库约束mapping
mapping是对索引库中文档的约束,常见的mapping属性包括
type
:字段数据类型,常见的简单类型有- 字符串:text(可分词文本)、keyword(精确值,例如:品牌、国家、ip地址;因为这些词,分词之后毫无意义)
- 数值:long、integer、short、byte、double、float
- 布尔:boolean
- 日期:date
- 对象:object
index
:是否创建索引(是否参与搜索),默认为true,默认情况下会对所有字段创建倒排索引(如果是文档会建立先分词,后建立倒排索引,如果是其他数据类型也会建立倒排索引,但是是整体作为倒排索引的词条),即每个字段都可以被搜索。在创建字段映射时,一定要判断一下这个字段是否参与搜索,如果不参与搜索,则将其设置为false;此外对于字符串字段一些词只有作为整体才有意义比如品牌的名字,邮箱号等,不需要拆分 ,但如果你这时为text类型,它会默认先分词后建立倒排索引,但是我们不需要,可以将其设置为 这个字段的类型(type)不要设置为 text 而是 keyword
analyzer
:针对text类型的字段,如果建立了倒排索引,使用哪种分词器?中文的话,一定要设置自己的分词器,不然默认的分词器效果很差properties
:该字段的子字段表名特别要注意的是
id
是索引库中特殊的一个字段,他必须唯一而且是字符串类型可以认为是keyweord类型,一般会默认创建,不用自己单独创建,查询表结构的时候也也不会显示出来,但是当查询/增删改具体文档的时候必须要指定id,id也会显示出来
如如果要存储这样一个json文档,
{
"age": 32,
"weight": 48,
"isMarried": false,
"info": "次元游击兵--恶灵",
"email": "wraith@Apex.net",
"score": [99.1, 99.5, 98.9],
"name": {
"firstName": "雷尼",
"lastName": "布莱希"
}
}
对应的每个字段映射(mapping):
| 字段名 fieldname | 类型type | 是否建立索引index | 分词器analyzer |
| :———————: | :———: | :———————-: | :——————: |
| age | integer | true | null |
| weight | float | true | null |
| isMarried | boolean | true | null |
| info | text | true | ik_smart |
| email | keyword | false | null |
| score | float | true | null |
| name | object | | |
| name.firstName | keyword | true | null |
| name.lastName | keyword | true | null |
- 其中
score
:虽然是数组,但是我们只看其中元素的类型,类型为float(ES不支持数组,但是允许该字段有多个该类型的参数);email
不参与搜索,所以index
为false
;info
参与搜索,且需要分词,所以需要设置一下分词器
小结
- mapping常见字段的属性(properties)有哪些?
- type:数据类型
- index:是否创建索引
- analyzer:选择分词器(针对于text类型)
- properties:子字段 (针对于object类型)
- type常见的有哪些
- 字符串:text、keyword
- 数值:long、integer、short、byte、double、float
- 布尔:boolean
- 日期:date
- 对象:object (子字段)
索引库的CRUD
- 这里是使用的Kibana提供的DevTools编写DSL语句
创建索引库和映射
- 基本语法
- 请求方式:
PUT
- 请求路径:
/{索引库名}
,可以自定义 - 请求参数:
mapping映射
- 请求方式:
PUT /{索引库名}
{
"mappings": {
"properties": {
"字段名1": {
"type": "text ",
"analyzer": "standard"
},
"字段名2": {
"type": "text",
"index": true
},
"字段名3": {
"type": "object", // 这一句 不用写(因为下面有 properties,就表明这是一个对象了)
"properties": {
"子字段1": {
"type": "keyword"
},
"子字段2": {
"type": "keyword"
}
}
}
}
}
}
如针对下面的文档添加mapping映射
{
"info": "次元游击兵--恶灵",
"email": "wraith@Apex.net",
"name": {
"firstName": "雷尼",
"lastName": "布莱希"
}
}
PUT /test001
{
"mappings": {
"properties": {
"info": {
"type": "text",
"analyzer": "ik_smart"
},
"email": {
"type": "keyword",
"index": false
},
"name": {
"type": "object",
"properties": {
"firstName": {
"type": "keyword"
},
"lastName": {
"type": "keyword"
}
}
}
}
}
}
结果
{
"acknowledged" : true,
"shards_acknowledged" : true,
"index" : "test001"
}
查询索引库
- 基本语法
- 请求方式:
GET
- 请求路径:
/{索引库名}
- 请求参数:
无
- 请求方式:
- 格式:
GET /{索引库名}
- 举例
GET /test001
- 结果
{
"test001" : {
"aliases" : { },
"mappings" : {
"properties" : {
"email" : {
"type" : "keyword",
"index" : false
},
"info" : {
"type" : "text",
"analyzer" : "ik_smart"
},
"name" : {
"properties" : {
"firstName" : {
"type" : "keyword"
},
"lastName" : {
"type" : "keyword"
}
}
}
}
},
"settings" : {
"index" : {
"routing" : {
"allocation" : {
"include" : {
"_tier_preference" : "data_content"
}
}
},
"number_of_shards" : "1",
"provided_name" : "test001",
"creation_date" : "1681267481035",
"number_of_replicas" : "1",
"uuid" : "waQIx2FrQGKz79Ca2nD6sA",
"version" : {
"created" : "7120199"
}
}
}
}
}
修改索引库(无法真正修改只能添加属性。。。)
- 基本语法
- 请求方式:
PUT
- 请求路径:
/{索引库名}/_mapping
- 请求参数:
mapping映射
- 请求方式:
PUT /{索引库名}/_mapping
{
"properties": {
"新字段名":{
"type": "integer"
}
}
}
- 倒排索引结构虽然不复杂,但是一旦数据结构改变(比如改变了分词器),就需要重新创建倒排索引,这简直是灾难。因此索引库
一旦创建,就无法修改mapping
- 虽然无法修改mapping中已有的字段,但是却允许添加新字段到mapping中,因为不会对倒排索引产生影响
- 如果强行改,则会报错
删除索引库
- 基本语法:
- 请求方式:
DELETE
- 请求路径:
/{索引库名}
- 请求参数:无
- 请求方式:
DELETE /{索引库名}
总结
- 索引库操作有哪些?
- 创建索引名:PUT /{索引库名}
- 查询索引库:GET /{索引库名}
- 删除索引库:DELETE /{索引库名}
- 添加字段:PUT /{索引库名}/_mapping
文档操作
文档有一个特殊字段是单独列出来的—— id
新增文档
POST /{索引库名}/_doc/{文档id}
{
"字段1": "值1",
"字段2": "值2",
"字段3": {
"子属性1": "值3",
"子属性2": "值4"
},
// ...
}
示例
POST /test001/_doc/1
{
"info": "次元游记兵--恶灵",
"email": "wraith@Apex.net",
"name": {
"firstName": "雷尼",
"lastName": "布莱希"
}
}
查询文档
- 根据rest风格,新增是post,查询应该是get,而且一般查询都需要条件,这里我们把文档id带上
- 语法
GET /{索引库名}/_doc/{id}
示例
示例:根据id删除数据, 若删除的文档不存在, 则result为not found
GET /test001/_doc/1
删除文档
删除使用DELETE请求,同样,需要根据id进行删除
语法
DELETE /{索引库名}/_doc/{id}
示例:根据id删除数据, 若删除的文档不存在, 则result为not found
DELETE /test001/_doc/1
修改文档
- 修改有两种方式
- 全量修改:直接覆盖原来的文档
- 增量修改:修改文档中的部分字段
全量修改
全量修改是覆盖原来的文档,其本质是
根据指定的id删除文档
新增一个相同id的文档
注意:如果根据id删除时,id不存在,第二步的新增也会执行,也就从修改变成了新增操作了
语法
PUT /{索引库名}/_doc/{文档id}
{
"字段1": "值1",
"字段2": "值2",
// ... 略
}
- 示例
PUT /test001/_doc/1
{
"info": "爆破专家--暴雷",
"email": "@Apex.net",
"name": {
"firstName": "沃尔特",
"lastName": "菲茨罗伊"
}
}
增量修改
- 增量修改只修改指定id匹配文档中的部分字段
- 语法
POST /{索引库名}/_update/{文档id}
{
"doc": {
"字段名": "新的值",
...
}
}
- 示例
POST /test001/_update/1
{
"doc":{
"email":"BestApex@Apex.net",
"info":"恐怖G7人--马文"
}
}
总结
- 文档的操作有哪些?
- 创建文档:POST /{索引库名}/_doc/{id}
- 查询文档:GET /{索引库名}/_doc/{id}
- 删除文档:DELETE /{索引库名}/_doc/{id}
- 修改文档
- 全量修改:PUT /{索引库名}/_doc/{id} —- 当id不存在时 等价于 创建文档
- 增量修改:POST /{索引库名}/_update/{id}
DSL查询文档(非id查询)
ElasticSearch的查询依然是基于JSON风格的DSL来实现的
DSL查询分类
ElasticSearch提供了基于DSL来定义查询。常见的查询类型包括
查询所有
:查询出所有数据,一般测试用。例如- 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、match、multi_match等
"查询条件": "条件值"
}
}
}
查询所有
这里以查询所有为例
- 查询类型为
match_all
- 没有查询条件
GET /hotel/_search
{
"query": {
"match_all": { }
}
}
// 这一句等价于
GET /hotel/_search
- 其他的无非就是
查询类型
和查询条件
的变化
全文检索的查询
使用场景
- 全文检索的查询流程基本如下
- 根据用户搜索的内容做分词,得到词条
- 根据词条去倒排索引库中匹配,得到文档id
- 根据文档id找到的文档,返回给用户
- 比较常用的场景包括
- 商城的输入框搜索
- 百度输入框搜索
- 因为是拿着词条去匹配,因此参与搜索的字段也必须是可分词的text类型的字段
基本语法
- 常见的全文检索包括
- match查询:单字段查询
- multi_match查询:多字段查询,任意一个字段符合条件就算符合查询条件
- match查询语法如下
GET /indexName/_search
{
"query": {
"match": {
"FIELD": "TEXT"
}
}
}
- multi_match语法如下
GET /indexName/_search
{
"query": {
"multi_match": {
"fields": ["FIELD1", "FIELD2"]
}
}
}
示例
查询上海外滩的酒店数据
- 以match查询示例,这里的
all
字段(虽然看不到,但是它真的存在于表结构中相当于复合索引)是之前由name
、city
、business
这三个字段拷贝得来的, - 也就是 这要这三个字段出现 任意一个字段匹配上了 ”上海外滩“ 就能”命中”
GET /hotel/_search
{
"query": {
"match": {
"all": "上海外滩"
}
}
}
- 结果:
{
"took" : 5,
"timed_out" : false,
"_shards" : {
"total" : 1,
"successful" : 1,
"skipped" : 0,
"failed" : 0
},
"hits" : {
"total" : {
"value" : 82,
"relation" : "eq"
},
"max_score" : 11.069907,
"hits" : [****] // 我折叠了/其实也没有全部显示,为了避免差太多内存泄漏,只会显示部分
}
}
命中了82条数据
- 以multi_match查询示例
GET /hotel/_search
{
"query": {
"multi_match": {
"query": "上海外滩",
"fields": ["brand", "city", "business"]
}
}
}
- 可以看到,这两种查询的结果是一样的,为什么?
- 因为我们将
name
、city
、business
的值都利用copy_to
复制到了all字段中,因此根据这三个字段搜索和根据all字段搜索的结果当然一样了 - 但是搜索的字段越多,对查询性能影响就越大,因此建议采用
copy_to
,然后使用单字段查询的方式
- 因为我们将
小结
- match和multi_match的区别是什么?
- match:根据一个字段查询
- multi_match:根据多个字段查询,参与查询的字段越多,查询性能就越差
精确查询
- 精确查询一般是查找keyword、数值、日期、boolean等类型字段(品牌,城市等信息)。所以不会对搜索条件分词。常见的有
term
:根据词条精确值查询range
:根据值的范围查询
term查询
因为紧缺查询的字段是不分词的字段,因此查询的条件也必须是部分词的词条。查询时,用户输入的内容跟字段值完全匹配时才认为符合条件。如果用户输入的内容过多或过少,都会搜索不到数据
语法说明
GET /indexName/_search { "query": { "term": { "FIELD": { "value": "VALUE" } } } }
示例:查询北京的酒店数据
GET /hotel/_search { "query": { "term": { "city": { "value": "北京" } } } }
但是当搜索的内容不是词条时,而是多个词语组成的短语时,反而搜索不到(不会自动分词,是作为一个整体查询)
range查询
- 范围查询,一般应用在对数值类型做范围过滤的时候。例如做价格范围的过滤(数字,日期,keyword)
- 基本语法
GET /hotel/_search
{
"query": {
"range": {
"FIELD": {
"gte": 10, //这里的gte表示大于等于,gt表示大于
"lte": 20 //这里的let表示小于等于,lt表示小于
}
}
}
}
示例:查询酒店价格在1000~3000的酒店
GET /hotel/_search
{
"query": {
"range": {
"price": {
"gte": 1000,
"lte": 3000
}
}
}
}
小结
- 精确查询常见的有哪些?
- term查询:根据词条精确匹配,一般搜索keyword类型、数值类型、布尔类型、日期类型字段
- range查询:根据数值范围查询,可以使数值、日期的范围
地理坐标查询
- 所谓地理坐标查询,其实就是根据经纬度查询,官方文档:https://www.elastic.co/guide/en/elasticsearch/reference/current/geo-queries.html
- 常见的使用场景包括
- 携程:搜索附近的酒店
- 滴滴:搜索附近的出租车
- 微信:搜索附近的人
矩形范围查询 (geo_bounding_box)
- 矩形范围查询,也就是geo_bounding_box查询,查询坐标落在某个矩形范围内的所有文档
- 查询时。需指定矩形的左上、游戏啊两个点的坐标,然后画出一个矩形,落在该矩形范围内的坐标,都是符合条件的文档
- 基本语法
GET /indexName/_search
{
"query": {
"geo_bounding_box": {
"FIELD": {
"top_left": { // 左上点
"lat": 31.1, // lat: latitude 纬度
"lon": 121.5 // lon: longitude 经度
},
"bottom_right": { // 右下点
"lat": 30.9, // lat: latitude 纬度
"lon": 121.7 // lon: longitude 经度
}
}
}
}
}
示例
GET /hotel/_search
{
"query": {
"geo_bounding_box": {
"location": {
"top_left": {
"lat": 31.1,
"lon": 121.5
},
"bottom_right": {
"lat": 30.9,
"lon": 121.7
}
}
}
}
}
附近查询(geo_distance)
- 附近查询,也叫做举例查询(geo_distance):查询到指定中心点的距离小于等于某个值的所有文档
- 换句话说,也就是以指定中心点为圆心,指定距离为半径,画一个圆,落在圆内的坐标都算符合条件
- 语法说明
示例:查询我附近3km内的酒店文档
GET /hotel/_search
{
"query": {
"geo_distance": {
"distance": "3km", // 半径
"location": "39.9, 116.4" // 圆心
}
}
}
复合查询
- 复合(compound)查询:复合查询可以将其他简单查询组合起来,实现更复杂的搜索逻辑,常见的有两种
- function score:算分函数查询,可以控制文档相关性算分,控制文档排名(例如搜索引擎的排名,第一大部分都是广告)
- bool query:布尔查询,利用逻辑关系组合多个其他的查询,实现复杂搜索
相关性算分 (score)
- 当我们利用match查询时,文档结果会根据搜索词条的关联度打分(_score),返回结果时按照分值降序排列
- 例如我们搜索虹桥如家,结果如下
[
{
"_score" : 17.850193,
"_source" : {
"name" : "虹桥如家酒店真不错",
}
},
{
"_score" : 12.259849,
"_source" : {
"name" : "外滩如家酒店真不错",
}
},
{
"_score" : 11.91091,
"_source" : {
"name" : "迪士尼如家酒店真不错",
}
}
]
“_score”即为相关度算分:他早期是根据TF(词条出现频率)计算的,后来发现这种算法效果很差,于是引入了 TF-IDF(逆文档频率)算法
以词条”如家“为例,包含”如家“的文档总数为3,假设总文档数也为3,那么IDF权重就是 log(3/3)=0;
以词条”虹桥“为例,包含”虹桥“的文档总数为1,假设总文档数也为3,那么IDF权重就是 log(3/1)=log3;
则词条虹桥的权重更高,出现虹桥这个词的词条的分数更高
再后来的5.1版本升级中,ES将算法改进为BM25算法,公式如下
TF-IDF算法有一种缺陷,就是词条频率越高,文档得分也会越高,单个词条对文档影响较大。而BM25则会让单个词条的算分有一个上限,曲线更平滑
- 小结:ES会根据词条和文档的相关度做打分,算法有两种
- TF-IDF算法
- BM25算法, ES 5.1版本后采用的算法
算分函数查询 (function score query)
- 根据相关度打分是比较合理的需求,但是合理的并不一定是产品经理需要的
- 以某搜索引擎为例,你在搜索的结果中,并不是相关度越高就越靠前,而是谁掏的钱多就让谁的排名越靠前
- 要想控制相关性算分,就需要利用ES中的
function score
查询了
语法说明
- 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的运行流程如下
- 根据
原始条件
查询搜索文档,并且计算相关性算分,称为原始算法(query score) - 根据
过滤条件
,过滤文档 - 符合
过滤条件
的文档,基于算分函数
运算,得到函数算分
(function score) - 将
原始算分
(query score)和函数算分
(function score)基于运算模式
做运算,得到最终给结果,作为相关性算分
- 根据
- 因此,其中的关键点是:
- 过滤条件:决定哪些文档的算分被修改
- 算分函数:决定函数算分的算法
- 运算模式:决定最终算分结果
案例
需求:给如家
这个品牌的酒店排名靠前一点
思路:过滤条件为"brand": "如家"
,算分函数和运算模式我们可以暴力一点,固定算分结果相乘
对应的DSL语句如下,我们搜索外滩的酒店,对如家品牌过滤,最终的运算结果是10倍的原始算分
GET /hotel/_search
{
"query": {
"function_score": {
"query": {
"match": {
"all": "外滩"
}
},
"functions": [
{
"filter": {
"term": {
"brand": "如家"
}
},
"weight": 10
}
],
"boost_mode": "multiply"
}
}
}
可以看到,如家的算分达到了38,而第二名仅有6,成功将如家品牌的酒店提升到第一名
布尔查询 (bool query)
布尔查询是一个或多个子查询的组合,每一个子句就是一个子查询。子查询的组合方式有
must
:必须匹配每个子查询,类似与
should
:选择性匹配子查询,类似或
must_not
:必须不匹配,不参与算分
,类似非
filter
:必须匹配,不参与算分
例如在搜索酒店时,除了关键字搜索外,我们还可能根据酒店品牌、价格、城市等字段做过滤
- 每一个不同的字段,其查询条件、方式都不一样,必须是多个不同的查询,而要组合这些查询,就需要用到布尔查询了
需要注意的是,搜索时,参与打分的字段越多,查询的性能就越差,所以在多条件查询时
- 搜索框的关键字搜索,是全文检索查询,使用must查询,参与算分
- 其他过滤条件,采用filter和must_not查询,不参与算分
案例
需求:搜索名字中包含如家
,价格不高于400
,在坐标39.9, 116.4
周围10km
范围内的酒店
分析:
- 名称搜索,属于全文检索查询,应该参与算分,放到
must
中 - 价格不高于400,用range查询,属于过滤条件,不参与算分,放到
must_not
中 - 周围10km范围内,用geo_distance查询,属于过滤条件,放到
filter
中
GET /hotel/_search
{
"query": {
"bool": {
"must": [
{
"match": {
"name": "如家"
}
}
],
"must_not": [
{
"range": {
"price": {
"gt": 400
}
}
}
],
"filter": [
{
"geo_distance": {
"distance": "10km",
"location": {
"lat": 31.21,
"lon": 121.5
}
}
}
]
}
}
}
案例
需求:搜索城市在上海,品牌为皇冠假日
或华美达
,价格不低于500
,且用户评分在45分
以上的酒店
GET /hotel/_search
{
"query": {
"bool": {
"must": [
{"term": {
"city": {
"value": "上海"
}
}}
],
"should": [
{"term": {
"brand": {
"value": "皇冠假日"
}
}},
{"term": {
"brand": {
"value": "华美达"
}
}}
],
"must_not": [
{"range": {
"price": {
"lte": 500
}
}}
],
"filter": [
{"range": {
"score": {
"gte": 45
}
}}
]
}
}
}
- 如果细心一点,就会发现这里的should有问题,must和should一起用的时候,should会不生效,结果中会查询到除了
皇冠假日
和华美达
之外的品牌。 - 对于DSL语句的解决方案比较麻烦,需要在must里再套一个bool,里面再套should,但是对于Java代码来说比较容易修改
- 小结:布尔查询有几种逻辑关系?
- must:必须匹配的条件,可以理解为
与
- should:选择性匹配的条件,可以理解为
或
- must_not:必须不匹配的条件,不参与打分,可以理解为
非
- filter:必须匹配的条件,不参与打分
- must:必须匹配的条件,可以理解为
搜索结果处理
- 搜索的结果可以按照用户指定的方式去处理或展示
排序
- ES默认是根据相关度算分(_score)来排序,但是也支持自定义方式对搜索结果排序。可以排序的字段有:keyword类型、数值类型、地理坐标类型、日期类型等
普通字段排序
keyword、数值、日期类型排序的语法基本一致
GET /hotel/_search
{
"query": {
"match_all": {
}
},
"sort": [
{
"FIELD": {
"order": "desc"
},
"FIELD": {
"order": "asc"
}
}
]
}
排序条件是一个数组,也就是可以写读个排序条件。按照声明顺序,当第一个条件相等时,再按照第二个条件排序,以此类推
案例
需求:酒店数据按照用户评价(score)降序排序,评价相同再按照价格(price)升序排序
GET /hotel/_search
{
"query": {
"match_all": {}
},
"sort": [
{
"score": {
"order": "desc"
},
"price": {
"order": "asc"
}
}
]
}
地理坐标排序
语法说明
GET /indexName/_search
{
"query": {
"match_all": {}
},
"sort": [
{
"_geo_distance": {
"FIELD": {
"lat": 40,
"lon": -70
},
"order": "asc",
"unit": "km"
}
}
]
}
这个查询的含义是
- 指定一个坐标,作为目标点
计算每一个文档中,指定字段(必须是geo_point类型)的坐标,到目标点的距离是多少
根据距离排序
案例
需求:实现酒店数据按照到你位置坐标的距离升序排序
GET /hotel/_search
{
"query": {
"match_all": {}
},
"sort": [
{
"_geo_distance": {
"location": {
"lat": 39.9,
"lon": 116.4
},
"order": "asc",
"unit": "km"
}
}
]
}
从结果中可以看到,最近的是1.6km
分页
- ES默认情况下只返回
top10
的数据。而如果要查询更多数据就需要修改分页参数了。 - ES中通过修改from、size参数来控制要返回的分页结果
from
:从第几个文档开始size
:总共查询几个文档
- 类似于mysql中的
limit ?, ?
start , length
基本的分页
- 分页的基本语法如下
GET /indexName/_search
{
"query": {
"match_all": {}
},
"from": 0,
"size": 20
}
深度分页问题
现在,我要查询990~1000条数据
GET /hotel/_search
{
"query": {
"match_all": {}
},
"from": 990,
"size": 10,
"sort": [
{
"price": {
"order": "asc"
}
}
]
}
- 这里是查询990开始的数据,也就是第991~第1000条数据
- 不过,ES内部分页时,必须先查询
0~1000
条,然后截取其中990~1000
的这10条
- 查询
TOP1000
,如果ES是单点模式,那么并无太大影响 - 但是ES将来一定是集群部署模式,例如我集群里有5个节点,我要查询
TOP1000
的数据,并不是每个节点查询TOP200
就可以了。 - 因为节点A的TOP200,可能在节点B排在10000名开外了
- 因此想获取整个集群的
TOP1000
,就必须先查询出每个节点的TOP1000
,汇总结果后,重新排名,重新截取TOP1000
- 那么如果要查询
9900~10000
的数据呢?是不是要先查询TOP10000
,然后汇总每个节点的TOP10000
,重新排名呢? - 当查询分页深度较大时,汇总数据过多时,会对内存和CPU产生非常大的压力,因此ES会禁止
form + size > 10000
的请求
针对深度分页,ES提供而两种解决方案,官方文档:
https://www.elastic.co/guide/en/elasticsearch/reference/current/paginate-search-results.html
- search after:分页时需要排序,原理是从上一次的排序值开始,查询下一页数据。官方推荐使用的方式
- scrool:原理是将排序后的文档id形成快照,保存在内存,内存消耗过大。官方已经不推荐使用
小结
- 分页查询的常见实现方案以及优缺点
- from + size:
- 优点:支持随机翻页
- 缺点:深度分页问题,默认查询上限是10000(from + size)
- 场景:百度、京东、谷歌、淘宝这样的随机翻页搜索(百度现在支持翻页到75页,然后显示
提示:限于网页篇幅,部分结果未予显示。
)
- after search:
- 优点:没有查询上限(单词查询的size不超过10000)
- 缺点:只能向后逐页查询,不支持随机翻页
- 场景:没有随机翻页需求的搜索,例如手机的向下滚动翻页
- scroll:
- 优点:没有查询上限(单词查询的size不超过10000)
- 缺点:会有额外内存消耗,并且搜索结果是非实时的(快照保存在内存中,不可能每搜索一次都更新一次快照)
- 场景:海量数据的获取和迁移。从ES7.1开始不推荐,建议使用after search方案
- from + size:
高亮
高亮原理
- 什么是高亮呢?
我们在百度搜索时,关键字会变成红色,比较醒目,这就叫高亮显示
高亮显示的实现分为两步
- 给文档中的所有关键字都添加一个标签,例如
<em>
标签 - 页面给
<em>
标签编写CSS样式 - 但默认情况下就是加的
<em>
标签,所以我们也可以省略
- 给文档中的所有关键字都添加一个标签,例如
实现高亮
高亮的语法
GET /indexName/_search
{
"query": {
"match": {
"FIELD": "TEXT"
}
},
"highlight": {
"fields": {
"FIELD": {
"pre_tags": "<em>",
"post_tags": "</em>"
}
}
}
}
注意:
- 高亮是对关键词高亮,因此搜索条件必须带有关键字,而不能是范围这样的查询
- 默认情况下,高亮的字段,必须与搜索指定的字段一致,否则无法高亮
- 如果要对非搜索字段高亮,则需要添加一个属性:
required_field_match=false
示例
GET /hotel/_search
{
"query": {
"match": {
"all": "上海如家"
}
},
"highlight": {
"fields": {
"name": {
"pre_tags": "<em>",
"post_tags": "</em>",
"require_field_match": "false" // 因为这里name非搜索字段(虽然在all里面)
}
}
}
}
小结
- 查询的DSL是一个大的JSON对象,包含以下属性
- query:查询条件
- from和size:分页条件
- sort:排序条件
- highlight:高亮条件