重要词项(significant_terms)聚合

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

用例示例:

  • 当用户在文本中搜索“禽流感”时,提示“H5N1”
  • 从信用卡持有人挂失的交易历史中确定“妥协点”的商家
  • 为自动新闻分类器建议与股票代码$ATI相关的关键词
  • 发现那个诊断出更多鞭伤的骗子医生
  • 发现爆胎数量不成比例的轮胎制造商

在这几种情况下,被选择的词项不仅仅都是一组最流行的词项。 它们是在前景(foreground)背景(background)集之间受欢迎程度发生了显著变化的词项。 如果词项“H5N1”仅存在于1000万个文档索引中的5个文档中,但是在构成用户搜索结果的100个文档中的4个中出现,则这是重要的并且可能与他们的搜索非常相关。 频率 5/10,000,000 与 4/100 相比差别很大。

单个集合分析(Single-set analysis)

在最简单的情况下,感兴趣的前景(foreground)集是与查询匹配的搜索结果,而用于统计比较的背景(background)集则是从中收集结果的一个或多个索引。

示例:

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
                }
                ...
            ]
        }
    }
}

当查询一个所有警察部队的所有犯罪的索引时,这些结果表明,英国交通警察部队(British Transport Police)在处理不成比例的大量自行车盗窃案件中表现突出。 通常情况下,自行车盗窃只占犯罪的1%(66799/5064554),但对于负责处理铁路和车站犯罪的英国交通警察来说,7%的犯罪(3640/47347)是自行车盗窃。 这在频率上显著增加了七倍,因此这种异常被强调为最常见的犯罪类型。

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

这可能是一种在索引中寻找不寻常模式的乏味方法

多个集合分析(Multi-set analysis)

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

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

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
                        }
                        ...
                    ]
                }
            }
        ]
    }
  }
}

现在,我们只需一个请求,就可以对每支警察部队进行异常检测。

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

GET /_search
{
    "aggs": {
        "hotspots": {
            "geohash_grid": {
                "field": "location",
                "precision": 5
            },
            "aggs": {
                "significant_crime_types": {
                    "significant_terms": {"field": "crime_type"}
                }
            }
        }
    }
}

上面这个示例使用geohash_grid聚合来创建代表地理区域的结果桶,在每个桶中,我们可以识别这些高度集中的区域中犯罪类型的异常级别,例如:

  • 机场缴获武器的数量之多的异常
  • 大学里自行车盗窃的上升趋势

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

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

在自定义文本(free-text)字段上使用

significant_terms聚合可以有效地用于词元化(tokenized)的自定义文本(free-text)字段,以建议:

  • 优化最终用户搜索的关键字
  • 用于percolator查询的关键字

选择一个自定义文本(free-text)作为重要词项分析的主题的成本可能会很高! 它会尝试将每个唯一的词加载到内存中。 建议仅在较小的索引上使用此选项。

在上下文中显示 significant_terms
自定义文本的 significant_terms 在上下文中更容易理解。 从自定义文本字段中获取significant_terms建议的结果,并在带有highlight子句的相同字段上的terms查询中使用它们,以向用户呈现文档的示例片段。 当词项不加词根、高亮显示、并以正确的大小写、正确的顺序和特定的上下文出现时,它们的重要性/意义就会更明显。

自定义背景集(Custom background sets)

通常,前景文档集与索引中所有文档的背景集是“不同”的。 然而,有时使用较窄的背景集作为比较的基础可能证明是有用的。 例如,在包含世界各地内容的索引中查询与“Madrid”相关的文档,可能会发现“Spanish”是一个重要的词项。 这可能是真的,但是如果你想要一些更集中的词项,你可以对词项spain使用background_filter来建立一个更窄的文档集作为上下文。 有了这一背景,“Spanish”现在会被视为司空见惯的,因此没有像“capital”这样与Madrid联系更紧密的词那么重要。 请注意,使用 background_filter 会减慢速度——每个词项的背景频率现在必须从过滤发布列表中即时获得,而不是读取索引中为某个词项预先计算的计数。

局限性

重要词项必须是索引值

与 terms 聚合不同,目前无法使用脚本生成的词项进行计数。 由于 significant_terms 聚合必须考虑前景背景频率,因此在整个索引上使用脚本来获取背景频率进行比较的成本非常高。 出于类似的原因,文档值(DocValues)也不支持作为词项数据的来源。

不分析浮点(float)字段

目前不支持浮点字段作为 significant_terms 分析的主题。 虽然整型(integer)或长整形(long)字段可用于表示银行账号或类别号等概念,跟踪这些概念可能会很有趣,但浮点型(float)字段通常用于表示数量。 因此,单个浮点项对于这种形式的频率分析没有用处。

作为父聚合(parent aggregation)使用

如果存在match_all查询的等价查询,或者没有提供索引子集的查询标准,则 significant_terms 聚合不应用作最顶层的聚合——在这种情况下,前景集与背景集完全相同,因此在要观察的文档频率和从中做出合理建议的文档频率方面没有差异。

另一个考虑因素是 significant_terms 聚合在分片级别产生许多候选结果,这些结果只是在所有分片的所有统计信息合并后,在缩减节点(reducing node)上进行删减。 因此,从内存的角度来说,将大型子聚合嵌入到一个 significant_terms 聚合下是低效和昂贵的,这个聚合稍后会丢弃许多候选词项。 在这些情况下,建议执行两次搜索——第一次是提供一个合理化的 significant_terms 列表,然后将这个词项列表添加到第二次查询中,以便返回并获取所需的子聚合。

