Elasticsearch:使用 Path Hierarchy Tokenizer 搜索层次结构
Posted 中国社区官方博客
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Elasticsearch:使用 Path Hierarchy Tokenizer 搜索层次结构相关的知识,希望对你有一定的参考价值。
本文介绍了如何解决一个常见问题,该问题通常通过使用 Elasticsearch 中称为路径层次结构 tokenizer 的功能来查看不同的解决方案,从而通过父/子关系或递归关系来解决。
你可以用这个覆解决什么用例?
在我们深入研究之前,让我们先了解一下使用案例! 有两个常见的用例,我立即想到了 - 我相信还有更多! 第一个是关于对文件系统建模,文件路径如下所示:
/home/alr/devel/spinscale.de/website
/home/alr/devel/spinscale.de/website/index.html
/tmp/trash.xml
第二个用例来自我以前工作的主要例子,电子商务产品搜索引擎。 每个产品都可以属于多个类别,这些类别被建模为面 breadcrumbs。
Electronics > White goods > Refrigerators > Fridge/Freezer Combination
在我们深入研究如何存储它的细节之前,让我们先解决一个问题。 可以使用 join 字段类型吗?
为什么不用连接字段类型?
你可能已经偶然发现了 join 字段类型。该字段类型与 has_parent 查询、has_child 查询、children aggregation 和 inner hits functionality 相结合,允许在你的数据中创建单个 1:n 或父/子关系。每个父母和孩子都是单独的文件。这有时被认为是上述用例的解决方案,因为这允许创建层次结构 - 这对于上面的 breadcrumbs 可能是一个好主意。有一个层次电子,其中包含所有引用它的白色家电。这有几个优点和缺点:
- 单个文档更新很容易
- 将一个类别移到另一个类别很容易,使用新父级的单个文档更新就足够了
- 强大的查询能力,在同一个查询中对父子项执行全文搜索
- 要沿着层次结构向上走,你需要与深度一样多的父查询
- 每个文档只能有一个父级
- 一切都需要存储在一个分片上
最后两点是致命的,不能认为这是一个适当的解决方案。jooin 字段类型有其用例(特别是在子级或父级上的高更新率以及当你没有单个根时),但如果可能,请尝试尽量避免使用它。并且上述电子商务分类或 Unix 路径的用例不需要连接字段类型。
一起来看看怎么做吧!
了解 path_hierarchy 标记器
使一切正常工作的主要组件是路径层次结构 tokenizer。 让我们首先启动并运行一个适当的分析示例:
GET _analyze?filter_path=**.token
{
"tokenizer": {
"type": "path_hierarchy",
"delimiter": ">"
},
"text": [
"Electronics > White goods > Refrigerators > Fridge/Freezer Combination"
]
}
这将返回以下 token:
{
"tokens" : [
{
"token" : "Electronics "
},
{
"token" : "Electronics > White goods "
},
{
"token" : "Electronics > White goods > Refrigerators "
},
{
"token" : "Electronics > White goods > Refrigerators > Fridge/Freezer Combination"
}
]
}
这有一个小问题,那就是最后使用了空格,让我们通过添加修剪过滤器来解决这个问题:
GET _analyze?filter_path=**.token
{
"tokenizer": {
"type" : "path_hierarchy",
"delimiter" : ">"
},
"filter": ["trim"],
"text": [
"Electronics > White goods > Refrigerators > Fridge/Freezer Combination"
]
}
上面的结果返回:
{
"tokens" : [
{
"token" : "Electronics"
},
{
"token" : "Electronics > White goods"
},
{
"token" : "Electronics > White goods > Refrigerators"
},
{
"token" : "Electronics > White goods > Refrigerators > Fridge/Freezer Combination"
}
]
}
旁注:如果你的层次结构颠倒了,你可以使用 “reverse”:true
GET _analyze?filter_path=**.token
{
"tokenizer": {
"type": "path_hierarchy",
"delimiter": ">",
"reverse": true
},
"filter": [
"trim"
],
"text": [
"Electronics > White goods > Refrigerators > Fridge/Freezer Combination"
]
}
上面将返回:
{
"tokens" : [
{
"token" : "Electronics > White goods > Refrigerators > Fridge/Freezer Combination"
},
{
"token" : "White goods > Refrigerators > Fridge/Freezer Combination"
},
{
"token" : "Refrigerators > Fridge/Freezer Combination"
},
{
"token" : "Fridge/Freezer Combination"
}
]
}
对于我们的用例,我们将坚持使用常规 tokenizer 而不是使用 reverse。 使用适当的映射创建索引:
PUT products
{
"mappings": {
"properties": {
"category": {
"type": "text",
"analyzer": "breadcrumb"
}
}
},
"settings": {
"analysis": {
"tokenizer": {
"breadcrumb_path_hierarchy": {
"type": "path_hierarchy",
"delimiter": ">"
}
},
"analyzer": {
"breadcrumb": {
"type": "custom",
"tokenizer": "breadcrumb_path_hierarchy",
"filter": [
"trim"
]
}
}
}
}
}
和索引示例文档:
PUT products/_doc/lg-freezer
{
"category": "Electronics > White goods > Refrigerators > Fridge/Freezer Combination",
"name": "LG Freezer"
}
PUT products/_doc/bauknecht-washing-machine
{
"category": "Electronics > White goods > Washing > Washing Machines",
"name": "Bauknecht P-500"
}
PUT products/_doc/bauknecht-washing-machine-p-200
{
"category": "Electronics > White goods > Washing > Washing Machines",
"name": "Bauknecht P-200"
}
我们分别执行上面的三个命令。
查询前缀
现在让我们执行一些搜索以了解我们可以搜索的内容。 让我们先搜索洗衣机中的所有产品,然后搜索所有白色家电:
GET products/_search
{
"query": {
"bool": {
"filter": [
{
"term": {
"category": "Electronics > White goods > Washing > Washing Machines"
}
}
]
}
}
}
上面命令返回的结果是:
"hits" : [
{
"_index" : "products",
"_type" : "_doc",
"_id" : "bauknecht-washing-machine",
"_score" : 0.0,
"_source" : {
"category" : "Electronics > White goods > Washing > Washing Machines",
"name" : "Bauknecht P-500"
}
},
{
"_index" : "products",
"_type" : "_doc",
"_id" : "bauknecht-washing-machine-p-200",
"_score" : 0.0,
"_source" : {
"category" : "Electronics > White goods > Washing > Washing Machines",
"name" : "Bauknecht P-200"
}
}
]
我们可以看到有两个文档被搜索到了。如果你想搜索白色家电,你必须像这样提供整个 breadcrumb:
GET products/_search
{
"query": {
"bool": {
"filter": [
{
"term": {
"category": "Electronics > White goods"
}
}
]
}
}
}
上面的搜索将返回如下的结果:
"hits" : [
{
"_index" : "products",
"_type" : "_doc",
"_id" : "lg-freezer",
"_score" : 0.0,
"_source" : {
"category" : "Electronics > White goods > Refrigerators > Fridge/Freezer Combination",
"name" : "LG Freezer"
}
},
{
"_index" : "products",
"_type" : "_doc",
"_id" : "bauknecht-washing-machine",
"_score" : 0.0,
"_source" : {
"category" : "Electronics > White goods > Washing > Washing Machines",
"name" : "Bauknecht P-500"
}
},
{
"_index" : "products",
"_type" : "_doc",
"_id" : "bauknecht-washing-machine-p-200",
"_score" : 0.0,
"_source" : {
"category" : "Electronics > White goods > Washing > Washing Machines",
"name" : "Bauknecht P-200"
}
}
]
因此,这样我们就可以查询某个类别中的所有文档,无论它们是否在子类别中。 由于我们拥有全文搜索的能力,我们可以使用 bool 查询的 should 部分来影响分数 - 可能对最近购买的产品进行评分,或者考虑用户的偏好。
使用术语查找查询
为了在同一类别中查找产品,可以使用 terms lookup query,也可以选择排除自己的文档,如下所示:
GET products/_search
{
"query": {
"bool": {
"must_not": [
{
"term": {
"_id": "bauknecht-washing-machine"
}
}
],
"filter": [
{
"terms": {
"category": {
"index": "products",
"id": "bauknecht-washing-machine",
"path": "category"
}
}
}
]
}
}
上面查询的结果为:
"hits" : [
{
"_index" : "products",
"_type" : "_doc",
"_id" : "bauknecht-washing-machine-p-200",
"_score" : 0.0,
"_source" : {
"category" : "Electronics > White goods > Washing > Washing Machines",
"name" : "Bauknecht P-200"
}
}
]
如果你对 terms query lookup 还不是很了解的话,请参阅我之前的文章 “Elasticsearch:Terms lookup query - 关联两个不同索引的搜索”。 以上仅适用于最深的 category,因为这将直接从源中提取字段。
更新的复杂性
我在这篇文章中没有充分强调这一点,在这个例子中它可能并不明显:能够对这样的层次结构建模并在该层次结构中进行搜索,而无需实际创建树,但通过全文搜索解决这个问题是一个非常好的技巧因为不用类似 join 字段类型的东西。
我通常在培训或公开演讲中说的第一件事是,搜索总是一种权衡。 你要么在索引时做一些事情(比如丰富你的文档或改变结构)以便在查询时快速(使用 path_hierarchy tokenizer)或者你不花 CPU 时间在索引上,但最终可能会导致查询速度变慢( 这是通过使用内存中的数据结构在查询时连接文档来实现 join 字段类型的)。
想象一下,你已更改类别结构,并将所有洗衣机从
Electronics > White goods > Washing > Washing Machines
变为:
Electronics > Household > Washing Machines
如果你通过 join 字段类型为你的产品类别建模,这可能只是一个需要新名称的文档。 在我们的路径层次 tokenizer 案例中,我们需要运行 update_by_query 操作。
POST products/_update_by_query
{
"script": {
"source": "ctx._source.category = 'Electronics > Household > Washing Machines'",
"lang": "painless"
},
"query": {
"term": {
"category": "Electronics > White goods > Washing > Washing Machines"
}
}
}
因此,更新次数和持续时间取决于 category 中的文档数量 - 因此对于大量数据集可能需要一些时间。 由于这不是事务性的,适当的更新策略可能是在两次运行中更新它。 首先添加新 cetegory,然后删除旧 category - 请参阅下一段。
这是必须在索引时执行工作以确保查询时操作尽可能快的常见示例。
支持多路径
另一个驱动特性是支持多路径 - 这在 join 字段类型中是不可能的,因为你只能指定一个父项。 你所要做的就是在类别字段中有一个字符串数组,如下所示:
PUT products/_doc/bauknecht-washing-machine
{
"category": [
"Electronics > Household > Washing Machines",
"Electronics > White goods > Washing > Washing Machines"
],
"name": "Bauknecht P-500"
}
现在你可以选择任何 categories 中的 category(或任何前缀,如 Electronics > Household 来过滤文档)。
使用不同的搜索分词器
你可能已经注意到,我在所有示例中只使用术语查询。 发生这种情况的部分原因是懒惰并保持示例较小,但可能无法反映现实世界。 在上面的例子中,由于分词器配置,没有必要考虑同义词支持或小写之类的东西。
让我们举个例子,为什么使用索引分词器分析的查询是一个问题:
PUT products/_doc/wrong-shown-product
{
"category": [
"Electronics > Washing > Washing Machines"
],
"name": "SHOULD NOT BE SHOWN"
}
GET products/_search
{
"query": {
"bool": {
"filter": [
{
"match": {
"category": "Electronics > Household > Washing Machines"
}
}
]
}
}
}
上面的查询运行的结果是:
"hits" : [
{
"_index" : "products",
"_type" : "_doc",
"_id" : "bauknecht-washing-machine",
"_score" : 0.0,
"_source" : {
"name" : "Bauknecht P-500",
"category" : "Electronics > Household > Washing Machines"
}
},
{
"_index" : "products",
"_type" : "_doc",
"_id" : "bauknecht-washing-machine-p-200",
"_score" : 0.0,
"_source" : {
"name" : "Bauknecht P-200",
"category" : "Electronics > Household > Washing Machines"
}
},
{
"_index" : "products",
"_type" : "_doc",
"_id" : "lg-freezer",
"_score" : 0.0,
"_source" : {
"category" : "Electronics > White goods > Refrigerators > Fridge/Freezer Combination",
"name" : "LG Freezer"
}
},
{
"_index" : "products",
"_type" : "_doc",
"_id" : "wrong-shown-product",
"_score" : 0.0,
"_source" : {
"category" : [
"Electronics > Washing > Washing Machines"
],
"name" : "SHOULD NOT BE SHOWN"
}
}
]
运行最后一个查询将返回所有结果,即使我们可能只期望两个结果。 为什么是这样?
让我们使用 explain API 来查看我们没想到的产品的 query 解释的输出:
GET products/_explain/wrong-shown-product
{
"query": {
"bool": {
"filter": [
{
"match": {
"category": "Electronics > Household > Washing Machines"
}
}
]
}
}
}
显示了上述索引产品的以下内容:
{
"_index" : "products",
"_type" : "_doc",
"_id" : "wrong-shown-product",
"matched" : true,
"explanation" : {
"value" : 0.0,
"description" : "ConstantScore(Synonym(category:Electronics category:Electronics > Household category:Electronics > Household > Washing Machines))^0.0",
"details" : [ ]
}
}
由于在索引数据时使用相同的分词器,此搜索实际上搜索的是 category:Electronics 以便每个文档都匹配。 让我们切换正在使用的分析器(注意查询的结构略有变化):
GET products/_explain/wrong-shown-product
{
"query": {
"bool": {
"filter": [
{
"match": {
"category": {
"query": "Electronics > Household > Washing Machines",
"analyzer" : "keyword"
}
}
}
]
}
}
}
GET products/_explain/wrong-shown-product
{
"query": {
"bool": {
"filter": [
{
"match": {
"category": {
"query": "Electronics > Household > Washing Machines",
"analyzer" : "keyword"
}
}
}
]
}
}
}
上面命令输出的结果为:
{
"_index" : "products",
"_type" : "_doc",
"_id" : "wrong-shown-product",
"matched" : false,
"explanation" : {
"value" : 0.0,
"description" : "ConstantScore(category:Electronics > Household > Washing Machines) doesn't match id 1",
"details" : [ ]
}
}
这将正确返回 matched:false,因为 query 的内容将不会被分析 - 在这种情况下,这与使用术语查询相同。
那么,当你需要分析查询时,什么是好的用例? 每当类别过滤器基于用户输入时。 如果你要对文件路径而不是电子商务类别进行建模,你可能希望在按类别过滤之前先将路径小写,因为你对不区分大小写的文件系统进行建模。
总结
我希望你喜欢这次阅读。 这篇博文的主要目的不是向你展示如何解决某个用例的任何示例,而是让你保持头脑清醒,即有时解决搜索用例需要不同的思考。
以上是关于Elasticsearch:使用 Path Hierarchy Tokenizer 搜索层次结构的主要内容,如果未能解决你的问题,请参考以下文章
Hie with the Pie(POJ 3311)状压DP