Elasticsearch轻量搜索与分析

前言

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 的追随者?”

作者:红雨
出处:https://www.cnblogs.com/52why
微信公众号: 红雨python
原文地址:https://www.cnblogs.com/52why/p/14430706.html