同样,却又不一样:借助同义词让 Elasticsearch 更增强大

同样,却又不一样:借助同义词让 Elasticsearch 更增强大
做者[Christoph Büscher]node

毫无疑问,使用同义词是搜索工程师工具箱中最重要的技巧之一。尽管新手有时会低估同义词的重要性,但几乎全部搜索系统都离不开它。与此同时,人们有时仍会低估与使用同义词相关的一些复杂状况和微妙情形,甚至高级用户也不例外。同义词筛选器是将输入文本转化为可搜索字词这一分析过程当中的一部分;虽然这一工具入门相对简单,但其使用方法十分多样,首先须要深刻了解一些概念,而后才能在实际情境中成功应用。算法

最近咱们在 Elasticsearch 中进行了一些分析方面的改进。最重大的一个功能可能就是容许从新加载搜索时使用的分析器,这样用户即可更改并从新加载搜索时使用的同义词。除了向你们演示这一新的 API 以外,本篇博文还会回答一些有关同义词使用方法的常见问题,并指出一些常常须要注意的相关事项。app

为什么使用同义词?

为帮助你们理解同义词的巨大做用和灵活性,咱们来快速看一下当今大多数搜索引擎的内在工做原理。搜索引擎会对文档和查询进行分析并将其拆解为最小的单元(一般称为词元,实际上就是抽象的符号)。搜索时,匹配过程会使用简单字串类似度,因此若是查询中有一些十分微小的拼写错误(例如“hous”,只比“house”少一个字母 e)或者使用名词的复数形式(“houses”),即便文档中包含名词的单数形式(“house”),搜索引擎也不会匹配到这份文档。词干提取器或模糊查询等工具虽然能够解决一些最多见的此类问题,可是它们并不能消除相关联的概念或想法之间的差别,也不能将文档或查询中稍有不一样的单词用法视为等同。框架

这时同义词就派上了大用场。同义词的英文 synonym 来自于希腊语,分别是前缀 _σύν(syn,表示“一块儿”)_和 _ὄνομα(ónoma,表示“名称”)_。从它的词源能够看出,同义词表示的是在同一语言或领域中具备彻底或基本相赞成思的不一样词语。实际上,同义词的范围很是普遍,包括通常同义词(“疲劳”和“困倦”)、缩写(英镑的两种写法“lb.”和“pound”)、电商搜索中产品的不一样拼写(“iPod”和“i-Pod”)、细微的语言差别(例如均表示电梯的英式英语“lift”和美式英语“elevator”)、专业用词和普通用词(例如“犬”和“狗”),甚至单纯表示同一律念的两种方式(“宇宙”和“太空”)。经过提供恰当的同义词规则,搜索工程师可以就哪些词在各自领域内具备类似意思并应该采起类似处理方法提供相关信息。工具

对搜索引擎而言,至为重要的是知道文档中的哪一个字词与查询内容相匹配,即便它们可能看起来并不同。因为这涉及到十分具体的领域知识,因此用户须要提供恰当的规则。同义词筛选器可在定制分析器中使用,其可以基于用户定义的规则替换或添加其余词元,既可在索引时进行以便在索引后的文档中同时存储这些内容(例如词语的两种变体),也可在索引时进行以扩展搜索词并匹配到更多相关文档。咱们稍后会讨论这两种方法的优缺点。性能

使用同义词时须要注意的几种情形

同义词分析器是一款十分灵活的工具,但可能会致使人们在特定情形中过分使用。例如,有时人们会强行用其来替代词干提取器,这样便会致使同义词文件很大,由于其中包含动词和名词的各类语法变形(针对英语)。尽管这种方法可能奏效,但相较于使用真正的词干提取器或词形还原工具,一般性能较差并且维护起来也较为困难。用其来纠正拼写错误的话,后果也是这样。若是仅有几个特别常见的拼写错误,例如针对电商平台,尝试经过使用同义词来纠正有时还算可取。但若是问题更加普遍,那么使用模糊查询或字符级别的 ngram方法可能更具备持续性。在分析链中还可考虑使用同义词扩展法的替代方案。有时,相对于在限制性更强的分析过程当中使用同义词,在采集管道或某些其余客户端过程当中改善文档反倒更加灵活和易于管理。例如,您可使用命名实体识别 (NER) 框架对您文档中的命名实体进行检测,而后在您的预处理管道中或者在采集时以您特有的标识符对其进行编码。若是您随后对用户的查询应用一样的过程,而后再将它们发送到 Elasticsearch,您能够实现一样的效果,但一般还能拥有更多控制权。测试

