重要词项聚合编辑

一种聚合,返回集合中有趣或不寻常的词项出现情况。

用例示例

  • 当用户在文本中搜索“禽流感”时,建议使用“H5N1”。
  • 从报告损失的信用卡所有者的交易历史中识别出“共同的妥协点”商家。
  • 为自动新闻分类器建议与股票代码 $ATI 相关的关键词。
  • 发现诊断出比他们应得的更多的鞭打伤的欺诈医生。
  • 发现爆胎数量不成比例的轮胎制造商。

在所有这些情况下,被选择的词项不仅仅是集合中最流行的词项。它们是在*前景*和*背景*集合之间测量的流行度发生了显著变化的词项。如果“H5N1”一词仅存在于 1000 万个文档索引中的 5 个文档中,但在构成用户搜索结果的 100 个文档中有 4 个文档中找到,那么这意义重大,并且可能与其搜索非常相关。5/10,000,000 与 4/100 的频率差异很大。

单集分析编辑

在最简单的情况下,感兴趣的*前景*集是与查询匹配的搜索结果,而用于统计比较的*背景*集是收集结果的索引。

示例

response = client.search(
  body: {
    query: {
      terms: {
        force: [
          'British Transport Police'
        ]
      }
    },
    aggregations: {
      significant_crime_types: {
        significant_terms: {
          field: 'crime_type'
        }
      }
    }
  }
)
puts response
GET /_search
{
  "query": {
    "terms": { "force": [ "British Transport Police" ] }
  },
  "aggregations": {
    "significant_crime_types": {
      "significant_terms": { "field": "crime_type" }
    }
  }
}

响应

{
  ...
  "aggregations": {
    "significant_crime_types": {
      "doc_count": 47347,
      "bg_count": 5064554,
      "buckets": [
        {
          "key": "Bicycle theft",
          "doc_count": 3640,
          "score": 0.371235374214817,
          "bg_count": 66799
        }
              ...
      ]
    }
  }
}

在查询所有警察部队的所有犯罪索引时,这些结果表明,英国交通警察部队在处理自行车盗窃案方面表现突出。通常情况下,自行车盗窃案仅占犯罪案件的 1% (66799/5064554),但对于负责铁路和车站犯罪的英国交通警察来说,7% 的犯罪案件 (3640/47347) 是自行车盗窃案。这是一个显著的七倍频率增长,因此这种异常现象被突出显示为首要犯罪类型。

使用查询来发现异常的问题在于,它只给我们提供了一个子集来进行比较。为了发现所有其他警察部队的异常情况,我们必须对每个不同的部队重复查询。

这可能是一种在索引中查找异常模式的繁琐方法。

多集分析编辑

在多个类别之间执行分析的一种更简单的方法是使用父级聚合对数据进行分段,以便进行分析。

使用父级聚合进行分段的示例

response = client.search(
  body: {
    aggregations: {
      forces: {
        terms: {
          field: 'force'
        },
        aggregations: {
          significant_crime_types: {
            significant_terms: {
              field: 'crime_type'
            }
          }
        }
      }
    }
  }
)
puts response
GET /_search
{
  "aggregations": {
    "forces": {
      "terms": { "field": "force" },
      "aggregations": {
        "significant_crime_types": {
          "significant_terms": { "field": "crime_type" }
        }
      }
    }
  }
}

响应

{
 ...
 "aggregations": {
    "forces": {
        "doc_count_error_upper_bound": 1375,
        "sum_other_doc_count": 7879845,
        "buckets": [
            {
                "key": "Metropolitan Police Service",
                "doc_count": 894038,
                "significant_crime_types": {
                    "doc_count": 894038,
                    "bg_count": 5064554,
                    "buckets": [
                        {
                            "key": "Robbery",
                            "doc_count": 27617,
                            "score": 0.0599,
                            "bg_count": 53182
                        }
                        ...
                    ]
                }
            },
            {
                "key": "British Transport Police",
                "doc_count": 47347,
                "significant_crime_types": {
                    "doc_count": 47347,
                    "bg_count": 5064554,
                    "buckets": [
                        {
                            "key": "Bicycle theft",
                            "doc_count": 3640,
                            "score": 0.371,
                            "bg_count": 66799
                        }
                        ...
                    ]
                }
            }
        ]
    }
  }
}

现在,我们使用单个请求对每个警察部队进行异常检测。

我们可以使用其他形式的顶级聚合来对数据进行分段,例如按地理区域进行分段,以识别特定犯罪类型的异常热点,例如