近似计数

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

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

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

参数

JLH 分数

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

	 "jlh": {
	 }

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

交互信息(Mutual information)

在 Manning 等人的 "Information Retrieval" 第13.5.1章中描述的交互信息可以通过添加参数用作重要性分数

	 "mutual_information": {
	      "include_negatives": true
	 }

交互信息不区分描述子集或子集之外的文档的词项。 因此,重要的词项可以包含在子集内出现频率比在子集外出现频率高或低的词项。 要过滤掉子集中出现频率低于子集外文档的词项,可以将include_negatives设置为false

默认情况下,假设桶中的文档也包含在背景中。 相反,如果你定义了一个自定义背景过滤器,它代表了你要比较的一组不同的文档,请设置:

"background_is_superset": false

卡方(Chi square)

如 Manning 等人在"Information Retrieval"第13.5.2章中述,卡方(Chi square)可以通过添加参数用作重要性分数:

	 "chi_square": {
	 }

卡方(Chi square)表现得像交互信息,并且可以用相同的参数include_negativesbackground_is_superset来配置。

标准Google距离(Google normalized distance)

如 Cilibrasi 和 Vitanyi 在2007年出版的"The Google Similarity Distance"(http://arxiv.org/pdf/cs/0412098v3.pdf)中所描述的,标准Google距离可以通过添加参数用作重要性分数:

	 "gnd": {
	 }

gnd还接受参数background_is_superset

百分比(Percentage)

一个简单的计算方法,用前景样本中带有词项的文档数除以背景样本中带有词项的文档数。 默认情况下,这会产生一个大于0、小于1的分数。

这种启发式的好处是,评分逻辑很容易向熟悉“人均”统计数据的人解释。 然而,对于高基数(high cardinality)的字段,这种试探倾向于选择最罕见的词项,例如只出现一次的错别字,因为它们的得分为 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"。

如果上述方法都不适合你的用例,那么另一个选择是实施自定义重要性的判断方法:

脚本化的(Scripted)

可以通过脚本实现自定义的分数:

	    "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 和 shard size

可以设置参数size来定义应该从整个词项列表中返回多少个词项桶。 默认情况下,协调搜索过程的节点将请求每个分片提供各自的前几个词项桶,一旦所有分片做出响应,它将把结果缩减为最终列表,然后返回给客户端。 如果唯一的词项的数量大于size,则返回的列表可能稍有偏差且不准确(可能是词项计数稍有偏差,甚至可能是本应该是前size个桶中的词项没有返回)。

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

所有结果合并后,低频词项可能会成为最有趣的词项,因此当参数shard_size设置为明显高于size设置的值时,significant_terms聚合可以生成更高质量的结果。 这确保了在最终选择之前,缩减节点会对大量有希望的候选词项进行综合审查。 很明显,大的候选词项列表将导致额外的网络流量和内存使用,因此这是需要平衡的质量/成本权衡。 如果shard_size设置为-1(默认值),那么shard_size将根据分片数量和size参数自动估算。

shard_size不能小于size(因为没有太大意义)。 当出现这种情况时,Elasticsearch将覆盖它并将其重置为与size相等。

最小文档数量

使用min_doc_count选项可以只返回匹配超过配置的命中数的词项:

GET /_search
{
    "aggs" : {
        "tags" : {
            "significant_terms" : {
                "field" : "tag",
                "min_doc_count": 10
            }
        }
    }
}

上面这个聚合将仅返回在10次或更多次点击中出现的tag。默认值为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设置为1,除非你显式设置它,否则它不起作用。

通常不建议将min_doc_count设置为1,因为它会返回拼写错误或其他奇怪的词。 找到一个词项的不止一个实例有助于强化这一点,尽管这种情况仍然很少见,但这个词项不是一次性事故的结果。 默认值 3 用于提供最小证据权重。 shard_min_doc_count设置得太高会导致重要的候选词项在分片级别被过滤掉。 该值应设置为远低于 min_doc_count/#shards

自定义背景上下文

背景词项频率的统计信息的默认来源是整个索引,并且可以通过使用background_filter来缩小该范围,以便在更窄的上下文中聚焦于重要的词项:

GET /_search
{
    "query" : {
        "match" : {
            "city" : "madrid"
        }
    },
    "aggs" : {
        "tags" : {
            "significant_terms" : {
                "field" : "tag",
                "background_filter": {
                	"term" : { "text" : "spain"}
                }
            }
        }
    }
}

上述过滤器将有助于集中在 Madrid 市特有的词项上,而不是显示诸如“Spanish”的词项,这些词项在完整索引的全局上下文中是不常见的,但是在包含单词“Spain”的文档子集中是常见的。

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

过滤值

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

回收模式

为了避免内存问题,significant_terms聚合始终以 广度优先(breadth_first) 模式计算子聚合。 不同收集模式的描述可以在terms聚合文档中找到。

执行机制(execution hint)

可以通过不同的机制来执行词项聚合(terms aggregation):

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

Elasticsearch会尝试使用合理的默认值,所以这通常不需要配置。

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

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

GET /_search
{
    "aggs" : {
        "tags" : {
             "significant_terms" : {
                 "field" : "tags",
                 "execution_hint": "map" 
             }
         }
    }
}

可能的值有mapglobal_ordinals

请注意,如果不适用,Elasticsearch将忽略这个执行机制(execution_hint)。