前言
Elasticsearch是一个文档存储系统,但是它更是一个搜索和数据分析引擎。了解Elasticsearch,就不得不认识elasticsearch的搜索与分析。该篇笔记主要记录Elasticsearch的搜索与分析。
空搜索
搜索API的最基础的形式是没有指定任何查询的空搜索,它简单地返回集群中所有索引下的所有文档:
GET /_search
返回结果:
{
"took" : 10,
"timed_out" : false,
"_shards" : {
"total" : 23,
"successful" : 23,
"skipped" : 0,
"failed" : 0
},
"hits" : {
"total" : {
"value" : 10000,
"relation" : "gte"
},
"max_score" : 1.0,
"hits" : [
{
"_index" : ".kibana_1",
"_type" : "_doc",
"_id" : "space:default",
"_score" : 1.0,
"_source" : {
"space" : {
"name" : "Default",
"description" : "This is your default space!",
"color" : "#00bfb3",
"disabledFeatures" : [ ],
"_reserved" : true
},
"type" : "space",
"references" : [ ],
"migrationVersion" : {
"space" : "6.6.0"
},
"updated_at" : "2020-12-08T06:47:14.690Z"
}
},
... 9 RESULTS REMOVED ...
]
}
}
返回参数介绍
hits:返回结果中最重要的部分是 hits ,它包含 total 字段来表示匹配到的文档总数,并且一个 hits 数组包含所查询结果的前十个文档。
在 hits 数组中每个结果包含文档的 _index 、 _type 、 _id ,加上 _source 字段。这意味着我们可以直接从返回的搜索结果中使用整个文档。这不像其他的搜索引擎,仅仅返回文档的ID,需要你单独去获取文档。
每个结果还有一个 _score ,它衡量了文档与查询的匹配程度。默认情况下,首先返回最相关的文档结果,就是说,返回的文档是按照 _score 降序排列的。在这个例子中,我们没有指定任何查询,故所有的文档具有相同的相关性,因此对所有的结果而言 1 是中性的 _score 。
max_score 值是与查询所匹配文档的 _score 的最大值。
took:执行整个搜索请求耗费了多少毫秒。
_shards:在查询中参与分片的总数,以及这些分片成功了多少个失败了多少个。正常情况下我们不希望分片失败,但是分片失败是可能发生的。如果我们遭遇到一种灾难级别的故障,在这个故障中丢失了相同分片的原始数据和副本,那么对这个分片将没有可用副本来对搜索请求作出响应。假若这样,Elasticsearch 将报告这个分片是失败的,但是会继续返回剩余分片的结果。
timeout:查询是否超时。默认情况下,搜索请求不会超时。不过我们可以指定搜索的最大时间:
GET /_search?timeout=1ms
指定索引与类型
如果看过Elasticsearch文档的资料的,应该对于在url中添加索引与类型很熟悉,在搜索中,我们也一样可以在url指定索引与类型:
GET /_search
#在所有的索引中搜索所有的类型
GET /gb/_search
#在 gb 索引中搜索所有的类型
GET /gb,us/_search
#在 gb 和 us 索引中搜索所有的文档
GET /g*,u*/_search
#在任何以 g 或者 u 开头的索引中搜索所有的类型
GET /gb/user/_search
#在 gb 索引中搜索 user 类型
GET /gb,us/user,tweet/_search
#在 gb 和 us 索引中搜索 user 和 tweet 类型
GET /_all/user,tweet/_search
#在所有的索引中搜索 user 和 tweet 类型
分页
在默认搜索时,Elasticsearch默认只会返回10条数据,如果我们需要看到其它的文档的话就需要知道如何分页了。如下(size表示返回数量,from表示偏移量):
GET /_search?size=2&from=1
返回结果:
{
"took" : 8,
"timed_out" : false,
"_shards" : {
"total" : 23,
"successful" : 23,
"skipped" : 0,
"failed" : 0
},
"hits" : {
"total" : {
"value" : 10000,
"relation" : "gte"
},
"max_score" : 1.0,
"hits" : [
{
"_index" : ".kibana_1",
"_type" : "_doc",
"_id" : "config:7.7.1",
"_score" : 1.0,
"_source" : {
"config" : {
"buildNum" : 30896
},
"type" : "config",
"references" : [ ],
"updated_at" : "2020-12-08T07:34:08.496Z"
}
},
{
"_index" : ".kibana_1",
"_type" : "_doc",
"_id" : "upgrade-assistant-telemetry:upgrade-assistant-telemetry",
"_score" : 1.0,
"_source" : {
"upgrade-assistant-telemetry" : {
"ui_open.overview" : 1
},
"type" : "upgrade-assistant-telemetry",
"updated_at" : "2021-02-19T06:22:35.612Z"
}
}
]
}
}
在分布式系统中深度分页
假设在一个有 5 个主分片的索引中搜索。 当我们请求结果的第一页(结果从 1 到 10 ),每一个分片产生前 10 的结果,并且返回给 协调节点 ,协调节点对 50 个结果排序得到全部结果的前 10 个。
现在假设我们请求第 1000 页—结果从 10001 到 10010 。所有都以相同的方式工作除了每个分片不得不产生前10010个结果以外。 然后协调节点对全部 50050 个结果排序最后丢弃掉这些结果中的 50040 个结果。
可以看到,在分布式系统中,对结果排序的成本随分页的深度成指数上升。这就是 web 搜索引擎对任何查询都不要返回超过 1000 个结果的原因。
轻量搜索
Elasticsearch有两种搜索形式,一个是直接将参数添加在url上的轻量搜索,还有一个就是将参数包含在消息体的JSON中的搜索。在项目中不推荐使用轻量搜索,因为可读性不强,功能也没有那么全。
下面借用几个例子来进行介绍:
GET /_all/tweet/_search?q=tweet:elasticsearch
GET /_all/tweet/_search?q=+name:john +tweet:mary
GET /_search?q=mary
GET /_all/tweet/_search?q=+name:(mary john) +date:>2014-09-10 +(aggregations geo)
样例介绍
第一个查询:查询在 tweet 类型中 tweet 字段包含 elasticsearch 单词的所有文档。
第二个查询:查询在 name 字段中包含 john 并且在 tweet 字段中包含 mary 的文档。
第三个查询:搜索返回包含 mary 的所有文档。没有指定参数的,会将文档所有参数值组成一个大字符串进行搜索。
第四个查询:查询name 字段中包含 mary 或者 john,date 值大于 2014-09-10,_all 字段包含 aggregations 或者 geo的文档。
注意
- +前缀表示必须与查询条件匹配。类似地, - 前缀表示一定不与查询条件匹配。没有 + 或者 - 的所有其他条件都是可选的——匹配的越多,文档就越相关。
- 这种精简让调试更加晦涩和困难。而且很脆弱,一些查询字符串中很小的语法错误,像 - , : , / 或者 " 不匹配等,将会返回错误而不是搜索结果。
精确值与全文
Elasticsearch中的数据可以概括的分成:精确值与全文。
精确值就是表示一个精确数据的值,如日期、用户id等,同时字符串其实也可以表示精确值,如用户名称或邮箱。全文指的是文本数据,比如一篇文章或者一封邮件的内容。
精确值的查询,要么是匹配,要么是不匹配,如mysql查询:
SELECT * FROM TABLE_NAME
WHERE name = "John Smith"
AND user_id = 2
AND date > "2014-09-15"
相比之下,查询全文数据就复杂很多,我们一般是查询“该文档与我查询关键词的匹配度有多高?”。现在的搜索引擎使用的都是全文搜索,与精确值搜索不同,全文搜索我们希望能有以下效果:
- 搜索 UK ,会返回包含 United Kindom 的文档。
- 搜索 jump ,会匹配 jumped , jumps , jumping ,甚至是 leap 。
- 搜索 johnny walker 会匹配 Johnnie Walker , johnnie depp 应该匹配 Johnny Depp 。
- fox news hunting 应该返回福克斯新闻( Foxs News )中关于狩猎的故事,同时, fox hunting news 应该返回关于猎狐的故事。
目前搜索引擎都是使用倒排索引来实现这个功能,接下来我们了解一下倒排索引。
倒排索引
倒排索引又叫反向索引,通俗的理解:正向索引是通过key找value,反向就是通过value找key。一个未经处理的数据库中,一般是以文档ID作为索引,以文档内容作为记录。
而Inverted index 指的是将单词或记录作为索引,将文档ID作为记录,这样便可以方便地通过单词或记录查找到其所在的文档。一个倒排索引由文档中所有不重复词的列表构成,对于其中每个词,有一个包含它的文档列表。
如我们有两个文档,他们的内容分别是如下内容:
1.The quick brown fox jumped over the lazy dog
2.Quick brown foxes leap over lazy dogs in summer
为了创建倒排索引,我们首先将每个文档的 content 域拆分成单独的 词(我们称它为 词条 或 tokens ),创建一个包含所有不重复词条的排序列表,然后列出每个词条出现在哪个文档。结果如下所示:
Term | Doc_1 | Doc_2 |
---|---|---|
Quick | X | |
The | X | |
brown | X | X |
dog | X | |
dogs | X | |
fox | X | |
foxes | X | |
in | X | |
jumped | X | |
lazy | X | X |
leap | X | |
over | X | X |
quick | X | |
summer | X | |
the | X |
如搜索 quick brown ,我们只需要查找包含每个词条的文档:
Term | Doc_1 | Doc_2 |
---|---|---|
brown | X | X |
quick | X | |
Total | 2 | 1 |
两个文档都匹配,但是第一个文档比第二个匹配度更高。如果我们使用仅计算匹配词条数量的简单 相似性算法 ,那么,我们可以说,对于我们查询的相关性来讲,第一个文档比第二个文档更佳。
但是如果我们这样查询的话,还是会存在一些问题。
- Quick 和 quick 以独立的词条出现,然而用户可能认为它们是相同的词。
- fox 和 foxes 非常相似, 就像 dog 和 dogs ;他们有相同的词根。
- jumped 和 leap, 尽管没有相同的词根,但他们的意思很相近。他们是同义词。
所以我们还需要搜索有以下效果:
- Quick 可以小写化为 quick 。
- foxes 可以 词干提取 --变为词根的格式-- 为 fox 。
- jumped 和 leap 是同义词,可以索引为相同的单词jump。
这里就涉及到了Elasticsearch另外几个知识点,分词、标准化和同义词。
分析与分析器
Elasticsearch的分析包括了下面过程:
- 分词,将一块文本分成适合于倒排索引的独立的词语。
- 标准化,将这些词语标准化,提高它们的“可搜索性”。
分析器执行上面的工作,分析器实际有以下3个功能:
- 字符过滤器:在分词之前对字符串进行初步整理。一个字符过滤器可以用来去掉HTML,或者将 & 转化成 and。
- 分词器:将文本切割成单词。
- Token过滤器:过滤器可能会改变词条(例如,小写化 Quick ),删除词条(例如, 像 a, and, the 等无用词),或者增加词条(例如,像 jump 和 leap 这种同义词)。
内置分析器
Elasticsearch有一些自带的分析器,这里以官方案例简单记录一下:
#待分析文本
Set the shape to semi-transparent by calling set_trans(5)
- 标准分析器:标准分析器是Elasticsearch默认使用的分析器。它是分析各种语言文本最常用的选择。它根据 Unicode 联盟 定义的 单词边界 划分文本。删除绝大部分标点。最后,将词条小写。处理结果为:
set, the, shape, to, semi, transparent, by, calling, set_trans, 5
- 简单分析器:简单分析器在任何不是字母的地方分隔文本,将词条小写。处理结果为:
set, the, shape, to, semi, transparent, by, calling, set, trans
- 空格分析器:空格分析器在空格的地方划分文本。处理结果为:
Set, the, shape, to, semi-transparent, by, calling, set_trans(5)
- 语言分析器:特定语言分析器可用于 很多语言。它们可以考虑指定语言的特点。例如, 英语 分析器附带了一组英语无用词(常用单词,例如 and 或者 the ,它们对相关性没有多少影响),它们会被删除。英语分析器的处理结果为:
set, shape, semi, transpar, call, set_tran, 5
分析器测试
对于分析器不熟悉的,可以在analyze API进行测试,如下:
GET /_analyze
{
"analyzer": "standard",
"text": "Failure is never quite so frightening as regret."
}
返回结果:
{
"tokens" : [
{
"token" : "failure",
"start_offset" : 0,
"end_offset" : 7,
"type" : "<ALPHANUM>",
"position" : 0
},
{
"token" : "is",
"start_offset" : 8,
"end_offset" : 10,
"type" : "<ALPHANUM>",
"position" : 1
},
{
"token" : "never",
"start_offset" : 11,
"end_offset" : 16,
"type" : "<ALPHANUM>",
"position" : 2
},
{
"token" : "quite",
"start_offset" : 17,
"end_offset" : 22,
"type" : "<ALPHANUM>",
"position" : 3
},
{
"token" : "so",
"start_offset" : 23,
"end_offset" : 25,
"type" : "<ALPHANUM>",
"position" : 4
},
{
"token" : "frightening",
"start_offset" : 26,
"end_offset" : 37,
"type" : "<ALPHANUM>",
"position" : 5
},
{
"token" : "as",
"start_offset" : 38,
"end_offset" : 40,
"type" : "<ALPHANUM>",
"position" : 6
},
{
"token" : "regret",
"start_offset" : 41,
"end_offset" : 47,
"type" : "<ALPHANUM>",
"position" : 7
}
]
}
token 是实际存储到索引中的词条。 position 指明词条在原始文本中出现的位置。 start_offset 和 end_offset 指明字符在原始字符串中的位置。
映射
在搜索时,不能将所以的数据都作为文本来进行处理。为了能够将时间域视为时间,数字域视为数字,字符串域视为全文或精确值字符串, Elasticsearch 需要知道每个域中数据的类型。这个信息包含在映射中。
核心简单域类型
Elasticsearch 支持如下简单域类型:
- 字符串: string
- 整数 : byte, short, integer, long
- 浮点数: float, double
- 布尔型: boolean
- 日期: date
在Elasticsearch中,一个包含新域的文档如果没有指定映射关系的话,Elasticsearch将会使用动态映射,根据JSON中的基本类型来自动映射:
JSON type | 域 type |
---|---|
布尔型: true 或者 false | boolean |
整数: 123 | long |
浮点数: 123.45 | double |
字符串,有效日期: 2014-09-15 | date |
字符串: foo bar | string |
查看映射
当我们需要查看到映射信息时,可以通过下面 方法:
GET /gp_gamegroupdata_v1/_mapping
返回信息:
{
"gp_gamegroupdata_v1" : {
"mappings" : {
"properties" : {
"@timestamp" : {
"type" : "date"
},
"@version" : {
"type" : "text",
"fields" : {
"keyword" : {
"type" : "keyword",
"ignore_above" : 256
}
}
},
"id" : {
"type" : "long"
},
"name" : {
"type" : "text",
"fields" : {
"keyword" : {
"type" : "keyword",
"ignore_above" : 256
}
},
"analyzer" : "ik_max_word"
},
"status" : {
"type" : "long"
},
"update_time" : {
"type" : "long"
}
}
}
}
}
注意:较低版本的话,可能需要在url后面加上type。
GET /{index}/_mapping/{type}
自定义域映射
尽管在很多情况下基本域数据类型已经够用,但你经常需要为单独域自定义映射,特别是字符串域。自定义映射允许你执行下面的操作:
- 全文字符串域和精确值字符串域的区别
- 使用特定语言分析器
- 优化域以适应部分匹配
- 指定自定义数据格式
域最重要的属性是 type 。对于不是 string 的域,你一般只需要设置 type :
{
"number_of_clicks": {
"type": "integer"
}
}
string 域映射的两个最重要属性是 index 和 analyzer 。
index 属性控制怎样索引字符串。它可以是下面三个值:
- analyzed:
首先分析字符串,然后索引它。换句话说,以全文索引这个域。 - not_analyzed:索引这个域,所以它能够被搜索,但索引的是精确值。不会对它进行分析。
- no:不索引这个域。这个域不会被搜索到。
{
"tag": {
"type": "string",
"index": "not_analyzed"
}
}
analyzer 属性指定在搜索和索引时使用的分析器。默认, Elasticsearch 使用 standard 分析器, 但你可以指定一个内置的分析器替代它,例如 whitespace 、 simple 和 english:
{
"tweet": {
"type": "string",
"analyzer": "english"
}
}
测试映射
与分析器一样,我们可以对于映射进行测试:
GET /index_name/_analyze
{
"field": "name",
"text": "Black-cats"
}
返回结果:
{
"tokens" : [
{
"token" : "black-cats",
"start_offset" : 0,
"end_offset" : 10,
"type" : "LETTER",
"position" : 0
},
{
"token" : "black",
"start_offset" : 0,
"end_offset" : 5,
"type" : "ENGLISH",
"position" : 1
},
{
"token" : "cats",
"start_offset" : 6,
"end_offset" : 10,
"type" : "ENGLISH",
"position" : 2
}
]
}
复杂核心域类型
除了前面介绍的简单类型之外,Elasticsearch还支持一些复杂的类型,如null、数组、对象等。
数组
如果我们希望 tag 域包含多个标签。我们可以以数组的形式索引标签:
{ "tag": [ "search", "nosql" ]}
对于数组,没有特殊的映射需求。任何域都可以包含0、1或者多个值,就像全文域分析得到多个词条。数组中所有的值必须是相同数据类型的 。你不能将日期和字符串混在一起。如果你通过索引数组来创建新的域,Elasticsearch 会用数组中第一个值的数据类型作为这个域的 类型 。
空域
下面三种域被认为是空的,它们将不会被索引:
"null_value": null,
"empty_array": [],
"array_with_null_value": [ null ]
对象
对象 -- 在其他语言中称为哈希,哈希 map,字典或者关联数组。
内部对象 经常用于嵌入一个实体或对象到其它对象中。例如,与其在 tweet 文档中包含 user_name 和 user_id 域,我们也可以这样写:
{
"tweet": "Elasticsearch is very flexible",
"user": {
"id": "@johnsmith",
"gender": "male",
"age": 26,
"name": {
"full": "John Smith",
"first": "John",
"last": "Smith"
}
}
}
内部对象映射
Elasticsearch 会动态监测新的对象域并映射它们为 对象 ,在 properties 属性下列出内部域:
{
"gb": {
"tweet": { (1)
"properties": {
"tweet": { "type": "string" },
"user": { (2)
"type": "object",
"properties": {
"id": { "type": "string" },
"gender": { "type": "string" },
"age": { "type": "long" },
"name": {
"type": "object",
"properties": {
"full": { "type": "string" },
"first": { "type": "string" },
"last": { "type": "string" }
}
}
}
}
}
}
}
}
- 1为根对象
- 2为内部对象
内部对象索引
Lucene 不理解内部对象。 Lucene 文档是由一组键值对列表组成的。为了能让 Elasticsearch 有效地索引内部类,它会将文档进行转化:
{
"tweet": [elasticsearch, flexible, very],
"user.id": [@johnsmith],
"user.gender": [male],
"user.age": [26],
"user.name.full": [john, smith],
"user.name.first": [john],
"user.name.last": [smith]
}
内部域 可以通过名称引用(例如, first )。为了区分同名的两个域,我们可以使用全 路径 (例如, user.name.first ) 或 type 名加路径( tweet.user.name.first )。
内部对象数组
内部对象的数组是如何被索引的。 假设我们有个 followers 数组:
{
"followers": [
{ "age": 35, "name": "Mary White"},
{ "age": 26, "name": "Alex Jones"},
{ "age": 19, "name": "Lisa Smith"}
]
}
这个数组将会被扁平化处理:
{
"followers.age": [19, 26, 35],
"followers.name": [alex, jones, lisa, smith, mary, white]
}
{age: 35} 和 {name: Mary White} 之间的相关性已经丢失了,因为每个多值域只是一包无序的值,而不是有序数组。这足以让我们问,“有一个26岁的追随者?”
但是我们不能得到一个准确的答案:“是否有一个26岁 名字叫 Alex Jones 的追随者?”