显著文本聚合
编辑显著文本聚合
编辑一种聚合,返回集合中自由文本词条中有趣或不寻常的出现。它类似于显著词条聚合,但不同之处在于:
- 它专门设计用于
text
类型的字段。 - 它不需要字段数据或 doc-values。
- 它会动态地重新分析文本内容,这意味着它还可以过滤掉嘈杂文本中重复的部分,否则这些部分会歪曲统计数据。
示例用例
- 当用户搜索“禽流感”时,建议“H5N1”以帮助扩展查询
- 建议与股票代码 $ATI 相关的关键字,用于自动新闻分类器
在这些情况下,选择的词条不仅仅是结果中最流行的词条。最流行的词往往非常无聊(and、of、the、we、I、they ...)。显著词条是在前景和背景集之间衡量流行度发生显著变化的词条。如果“H5N1”这个词在 1000 万个文档的索引中只存在于 5 个文档中,但在构成用户搜索结果的 100 个文档中却有 4 个文档包含该词,那么这很重要,并且可能与他们的搜索非常相关。5/10,000,000 与 4/100 相比,频率变化很大。
基本用法
编辑在典型的用例中,感兴趣的前景集合是查询的匹配度最高的搜索结果的选择,而用于统计比较的背景集合是收集结果的索引或多个索引。
示例
resp = client.search( index="news", query={ "match": { "content": "Bird flu" } }, aggregations={ "my_sample": { "sampler": { "shard_size": 100 }, "aggregations": { "keywords": { "significant_text": { "field": "content" } } } } }, ) print(resp)
response = client.search( index: 'news', body: { query: { match: { content: 'Bird flu' } }, aggregations: { my_sample: { sampler: { shard_size: 100 }, aggregations: { keywords: { significant_text: { field: 'content' } } } } } } ) puts response
const response = await client.search({ index: "news", query: { match: { content: "Bird flu", }, }, aggregations: { my_sample: { sampler: { shard_size: 100, }, aggregations: { keywords: { significant_text: { field: "content", }, }, }, }, }, }); console.log(response);
GET news/_search { "query": { "match": { "content": "Bird flu" } }, "aggregations": { "my_sample": { "sampler": { "shard_size": 100 }, "aggregations": { "keywords": { "significant_text": { "field": "content" } } } } } }
响应
{ "took": 9, "timed_out": false, "_shards": ..., "hits": ..., "aggregations" : { "my_sample": { "doc_count": 100, "keywords" : { "doc_count": 100, "buckets" : [ { "key": "h5n1", "doc_count": 4, "score": 4.71235374214817, "bg_count": 5 } ... ] } } } }
结果表明,“h5n1”是与禽流感密切相关的几个词条之一。它在我们的整个索引中只出现 5 次(请参阅 bg_count
),但在我们 100 个“禽流感”结果文档样本中,有幸出现 4 次。这表明这是一个重要的词,用户可能会将其添加到他们的搜索中。
使用 filter_duplicate_text
处理嘈杂数据
编辑自由文本字段通常包含原始内容和文本的机械副本(复制粘贴的传记、电子邮件回复链、转推、样板页眉/页脚、页面导航菜单、侧边栏新闻链接、版权声明、标准免责声明、地址)。
在实际数据中,如果不进行过滤,这些重复的文本部分往往会在 significant_text
结果中大量出现。在索引时过滤近乎重复的文本是一项艰巨的任务,但我们可以使用 filter_duplicate_text
设置在查询时动态清理数据。
首先,让我们使用Signal 媒体数据集(包含 100 万篇涵盖各种新闻的新闻文章),查看一个未过滤的实际示例。以下是搜索提及“elasticsearch”的文章的原始显著文本结果
{ ... "aggregations": { "sample": { "doc_count": 35, "keywords": { "doc_count": 35, "buckets": [ { "key": "elasticsearch", "doc_count": 35, "score": 28570.428571428572, "bg_count": 35 }, ... { "key": "currensee", "doc_count": 8, "score": 6530.383673469388, "bg_count": 8 }, ... { "key": "pozmantier", "doc_count": 4, "score": 3265.191836734694, "bg_count": 4 }, ... }
未经清理的文档出现了一些看起来很奇怪的词条,这些词条表面上与我们的搜索词“elasticsearch”的出现具有统计相关性,例如“pozmantier”。我们可以深入研究这些文档的示例,以了解为什么 pozmantier 是通过此查询连接的
resp = client.search( index="news", query={ "simple_query_string": { "query": "+elasticsearch +pozmantier" } }, source=[ "title", "source" ], highlight={ "fields": { "content": {} } }, ) print(resp)
response = client.search( index: 'news', body: { query: { simple_query_string: { query: '+elasticsearch +pozmantier' } }, _source: [ 'title', 'source' ], highlight: { fields: { content: {} } } } ) puts response
const response = await client.search({ index: "news", query: { simple_query_string: { query: "+elasticsearch +pozmantier", }, }, _source: ["title", "source"], highlight: { fields: { content: {}, }, }, }); console.log(response);
GET news/_search { "query": { "simple_query_string": { "query": "+elasticsearch +pozmantier" } }, "_source": [ "title", "source" ], "highlight": { "fields": { "content": {} } } }
结果显示了一系列关于多个技术项目评审小组的非常相似的新闻文章
{ ... "hits": { "hits": [ { ... "_source": { "source": "Presentation Master", "title": "T.E.N. Announces Nominees for the 2015 ISE® North America Awards" }, "highlight": { "content": [ "City of San Diego Mike <em>Pozmantier</em>, Program Manager, Cyber Security Division, Department of", " Janus, Janus <em>ElasticSearch</em> Security Visualization Engine " ] } }, { ... "_source": { "source": "RCL Advisors", "title": "T.E.N. Announces Nominees for the 2015 ISE(R) North America Awards" }, "highlight": { "content": [ "Mike <em>Pozmantier</em>, Program Manager, Cyber Security Division, Department of Homeland Security S&T", "Janus, Janus <em>ElasticSearch</em> Security Visualization Engine" ] } }, ...
Mike Pozmantier 是评审小组的众多评委之一,而 elasticsearch 被用于正在评审的多个项目之一。
通常情况下,此篇冗长的新闻稿被多个新闻站点复制粘贴,因此其中包含的任何罕见名称、数字或拼写错误都会与我们匹配的查询在统计上相关联。
幸运的是,相似的文档往往排名相似,因此,作为检查匹配度最高的文档流的一部分,significant_text 聚合可以应用过滤器来删除已出现过的任何 6 个或更多令牌的序列。现在,让我们尝试相同的查询,但启用 filter_duplicate_text
设置
resp = client.search( index="news", query={ "match": { "content": "elasticsearch" } }, aggs={ "sample": { "sampler": { "shard_size": 100 }, "aggs": { "keywords": { "significant_text": { "field": "content", "filter_duplicate_text": True } } } } }, ) print(resp)
response = client.search( index: 'news', body: { query: { match: { content: 'elasticsearch' } }, aggregations: { sample: { sampler: { shard_size: 100 }, aggregations: { keywords: { significant_text: { field: 'content', filter_duplicate_text: true } } } } } } ) puts response
const response = await client.search({ index: "news", query: { match: { content: "elasticsearch", }, }, aggs: { sample: { sampler: { shard_size: 100, }, aggs: { keywords: { significant_text: { field: "content", filter_duplicate_text: true, }, }, }, }, }, }); console.log(response);
GET news/_search { "query": { "match": { "content": "elasticsearch" } }, "aggs": { "sample": { "sampler": { "shard_size": 100 }, "aggs": { "keywords": { "significant_text": { "field": "content", "filter_duplicate_text": true } } } } } }
对于任何熟悉 elastic stack 的人来说,分析重复数据删除后的文本的结果显然质量更高
{ ... "aggregations": { "sample": { "doc_count": 35, "keywords": { "doc_count": 35, "buckets": [ { "key": "elasticsearch", "doc_count": 22, "score": 11288.001166180758, "bg_count": 35 }, { "key": "logstash", "doc_count": 3, "score": 1836.648979591837, "bg_count": 4 }, { "key": "kibana", "doc_count": 3, "score": 1469.3020408163263, "bg_count": 5 } ] } } } }
由于复制粘贴操作或其他形式的机械重复,Pozmantier 先生和其他与 elasticsearch 的一次性关联不再出现在聚合结果中。
如果可以通过单值索引字段(可能是文章的 title
文本或 original_press_release_url
字段的哈希)来识别重复或近乎重复的内容,则使用父多样化采样器聚合来基于该单个键从样本集中消除这些文档会更有效。预先馈送到 significant_text 聚合的重复内容越少,性能就越好。
限制
编辑不支持子聚合
编辑significant_text 聚合故意不支持添加子聚合,因为
- 它会带来很高的内存成本
- 它不是一个普遍有用的功能,并且对于那些需要它的用户有一个解决方法
候选词条的数量通常非常多,并且在返回最终结果之前会对其进行大量修剪。支持子聚合会产生额外的搅动,并且效率低下。客户端始终可以从 significant_text
请求中获取经过大量修剪的结果集,并使用带有 include
子句和子聚合的 terms
聚合进行后续查询,以更有效的方式对选定的关键字进行进一步分析。
不支持嵌套对象
编辑significant_text 聚合当前也不能与嵌套对象中的文本字段一起使用,因为它使用文档 JSON 源。考虑到匹配的 Lucene docID,这使得从存储的 JSON 中匹配嵌套文档时,此功能的效率很低。
近似计数
编辑结果中提供的包含词条的文档数量基于对每个分片返回的样本求和,因此可能
- 如果某些分片未在其顶部样本中提供给定词条的数字,则较低
- 在考虑背景频率时较高,因为它可能会计算已删除文档中找到的出现次数
与大多数设计决策一样,这是权衡的基础,我们选择以牺牲一些(通常很小)的不准确性为代价来提供快速性能。但是,下一节中介绍的 size
和 shard 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
(因为这没有意义)。如果小于,Elasticsearch 将会覆盖它,并将其重置为等于 size
。
最小文档计数
编辑可以使用 min_doc_count
选项来配置只返回匹配超过指定命中次数的词条。默认值为 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( index="news", query={ "match": { "content": "madrid" } }, aggs={ "tags": { "significant_text": { "field": "content", "background_filter": { "term": { "content": "spain" } } } } }, ) print(resp)
response = client.search( index: 'news', body: { query: { match: { content: 'madrid' } }, aggregations: { tags: { significant_text: { field: 'content', background_filter: { term: { content: 'spain' } } } } } } ) puts response
const response = await client.search({ index: "news", query: { match: { content: "madrid", }, }, aggs: { tags: { significant_text: { field: "content", background_filter: { term: { content: "spain", }, }, }, }, }, }); console.log(response);
GET news/_search { "query": { "match": { "content": "madrid" } }, "aggs": { "tags": { "significant_text": { "field": "content", "background_filter": { "term": { "content": "spain" } } } } } }
上面的过滤器将有助于关注马德里市特有的词条,而不是显示像“西班牙语”这样的词条,这些词条在整个索引的全球背景下是不寻常的,但在包含“西班牙”一词的文档子集中是很常见的。
使用背景过滤器会减慢查询速度,因为必须过滤每个词条的倒排表以确定频率。
处理源和索引映射
编辑通常,索引字段名称和检索的原始 JSON 字段共享相同的名称。然而,对于更复杂的字段映射,使用诸如 copy_to
等功能,源 JSON 字段和聚合的索引字段可能会有所不同。在这些情况下,可以使用 source_fields
参数列出将从中分析文本的 JSON _source 字段。
resp = client.search( index="news", query={ "match": { "custom_all": "elasticsearch" } }, aggs={ "tags": { "significant_text": { "field": "custom_all", "source_fields": [ "content", "title" ] } } }, ) print(resp)
response = client.search( index: 'news', body: { query: { match: { custom_all: 'elasticsearch' } }, aggregations: { tags: { significant_text: { field: 'custom_all', source_fields: [ 'content', 'title' ] } } } } ) puts response
const response = await client.search({ index: "news", query: { match: { custom_all: "elasticsearch", }, }, aggs: { tags: { significant_text: { field: "custom_all", source_fields: ["content", "title"], }, }, }, }); console.log(response);
GET news/_search { "query": { "match": { "custom_all": "elasticsearch" } }, "aggs": { "tags": { "significant_text": { "field": "custom_all", "source_fields": [ "content", "title" ] } } } }