response = client.search(
  body: {
    aggregations: {
      hotspots: {
        geohash_grid: {
          field: 'location',
          precision: 5
        },
        aggregations: {
          significant_crime_types: {
            significant_terms: {
              field: 'crime_type'
            }
          }
        }
      }
    }
  }
)
puts response
GET /_search
{
  "aggs": {
    "hotspots": {
      "geohash_grid": {
        "field": "location",
        "precision": 5
      },
      "aggs": {
        "significant_crime_types": {
          "significant_terms": { "field": "crime_type" }
        }
      }
    }
  }
}

此示例使用 geohash_grid 聚合创建表示地理区域的结果桶,并且在每个桶内,我们可以识别这些紧密关注区域中犯罪类型的异常水平,例如

  • 机场的武器没收数量异常
  • 大学的自行车盗窃案有所增加

在具有更大覆盖区域的更高 geohash_grid 缩放级别,我们将开始看到整个警察部队可能正在处理异常数量的特定犯罪类型。

显然,基于时间的顶级分段将有助于识别每个时间点的当前趋势,而简单的 terms 聚合通常会显示在所有时间段内持续存在的非常流行的“常量”。

在自由文本字段上使用编辑

significant_terms 聚合可以有效地用于标记化的自由文本字段,以建议

  • 用于优化最终用户搜索的关键词
  • 用于渗透器查询的关键词

选择自由文本字段作为重要词项分析的主题可能会很昂贵!它将尝试将每个唯一的单词加载到 RAM 中。建议仅在较小的索引上使用此功能。

**在上下文中显示 significant_terms**在上下文中查看时,更容易理解自由文本 significant_terms。从自由文本字段中获取 significant_terms 建议的结果,并在具有 highlight 子句的同一字段上的 terms 查询中使用它们,以向用户呈现文档的示例片段。当词项以未词干化、突出显示、大小写正确、顺序正确并在一定上下文中呈现时,它们的意义/含义就更容易理解。

自定义背景集编辑

通常,文档的前景集是与索引中所有文档的背景集进行“比较”的。但是,有时使用更窄的背景集作为比较的基础可能很有用。例如,在包含来自世界各地内容的索引中,对与“马德里”相关的文档进行查询可能会发现“西班牙语”是一个重要的词项。这可能是真的,但如果您想要一些更集中的词项,您可以对*西班牙*一词使用 background_filter 来建立更窄的文档集作为上下文。以此为背景,“西班牙语”现在将被视为司空见惯,因此不像与马德里关系更密切的“首都”等词语那样重要。请注意,使用背景过滤器会降低速度 - 现在必须通过过滤发布列表而不是读取索引中预先计算的词项计数来动态推导出每个词项的背景频率。

限制编辑

重要词项必须是索引值编辑

与 terms 聚合不同,目前无法将脚本生成的词项用于计数目的。由于 significant_terms 聚合必须同时考虑*前景*和*背景*频率的方式,因此在整个索引上使用脚本来获取用于比较的背景频率将非常昂贵。出于类似的原因,也不支持将 DocValues 用作词项数据的来源。

不分析浮点字段编辑

目前不支持将浮点字段作为 significant_terms 分析的主题。虽然可以使用整数或长整数字段来表示银行帐号或类别号等概念,这些概念可能很有趣,但浮点字段通常用于表示某物的数量。因此,单个浮点词项对于这种形式的频率分析没有用处。

用作父级聚合编辑

如果存在等效于 match_all 查询或没有查询条件提供索引子集的情况,则不应将 significant_terms 聚合用作最顶层的聚合 - 在这种情况下,*前景*集与*背景*集完全相同,因此观察到的文档频率没有差异,也无法从中提出合理的建议。

另一个考虑因素是,significant_terms 聚合在分片级别会产生许多候选结果,这些结果只有在合并了来自所有分片的所有统计信息后才会在归约节点上进行修剪。因此,在稍后会丢弃许多候选词项的 significant_terms 聚合下嵌入大型子聚合,在 RAM 方面效率低下且成本高昂。在这些情况下,建议执行两次搜索 - 第一次搜索提供合理化的 significant_terms 列表,然后将此词项候选列表添加到第二个查询中,以返回并获取所需的子聚合。

近似计数编辑

结果中提供的包含某个词项的文档数量是基于对从每个分片返回的样本求和得出的,因此可能是

  • 如果某些分片在其顶部样本中没有提供给定词项的数字,则该数字较低
  • 在考虑背景频率时较高,因为它可能会计算已删除文档中出现的次数

