Elasticsearch:使用新的 wildcard 字段更快地在字符串中查找字符串 - 7.9 新功能

在 Elasticsearch 7.9 中,我们将引入一种新的 “wildcard” 字段类型,该字段类型经过优化,可在字符串值中快速查找模式。这种新的字段类型采用了一种全新的方式来索引字符串数据,从而解决了在日志安全性数据中高效索引和搜索的最佳实践。根据你现有的字段用法,通配符可以提供:

  • 更简单的搜索表达式(无需将 AND /单词/短语组合在一起)
  • 更简单的索引编制(无需定义分析器选择)
  • 不再有的缺失值
  • 更快的搜索
  • 更少的磁盘使用

这种新数据类型最令人兴奋的功能是简化了部分匹配。使用通配符,你不再需要担心文本模式在字符串中的位置。只需使用常规查询语法进行搜索,Elasticsearch 就会在字符串中的任何位置找到所有匹配项。没错,我们完成了所有这些操作,而无需您更改查询语法。

当涉及字符串值的部分匹配时,通配符数据类型解决了安全性领域中那些人指出的问题,但在可观察性及其他方面具有应用程序(如下所述)。尤其是,希望搜索计算机生成的日志的用户应发现此新字段类型比现有选项更自然。

在此博客中,我们将介绍如何做到这一点,以及如何从通配符字段中获得最大收益。让我们来看看。

 

索引字符串的现有选项

直到7.9,在搜索字符串时,Elasticsearch 大致提供了两种选择:

  • text fields
  • keyword fields

文本字段将字符串 “tokenize” 为多个 token,每个token 通常代表单词。搜索 quick foxes 的搜索者因此可以匹配谈论 quick brown fox 的文章。

keyword 字段通常用于较短的结构化内容,例如国家/地区代码,无需分析即可将其编入索引作为单个 token。然后,分析师可以使用这些数据,比如,运用 aggregation 可视化受欢迎的度假胜地等。

如果没有用户明确的映射选择,Elasticsearch 的默认索引模板假定 JSON 文档中显示的字符串值应同时索引为 text 字段和 keyword 字段。目的是,如果字符串表示应该被切成多个单词 token 以进行搜索的散文,或者当被用于 “热门目的地”条形图中展示时应作为单个结构化值(例如城市名称 “New York”)。

 

文字和关键字字段不足时

对于 text 或 keyword 字段效果不佳的字符串内容,我们不必走得太远。 Elasticsearch 自己的日志文件产生的消息不能很好地适合任一类别。让我们检查一下原因。

每个 Elasticsearch 日志文件条目都遵循记录事件的经典最低要求- timestam 和 message。记录的 message 用于以半结构形式描述各种事件,例如:

  • 正在创建或删除的索引
  • 服务器加入或离开集群
  • 资源压力警告
  • 具有长堆栈跟踪的详细错误消息

上面是一个已记录事件的示例。上面高亮显示的是我想搜索以查找类似记录错误的消息部分。那将是在文本编辑器中的简单 “Cmd / Ctrl-F” 键按下,但是使用 Elasticsearch 并不是那么简单。请记住,Elasticsearch 是很容扩展的,因此 Ctrl-F 非常适合小的文本文件,但不适用于TB级数量的日志。需要某种索引才能快速搜索。

将消息记录为 text 字段

如果我们将消息内容索引为 text 字段,则最终用户将立即遇到翻译问题。选定的子字符串是“ NodeNotConnectedException:[54b_data_2]”,但关键的问题是 “索引中该如何表示”?

  • 是半个 token 吗?
  • 一个 token?
  • 一个半 token?
  • 二个?两个半? ...

之所以会出现这个谜,是因为与英语文本(例如“quick brown fox”)不同,我们不知道单词在日志消息中的开头或结尾的位置,例如上面显示的堆栈跟踪。这不是人类自然会讲的语言,因此很容易意外地仅选择单词的一部分。

