首页 > 解决方案 > 在elasticsearch中聚合后查询?

问题描述

我们有一个弹性搜索之外的联系人数据库。这些联系人中的每一个都有许多动态属性(性别:男性,年份:1985,汽车颜色:蓝色等)

我们想将弹性搜索集成到我们的设置中,我们决定按属性索引以实现可伸缩性。因此,elasticsearch 中的文档示例如下所示:

{
    contactId:"XYZ",
    attribute:"gender",
    value:"male"
}

这样,我们可以为任何联系人添加无限属性,而无需重新索引任何文档。

当需要在这些文档中进行搜索时,我们的问题就出现了。我们希望能够列出将属性定义传递给弹性搜索的联系人,即(能够列出男性且拥有蓝色汽车等的联系人)

所以我们想做类似的事情

  1. 按以下方式汇总文档contactId
  2. 为属性需求编写查询
  3. 对结果进行分页

我们想出了这样的东西。

{
  query: {
    bool: {
      should: [
        {
          bool: {
            must: [
              { match: { attribute: "gender" }},
              { match: { value: "f" }},
            ],
          },
        },
        {
          bool: {
            must: [
              { match: { attribute: "carColor" }},
              { match: { value: "blue" }},
            ],
          },
        }        
      ],
      minimum_should_match: 2,
    },
  },
  aggs: {
    contacts: {
      composite: {
        size: 15,
        sources: [
          {
            contactId: {
              terms: {
                field: "contactId",
              }
            }
          }
        ],
      },
    },
  },
}

但是我们真的可以得到我们想要的结果。

任何人都知道我们做错了什么和/或我们如何改进这个查询。

非常感谢 !

标签: elasticsearch

解决方案


您遇到的主要问题是 ElasticSearch 无法加入数据。我可以想象您的查询不匹配任何内容,因为 ElasticSearch 将条件逐个应用于每个文档。由于属性值对存储在单个文档中,因此如下所示的多条件查询无法匹配单个文档:

(attribute:gender AND value:male) AND (attribute:carColor AND value:blue)

展望未来,一种选择是获取所有符合给定条件的文档,然后聚合。我想,这就是你想要达到的目标。

假设数据:

   attribute   |   contactId   |     value     
---------------+---------------+---------------
gender         |XYZ            |male           
carColor       |XYZ            |blue           
gender         |ABS            |female         
pet            |ABS            |tiger          
carColor       |ABS            |red            
pet            |XYZ            |dog            
gender         |XXX            |female         
carColor       |XXX            |blue    

使用以下查询:

{
  "size": 0, 
  "query": {
    "query_string": {
      "query": "(attribute:gender AND value:male) OR (attribute:carColor AND value:blue)" // Lets read them as (C1) or (C2)
    }
  },
  "aggs": {
    "contacts": {
      "terms": {
        "field": "contactId",
        "order": {
          "_count": "asc"
        },
        "size": 5
      },
      "aggs": {
        "attribute": {
          "terms": {
            "field": "attribute",
            "order": {
              "_count": "asc"
            },
            "size": 5
          },
          "aggs": {
            "attribute_value": {
              "terms": {
                "field": "value",
                "order": {
                  "_count": "asc"
                },
                "size": 5
              }
            }
          }
        }
      }
    }
  }
}

结果是:

{
  "took" : 0,
  "timed_out" : false,
  "_shards" : {
    "total" : 1,
    "successful" : 1,
    "skipped" : 0,
    "failed" : 0
  },
  "hits" : {
    "total" : {
      "value" : 3,
      "relation" : "eq"
    },
    "max_score" : null,
    "hits" : [ ]
  },
  "aggregations" : {
    "contacts" : {
      "doc_count_error_upper_bound" : 0,
      "sum_other_doc_count" : 0,
      "buckets" : [
        {
          "key" : "XXX",
          "doc_count" : 1,
          "attribute" : {
            "doc_count_error_upper_bound" : 0,
            "sum_other_doc_count" : 0,
            "buckets" : [
              {
                "key" : "carColor",
                "doc_count" : 1,
                "attribute_value" : {
                  "doc_count_error_upper_bound" : 0,
                  "sum_other_doc_count" : 0,
                  "buckets" : [
                    {
                      "key" : "blue",
                      "doc_count" : 1
                    }
                  ]
                }
              }
            ]
          }
        },
        {
          "key" : "XYZ",
          "doc_count" : 2,
          "attribute" : {
            "doc_count_error_upper_bound" : 0,
            "sum_other_doc_count" : 0,
            "buckets" : [
              {
                "key" : "carColor",
                "doc_count" : 1,
                "attribute_value" : {
                  "doc_count_error_upper_bound" : 0,
                  "sum_other_doc_count" : 0,
                  "buckets" : [
                    {
                      "key" : "blue",
                      "doc_count" : 1
                    }
                  ]
                }
              },
              {
                "key" : "gender",
                "doc_count" : 1,
                "attribute_value" : {
                  "doc_count_error_upper_bound" : 0,
                  "sum_other_doc_count" : 0,
                  "buckets" : [
                    {
                      "key" : "male",
                      "doc_count" : 1
                    }
                  ]
                }
              }
            ]
          }
        }
      ]
    }
  }

这样,您可以通过“contactId”获得顶级分组。警告是

  • 需要嵌套聚合来连接数据(也许是在 ES 中动态连接数据的唯一方法)
  • 结果桶将包含任何可以从匹配 (C1) 或 (C2) 的记录中进行的分组。然后需要对桶进行后过滤以删除与所有给定条件不匹配的桶(在上面的示例中,需要删除单个匹配的桶)。
  • 对于 N-many 标准,嵌套的深度为 N,并且在 N>10 时会变得非常缓慢

最后,我建议重新编制索引可能没什么大不了的,特别是如果您的原始来源已经将这些字段放在一个文档/行中。这种方法的优点是:

  • 查询 ES 会更便宜(实现和计算方面)
  • 在 ElasticSearch 中更新文档就像第一次索引它一样简单。

总之,问题归结为:痛苦值得吗?换句话说:重新索引文档的成本是否高于或低于首先编写和维护这些查询的成本(加上随后的查询时间计算成本)。

PS:是的,您可以将查询部分粘贴到 Kibana 中并使用数据表可视化来生成嵌套聚合查询。这样,您还可以了解随着嵌套级别的增加查询何时开始变得非常慢(在数据表中添加更多列会为您做到这一点)。

再见


推荐阅读