Elasticsearch 查询通过重复使用的术语查询过滤器加速

Elasticsearch query speed up with repeated used terms query filter

我需要找出一个标签和另一组固定标签之间的整体共现次数。我有 10000 个不同的单个标签,固定标签集中有 10k 个标签。我在固定时间范围内的一组固定标签上下文下循环遍历所有单个标签。我在索引中总共有 10 亿个文档和 20 个分片。

这里是elasticsearch查询,elasticsearch 6.6.0:

es.search(index=index, size=0, body={ 
        "query": {
          "bool": {
              "filter": [
                  {"range": {
                      "created_time": {
                       "gte": fixed_start_time,  
                       "lte": fixed_end_time, 
                       "format": "yyyy-MM-dd-HH"
                       }}},
                        {"term": {"tags": dynamic_single_tag}},
                        {"terms": {"tags": {
                            "index" : "fixed_set_tags_list",
                            "id" : 2,
                            "type" : "twitter",
                            "path" : "tag_list"
                        }}}
                       ]

                }
          }, "aggs": {
             "by_month": {
              "date_histogram": {
                  "field": "created_time",
                  "interval": "month",
                              "min_doc_count": 0,
                              "extended_bounds": {
                                  "min": two_month_start_time,
                                  "max": start_month_start_time}

              }}}
        }) 

我的问题:是否有任何解决方案可以在 elasticsearch 中有一个固定的 10k 组标签术语查询和时间范围过滤器的缓存,从而加快查询时间?我上面的查询一个标签用了 1.5 秒。

您看到的是 Elasticsearch 聚合的正常行为(实际上,考虑到您有 10 亿个文档,性能相当不错)。

您可能会考虑几个选项:使用一批 filter 聚合、使用文档子集重新索引、从 Elasticsearch 下载数据并离线计算同现。

但可能值得尝试发送那些 10K 查询并查看 Elasticsearch 内置缓存是否起作用。

让我更详细地解释一下每个选项。

使用filter聚合

首先,让我们概述一下我们在原始 ES 查询中所做的事情:

  • 在一定时间内create_time过滤文档window;
  • 过滤包含所需标签的文档dynamic_single_tag
  • 还过滤至少具有列表中一个标签的文档 fixed_set_tags_list;
  • 统计某个时间段内每个月有多少这样的文档。

性能是个问题,因为我们有 10K 个标签来进行此类查询。

我们在这里可以做的是将 dynamic_single_tag 上的 filter 从查询移动到聚合:

POST myindex/_doc/_search
{
  "size": 0,
  "query": {
    "bool": {
      "filter": [
        { "terms": { ... } }
      ]
    }
  },
  "aggs": {
    "by tag C": {
      "filter": {
        "term": {
          "tags": "C" <== here's the filter
        }
      },
      "aggs": {
        "by month": {
          "date_histogram": {
            "field": "created_time",
            "interval": "month",
            "min_doc_count": 0,
            "extended_bounds": {
              "min": "2019-01-01",
              "max": "2019-02-01"
            }
          }
        }
      }
    }
  }
}

结果将如下所示:

  "aggregations" : {
    "by tag C" : {
      "doc_count" : 2,
      "by month" : {
        "buckets" : [
          {
            "key_as_string" : "2019-01-01T00:00:00.000Z",
            "key" : 1546300800000,
            "doc_count" : 2
          },
          {
            "key_as_string" : "2019-02-01T00:00:00.000Z",
            "key" : 1548979200000,
            "doc_count" : 0
          }
        ]
      }
    }

现在,如果您问这对性能有何帮助,技巧如下:为每个标签添加更多此类 filter 聚合:"by tag D""by tag E",等等

改进将来自执行 "batch" 个请求,combining many initial requests into one。将所有 10K 个标签放在一个查询中可能不切实际,但即使每个查询批量 100 个标签也可以改变游戏规则。

(旁注:通过 terms aggregation with include 过滤器参数可以实现大致相同的行为。)

这种方法当然需要亲自动手并编写更复杂的查询,但如果需要 运行 零准备随机查询此类查询,它会派上用场。

重新索引文档

第二种方法背后的想法是通过reindex API预先减少文档集。 reindex 查询可能如下所示:

POST _reindex
{
  "source": {
    "index": "myindex",
    "type": "_doc",
    "query": {
      "bool": {
        "filter": [
          {
            "range": {
              "created_time": {
                "gte": "fixed_start_time",
                "lte": "fixed_end_time",
                "format": "yyyy-MM-dd-HH"
              }
            }
          },
          {
            "terms": {
              "tags": {
                "index": "fixed_set_tags_list",
                "id": 2,
                "type": "twitter",
                "path": "tag_list"
              }
            }
          }
        ]
      }
    }
  },
  "dest": {
    "index": "myindex_reduced"
  }
}

此查询将创建一个新索引,myindex_reduced,仅包含满足前 2 个过滤子句的元素。

此时,原来的查询可以不用那两个子句来完成。

这种情况下的加速将来自于限制文档的数量,它越小,增益越大。所以,如果fixed_set_tags_list给你留下10亿的​​一小部分,这个绝对是你可以尝试的选项。

在 Elasticsearch 外部下载数据和处理

老实说,这个用例看起来更像是 pandas. If data analytics is your case, I would suggest using scroll API 提取磁盘上的数据然后使用任意脚本处理它的工作。

在 python 中,它可以像使用 elasticsearch 库的 .scan() 辅助方法一样简单。

为什么不试试蛮力法呢?

Elasticsearch 已经尝试通过 request cache 帮助您进行查询。它仅适用于纯聚合查询 (size: 0),因此应该适用于您的情况。

但是不会,因为查询的内容总是会不一样(整个JSON of the query is used as caching key,我们每次查询都有一个新的标签)。不同级别的缓存将开始播放。

Elasticsearch heavily relies on the filesystem cache,这意味着在引擎盖下,文件系统中更经常访问的块将被缓存(实际上加载到 RAM 中)。对于最终用户来说,这意味着 "warming up" 会慢慢到来,并且会有大量类似的请求。

在您的情况下,聚合和过滤将发生在 2 个字段上:create_timetags。这意味着在使用不同的标签执行 10 或 100 个请求后,响应时间将从 1.5 秒下降到更可以忍受的水平。

为了证明我的观点,下面是我对 Elasticsearch 性能研究的 Vegeta 图,该图来自我在使用固定 RPS 发送大量聚合的同一查询下的 Elasticsearch 性能:

如您所见,最初请求耗时约 10 秒,在 100 次请求后它减少到 200 毫秒。

我肯定会建议尝试这种 "brute force" 方法,因为如果它有效,那很好,如果无效 - 它没有成本。

希望对您有所帮助!