利用Elasticsearch改善Snyk的全文搜索的经验教训

141 阅读8分钟

工程

利用Elasticsearch改善Snyk的全文搜索的经验教训

Sergey Vasilkov 2021年11月4日

Elasticsearch是一个流行的开源搜索引擎。由于其实时速度和强大的API,它是需要在项目中添加全文搜索功能的开发者的热门选择。除了普遍流行之外,它也是我们目前正在为问题转移我们的Snyk报告功能的引擎!而一旦我们在问题上有了一切调整,我们将开始在其他报告领域使用Elasticsearch。

虽然Elasticsearch很强大,但它一开始也会显得很复杂(除非你有搜索引擎的背景)。但由于我最近在Snyk实施这个工具时刚刚了解了很多,所以我想我应该把一些知识传授给你,开发者之间的交流!

有时标准分析器是不够的

Elasticsearch通过对不同的数据进行不同的处理来保持快速。在各种各样的字段类型中,Elasticsearch有文本字段--一种用于文本内容(即字符串)的常规字段。为了使存储在该字段中的信息可以被搜索到,Elasticsearch在摄入时进行文本分析,将数据转换为标记(术语),并将这些标记和其他相关信息(如长度、位置)存储到索引中。默认情况下,它使用一个标准的分析器进行文本分析。

来自官方文档。"标准分析器为你提供了对大多数自然语言和使用情况的开箱即用支持。如果你选择按原样使用标准分析器,就不需要进一步配置。"

在基本情况下,标准分析就足够了。让我们通过运行以下命令来说明它是如何工作的,我们将能够看到对我们提供的任何文本生成的分析。

POST _analyze
{
  "analyzer": "standard",
  "text": "Regular Expression Denial of Service (ReDoS)"
}

And the result will look like:

{
  "tokens" : [
    {
      "token" : "regular",
      "start_offset" : 0,
      "end_offset" : 7,
      "type" : "<ALPHANUM>",
      "position" : 0
    },
    {
      "token" : "expression",
      "start_offset" : 8,
      "end_offset" : 18,
      "type" : "<ALPHANUM>",
      "position" : 1
    },
    {
      "token" : "denial",
      "start_offset" : 19,
      "end_offset" : 25,
      "type" : "<ALPHANUM>",
      "position" : 2
    },
    {
      "token" : "of",
      "start_offset" : 26,
      "end_offset" : 28,
      "type" : "<ALPHANUM>",
      "position" : 3
    },
    {
      "token" : "service",
      "start_offset" : 29,
      "end_offset" : 36,
      "type" : "<ALPHANUM>",
      "position" : 4
    },
    {
      "token" : "redos",
      "start_offset" : 38,
      "end_offset" : 43,
      "type" : "<ALPHANUM>",
      "position" : 5
    }
  ]
}

在这里,我们可以看到标准分析器到底做了什么:它把文本分解成标记,并为每一个块生成服务信息,以存储在索引中。

搜索我们索引的文本

一旦有了索引,我们就可以用标准分析器测试全文搜索。我们将创建一个有两个字段的样本文档:titledescription 。Elasticsearch会自动映射该文档并检测文本字段。

PUT text-search-index/_doc/1
{
  "title": "Regular Expression Denial of Service (ReDoS)",
  "description": "The Regular expression Denial of Service (ReDoS) is a type of Denial of Service attack. Regular expressions are incredibly powerful, but they aren'\''t very intuitive and can ultimately end up making it easy for attackers to take your site down."
}

现在让我们用下面的查询来搜索 "Express"。

GET text-search-index/_search
{
  "query": {
    "match": {
      "title": {
    	"query": "express"
      }
    }
  }
}

没有任何文件会被返回,尽管我们的标题中有 "expression"。这个结果是预料之中的(好吧,是我预料之中的,因为我以前也遇到过这种情况!),因为分析器没有创建 "expression "标记(我们可以从上面检索到的生成的标记中看到)。

如果我们把搜索查询改为 "expression",就会返回文档。

GET text-search-index/_search
{
  "query": {
    "match": {
      "title": {
    	"query": "expression"
      }
    }
  }
}

现在,有一些更昂贵的查询可以运行,以绕过这个标记匹配问题,比如模糊查询和通配符查询,但这些都是计算上的昂贵。因此,我们需要重新评估我们的分析器,而不是把CPU周期扔给这个问题。

很明显,如果我们想要更好、更灵活的搜索结果,一个标准的分析器可能是不够的。为了达到我们的目的,我们将配置一个自定义的分析器。我们的要求会是什么?

  • 使用较小的查询
  • 不区分大小写的搜索

为了实现这一点,我们的下一步将是。

  1. 创建一个自定义的分析器
  2. 根据使用情况配置一个更好的标记器
  3. 为Elasticsearch索引中的选定字段启用新的自定义分析器。

深入了解分析器

通过使用标准分析器,我们绕过了真正理解分析器的作用,使生活变得简单。现在我们要做一个自定义的分析器,我们需要了解文本分析过程中会发生什么

  1. 字符过滤器(可选)被应用于正在分析的文本,以剥除字符。
  2. A 标记器将文本分解为标记或术语。这可以用不同的方式来完成,按空白处、按字母等生成标记。
  3. 代币过滤器(可选)对标记进行额外的改变,如转换为小写字母,删除特定的标记,等等。

