Elasticsearch学习笔记
Elasticsearch 概述
Elasticsearch 是什么
The Elastic Stack, 包括 Elasticsearch、Kibana、Beats 和 Logstash(也称为 ELK Stack)。能够安全可靠地获取任何来源、任何格式的数据,然后实时地对数据进行搜索、分析和可视化。Elasticsearch,简称为 ES,ES 是一个开源的高扩展的分布式全文搜索引擎,是整个 Elastic Stack 技术栈的核心。它可以近乎实时的存储、检索数据;本身扩展性很好,可以扩展到上百台服务器,处理PB 级别的数据。
全文搜索引擎
Google,百度类的网站搜索,它们都是根据网页中的关键字生成索引,我们在搜索的时候输入关键字,它们会将该关键字即索引匹配到的所有网页返回;还有常见的项目中应用日志的搜索等等。对于这些非结构化的数据文本,关系型数据库搜索不是能很好的支持。
一般传统数据库,全文检索都实现的很鸡肋,因为一般也没人用数据库存文本字段。进行全文检索需要扫描整个表,如果数据量大的话即使对 SQL 的语法优化,也收效甚微。建立了索引,但是维护起来也很麻烦,对于 insert 和 update 操作都会重新构建索引。
基于以上原因可以分析得出,在一些生产环境中,使用常规的搜索方式,性能是非常差的:
搜索的数据对象是大量的非结构化的文本数据。
文件记录量达到数十万或数百万个甚至更多。
支持大量基于交互式文本的查询。
需求非常灵活的全文搜索查询。
对高度相关的搜索结果的有特殊需求,但是没有可用的关系数据库可以满足。
对不同记录类型、非文本数据操作或安全事务处理的需求相对较少的情况。为了解决结构化数据搜索和非结构化数据搜索性能问题,我们就需要专业,健壮,强大的全文搜索引擎
这里说到的全文搜索引擎指的是目前广泛应用的主流搜索引擎。它的工作原理是计算机索引程序通过扫描文章中的每一个词,对每一个词建立一个索引,指明该词在文章中出现的次数和位置,当用户查询时,检索程序就根据事先建立的索引进行查找,并将查找的结果反馈给用户的检索方式。这个过程类似于通过字典中的检索字表查字的过程。
Elasticsearch And Solr
Lucene 是 Apache 软件基金会 Jakarta 项目组的一个子项目,提供了一个简单却强大的应用程式接口,能够做全文索引和搜寻。在Java 开发环境里 Lucene 是一个成熟的免费开源工具。就其本身而言,Lucene 是当前以及最近几年最受欢迎的免费 Java 信息检索程序库。但 Lucene 只是一个提供全文搜索功能类库的核心工具包,而真正使用它还需要一个完善的服务框架搭建起来进行应用。
目前市面上流行的搜索引擎软件,主流的就两款:Elasticsearch 和 Solr ,这两款都是基于 Lucene 搭建的,可以独立部署启动的搜索引擎服务软件。由于内核相同,所以两者除了服务器安装、部署、管理、集群以外,对于数据的操作 修改、添加、保存、查询等等都十分类似。
在使用过程中,一般都会将 Elasticsearch 和 Solr 这两个软件对比,然后进行选型。这两个搜索引擎都是流行的,先进的的开源搜索引擎。它们都是围绕核心底层搜索库 - Lucene构建的 - 但它们又是不同的。像所有东西一样,每个都有其优点和缺点:
与 Solr 相比,Elasticsearch 易于安装且非常轻巧。此外,你可以在几分钟内安装并运行 Elasticsearch。但是,如果 Elasticsearch 管理不当,这种易于部署和使用可能会成为一个问题。基于 JSON 的配置很简单,但如果要为文件中的每个配置指定注释,那么它不适合您。总的来说,如果你的应用使用的是 JSON,那么 Elasticsearch 是一个更好的选择。否则,请使用 Solr,因为它的 schema.xml 和 solrconfig.xml 都有很好的文档记录。
Solr 拥有更大,更成熟的用户,开发者和贡献者社区。ES 虽拥有的规模较小但活跃的用户社区以及不断增长的贡献者社区。Solr 贡献者和提交者来自许多不同的组织,而 Elasticsearch 提交者来自单个公司
Solr 更成熟,但 ES 增长迅速,更稳定。
Solr 是一个非常有据可查的产品,具有清晰的示例和 API 用例场景。 Elasticsearch 的文档组织良好,但它缺乏好的示例和清晰的配置说明。
ES 核心概念介绍
集群(Cluster)
一个或者多个安装了 ES 节点的服务器组织在一起,就是集群,这些节点共同持有数据,共同提供搜索服务。
一个集群有一个名字,这个名字是集群的唯一标识,该名字成为 cluster name,默认的集群名称是 elasticsearch,具有相同名称的节点才会组成一个集群。
可以在 config/elasticsearch.yml 文件中配置集群名称:
1 | cluster.name: javaboy-es |
在集群中,节点的状态有三种:绿色、黄色、红色:
绿色:节点运行状态为健康状态。所有的主分片、副本分片都可以正常工作。
黄色:表示节点的运行状态为警告状态,所有的主分片目前都可以直接运行,但是至少有一个副本分片是不能正常工作的。
红色:表示集群无法正常工作。
节点(Node)
集群中的一个服务器就是一个节点,节点中会存储数据,同时参与集群的索引以及搜索功能。一个节点想要加入一个集群,只需要配置一下集群名称即可。默认情况下,如果我们启动了多个节点,多个节点还能够互相发现彼此,那么它们会自动组成一个集群,这是 es 默认提供的,但是这种方式并不可靠,有可能会发生脑裂现象。所以在实际使用中,建议一定手动配置一下集群信息。
索引(Index)
索引可以从两方面来理解:
名词:具有相似特征文档的集合。
动词:索引数据以及对数据进行索引操作。
类型(Type)
类型是索引上的逻辑分类或者分区。在 es6 之前,一个索引中可以有多个类型,从 es7 开始,一个索引中,只能有一个类型。在 es6.x 中,依然保持了兼容,依然支持单 index 多个 type 结构,但是已经不建议这么使用。
文档(Document)
一个可以被索引的数据单元。例如一个用户的文档、一个产品的文档等等。文档都是 JSON 格式的。
分片(Shards)
索引都是存储在节点上的,但是受限于节点的空间大小以及数据处理能力,单个节点的处理效果可能不理想,此时我们可以对索引进行分片。当我们创建一个索引的时候,就需要指定分片的数量。每个分片本身也是一个功能完善并且独立的索引。
默认情况下,一个索引会自动创建 1 个分片,并且为每一个分片创建一个副本。
副本(Replicas)
副本也就是备份,是对主分片的一个备份。
Settings
集群中对索引的定义信息,例如索引的分片数、副本数等等。
Mapping
Mapping 保存了定义索引字段的存储类型、分词方式、是否存储等信息。
Analyzer 和 Analysis
Analysis 只是一个概念,文本分析是将全文本转换为一系列单词的过程,也叫分词。
Analysis 是通过 analyzer (分词器) 来实现的,可以使用 ES 中自带的分词器,也可以自定义分词器。
除了在数据写入时将词条进行转换,在查询时也可以使用分析器对语句进行分析。
analyzer 由三部分组成,例如有
Hello a world, the world is beautiful
;Character Filter:将文本中的 html 标签剔除掉。
Tokenizer:按照规则进行分词,在英文中按照空格分词。
Token Filter:去掉 stop word(停顿词,a,an,the,is,are等),然后转换为小写。
类比
DBMS | ES |
---|---|
database | index |
table | type(7.0后固定为_doc) |
Row | Document |
Column | Field |
Schema(表信息约束) | Mapping |
SQL | DSL |
在 ES 7.0前,一个 index 可以创建多个 type,从 7.0 开始,一个索引只能创建一个类型,也就是 _doc
正排索引和倒排索引
正排索引
正排索引就是最普通的索引排序方式。正排索引也是采取key-value pair的方式对数据进行保存,key是doc-id,value则可以存储多种内容,如doc的分词词表、doc所在网页的属性信息等。由此可见,正排索引可以随意添加数据,但如果你要查询某个单词在哪些文档中出现,那么你就不得不将全部文档都遍历一遍,若文档库极大,则时间消耗是不可接受的。
在搜索引擎中每个文件都对应一个文件ID,文件内容被表示为一系列关键词的集合(实际上在搜索引擎索引库中,关键词也已经转换为关键词ID)。例如“文档1”经过分词,提取了20个关键词,每个关键词都会记录它在文档中的出现次数和出现位置。
正排索引一般通过 KEY 去找 VALUE
当用户在主页上搜索关键词“华为手机”时,假设只存在正向索引(forward index),那么就需要扫描索引库中的所有文档,找出所有包含关键词“华为手机”的文档,再根据打分模型进行打分,排出名次后呈现给用户。因为互联网上收录在搜索引擎中的文档的数目是个天文数字,这样的索引结构根本无法满足实时返回排名结果的要求。
倒排索引
倒排索引是 Lucene 和 ElasticSearch 用来做全文检索的标配。倒排索引类似将正排索引反过来,以全部文档中出现的所有words建立一个term dictionary ,然后对于term dictionary 中的每个词,它后面都会跟随一个链表,该链表就是 倒排表 ,倒排表 内存储着如下信息:
该词出现的doc-id
该词在某doc中的出现次数和出现位置
倒排索引以关键词作为主键
索引基本操作
创建索引(PUT 请求)
对比关系型数据库,创建索引就等同于创建数据库。
在 Postman 中,向 ES 服务器发起 PUT 请求,地址为:http://127.0.0.1:9200/shopping
- 请求结果为:
- 此时再次发送请求,会提示索引已存在!
获取 shopping 索引信息(GET 请求)
发送 GET 请求,请求地址为:http://127.0.0.1:9200/shopping
查看索引向 ES 服务器发送的请求路径和创建索引是一致的。但是HTTP 方法不一致。
获取当前所有索引信息(GET 请求)
发送 GET 请求,请求地址为:http://127.0.0.1:9200/_cat/indices?v
这里请求路径中的 _cat 表示查看的意思,indices 表示索引,所以整体含义就是查看当前 ES
服务器中的所有索引,就好像 MySQL 中的 show tables 的感觉,服务器响应结果如下
各参数及含义如下表
表头 | 含义 |
---|---|
health | 当前服务器健康状态:green(集群完整) 、yellow(单点正常、集群不完整) 、red(单点不正常) |
status | 索引打开、关闭状态 |
index | 索引统一编号 |
pri | 主分片数量 |
rep | 副本数量 |
docs.count | 可用文档数量 |
docs.deleted | 主分片和副分片整体占空间大小 |
pri.store.size | 主分片占空间大小 |
删除单个索引(DELETE 请求)
请求方式为 DELETE,在请求地址栏中输入链接如下:http://localhost:9200/shopping
再次访问 shopping 索引,会提示索引不存在
文档操作
创建文档(POST 请求)
重建 shopping 索引,然后创建文档,添加数据,添加数据的格式为 JSON 格式。
发送 POST 请求,请求地址为:http://localhost:9200/shopping/_doc
其中请求体内容为:
1 | { |
结果如下
在创建文档时自定义文档 id(POST 请求)
请求路径为 POST ,在请求路径中添加自定义的id,请求路径如下
http://localhost:9200/shopping/_doc/1001
根据 id 获取文档对象(GET 请求)
发送 GET 请求,请求地址为 http://localhost:9200/shopping/_doc/带查询id,这里我们查询 id 为1001的数据
如果查询一个不存在的数据,那么返回结果的 found 属性会为 false
查询索引下的所有文档对象(GET 请求)
发送 GET 请求,请求地址为:http://localhost:9200/索引名称/_search
- 查询 shopping 索引下的所有文档对象
1 | { |
文档的全量修改(PUT 请求)
发送 PUT 请求,请求地址为:http://localhost:9200/索引/_doc/要修改的文档id
然后在 Body 中传入新文档的信息,以 JSON 形式传递。
发送请求,查看结果
再次查看该数据
文档的局部修改(POST 请求)
发送 POST 请求,请求地址为:http://localhost:9200/索引名/_update/要修改的id,并在 Body 中指定要更新的字段及对应信息.
- 修改 shopping 索引中 id 为 1001 的字段,将 title 修改为 HUAWEI MATE P40
1 | { |
请求如下
结果如下
文档对象的删除(DELETE 请求)
发送 DELETE 请求,请求地址为:http://localhost:9200/索引名/_doc/待删除索引id
- 删除 id 为 1001 的文档对象
ElasticSearch 分词器介绍
内置分词器
ElasticSearch 核心功能就是数据检索,首先通过索引将文档写入 ES ,查询分析则主要分为两个步骤:
词条化:分词器将输入文本转换为一个一个的词条流。
过滤:比如停用词过滤器会从词条中去除不相干的词条;另外还有同义词过滤器,小写过滤器等,ElasticSearch 中内置了多种分词器可供使用。
ES 内置分词器如下:
中文分词器
在Es 中,使用较多的中文分词器是 elasticsearch-analysis-ik,这个是 es 的一个第三方插件,代码托管在 GitHub 上:
https://github.com/medcl/elasticsearch-analysis-ik
安装方式
首先打开分词器官网:
https://github.com/medcl/elasticsearch-analysis-ik在 https://github.com/medcl/elasticsearch-analysis-ik/releases 页面找到最新的正式版,下载下来。
将下载文件解压。
在 es/plugins 目录下,新建 ik目录,并将解压后的所有文件拷贝到 ik 目录下。
重启 es 服务。
测试分词器
创建一个 test 索引
传入文本进行分析
1 | POST test/_analyze |
- 结果
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60{
"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" : "伊拉克",
"start_offset" : 4,
"end_offset" : 7,
"type" : "CN_WORD",
"position" : 2
},
{
"token" : "的",
"start_offset" : 7,
"end_offset" : 8,
"type" : "CN_CHAR",
"position" : 3
},
{
"token" : "是",
"start_offset" : 8,
"end_offset" : 9,
"type" : "CN_CHAR",
"position" : 4
},
{
"token" : "一个",
"start_offset" : 9,
"end_offset" : 11,
"type" : "CN_WORD",
"position" : 5
},
{
"token" : "烂摊子",
"start_offset" : 11,
"end_offset" : 14,
"type" : "CN_WORD",
"position" : 6
},
{
"token" : "吗",
"start_offset" : 14,
"end_offset" : 15,
"type" : "CN_CHAR",
"position" : 7
}
]
}自定义扩展词库
本地自定义
在 plugins/ik/config 目录下,新建 ext.dic (文件名任意),在该文件中配置自定义词库
1 | 芜湖起飞 |
在 IKAnalyzer.cfg.xml 中配置自定义词库
1 |
|
重启 ES ,测试分词
1 | POST test/_analyze |
分词结果
字段类型
核心类型
字符串类型
string
这是一个已经过期的字符串类型。在 es5 之前,用这个来描述字符串,现在的话,它已经被 text 和 keyword 替代了。text
如果一个字段是要被全文检索的,比如说博客内容、新闻内容、产品描述,那么可以使用 text。
用了 text 之后,字段内容会被分析,在生成倒排索引之前,字符串会被分词器分成一个个词项。
text 类型的字段不用于排序,很少用于聚合。这种字符串也被称为 analyzed 字段。
使用场景:
存储全文搜索数据, 例如: 邮箱内容、地址、代码块、博客文章内容等。
默认结合standard analyzer(标准解析器)对文本进行分词、倒排索引。
默认结合标准分析器进行词命中、词频相关度打分。
keyword
这种类型适用于结构化的字段,例如标签、email 地址、手机号码等等,这种类型的字段可以用作过滤、排序、聚合等。
这种字符串也称之为 not-analyzed 字段。
数字类型
类型 | 取值范围 |
---|---|
long | -2^63^ 到 2^63^ - 1 |
integer | -2^31^ 到 2^31^ - 1 |
short | -2^15^ 到 2^15^ - 1 |
byte | -2^7^ 到 2^7^ - 1 |
double | 64 位的双精度浮点类型 |
float | 32 位的双精度浮点类型 |
half_float | 16 位的双精度浮点类型 |
scaled_float | 缩放类型的浮点类型 |
在满足需求的情况下,优先使用范围小的字段。字段长度越短,索引和搜索的效率越高。
浮点数,优先考虑使用 scaled_float。
日期类型
由于 JSON 中没有日期类型,所以 es 中的日期类型形式就比较多样:
2020-11-11 或者 2020-11-11 11:11:11
一个从 1970.1.1 零点到现在的一个秒数或者毫秒数。
1 | PUT product/_doc/1 |
以上三个时间都能被解析
布尔类型
JSON 中的 “true”、“false”、true、false 都可以。
二进制类型
二进制接受的是 base64 编码的字符串,默认不存储,也不可搜索。
范围类型
integer_range
float_range
long_range
double_range
date_range
ip_range
定义的时候,指定范围类型即可:
1 | PUT product |
在插入数据时指定范围,可以使用 gt、gte、lt、lte
1 | PUT product/_doc/1 |
复合类型
数组类型
es 中没有专门的数组类型。默认情况下,任何字段都可以有一个或者多个值。
需要注意的是,数组中的元素必须是同一种类型。
添加数组时,数组中的第一个元素决定了整个数组的类型。
对象类型
由于 JSON 本身具有层级关系,所以文档包含内部对象。内部对象中,还可以再包含内部对象。
地理类型
使用场景
查找某一个范围内的地理位置
通过地理位置或者相对中心点的距离来聚合文档
把距离整个到文档的评分中
通过距离对文档进行排序
geo_point
geo_point 就是一个坐标点,定义方式如下:
1 | PUT people |
创建文档时指定字段类型,存储的时候,有四种方式:
1 | PUT people/_doc/1 |
注意,使用数组描述,先经度后纬度
lat:纬度
lon:经度
特殊类型
IP
存储 IP 地址,类型是 ip
1 | PUT blog |
添加文档
1 | PUT blog/_doc/1 |
搜索结果
Mapping 的定义
语法格式如下
1 | PUT 索引名 |
定义mapping的建议方式:写入一个样本文档到临时索引中。此时 ES 会自动生成mapping信息,通过访问 mapping 信息的 API 查询 mapping 定义,修改自动生成的 mapping 成为我们需要的mapping,然后删去原有的索引即可。
建议在 ES 自动生成的基础上修改,ES 的mapping确定后不能修改
- 创建一个 user 索引,其中 name 为 text 类型,age 为 long 类型,birthday 为 date 类型
1 | PUT /user |
搜索
前期准备
创建索引
1 | PUT books |
导入数据
执行脚本,导入数据
1 | curl -XPOST "http://localhost:9200/books/_bulk?pretty" -H "content-type:application/json" --data-binary @bookdata.json |
文档保存与搜索
搜索分为两个过程:
当向索引中保存文档时,默认情况下,es 会保存两份内容,一份是 _source 中的数据,另一份则是通过分词、排序等一系列过程生成的倒排索引文件,倒排索引中保存了词项和文档之间的对应关系。
搜索时,当 es 接收到用户的搜索请求之后,就会去倒排索引中查询,通过的倒排索引中维护的倒排记录表找到关键词对应的文档集合,然后对文档进行评分、排序、高亮等处理,处理完成后返回文档。
词项 查询
term 是表达语义的最小单位,在搜索的时候基本都要使用到 term。
term 查询的种类有:Term Query、Range Query 等。
在 ES 中,Term的查询代表完全匹配, Term 查询不会对输入进行分词处理,而是将输入作为一个整体,在创建索引中使用查找准确的词项,我们也可以 Constant Score 将查询转换为一个 filter ,避免算分,利用缓存,提高查询效率。
Term 不会对我们传入的参数做任何处理,而是直接将参数丢进倒排索引库中。
term
查询书名中有 无机化学 的书籍, 用于查询的单词不会被进行任何分词处理
1 | GET books/_search |
结果
terms
terms 查询和 term 查询机制一样,都不会将指定关键词进行分词,而是直接去分词库中匹配,找到响应文档内容。
terms 是在针对一个字段包含多个值时使用,多个值间以 OR 分隔
- 查询书籍名称中含有 无机化学 或者 线性代数 的书籍
1 | GET books/_search |
结果
Constant Score
内部包装了过滤查询,故而不会计算相似度分,该查询返回的相似度分与字段上指定boost参数值相同
1 | GET books/_search |
结果
range
range query 中的参数主要有四个:
gt
lt
gte
lte
查询书籍价格在 10 - 20 元之间的书籍
1 | GET books/_search |
结果(局部)
wildcard
wildcard query 即通配符查询。支持单字符和多字符通配符:
? 表示一个任意字符。
- 表示零个或者多个字符。
查询所有姓张的作者的书
1 | GET books/_search |
结果(局部)
查询作者姓张,且名字只有两个字的书籍
1 | GET books/_search |
结果
ids
根据指定的 id 查询
查询id 为 1,2,3的数据
1 | GET books/_search |
结果
全文查询
全文查询的种类有:Match Query、Match Phrase Query 和 Query String Query 等
索引和搜索时都会进行分词,在查询时,会对输入进行分词,然后每个词项会逐个到底层进行查询,将最终结果进行合并。
match_phrase
match_phrase是短语搜索,它会将给定的短语(phrase)当成一个完整的查询条件。
会对输入做分词,但是需要结果中也包含所有的分词,而且顺序要求一样。
match_phrase 含有一个 slop 属性
slop 是指关键字之间的最小距离,但是注意不是关键之间间隔的字数。文档中的字段被分词器解析之后,解析出来的词项都包含一个 position 字段表示词项的位置,查询短语分词之后 的 position 之间的间隔要满足 slop 的要求。slop 默认为1
查询名字中带有 十一五化学 的文档
1 | GET books/_search |
此时无法查询到任何结果
我们增大 slop 的值,再次进行查询
1 | GET books/_search |
此时查询到 5 条数据
multi_match
多条件查询,会对传入的文本进行分词。
查询 name 属性或 info 属性中 匹配 化学无机数学 的文档
进行分词,其实就是查询 name 属性或 info 属性中含有 化学 、 无机 或 数学 的文档
1 | GET books/_search |
结果
这种查询方式还可以指定字段权重
1 | GET books/_search |
这个表示关键字出现在 name 中的权重是出现在 info 中权重的 4 倍。
match
match 会对查询语句进行分词,分词后,如果查询语句中的任何一个词项被匹配,则文档就会被索引到。查询条件相对来说比较宽松。
match 分词后,默认词项之间是 OR 的关系,也就是说,只需要文档中包含一个分词结果,那么就返回文档,可以通过 operator 修改分词后词项之间的关系。
词项间为 OR 的关系
1 | GET books/_search |
此时查看结果,发现返回的文档中 name 属性可以只包含 计算机 或只包含 化学
词项间为 AND 的关系
1 | GET books/_search |
此时命中的记录数为0
simple_query_string 和 query_string
query_string
和match类似,但是match需要指定字段名,query_string是在所有字段中搜索,范围更广泛。
允许我们在单个查询字符串中指定AND | OR | NOT条件,同时也和 multi_match query 一样,支持多字段搜索。
查询 title 中包含 beautiful 和 mind 的所有电影
检索同时包含Token【系统学、es】的文档
1 | GET /tehero_index/_doc/_search |
检索包含Token【系统学、es】二者之一的文档
1 | GET /tehero_index/_doc/_search |
simple_query_string
类似于query_string ,但是会忽略错误的语法,永远不会引发异常,并且会丢弃查询的无效部分。
simple_query_string支持以下特殊字符:
- 表示与运算,相当于query_string 的 AND
1 | GET /tehero_index/_doc/_search |
- | 表示或运算,相当于query_string 的 OR
1
2
3
4
5
6
7
8
9GET /tehero_index/_doc/_search
{
"query": {
"simple_query_string" : {
"fields" : ["content.ik_smart_analyzer"],
"query" : "系统学 | 间隔"
}
}
}
- 取反单个令牌,相当于query_string 的 NOT
1 | GET /tehero_index/_doc/_search |
- “”表示对检索词进行 match_phrase query
1 | GET /tehero_index/_doc/_search |
- 字词末尾表示前缀查询
1 | GET /tehero_index/_doc/_search |
fuzzy 模糊查询
在实际搜索中,有时我们可能会打错字,从而导致搜索不到,fuzzy 用于模糊查询,可以自动将拼写错误的搜索文本进行纠正,纠正后去尝试匹配索引中的数据。
fuzzy query 返回与搜索关键字相似的文档。怎么样就算相似?以 LevenShtein 编辑距离为准。编辑距离是指将一个字符变为另一个字符所需要更改字符的次数,更改主要包括四种:
更改字符(javb–〉java)
删除字符(javva–〉java)
插入字符(jaa–〉java)
转置字符(ajva–〉java)
1 | GET /my_index/my_type/_search |
其中 fuzziness 为调整次数,只能为0、1、2,最多纠正两个错误
传入拼写错误的文本 jaba,查看模糊查询返回的结果
1 | GET books/_search |
返回的结果如下
特殊查询
more_like_this
more_like_this query 可以实现基于内容的推荐,给定一篇文章,可以查询出和该文章相似的内容。
1 | GET 要查询的索引/_search |
fields:要匹配的字段,可以有多个
like:要匹配的文本
min_term_freq:词项的最低频率,默认为2,这个指词项在要匹配的文本中的频率,而不是在 ES 文档中的频率
max_query_terms:query 中包含的最大词项数目
min_doc_freq:最小的文档频率,搜索的词,至少在多少个文档中出现,少于指定数目,该词会被忽略
max_doc_freq:最大文档频率
analyzer:分词器,默认使用字段的分词器
stop_words:停用词列表
聚合查询
语法格式:
1 | GET indexName/_search |
ES 中的聚合分析我们主要从两个方面来学习:
指标聚合
桶聚合(类似 group by)
ES 指标聚合
Max Aggregation
统计最大值
查询价格最高的书籍
1 | GET books/_search |
结果
missing 参数:如果某个文档中缺少 price 字段,则设置该字段的值为 1000。
Min Aggregation
统计最小值,用法和 Max Aggregation 基本一致:
1 | GET books/_search |
结果
Avg Aggregation
统计平均值
1 | GET books/_search |
结果
Sum Aggregation
求和
1 | GET books/_search |
结果
Cardinality Aggregation
cardinality aggregation 用于基数统计。类似于 SQL 中的 distinct count(0)
查询总共有多少个岗位
1 | # 计算岗位个数 |
查看结果
Stats Aggregation
基本统计,count、max、min、avg和sum,要求计算属性的类型为数字类型
查询员工工资的信息
1 | # 查询员工工资基本信息 |
结果
Extends Stats Aggregation
高级统计,比 stats 多出来:平方和、方差、标准差、平均值加减两个标准差的区间
1 | GET books/_search |
结果
桶聚合
Terms Aggregation
Terms Aggregation 用于分组聚合,例如,统计各个出版社出版的图书总数量
统计每个职位员工数量
1 | GET employee/_search |
结果
在 terms 分桶的基础上,还可以对每个桶进行指标聚合
统计每个岗位中员工工资信息
1 | GET employee/_search |
结果
Filter Aggregation
过滤器聚合。可以将符合过滤器中条件的文档分到一个桶中,然后可以求其平均值。
例如查询书名中包含 化学 的图书的平均价格:
1 | GET books/_search |
结果
Filters Aggregation
多过滤器聚合。过滤条件可以有多个。
查询书名中含有 java 或者 化学 的书籍,并且平均价格
1 | GET books/_search |
结果
range Aggregation
按照范围聚合,在某一个范围内的文档数统计。
例如统计图书价格在 0-50、50-100、100-150、150以上的图书数量:
1 | GET books/_search |
结果
Date Range Aggregation
Range Aggregation 也可以用来统计日期,但是也可以使用 Date Range Aggregation,后者的优势在于可以使用日期表达式。
统计两年前到一年后的博客数量
1 | GET blog/_search |
结果
12M/M 表示 12 个月。
1y/y 表示 1年。
d 表示天
Missing Aggregation
空值聚合。
统计所有没有 price 字段的文档:
1 | GET books/_search |
结果
IP Range Aggregation
IP 地址范围查询。
1 | GET blog/_search |
聚合过滤问题
查询年龄大于30岁的员工的平均工资
先查询出年龄大于30岁的员工,然后进行聚合
1 | GET employee/_search |
结果
1 | "aggregations" : { |
查询Java员工的平均工资
先查询出职业为 java 或者 Java 的员工,然后根据 平均工资进行聚合。
1 | GET employee/_search |
结果
1 | "aggregations" : { |
高亮显示
日常生活中我们使用搜索工具尝试查询一些信息的时候,常常可以看到返回的结果集中和我们查询条件相符合的字段被特殊的颜色所标记,这就是结果高亮显示。通过高亮显示用户可以明显的发现查询匹配的位置,
ES使用highlight来实现搜索结果中一个或多个字段突出显示。
highlight 的层级与 query 同级,语法如下
1 | GET employee/_search |
结果
我们可以自定义高亮所用的标签,使用 pre_tags 定义前置标签, post_tags 定义后置标签
1 | GET employee/_search |
结果
可以单独定义 job 的前置后置标签
1 | GET employee/_search |
Spring Data Elasticsearch
简介
Spring Data Elasticsearch是Spring Data项目下的一个子模块。
Spring Data的官网:http://projects.spring.io/spring-data/
查看 Spring Data Elasticsearch的页面:https://projects.spring.io/spring-data-elasticsearch/
特征:
支持Spring的基于@Configuration的java配置方式,或者XML配置方式
提供了用于操作ES的便捷工具类ElasticsearchTemplate。包括实现文档到POJO之间的自动智能映射。
利用Spring的数据转换服务实现的功能丰富的对象映射
基于注解的元数据映射方式,而且可扩展以支持更多不同的数据格式
根据持久层接口自动生成对应实现方法,无需人工编写基本操作代码(类似 mybatis,根据接口自动得到实现)。当然,也支持人工定制查询
前期准备
使用 Spring Initializer 创建一个项目
pom.xml 文件如下
1 | <parent> |
核心配置文件
1 | server: |
创建索引,导入数据
这里以上面的 books 索引及其数据为例。
实体类及注解
实体类
1 |
|
注解
Spring Data 通过注解来声明字段的映射属性,有下面三个注解
@Document
作用在类上,标记这个实体类为文档对象,一般有下面四个属性- indexName:对应 ES 中索引库的名称
- type:对应在索引库中的类型,在 ES 7后,这个属性已过时,因为 ES 7中只有一个类型,即 doc
- shards:分片数量,默认5
- replicas:副本数量,默认1
@Id
作用在成员变量,标记一个字段作为id主键
- @Field
作用在成员变量,标记为文档的字段,并指定字段映射属性
1. type:字段类型,取值是枚举:FieldType
2. index:是否索引,布尔类型,默认是true
3. store:是否存储,布尔类型,默认是false
4. analyzer:分词器名称:ik_max_word,也可以是 ik_smart
完整实体类
1 |
|
在测试类中使用 ElasticsearchRestTemplate 进行索引操作
说明
在新版本的 Spring Data Elasticsearch 中,ElasticsearchTemplate 已被弃用,我们可以使用 ElasticsearchRestTemplate 操作索引。
创建索引及映射
ElasticsearchTemplate 中创建索引与映射关系的方法已经过时,查看 ElasticsearchTemplate 源码可以找到替代方法
进入 IndexOperations 接口,可以看到方法如下
创建索引映射,使用 IndexOperations 接口中的 createMapping 方法
通过与该 IndexOperations 接口绑定的实体类创建
1 | /** |
在创建 IndexOperations 对象时进行实体类绑定
1 | IndexOperations indexOperations = elasticsearchRestTemplate.indexOps(Book.class); |
传入一个实体类的 class 属性进行创建
1 | /** |
创建索引的方法,直接使用 IndexOperations 的 create 方法即可。
1 | indexOperations.create(); |
测试
1 |
|
结果
1 | MapDocument@?#? |
此时在 kibana 中看到了新创建的索引
判断索引是否存在
方法同样存在于 IndexOperations 接口中
1 | /** |
测试
1 |
|
结果
删除索引
方法存在于 IndexOperations 接口中,删除与该 IndexOperations 接口绑定的实体类对应的索引。
1 | /** |
测试
1 |
|
结果
创建接口己成 ElasticsearchRepository 实现文档操作
我们需要定义一个 接口,这个接口继承 ElasticsearchRepository,然后就可以实现对文档的基础 CRUD 操作
ElasticsearchRepository 中有两个泛型,第一个为操作的实体类,第二个为实体类主键 ID 类型
创建一个接口继承 ElasticsearchRepository
这样我们就获得了对文档进行基础 CRUD 的功能
1 | public interface BookRepository extends ElasticsearchRepository<Book,Long> { |
测试插入文档数据
使用 ElasticsearchRepository 接口中的 save 方法
测试
1 |
|
结果
测试更新文档信息
同样使用 save 方法,当一条数据在索引中已经存在时,再次调用 save 方法即为更新
1 |
|
结果
删除文档
有两个方法
传入待删除的文档对象 id
传入待删除文档对象
1 | /** |
删除 id 为一的文档对象
1 |
|
结果,此时索引中不存在任何数据
批量插入
使用 ElasticsearchRepository 接口中的 saveAll 方法
1 | /** |
进行批量插入
1 |
|
结果
数据查询
根据 id 查询一个文档对象
使用 ElasticsearchRepository 的 findById 方法,该方法返回一个 Optional 对象,Optional 类中存在一个 value 属性,就是我们要获取的文档对象,可以通过 Optional 对象的 get 方法获取
测试
1 |
|
查看结果
自定义方法
Spring Data 的另一个强大功能,是根据方法名称自动实现功能。
比如:你的方法名叫做:findByTitle,那么它就知道你是根据title查询,然后自动帮你完成,无需写实现类。
当然,方法名称要符合一定的约定:
Keyword | Sample | Elasticsearch Query String |
---|---|---|
And | findByNameAndPrice | {“bool” : {“must” : [ {“field” : {“name” : “?”}}, {“field” : {“price” : “?”}} ]}} |
Or | findByNameOrPrice | {“bool” : {“should” : [ {“field” : {“name” : “?”}}, {“field” : {“price” : “?”}} ]}} |
Is | findByName | {“bool” : {“must” : {“field” : {“name” : “?”}}}} |
Not | findByNameNot | {“bool” : {“must_not” : {“field” : {“name” : “?”}}}} |
Between | findByPriceBetween | {“bool” : {“must” : {“range” : {“price” : {“from” : ?,”to” : ?,”include_lower” : true,”include_upper” : true}}}}} |
LessThanEqual | findByPriceLessThan | {“bool” : {“must” : {“range” : {“price” : {“from” : null,”to” : ?,”include_lower” : true,”include_upper” : true}}}}} |
GreaterThanEqual | findByPriceGreaterThan | {“bool” : {“must” : {“range” : {“price” : {“from” : ?,”to” : null,”include_lower” : true,”include_upper” : true}}}}} |
Before | findByPriceBefore | {“bool” : {“must” : {“range” : {“price” : {“from” : null,”to” : ?,”include_lower” : true,”include_upper” : true}}}}} |
After | findByPriceAfter | {“bool” : {“must” : {“range” : {“price” : {“from” : ?,”to” : null,”include_lower” : true,”include_upper” : true}}}}} |
Like | findByNameLike | {“bool” : {“must” : {“field” : {“name” : {“query” : “?*”,”analyze_wildcard” : true}}}}} |
StartingWith | findByNameStartingWith | {“bool” : {“must” : {“field” : {“name” : {“query” : “?*”,”analyze_wildcard” : true}}}}} |
EndingWith | findByNameEndingWith | {“bool” : {“must” : {“field” : {“name” : {“query” : “*?”,”analyze_wildcard” : true}}}}} |
Contains/Containing | findByNameContaining | {“bool” : {“must” : {“field” : {“name” : {“query” : “?“,”analyze_wildcard” : true}}}}} |
In | findByNameIn(Collection |
{“bool” : {“must” : {“bool” : {“should” : [ {“field” : {“name” : “?”}}, {“field” : {“name” : “?”}} ]}}}} |
NotIn | findByNameNotIn(Collection |
{“bool” : {“must_not” : {“bool” : {“should” : {“field” : {“name” : “?”}}}}}} |
Near | findByStoreNear | Not Supported Yet ! |
True | findByAvailableTrue | {“bool” : {“must” : {“field” : {“available” : true}}}} |
False | findByAvailableFalse | {“bool” : {“must” : {“field” : {“available” : false}}}} |
OrderBy | findByAvailableTrueOrderByNameDesc | {“sort” : [{ “name” : {“order” : “desc”} }],”bool” : {“must” : {“field” : {“available” : true}}}} |
修改实体类中的索引为 books,在我们自定义的接口中编写一个根据价格排序的方法声明
1 | public interface BookRepository extends ElasticsearchRepository<Book,Long> { |
编写一个测试方法,获取所有书籍,根据价格排序
1 |
|
结果(局部)
编写一个方法,返回价格在 100-200 之间的所有书籍
BookRepository
1 | public interface BookRepository extends ElasticsearchRepository<Book,Long> { |
测试方法
1 |
|
查看结果,这里只输出价格
高级查询
虽然基本查询和自定义方法已经很强大了,但是如果是复杂查询(模糊、通配符、词条查询等)就显得力不从心了。此时,我们只能使用原生查询。
需要 ElasticsearchRestTemplate 对象的 search 方法和 QueryBuilders 对象配合使用
QueryBuilders
QueryBuilders 提供了大量的静态方法,用于生成各种不同类型的查询对象,例如:词条、模糊、通配符等 QueryBuilder 对象。
构建一个 NativeSearchQueryBuilder 对象
1 | NativeSearchQueryBuilder queryBuilder = new NativeSearchQueryBuilder(); |
调用构造的 NativeSearchQueryBuilder 对象的 withQuery 方法,这个方法接收一个 QueryBuilder 对象,这个QueryBuilder 对象可以通过QueryBuilders 的静态方法获取。
使用 match_all 查询全部
1 |
|
结果
使用 match 进行查询,查询书名中含有 “无机” 和 “化学” 的书名
1 |
|
结果
分页查询
使用 NativeSearchQueryBuilder 对象的 withPageable 方法,这个方法要传入 Pageable 对象
1 | public NativeSearchQueryBuilder withPageable(Pageable pageable) { |
这个对象可以使用 PageRequest 对象的 of 方法构造,需要传入 page 和 size
1 | public static PageRequest of(int page, int size) { |
查询书名中带有 “无机” “化学” 的前十条数据
1 |
|
结果
指定要查询的字段和不查询的字段
使用 NativeSearchQueryBuilder 对象的 withSourceFilter 方法,这个方法需要传入一个 FetchSourceFilter 对象,这个对象需要传入两个字符串数组,分别为要返回的属性数组和排除的属性数组
1 |
|
结果,可以看到只返回了 includes 数组中的属性
范围匹配
使用 NativeSearchQueryBuilder 对象的 withQuery 方法,需要传入一个 RangeQueryBuilder 对象。
使用 gte/ gt 方法指定下界
使用 lte / lt方法指定上界
上面两个方法可以配合使用
- 使用 from 方法指定下界, to 方法指定上界
上面两个方法只能指定选择一个使用。
查询价格在 100.0 - 200.0 的书籍
1 |
|
结果
结果排序
使用 NativeSearchQueryBuilder 对象的 withSort 方法,需要传入一个 SortBuilder 对象。
SortBuilders.fieldSort 表示根据属性排序,这里根据 price 排序,order 方法中传入一个枚举对象,这里 SortOrder.DESC 指降序排序。
1 |
|
结果
聚合查询
聚合为桶
桶就是分组,比如这里我们按照出版社 price 进行分组:
terms 方法中传入聚合的名称,field 指定要进行桶分组的属性
使用 withSourceFilter 过滤所有普通结果,当 includes 为空数组时,不返回任何结果
1 |
|
结果
嵌套聚合
使用 subAggregation 添加子聚合
然后在遍历中解析子聚合
1 |
|