与大多数设计决策一样,这是权衡的基础,我们选择以牺牲一些(通常很小)的准确性为代价来提供快速性能。但是,下一节中介绍的 sizeshard size 设置提供了有助于控制准确性级别的工具。

参数编辑

JLH 分数编辑

通过添加参数,可以使用 JLH 分数作为重要性分数

	 "jlh": {
	 }

分数来自*前景*和*背景*集中的文档频率。流行度的*绝对*变化(foregroundPercent - backgroundPercent)有利于常见词项,而流行度的*相对*变化(foregroundPercent/ backgroundPercent)有利于罕见词项。罕见与常见本质上是精度与召回率之间的平衡,因此将绝对变化和相对变化相乘以提供精度和召回率之间的最佳点。

互信息编辑

如 Manning 等人合著的《信息检索》第 13.5.1 章中所述,可以通过添加以下参数将互信息用作显著性评分:

	 "mutual_information": {
	      "include_negatives": true
	 }

互信息不会区分对子集或子集外文档具有描述性的词条。 因此,显著词条可能包含在子集中出现频率高于或低于子集外文档的词条。 要筛选出在子集中出现频率低于子集外文档的词条,可以将 include_negatives 设置为 false

默认情况下,假设存储桶中的文档也包含在背景中。 如果您定义了一个自定义背景过滤器来表示要比较的不同文档集,请设置

"background_is_superset": false

卡方编辑

如 Manning 等人合著的《信息检索》第 13.5.2 章中所述,可以通过添加以下参数将卡方用作显著性评分:

	 "chi_square": {
	 }

卡方的行为类似于互信息,并且可以使用相同的参数 include_negativesbackground_is_superset 进行配置。

谷歌标准化距离编辑

可以通过添加以下参数将 Cilibrasi 和 Vitanyi 在 2007 年发表的论文《谷歌相似度距离》(“The Google Similarity Distance”)中所述的谷歌标准化距离用作显著性评分:

	 "gnd": {
	 }

gnd 也接受 background_is_superset 参数。

p 值评分编辑

p 值是在原假设正确的情况下,获得至少与实际观察结果一样极端的测试结果的概率。 计算 p 值时假设前景集和背景集是独立的 伯努利试验,并且原假设是概率相同。

示例用法编辑

此示例计算在“以失败结束”与“未以失败结束”的前景集中,词条 user_agent.version 的 p 值评分。

"background_is_superset": false 指示背景集不包含前景集的计数,因为它们已被过滤掉。

"normalize_above": 1000 有助于在各种规模下返回一致的显著性结果。 1000 指示大于 1000 的词条计数按 1000/term_count 的系数缩减。

response = client.search(
  body: {
    query: {
      bool: {
        filter: [
          {
            term: {
              'event.outcome' => 'failure'
            }
          },
          {
            range: {
              "@timestamp": {
                gte: '2021-02-01',
                lt: '2021-02-04'
              }
            }
          },
          {
            term: {
              'service.name' => {
                value: 'frontend-node'
              }
            }
          }
        ]
      }
    },
    aggregations: {
      failure_p_value: {
        significant_terms: {
          field: 'user_agent.version',
          background_filter: {
            bool: {
              must_not: [
                {
                  term: {
                    'event.outcome' => 'failure'
                  }
                }
              ],
              filter: [
                {
                  range: {
                    "@timestamp": {
                      gte: '2021-02-01',
                      lt: '2021-02-04'
                    }
                  }
                },
                {
                  term: {
                    'service.name' => {
                      value: 'frontend-node'
                    }
                  }
                }
              ]
            }
          },
          p_value: {
            background_is_superset: false,
            normalize_above: 1000
          }
        }
      }
    }
  }
)
puts response
GET /_search
{
  "query": {
    "bool": {
      "filter": [
        {
          "term": {
            "event.outcome": "failure"
          }
        },
        {
          "range": {
            "@timestamp": {
              "gte": "2021-02-01",
              "lt": "2021-02-04"
            }
          }
        },
        {
          "term": {
            "service.name": {
              "value": "frontend-node"
            }
          }
        }
      ]
    }
  },
  "aggs": {
    "failure_p_value": {
      "significant_terms": {
        "field": "user_agent.version",
        "background_filter": {
          "bool": {
            "must_not": [
              {
                "term": {
                  "event.outcome": "failure"
                }
              }
            ],
            "filter": [
              {
                "range": {
                  "@timestamp": {
                    "gte": "2021-02-01",
                    "lt": "2021-02-04"
                  }
                }
              },
              {
                "term": {
                  "service.name": {
                    "value": "frontend-node"
                  }
                }
              }
            ]
          }
        },
        "p_value": {"background_is_superset": false, "normalize_above": 1000}
      }
    }
  }
}

