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 aggregationinner 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 的内容将不会被分析 - 在这种情况下,这与使用术语查询相同。

那么,当你需要分析查询时,什么是好的用例? 每当类别过滤器基于用户输入时。 如果你要对文件路径而不是电子商务类别进行建模,你可能希望在按类别过滤之前先将路径小写,因为你对不区分大小写的文件系统进行建模。

总结

我希望你喜欢这次阅读。 这篇博文的主要目的不是向你展示如何解决某个用例的任何示例,而是让你保持头脑清醒,即有时解决搜索用例需要不同的思考。

原文:Search hierarchies using the path hierarchy tokenizer

以上是关于Elasticsearch:使用 Path Hierarchy Tokenizer 搜索层次结构的主要内容,如果未能解决你的问题,请参考以下文章

POJ3311 Hie with the Pie

poj3311Hie with the Pie

Hie with the Pie(poj3311)

Hie with the Pie(POJ 3311)状压DP

POJ 3311---Hie with the Pie(状压DP)

Hie with the Pie-状压DP