此外,您可能还会倾向于使用同义词来处理其余“相同”概念,例如将特定动物物种分组到一个经常使用字词下,甚至针对您的领域构建分类学支持内容。这时事情就会变得特别有趣,也有不少问题须要探索,但要记住同义词有时并不是最佳选择,如使用不慎可能会致使您的系统出现异常行为。搜索引擎

索引时使用同义词和搜索时使用同义词的对比

同义词在分析器中使用,其既可在索引时使用,也可在搜索时使用。关于在 Elasticsearch 中如何使用同义词筛选器,最多见的问题之一就是:“我应该索引时使用,仍是搜索时使用,仍是同时都用?” 咱们首先看一下在_索引时_应用同义词筛选。这意味着会在索引后的文档中对字词进行一次性替换或扩展,结果将一直保存在搜索索引中。编码

索引时使用同义词有几个劣势:spa

  • 因为必须对全部同义词进行索引,因此索引规模会变大。
  • 搜索得分(依赖于字词统计数据)可能会受影响,由于同义词也会计算在内,因此不常见单词的统计数据会存在误差。
  • 除非进行从新索引,不然没法针对既有文档更改同义词规则。

最后两条尤为是巨大劣势。索引时应用同义词的惟一潜在好处是性能好,由于您在前期已费心完成了扩展过程,因此无需再在每次查询时完成一遍扩展过程,这有可能导致须要与更多的字词进行匹配。然而这一点在实践中一般并不是真正的问题。

相反,在搜索时所用的分析工具中使用同义词则能够避免不少上述问题:

  • 索引规模不受影响。
  • 语料库中的字词统计数据保持不变。
  • 如需变动同义词规则,无需对文档进行从新索引。

这些优点一般要高出惟一的劣势,即每次查询时都必须执行同义词扩展操做,这有可能致使须要匹配更多字词。不只如此,搜索时扩展同义词还可以容许使用更加复杂的 synonym_graph词元筛选器,这一工具可以正确处理多单词同义词,而且仅可在搜索分析器中使用。

通常而言,搜索时使用同义词的优点一般要高于索引时使用同义词可能实现的微小性能改进。

然而,若是在搜索时使用同义词,过去还须要注意另一个问题。尽管更改同义词规则不须要对文档进行从新索引,可是如要更改的话,您必须暂时关闭再从新打开索引。这一点颇有必要,由于分析器在下列时候才会建立实例:建立索引时,重启节点时,以及从新打开已关闭的索引时。为了让对同义词规则文件所作的变动对索引可见,用户必须首先在全部节点上更新文件,而后再关闭并从新打开索引。可是这个问题已经得以解决。

同义词,从新加载成功

从 Elasticsearch 7.3 开始,无需从新打开索引便能看到同义词文件中的变动。咱们新增了一个端点,让用户可以按需触发分析器资源的从新加载操做。调用这个新端点将会从新加载索引中的全部分析器,前提是这些索引中的组件已被标记为可更新。相应地,这会使这些组件只能在搜索时使用。

对同义词筛选器而言,将其标记为可更新并调用“从新加载 API”会使每一个节点上的同义词配置文件对分析过程可见。虽然仍不能(经过同义词参数)更新筛选器定义中的同义词规则,但这些同义词规则应该主要用于偶尔的测试目的。不管何种状况,用配置文件来配置同义词有几个优点:

  • 管理更简单!在生产系统中,可能会有不少同义词规则,并且因为这些同义词规则会显著影响搜索相关度,因此应该将其视为配置中不可或缺的一部分,且对于任何更新,都须要进行版本控制和测试。
  • 同义词一般来自其余来源,或由您的数据上所运行的算法建立。从文件读取的话,便无需将这些同义词加入到筛选器配置中。
  • 同一份同义词文件可在不一样筛选器中使用。
  • 较大的同义词规则集会占用 Elasticsearch 集群状态(用于存储索引设置的相关元信息)中的大量内存。为避免无谓地增长集群规模,咱们建议将较大的同义词规则集存储在配置文件中。