match 查询可以帮助用户规避必须知道选择中的:or [ or _ 字符是单词标记的一部分还是仅用于拆分单词。但是,match 查询将无助于知道我们选择的开始或结束是否会裁剪掉完整的 token。如果分析器的选择保留 Java 包名中的逗点 .,我们将不得不使用昂贵的通配符搜索 *NodeNotConnectedException 或向后退选择以添加前面的 org.elasticsearch.transport. 到我们的 “match” 查询中的字符中。

即使对于有经验的 Elasticsearch 开发人员,构造一个尊重 tokenization 策略的查询也是一个巨大的挑战。

将消息记录为 keyword 字段

keyword 字段对于如何存储索引内容的奥秘要少得多,它是一个字符串,我们可以使用单个正则表达式或通配符表达式来匹配字段值内的任意片段(例如我们的搜索字符串)。

为用户简化了搜索,但是执行过程存在两个主要问题:

  • 搜索速度
  • 缺失数据

如果像我们的搜索一样,我们正在字符串中间搜索某些东西并且有许多唯一值,则搜索速度会非常慢。尽管关键字字段已建立索引,但这是一个错误的数据结构。一个例子就是研究人员尝试在词典中查找所有带有 “oo” 的单词。他们必须扫描从 A 到 Z 的每一页上的每个单词,才能找到 “afoot” 到 “zoo” 的值。从本质上讲,keyword 字段与每个唯一值有关,而索引对于加快此过程没有任何帮助。使事情变得更加困难的是,如果日志消息像 Elasticsearch 产生的那样包含诸如时间和内存大小等变量,则日志消息通常会产生许多唯一的字符串值。

keyword 字段的另一个大问题是它不能处理很长的字段。默认的字符串映射将忽略长度超过256个字符的字符串,并从索引术语列表中静默删除值。 Elasticsearch 的大多数日志文件消息都超过了此限制。

即使你确实提高了 Elasticsearch 限制,也不能超过单个 token 的 32k 硬 Lucene 限制,并且 Elasticsearch 当然会记录一些超出此限制的消息。

这对于我们的日志记录来说是一个盲点,这是不希望的,在某些安全情况下是不可接受的。

 

引入新的 wildcard 字段

为了解决这些问题,我们有一个新的 wildcard 字段,该字段擅长在任意字符串值的中间查找任何模式。

  • 与 text 字段不同,它不会将字符串视为由标点符号分隔的单词的集合。
  • 与 keyword 字段不同,它可以快速地搜索许多唯一值,并且没有大小限制。

这个是怎么工作的呢?

详细的模式匹配操作(例如通配符或正则表达式)在大型数据集上运行可能会非常昂贵。加快这些操作的关键是限制应用这些详细比较的数据量。

新的 wildcard 字段使用以下两种数据结构以这种方式自动加速通配符和正则表达式搜索:

  • 字符串中所有3个字符序列的  n-gram” 索引
  • 完整原始文档值的 “二进制 doc value” 存储

基于来自搜索字符串的匹配 n-gram 片段,第一种结构用于提供候选对象的快速但粗略的缩小。

第二个数据结构用于通过自动查询验证由 n-gram 语法匹配产生的匹配候选。

 

wildcard 例子

安全分析人员可能会对查找黑客可能提到 “shell” 一词的任何日志消息感兴趣。 给定通配符查询 “*shell*”,通配符字段会自动将此搜索字符串拆分为3个字符的 n-gram,从而创建与 Lucene 查询等效的内容:

she AND ell

这样可以减少我们需要考虑的文档数量,但可能会产生一些误报-例如 包含字符串的文档:

/Users/Me/Documents/Fell walking trip with Sheila.docx

显然,搜索字符串越长,产生的误报就越少。 例如,搜索 “* powershell.exe*” 将产生更具选择性的 n-gram 查询

pow AND wer AND rsh AND hel AND l.e AND exe

消除任何误报是必不可少的完成阶段,因此 wildcar 字段将对由粗糙的 n-gram 查询产生的所有匹配项进行此检查。 这是通过从 Lucene 二进制 doc value 存储中检索完整的原始值,并对完整值运行通配符或 regexp 模式来完成的。

 

Regexp 例子

正则表达式更为复杂,并且可以包含值路径的选择,例如 此搜索对 DLL 或 EXE 文件的引用:

.*\.(dll|exe)

为了加速这种查询,将表达式解析为使用布尔逻辑排列的 n-gram 语法:

(.dl AND ll_) OR (.ex AND xe_)

请注意,在此示例中,_ 字符用于显示在何处添加了不可打印的字符以标记索引字符串的结尾。

 

正则表达式及区分大小写

我们知道安全分析师已经调整了他们的 Elasticsearch 正则表达式搜索以使用混合大小写表达式,例如在搜索对 cmd.exe 的引用时无论他们将写哪种大小写:

[Cc][Mm][Dd].[Ee][Xx][Ee]

wildcard 字段可识别这些混合大小写的表达式,并且将优化上述表达式的粗略查询,使其等效于此 Lucene 查询:

cmd AND d.e AND exe

(请注意,用于加速查询的 n-gram 索引始终为小写)。

在索引字段中使用 wildcard 字段的内容时,在 keyword 字段上使用这些大小写混合的现有安全规则应运行得更快。

但是,这些表达式看起来仍然很难看,很难编写,因此在将来的 Elasticsearch 版本中,我们还希望在 regexp 查询中引入区分大小写的标志,可以将其设置为 “false” 以使查询不区分大小写。 从本质上来说,它将搜索 abc 搜索 [Aa] [Bb] [Cc],而无需用户写所有这些方括号。 最终结果将是更简单,更快速的规则。

 

与 keyword 字段比较

keyword 字段的一个主要目标是在可行的情况下替代 keyword 字段。 我们一直努力确保它对所有查询类型产生相同的结果-尽管有时 wildcard 会更快,有时会更慢。 该表应有助于比较两个字段:

  1. 从32个压缩块中检索 doc value 时速度稍慢
  2. 有点慢,因为与 n-gram 的近似匹配需要验证
  3. keyword 字段仅访问每个唯一值一次,而 wildcard 字段则评估每个值的发音
  4. 如果启用了 “allow expensive queries” 设置
  5. 它取决于通用前缀-keyword 字段具有基于通用前缀的压缩,而 wildcard 字段则是整值 LZ4 压缩
  6. 内容会有所不同,但测试索引网站日志需要499秒,而 keyword 是365秒

下一步是什么

为了使搜索和分析应用程序可以考虑到越来越多的字段类型列表,从而简化了工作,我们简化了索引描述字段功能 API 中字段的方式。功能上与关键字字段等效的物理字段(例如 wildcard 和 constant_keyword)现在将自己描述为属于 keyword 家族。引入类型意味着将来可以进行其他字段类型分组,并且可以采用它们,而下游客户端应用程序无需了解新的物理字段类型。只要它们的行为与同一家族的其他成员相同,就可以在不更改客户端应用程序的情况下使用它们。

弹性通用模式(ECS)为流行的数据字段定义了标准化的索引映射。

在7.9中,尚未将 ECS 更新为使用 wildcard 字段,但是在将来,我们可以期望将几个高基数 keyword 字段切换为使用通配符字段。发生这种情况时,客户端应用程序通常不需要因此而进行任何更改,而应受益于更快的搜索。

 

听起来不错,代价是什么呢?

像往常一样,答案是“取决于”。 存储成本主要取决于字段数据的重复级别。 对于大多数唯一值,存储成本实际上可以比使用 keyword 字段的存储成本低。 这是因为我们增加了 Lucene 来压缩用于访问原始内容的二进制 doc value 存储。

我们测试了一个包含200万个 Weblog 记录的示例数据集,其中文件中的每一行都被索引为一个字段中包含的单个值。 使用关键字字段的索引为 310MB,但由于压缩,使用新通配符字段的等效索引为 227MB。 不可否认,时间戳有助于确保每个文档的值都是唯一的,但是URL,IP地址和用户代理的组合也可以导致许多唯一的字符串。

 

摘要备忘单-我什么时候应该使用通配符字段?

我们认识到以上所有内容都需要考虑很多,因此下面给出了适合使用通配符字段的粗略指南。

 

动手实践

讲了这么多,那么我们该如何使用这个 wildcard 的新数据类型呢?接下来,我们使用一个例子来进行展示。打开我们的 Kibana (记得安装 Elastic Stack 7.9),并打入如下的命令:

PUT my-index-000001
{
  "mappings": {
    "properties": {
      "my_wildcard": {
        "type": "wildcard"
      }
    }
  }
}

在上面,我们定义了一个叫做 my_wildcard 的字段。它的数据类型是 wildcard。接着我们创建如下的文档:

PUT my-index-000001/_doc/1
{
  "my_wildcard" : "This string can be quite lengthy"
}

我们使用如下的方法来进行搜索:

GET my-index-000001/_search
{
  "query": {
    "match": {
      "my_wildcard": "*quite*lengthy"
    }
  }
}

上面的搜索显示的结果是:

{
  "took" : 6,
  "timed_out" : false,
  "_shards" : {
    "total" : 1,
    "successful" : 1,
    "skipped" : 0,
    "failed" : 0
  },
  "hits" : {
    "total" : {
      "value" : 1,
      "relation" : "eq"
    },
    "max_score" : 3.8610575,
    "hits" : [
      {
        "_index" : "my-index-000001",
        "_type" : "_doc",
        "_id" : "1",
        "_score" : 3.8610575,
        "_source" : {
          "my_wildcard" : "This string can be quite lengthy"
        }
      }
    ]
  }
}