百分比编辑

一个简单的计算方法是,将前景样本中包含某个词条的文档数量除以背景中包含该词条的文档数量。 默认情况下,这会产生一个大于零且小于一的评分。

这种启发式方法的优点是,评分逻辑很容易向熟悉“人均”统计数据的人解释。 但是,对于基数高的字段,这种启发式方法倾向于选择最罕见的词条,例如只出现一次的拼写错误,因为它们的评分为 1/1 = 100%。

如果仅仅根据获胜场次的百分比来评判冠军,那么经验丰富的拳击手将很难赢得冠军——按照这些规则,一个只有一场比赛经验的新人将是不可战胜的。 通常需要多次观察才能强化一种观点,因此建议在这种情况下将 min_doc_countshard_min_doc_count 都设置为更高的值,例如 10,以便过滤掉本来会优先考虑的低频词条。

	 "percentage": {
	 }

哪一个最好?编辑

粗略地说,mutual_information 偏向于高频词条,即使它们在背景中也经常出现。 例如,在自然语言文本分析中,这可能会导致选择停用词。 mutual_information 不太可能选择非常罕见的词条,例如拼写错误。 gnd 偏向于共现率高的词条,并避免选择停用词。 它可能更适合于同义词检测。 但是,gnd 倾向于选择非常罕见的词条,例如拼写错误的结果。 chi_squarejlh 介于两者之间。

很难说哪一种启发式方法是最佳选择,因为它取决于显著词条的用途(例如,有关使用显著词条进行文本分类特征选择的研,请参见 Yang 和 Pedersen 在 1997 年发表的论文 “A Comparative Study on Feature Selection in Text Categorization”)。

如果上述指标都不适合您的用例,那么另一种选择是实现自定义显著性指标

脚本化编辑

可以通过脚本实现自定义评分

	    "script_heuristic": {
              "script": {
	        "lang": "painless",
	        "source": "params._subset_freq/(params._superset_freq - params._subset_freq + 1)"
	      }
            }

脚本可以是内联的(如上例所示)、索引的或存储在磁盘上的。 有关选项的详细信息,请参阅脚本文档

脚本中可用的参数有

_subset_freq

词条出现在子集中的文档数量。

_superset_freq

词条出现在超集中的文档数量。

_subset_size

子集中的文档数量。

_superset_size

超集中的文档数量。

大小和分片大小编辑

可以设置 size 参数来定义从总体词条列表中返回多少个词条存储桶。 默认情况下,协调搜索过程的节点将请求每个分片提供其自己的顶级词条存储桶,并且一旦所有分片都响应,它将把结果缩减为最终列表,然后将其返回给客户端。 如果唯一词条的数量大于 size,则返回的列表可能会略有不准确(词条计数可能会略有不准确,甚至可能导致应该位于顶级 size 存储桶中的词条未返回)。

为了确保更高的准确性,最终 size 的倍数将用作从每个分片请求的词条数量(2 * (size * 1.5 + 10))。 要手动控制此设置,可以使用 shard_size 参数来控制每个分片生成的候选词条的数量。

一旦合并所有结果,低频词条可能会成为最有趣的词条,因此当 shard_size 参数设置为明显高于 size 设置的值时,significant_terms 聚合可以产生更高质量的结果。 这可以确保在最终选择之前,归约节点会对更大范围的有希望的候选词条进行综合审查。 显然,大型候选词条列表会导致额外的网络流量和 RAM 使用量,因此需要权衡质量/成本之间的关系。 如果将 shard_size 设置为 -1(默认值),则将根据分片数量和 size 参数自动估算 shard_size

shard_size 不能小于 size(因为这没有多大意义)。 如果小于 size,Elasticsearch 将覆盖它并将其重置为等于 size

最小文档计数编辑

可以使用 min_doc_count 选项仅返回匹配次数超过配置值的词条

response = client.search(
  body: {
    aggregations: {
      tags: {
        significant_terms: {
          field: 'tag',
          min_doc_count: 10
        }
      }
    }
  }
)
puts response
GET /_search
{
  "aggs": {
    "tags": {
      "significant_terms": {
        "field": "tag",
        "min_doc_count": 10
      }
    }
  }
}