为演示起见,咱们假设您将包含下列单条规则的初始 my_synonyms.txt 文件添加到 Elasticsearch 节点的 config 目录中。咱们假设此文件最初仅包含下列一条规则:

universe, cosmos

接下来,咱们须要定义一个分析器,并让其在同义词筛选器中引用此文件:

PUT /synonym_test
{
  "settings": {
    "index": {
      "analysis": {
        "analyzer": {
          "synonym_analyzer": {
            "tokenizer": "whitespace",
            "filter": ["my_synonyms"]
          }
        },
        "filter": {
          "my_synonyms": {
            "type": "synonym",
            "synonyms_path": "my_synonyms.txt",
            "updateable": true
          }
        }
      }
    }
  }
}

请注意咱们将同义词筛选器标记为了 updateable(可更新)。这一点很重要,由于当咱们调用新的从新加载端点时,只会从新加载可更新的筛选器;但这样作也有缺点,由于在索引时不容许使用包含可更新筛选器的分析器。可是咱们首先来检查一下同义词是否已正确应用,能够经过 _analyze 端点运行一个简短的测试:

GET /synonym_test/_analyze
{
  "analyzer": "synonym_analyzer",
  "text": "cosmos"
}

此操做应该会返回两个词元,正如咱们所料,其中一个是“universe”。咱们接下来经过添加第二行向 synonyms.txt 文件添加另外一条规则:

lift, elevator

若是使用以前版本,您此时必须关闭并从新打开索引,以便显示这些变动。如今您能够简单地调用新端点:

POST /synonym_test/_reload_search_analyzers

虽然此请求并不要求有正文,但可能会限制为一个或多个使用典型索引通配符模式的索引。响应中包括的信息有已从新加载了哪些分析器,以及哪些节点受到了影响:

{
  [...],
  "reload_details": [{
    "index": "synonym_test",
    "reloaded_analyzers": ["synonym_analyzer"],
    "reloaded_node_ids": ["FXbmbgG_SsOrNRssrYcPow"]
  }]
}

如今对字词“lift”运行上面的 _analyze 请求,也会返回“elevator”(做为第二个同义词词元)。

然而,还有一些注意事项。如上面提到的,搜索时应该使用已标记为 updateable(可更新)的筛选器,因此在字段层面使用上面所定义的同义词分析器的正确方式以下:

POST /synonym_test/_mapping
{
  "properties": {
    "text_field": {
      "type": "text",
      "analyzer": "standard",
      "search_analyzer": "synonym_analyzer"
    }
  }
}

一样,从新加载仅适用于从文件中加载的同义词,亦即不支持更改经过筛选器中的设置所定义的同义词。最后,您在实践中须要确保对集群中的全部节点均应用对同义词文件进行的更新。若是某些节点上的分析器看到了文件的不一样版本,则您可能会收到不一样的搜索结果,具体取决于搜索中使用的是哪一个节点。若是发生与同义词相关的此种状况,首先须要检查每一个节点上的同义词文件是否同样,而后从新触发“从新加载”操做。

总结一下,新的 _reload_search_analyzer 端点可以让您快速修订和更改查询时应用的同义词,而无需从新打开索引。例如,经过检查查询日志,您能够肯定用户查询时所用的字词与已索引文档中的既有字词是否不一样,而后随时进行添加。可是添加同义词会对相关度得分产生意料以外的不良影响,因此咱们建议首先进行某些形式的测试(A/B 测试,或者排名评估API 等均可以),而后再直接在生产环境中应用这些变动。

做为分析链中的一部分

另一个有关同义词筛选器的常见问题是其在更复杂分析链中的行为。在大多数状况下,您会在同义词筛选器以前加入一些常见的字符或词元筛选器,例如 lowercase(小写)筛选器。这意味着流经分析链的全部词元都会变为小写,而后才会应用同义词筛选器。这是否意味着同义词规则中的输入同义词也须要变为小写才能匹配呢?咱们经过这个简单的示例看一下:

