显著词项聚合
编辑显著词项聚合
编辑一种聚合,返回集合中词项的有趣或不寻常出现。
示例用例
- 当用户在文本中搜索“禽流感”时,建议使用“H5N1”
- 从报告丢失的信用卡所有者的交易历史记录中识别出“共同妥协点”的商户
- 为自动化新闻分类器建议与股票代码 $ATI 相关的关键词
- 发现诊断鞭打伤数量超出合理范围的欺诈医生
- 发现爆胎数量异常多的轮胎制造商
在所有这些情况下,选择的词项不仅仅是集合中最受欢迎的词项。它们是在前景和背景集合之间测量的受欢迎程度发生了显著变化的词项。如果词项“H5N1”仅存在于一个包含 1000 万个文档的索引中的 5 个文档中,但在构成用户搜索结果的 100 个文档中的 4 个文档中找到,那么这就是显著的,并且可能与他们的搜索非常相关。5/10,000,000 与 4/100 是频率的巨大波动。
单集分析
编辑在最简单的情况下,感兴趣的前景集合是由查询匹配的搜索结果,用于统计比较的背景集合是收集结果的索引。
示例
resp = client.search( query={ "terms": { "force": [ "British Transport Police" ] } }, aggregations={ "significant_crime_types": { "significant_terms": { "field": "crime_type" } } }, ) print(resp)
response = client.search( body: { query: { terms: { force: [ 'British Transport Police' ] } }, aggregations: { significant_crime_types: { significant_terms: { field: 'crime_type' } } } } ) puts response
const response = await client.search({ query: { terms: { force: ["British Transport Police"], }, }, aggregations: { significant_crime_types: { significant_terms: { field: "crime_type", }, }, }, }); console.log(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) 是自行车盗窃案。这是频率显著增加了七倍,因此此异常情况被突出显示为首要犯罪类型。
使用查询来发现异常情况的问题在于,它只为我们提供一个子集用于比较。要发现所有其他警察部门的异常情况,我们必须对每个不同的部门重复查询。
这可能是在索引中寻找不寻常模式的一种繁琐的方式。
多集分析
编辑跨多个类别执行分析的更简单方法是使用父级聚合来分割数据以进行分析。
使用父级聚合进行分割的示例
resp = client.search( aggregations={ "forces": { "terms": { "field": "force" }, "aggregations": { "significant_crime_types": { "significant_terms": { "field": "crime_type" } } } } }, ) print(resp)
response = client.search( body: { aggregations: { forces: { terms: { field: 'force' }, aggregations: { significant_crime_types: { significant_terms: { field: 'crime_type' } } } } } } ) puts response
const response = await client.search({ aggregations: { forces: { terms: { field: "force", }, aggregations: { significant_crime_types: { significant_terms: { field: "crime_type", }, }, }, }, }, }); console.log(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 } ... ] } } ] } } }
现在,我们使用单个请求对每个警察部门进行异常检测。
我们可以使用其他形式的顶级聚合来分割我们的数据,例如按地理区域分割以识别特定犯罪类型的不寻常热点
resp = client.search( aggs={ "hotspots": { "geohash_grid": { "field": "location", "precision": 5 }, "aggs": { "significant_crime_types": { "significant_terms": { "field": "crime_type" } } } } }, ) print(resp)
response = client.search( body: { aggregations: { hotspots: { geohash_grid: { field: 'location', precision: 5 }, aggregations: { significant_crime_types: { significant_terms: { field: 'crime_type' } } } } } } ) puts response
const response = await client.search({ aggs: { hotspots: { geohash_grid: { field: "location", precision: 5, }, aggs: { significant_crime_types: { significant_terms: { field: "crime_type", }, }, }, }, }, }); console.log(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 聚合可以有效地用于分词自由文本字段,以建议
- 用于改进最终用户搜索的关键字
- 用于 percolator 查询的关键字
选择自由文本字段作为显著词项分析的主题可能会很昂贵!它会尝试将每个唯一单词加载到 RAM 中。建议仅在较小的索引上使用此功能。
在上下文中显示 significant_terms当在上下文中查看时,自由文本 significant_terms 更容易理解。从自由文本字段的 significant_terms
建议中获取结果,并在同一字段上的 terms
查询中使用它们,并使用 highlight
子句向用户呈现文档示例片段。当以未词干化的形式、突出显示、正确的形式、正确的顺序并带有某些上下文呈现词项时,它们的显著性/含义会更容易被理解。
自定义背景集
编辑通常,文档的前景集合与索引中所有文档的背景集合进行“差异”。但是,有时使用更窄的背景集合作为比较的基础可能很有用。例如,在包含来自世界各地内容的索引中查询与“马德里”相关的文档可能会显示“西班牙语”是一个显著词项。这可能是真的,但是如果您想要一些更集中的词项,则可以在词项西班牙上使用 background_filter
以建立一组更窄的文档作为上下文。以此为背景,“西班牙语”现在将被视为普通词,因此不如与马德里更相关的词(如“首都”)那么显著。请注意,使用背景筛选器会降低速度 - 现在必须从筛选帖子列表中动态导出每个词项的背景频率,而不是读取索引的词项预先计算的计数。
限制
编辑显著词项必须是索引值
编辑与词项聚合不同,当前无法使用脚本生成的词项进行计数。由于 significant_terms 聚合必须同时考虑前景和背景频率的方式,因此在整个索引上使用脚本来获取用于比较的背景频率的成本将高得令人望而却步。此外,由于类似的原因,不支持 DocValues 作为词项数据的来源。
不分析浮点字段
编辑当前不支持浮点字段作为 significant_terms 分析的主题。虽然可以使用整数或长整数字段来表示诸如银行帐号或类别编号之类的概念,这些概念可能很有趣,但浮点字段通常用于表示某些事物的数量。因此,单个浮点词项对于这种形式的频率分析没有用处。
用作父聚合
编辑如果存在等效于 match_all
查询或没有提供索引子集的查询条件,则不应将 significant_terms 聚合用作最顶层的聚合 - 在这种情况下,前景集与背景集完全相同,因此没有文档频率的差异可观察,并且从中提出明智的建议。
另一个考虑因素是,significant_terms 聚合会在分片级别生成许多候选结果,这些候选结果仅在所有分片的统计信息合并后才在缩减节点上修剪。因此,在 significant_terms 聚合下嵌入稍后会丢弃许多候选词项的大型子聚合在 RAM 方面可能效率低下且成本高昂。在这些情况下,建议执行两次搜索 - 第一次搜索提供规范化的 significant_terms 列表,然后将此词项短列表添加到第二个查询中以返回并获取所需的子聚合。
近似计数
编辑结果中提供的包含词项的文档数量的计数基于对每个分片返回的样本求和,因此可能是
- 低,如果某些分片未在其顶部样本中提供给定词项的数字
- 高,在考虑背景频率时,因为它可能会计算在已删除文档中找到的出现次数
像大多数设计决策一样,这是权衡的基础,我们在其中选择提供快速性能,但以一些(通常很小)不准确为代价。但是,下一节中介绍的 size
和 shard 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_negatives
和 background_is_superset
进行配置。
谷歌归一化距离
编辑“谷歌相似性距离”Cilibrasi 和 Vitanyi, 2007 中描述的谷歌归一化距离可用于添加参数作为显著性分数
"gnd": { }
gnd
还接受 background_is_superset
参数。
p 值分数
编辑p 值是在零假设成立的前提下,获得至少与实际观察结果一样极端的结果的概率。p 值的计算假设前景集合和背景集合是独立的 伯努利试验,零假设是概率相同。
示例用法
编辑此示例计算术语 user_agent.version
的 p 值分数,给定“以失败结束”与“未以失败结束”的前景集合。
"background_is_superset": false
表示背景集合不包含前景集合的计数,因为它们已被过滤掉。
"normalize_above": 1000
有助于在各种尺度上返回一致的显著性结果。1000
表示大于 1000
的术语计数将按 1000/term_count
的系数缩小。
resp = client.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 } } } }, ) print(resp)
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
const response = await client.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, }, }, }, }, }); console.log(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_count
和 shard_min_doc_count
都设置为更高的值,例如 10,以便过滤掉那些原本会优先的低频术语。
"percentage": { }
哪个最好?
编辑粗略地说,mutual_information
倾向于高频术语,即使它们在背景中也很频繁。例如,在自然语言文本分析中,这可能会导致选择停用词。mutual_information
不太可能选择非常罕见的术语,如拼写错误。gnd
倾向于具有高共现性的术语,并避免选择停用词。它可能更适合用于同义词检测。但是,gnd
倾向于选择非常罕见的术语,例如拼写错误的结果。chi_square
和 jlh
则介于两者之间。
很难说哪种不同的启发式方法是最佳选择,因为它取决于显著术语的用途(例如,有关使用显著术语进行文本分类的特征选择的研究,请参阅 Yang 和 Pedersen,“文本分类中特征选择的比较研究”,1997 年)。
如果以上方法都不适合您的用例,那么另一种选择是实现自定义显著性度量。
脚本
编辑可以通过脚本实现自定义分数
"script_heuristic": { "script": { "lang": "painless", "source": "params._subset_freq/(params._superset_freq - params._subset_freq + 1)" } }
脚本可以是内联的(如上例所示)、索引的或存储在磁盘上。有关选项的详细信息,请参阅 脚本文档。
脚本中可用的参数为
|
该术语在子集中出现的文档数。 |
|
该术语在超集中出现的文档数。 |
|
子集中的文档数。 |
|
超集中的文档数。 |
大小 & 分片大小
编辑可以设置 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
选项仅返回匹配超过配置命中数的术语。
resp = client.search( aggs={ "tags": { "significant_terms": { "field": "tag", "min_doc_count": 10 } } }, ) print(resp)
response = client.search( body: { aggregations: { tags: { significant_terms: { field: 'tag', min_doc_count: 10 } } } } ) puts response
const response = await client.search({ aggs: { tags: { significant_terms: { field: "tag", min_doc_count: 10, }, }, }, }); console.log(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
来缩小此范围,以专注于较窄上下文中的显著术语。
resp = client.search( query={ "match": { "city": "madrid" } }, aggs={ "tags": { "significant_terms": { "field": "tag", "background_filter": { "term": { "text": "spain" } } } } }, ) print(resp)
response = client.search( body: { query: { match: { city: 'madrid' } }, aggregations: { tags: { significant_terms: { field: 'tag', background_filter: { term: { text: 'spain' } } } } } } ) puts response
const response = await client.search({ query: { match: { city: "madrid", }, }, aggs: { tags: { significant_terms: { field: "tag", background_filter: { term: { text: "spain", }, }, }, }, }, }); console.log(response);
GET /_search { "query": { "match": { "city": "madrid" } }, "aggs": { "tags": { "significant_terms": { "field": "tag", "background_filter": { "term": { "text": "spain" } } } } } }
上述过滤器将有助于专注于马德里市特有的术语,而不是揭示诸如“西班牙语”之类的术语,这些术语在整个索引的全球范围内是不寻常的,但在包含“西班牙”一词的文档子集中却很常见。
使用背景过滤器会减慢查询速度,因为必须过滤每个术语的发布列表以确定频率。
执行提示
编辑可以通过不同的机制来执行术语聚合
- 直接使用字段值来按桶聚合数据(
map
) - 使用字段的全局序号,并为每个全局序号分配一个桶(
global_ordinals
)
Elasticsearch 尝试使用合理的默认值,因此通常不需要配置此项。
global_ordinals
是 keyword
字段的默认选项,它使用全局序号来动态分配桶,因此内存使用量与聚合范围内的文档值数量呈线性关系。
仅当很少的文档匹配查询时才应考虑使用 map
。否则,基于序号的执行模式会快得多。默认情况下,map
仅在对脚本运行聚合时使用,因为脚本没有序号。
resp = client.search( aggs={ "tags": { "significant_terms": { "field": "tags", "execution_hint": "map" } } }, ) print(resp)
response = client.search( body: { aggregations: { tags: { significant_terms: { field: 'tags', execution_hint: 'map' } } } } ) puts response
const response = await client.search({ aggs: { tags: { significant_terms: { field: "tags", execution_hint: "map", }, }, }, }); console.log(response);
GET /_search { "aggs": { "tags": { "significant_terms": { "field": "tags", "execution_hint": "map" } } } }
请注意,如果 Elasticsearch 不适用此执行提示,它将忽略此提示。