上述聚合将仅返回在 10 个或更多匹配中找到的标签。 默认值为 3

评分较高的词条将在分片级别收集,并在第二步中与从其他分片收集的词条合并。 但是,分片没有关于全局词条频率的可用信息。 是否将词条添加到候选列表的决定仅取决于使用本地分片频率(而不是词条的全局频率)在分片上计算的评分。 min_doc_count 条件仅在合并所有分片的本地词条统计信息后应用。 在某种程度上,决定将词条添加为候选词条时,并没有非常 *确定* 该词条是否真的能达到所需的 min_doc_count。 如果低频但评分高的词条填充了候选列表,则这可能会导致最终结果中缺少许多(全局)高频词条。 为了避免这种情况,可以增加 shard_size 参数,以便在分片上允许更多候选词条。 但是,这会增加内存消耗和网络流量。

shard_min_doc_count编辑

参数 shard_min_doc_count 控制分片对词条是否真的应该添加到候选列表中的 *确定性*(相对于 min_doc_count)。 仅当词条在集合中的本地分片频率高于 shard_min_doc_count 时,才会考虑该词条。 如果您的字典包含许多低频词条,并且您对这些词条不感兴趣(例如拼写错误),则可以设置 shard_min_doc_count 参数,以便在分片级别过滤掉即使在合并本地计数后也合理确定无法达到所需 min_doc_count 的候选词条。 默认情况下,shard_min_doc_count 设置为 0,除非您显式设置它,否则它不会生效。

通常不建议将 min_doc_count 设置为 1,因为它往往会返回拼写错误或其他奇怪的词汇。找到一个词的多个实例有助于强化这一观点:虽然仍然很少见,但这个词并非一次性意外的结果。默认值 3 用于提供最低证据权重。将 shard_min_doc_count 设置得太高会导致重要的候选词在分片级别被过滤掉。此值应设置得远低于 min_doc_count/#shards

自定义背景上下文编辑

背景词频统计信息的默认来源是整个索引,可以通过使用 background_filter 来缩小此范围,以关注较窄上下文中的重要词。

response = client.search(
  body: {
    query: {
      match: {
        city: 'madrid'
      }
    },
    aggregations: {
      tags: {
        significant_terms: {
          field: 'tag',
          background_filter: {
            term: {
              text: 'spain'
            }
          }
        }
      }
    }
  }
)
puts response
GET /_search
{
  "query": {
    "match": {
      "city": "madrid"
    }
  },
  "aggs": {
    "tags": {
      "significant_terms": {
        "field": "tag",
        "background_filter": {
          "term": { "text": "spain" }
        }
      }
    }
  }
}

上面的过滤器将有助于关注马德里市特有的词汇,而不是像“西班牙语”这样的词汇,这些词汇在整个索引的全球范围内并不常见,但在包含“西班牙”一词的文档子集中很常见。

使用背景过滤器会降低查询速度,因为必须过滤每个词的 postings 以确定频率。

过滤值编辑

可以(尽管很少需要)过滤将为其创建桶的值。这可以使用 includeexclude 参数来完成,这些参数基于正则表达式字符串或精确词数组。此功能反映了词条聚合文档中描述的功能。

收集模式编辑

为了避免内存问题,significant_terms 聚合始终以 breadth_first 模式计算子聚合。有关不同收集模式的说明,请参阅词条聚合文档。

执行提示编辑

词条聚合可以通过不同的机制执行

  • 通过直接使用字段值来聚合每个桶的数据(map
  • 通过使用字段的全局序号并为每个全局序号分配一个桶(global_ordinals

Elasticsearch 尝试设置合理的默认值,因此通常不需要配置此项。

global_ordinalskeyword 字段的默认选项,它使用全局序号动态分配桶,因此内存使用量与属于聚合范围的文档的值的数量成线性关系。

只有在很少有文档匹配查询时才应考虑使用 map。否则,基于序号的执行模式要快得多。默认情况下,map 仅在对脚本运行聚合时使用,因为它们没有序号。

response = client.search(
  body: {
    aggregations: {
      tags: {
        significant_terms: {
          field: 'tags',
          execution_hint: 'map'
        }
      }
    }
  }
)
puts response
GET /_search
{
  "aggs": {
    "tags": {
      "significant_terms": {
        "field": "tags",
        "execution_hint": "map" 
      }
    }
  }
}

可能的值为 mapglobal_ordinals

请注意,如果此执行提示不适用,Elasticsearch 将忽略它。