PUT /test_index
{
  "settings": {
    "index": {
      "analysis": {
        "analyzer": {
          "synonym_analyzer": {
            "tokenizer": "whitespace",
            "filter": ["lowercase", "my_synonyms"]
          }
        },
        "filter": {
          "my_synonyms": {
            "type": "synonym",
            "synonyms": ["Eins, Uno, One", "Cosmos => Universe"]
          }
        }
      }
    }
  }
}
GET /test_index/_analyze
{
  "analyzer": "synonym_analyzer",
  "text": "one"
}

在上面的示例中,您能够验证小写的输入文本扩展成了三个词元,这表示小写操做也会应用到同义词筛选器的规则中。一样,右边的替换规则(例如“Cosmos => Universe”规则)也会从新编写,正如您看到的下边示例的小写输出:

GET /test_index/_analyze
{
  "analyzer": "synonym_analyzer",
  "text": "cosmos"
}

通常而言,对于提供给前面分析链中所用分词器和筛选器的输入,同义词筛选器会对其进行从新编写。而后,这一规则有一些值得注意的例外状况:多个会输出堆叠式词元的筛选器(例如 common_gramsphonetic筛选器)均不容许在同义词筛选器以前使用,若是您尝试这样作,系统会报错。对于其余筛选器,例如复合词筛选器或同义词筛选器自身,若是它们在分析链中位于另外一个同义词筛选器以前,则会跳过他们。后面一条规则对于实现同义词筛选器链路十分重要。咱们能够在下面的示例中看一下实现过程。

若是您连着使用两个或多个同义词筛选器,会怎样呢?前项的输出会成为后项的输入吗,也就是将同义词筛选器的连接操做转变为部分意义上的传递操做?咱们用下面的示例来试一下:

PUT /synonym_chaining
{
  "settings": {
    "index": {
      "analysis": {
        "filter": {
          "first_synonyms": {
            "type": "synonym",
            "synonyms": ["a => b", "e => f"]
          },
          "second_synonyms": {
            "type": "synonym",
            "synonyms": ["b => c", "d => e"]
          }
        },
        "analyzer": {
          "synonym_analyzer": {
            "filter": [
              "first_synonyms",
              "second_synonyms"
            ],
            "tokenizer": "whitespace"
          }
        }
      }
    }
  }
}
GET /synonym_chaining/_analyze
{
  "analyzer": "synonym_analyzer",
  "text": "a"
}

输出词元为“c”,这表示两个筛选器已按顺序得以应用,即第一个筛选器将“a”替换为“b”,第二个筛选器继而将这一输入替换为“c”。若是您尝试将输入改成“d”,会将其替换为“e”(并未应用第一条规则);但若是您将输入改成“e”,会根据第一条规则将这个词元替换为“f”,致使第二个筛选器根本没有能够匹配的内容。

还记得吗?咱们刚才讲到了基于前面的词元筛选器进行从新编写时有一些例外状况。若是上面的 second_synonyms(第二组同义词)筛选器对其规则集应用了第一个筛选器的规则,那么它会将自身的规则 d => e 更改成 d => f(由于会应用前面筛选器的规则 e => f)。在 Elasticsearch 的早期版本中,这一行为过去经常给人们形成困扰,所以如今处理后面筛选器中的同义词规则时,会跳过同义词筛选器。在 6.6 及以后的版本中,它将会按照咱们所描述的那样运行。

展望将来

在这篇简短的博文中,咱们只是就同义词的用途向你们介绍了冰山一角,并尝试解决了与使用同义词相关的一些常见问题。同义词是一项强大工具,可以用来提高您搜索系统的从新调用率,可是还有一些很重要的细节也须要您知道并进行试验,尤为是与系统性的相关度测试一块儿进行试验。

咱们在 Elasticsearch 7.3 中增长了新 API 以便可以从新加载搜索时应用的分析器,这一 API 可以让您更加轻松地完成此类试验,由于您无需再像以前那样关闭再从新打开索引;此外,其还可以让您更新在搜索时应用的同义词规则,而无需让您的索引离线。咱们但愿经过一系列改进让用户更便利地管理大型集群中的同义词,此 API 只是这一过程当中迈出的一小步而已。欢迎告诉咱们您的想法,在Segmentfault发布的本篇文章中,经过【评论】给咱们提供反馈或提问。祝您分析愉快!

相关文章
相关标签/搜索