一个分析器是标记器和过滤器的组合。它执行文本分析,并使其为搜索做好准备。每个分析器都必须有一个标记器,但这两种过滤器类型可以少也可以多。有一件事永远不会改变,那就是操作的顺序。它总是这样:字符过滤器>标记器>标记过滤器。

创建我们的第一个自定义分析器

正如我们之前看到的,默认情况下,所有文本字段都使用标准分析器。Elasticsearch提供了大量的 内置分析器,可以在任何索引中使用,无需进一步配置,你也可以创建自己的分析器。我们将需要为这个用例创建我们自己的。

分析器可以在不同的层次上进行设置:索引、字段或查询。我们将创建一个自定义分析器,并为一个字段指定它。这就是自定义分析器的配置方式。注意: 配置被存储在索引设置中。

PUT text-search-index
{
  "settings":{
     "analysis":{
        "analyzer":{
           "my_analyzer":{
              "type":"custom",
              "tokenizer":"standard"
           }
        }
     }
  },
  "mappings":{
      "properties":{
         "title": {
            "type":"text",
            "analyzer":"my_analyzer"
         },
         "text": {
           "type": "text"
         }
      }
   }
}

下面是我们所做的。

  1. 为索引指定了一个名为my_analyzer 的新的自定义分析器
  2. my_analyzer 使用 tokenizer和 filterstandard lowercase
  3. my_analyzer 启用作为索引映射中标题属性的分析器

这个例子相当简单,没有改变标准分析器的行为。现在我们可以尝试调整它,使其有可能使用不完整的词进行不区分大小写的搜索。

选择和配置一个更好的标记器

在我们之前的例子中,我们使用了标准的标记器,现在是时候用edge_ngram来取代它了,它将根据长度(而不是根据空白)来分割标记。如果你好奇为什么我使用edge_ngram而不是stemmer,你就需要继续阅读了

...
"analysis":{
  "analyzer":{
    "my_analyzer":{
      "type":"custom",
      "tokenizer":"my_tokenizer"
    }
  },
  "tokenizer": {
    "my_tokenizer": {
      "type": "edge_ngram",
      "min": 3,
      "max": 8,
      "token_chars": ["letter", "digit"]
    }
  }
}
...

下面是我们所做的。

  1. my_tokenizer 使用 tokenizeredge_ngram
  2. 它将创建长度从38 符号的标记
  3. 令牌将只包括字母和数字

选择和配置一个更好的标记过滤器

我们在上面创建的分析器现在可以使用长度为3到8个字符的不完整单词进行搜索。剩下的最后一件事是使搜索不区分大小写,这可以通过向my_analyzer 添加一个适当的lowercase 标记过滤器来实现。

...
"analysis":{
  "analyzer":{
    "my_analyzer":{
      "type":"custom",
      "tokenizer":"my_tokenizer",
      "filter":[
        "lowercase"
      ]
    }
  },
  "tokenizer": {
    "my_tokenizer": {
      "type": "edge_ngram",
      "min": 3,
      "max": 8,
      "token_chars": ["letter", "digit"]
    }
  }
}
...

最终结果

在我们做了所有的修改之后,我们现在已经准备好用自定义分析器创建一个新的索引。值得注意的是,在改变分析器之前,它的设置不能使用更新映射API对现有字段进行更新。

下面是我们的基本测试索引的设置与映射的样子。

PUT text-search-index
{
  "settings":{
    "analysis":{
      "analyzer":{
        "my_analyzer":{
          "type":"custom",
          "tokenizer":"my_tokenizer",
          "filter":[
            "lowercase"
          ]
        }
      },
      "tokenizer": {
        "my_tokenizer": {
          "type": "edge_ngram",
          "min": 3,
          "max": 8,
          "token_chars": ["letter", "digit"]
        }
      }
    }
  },
  "mappings":{
    "properties":{
      "title": {
        "type":"text",
        "analyzer":"my_analyzer"
      },
      "text": {
        "type": "text"
      }
    }
  }
}

现在我们可以添加一个文件并运行不同的查询,以确保我们的搜索工作符合预期。

PUT text-search-index/_doc/1
{
  "title": "Regular Expression Denial of Service (ReDoS)",
  "description": "The Regular expression Denial of Service (ReDoS) is a type of Denial of Service attack. Regular expressions are incredibly powerful, but they aren'\''t very intuitive and can ultimately end up making it easy for attackers to take your site down."
}

搜索包含 "express "或其他不完整且不区分大小写的标记的标题。

GET text-search-index/_search
{
  "query": {
    "match": {
      "title": {
    	"query": "express" // try: EXPRESS, exp, expression
      }
    }
  }
}

请注意,我们没有为我们的索引的文本属性配置一个自定义的分析器,所以在这里我们仍然有一个好的老式标准(分析器)搜索。

接下来是什么?

在这篇简单的文章中,我们为Elasticsearch中更灵活的文本搜索配置了一个自定义分析器,我们涵盖了一些具体和基本的设置和配置。Elasticsearch提供了大量的内置分析器、标记器、过滤器、规范化器、停止搜索设置等等。

结合它们,我们可以实现伟大的自定义搜索结果。如果你是Elasticsearch的新手,我希望你觉得这对你有帮助。请继续关注,因为我们有计划分享更多关于我们的搜索之旅。下一步将是聚合!(你可以指望它......)

如果你想成为Snyk的一员,建立伟大的开发者安全工具,请查看我们开放的工程职位

SnykCon 2021已经结束了!

重温所有你喜欢的讲座,并查看你无法参加的讲座。

现在观看