
使用 Elasticsearch 和 Jina 嵌入的无监督文档聚类
从向量搜索到强大的 REST API,Elasticsearch 为开发者提供了最全面的搜索工具包。您可以在 Elasticsearch Labs 仓库中查看我们的示例笔记本,尝试一些新东西。您也可以开始您的免费试用或在本地运行 Elasticsearch。
向量搜索通常从查询开始,但如果您没有查询该怎么办?
组织通常积累大量的文档集合,如支持票据、法律文件、新闻源和研究论文等,需要在能够提出正确的问题之前理解其中的内容。没有标签或训练数据,手动审核成千上万的文档是不切实际的。当您不知道要搜索什么时,传统的搜索方式也无济于事。
本文介绍了一种基于 Elasticsearch 的无监督文档聚类和时间故事跟踪的方法来解决这个发现问题。到最后,您可以追踪跨越多天的故事情节:

时间故事链在 2025 年 2 月流动——每条彩色路径代表一个跨越多天的故事,链接宽度显示 kNN 重叠强度
您将发现:
msearch 通过密度探测的中心分类将文档按主题分组。significant_text 自动标记聚类,使主题无需训练模型即可读懂。这篇文章是从一个可运行的 Jupyter Notebook 生成的。 您在其中看到的内联输出是该流程的实际结果。克隆相关的笔记本以自行运行。
该流程使用大约 8500 篇来自 BBC News 和 The Guardian 的 2025 年 2 月的文章作为测试语料库。新闻具有明显的时间行为,但该模式适用于任何文档发现重要的场景:法律审核、合规性监控、研究综合、客户支持分类。
技术栈:
significant_text 标记和向量存储。bbq_disk 将量化向量存储在磁盘上,仅在堆中保留分区元数据,与 bbq_hnsw 相比,极大地降低了资源需求,同时保持较高的召回率。您需要:
bbq_disk 需要 8.18 或更高版本。可选的多样化检索部分需要 9.3+ 或 serverless。安装所需的软件包:
1
pip install elasticsearch pandas numpy plotly umap-learn python-dotenv pydantic-settings datasets requests
可选(仅当您运行此仓库中的抓取助手时):
1
pip install beautifulsoup4
然后在项目根目录的 .env 文件中配置 API 密钥:
1
2
3
4
ELASTIC_CLOUD_ID=your-cloud-id # 或 ELASTIC_HOST=https://...
ELASTIC_API_KEY=your-api-key
JINA_API_KEY=your-jina-key
GUARDIAN_API_KEY=your-guardian-key
此笔记本调用 load_dotenv(override=True),因此本地 .env 值优先。
1
连接到 Elasticsearch
大多数向量搜索使用的是检索嵌入,这些嵌入经过训练以将_查询_与相关的_文档_匹配。这对于搜索很完美,但对于发现来说并不理想。当您想在没有任何查询的情况下在语料库中找到有哪些主题时,您需要能够将相似文档聚集在一起的嵌入。
Jina v5 使用任务特定的低秩适配(LoRA)适配器解决了这个问题。LoRA 在保持大部分基础模型权重不变的情况下,向目标内部层添加小的低秩更新,因此模型行为会向特定任务转变,而无需完全重新训练。根据 task 参数,相同的基础模型会生成不同的嵌入:
任务 | 训练目标 | 使用场景 |
|---|---|---|
retrieval.passage | 查询-文档匹配 | 搜索,检索增强生成 (RAG) |
clustering | 主题分组(优化紧密聚类) | 发现,分类 |
聚类适配器经过训练,使关于相同主题的文档在嵌入空间中_更接近_,而关于不同主题的文档则_更远_。下面的视觉比较使这种区别更为具体。
为了看出差异,将一组文档用两种任务类型嵌入。聚类是在原始的 1024 维嵌入空间中进行的;统一流形近似与投影(UMAP)仅用于将这些嵌入投影到二维进行可视化。UMAP 保持局部邻域结构,使其在比较聚类分离时很有用。
在下面,相同的 480 个文档样本用两种任务类型嵌入,并用 UMAP 投影到二维。请注意聚类面板中的更紧密、分离得更开的颜色组。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
全数据集:8,495 篇文章
来源:guardian: 5749, bbc: 2746
日期范围:2025-02-01 至 2025-02-28
样本:480 篇文档跨越 8 个栏目
栏目
电影 60
世界新闻 60
澳大利亚新闻 60
观点 60
足球 60
美国新闻 60
体育 60
商业 60
聚类嵌入:480
检索嵌入: 480
UMAP 投影完成

检索嵌入与聚类嵌入的 UMAP 比较
检索嵌入(左)将主题广泛展开;聚类嵌入(右)从相同文档中生成更紧密、更分离的组。
聚类嵌入生成了更紧密、视觉上更明显的组。检索嵌入将主题更均匀地展开,适合搜索(细粒度相似性);但对于发现来说,紧密的主题聚类才是关键。
这就是为什么在此操作演练的其余部分使用 task="clustering"。
语料库结合了 2025 年 2 月的两个新闻来源:
拥有多个来源有助于验证聚类找到的是_主题_而不是_特定来源的风格_。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
总文章数: 8,495
来源分布:
来源
guardian 5749
bbc 2746
日期范围:2025-02-01 → 2025-02-28
覆盖天数:28
样本文章:
来源: guardian
标题: Carbon monoxide poisoning ruled out in death of Gene Hackman and wife, police sa
栏目: 电影
文本: Authorities have ruled out that Gene Hackman and his wife, Betsy Arakawa, died from carbon monoxide poisoning earlier this week in their home in Santa Fe, New Mexico. The Santa Fe county sheriff, Adan...
Jina v5 API 被调用时,所有文档都使用 task="clustering"。嵌入被缓存到磁盘,因此后续运行完全跳过 API。
API 调用非常简单。task 参数是与典型嵌入使用的主要区别:
1
payload = { "model": "jina-embeddings-v5-text-small", "input": texts, "task": "clustering", # ← 选择聚类 LoRA 适配器}
下面的时间反映了缓存命中。首次运行 API 的时间较长,具体取决于语料库大小。
1
2
嵌入准备就绪:8,495 个 1024 维向量
时间:0.6s
对于发现聚类,整个月份的数据进入一个索引(docs-clustering-all)。每日分区将在时间故事链接中进行。
索引映射使用 bbq_disk 作为向量字段:
1
{ "embedding": { "type": "dense_vector", "dims": 1024, "index": true, "similarity": "cosine", "index_options": { "type": "bbq_disk" // 用于 ANN 索引查找的分层 k-means 分区;与本文的聚类算法分离 } }}
一个 1024 维的 float32 向量是 4 KB。bbq_disk 使用分层 k-means 将向量划分为小聚类,对其进行二进制量化,并将全精度向量存储在磁盘上以进行重新评分。仅分区元数据驻留在堆中,因此即使对于大型语料库,内存需求也保持较低。对于可以承受更多堆内存的工作负载,bbq_hnsw 构建了一个分层可导航小世界(HNSW)图,用于在更高资源成本下更快的查找。
dense_vector 字段类型支持多种量化策略:bbq_disk 和 bbq_hnsw 是适用于高维嵌入(如本文使用的 1024 维向量)的最佳选择。
1
2
已索引 8,495 篇文档到 docs-clustering-all
时间:57.5s
传统的聚类算法如 HDBSCAN 假设您可以将完整的 N×d 向量矩阵保存在内存中,并运行重复的完整通道更新。对于 8,495 篇 1024 维的文档,这是可控的(约 35 MB),但该方法在没有额外基础设施的情况下无法扩展到数百万个文档。
此算法在概念上类似于 KMeans++ 初始化的 Voronoi 分配和噪声地板,但它使用 Elasticsearch kNN 搜索 作为计算原语,将几乎所有工作留在服务器端:
msearch kNN 进行探针密度。每个探针发出一个 kNN 查询并记录其邻居的平均相似度。高平均相似度 = 嵌入空间的密集区域。msearch 通过单个 HTTP 调用发送多个搜索请求,这在这里至关重要:密度探测生成数百个 kNN 查询,将它们批量处理可避免每个请求的开销。msearch kNN 将所有文档分类到中心点:每个种子作为中心点;一个 kNN 搜索检索到高于相似度阈值的邻近文档。每个文档被分配到返回它的最高得分的中心点。小聚类被解散为噪声。Elasticsearch 处理繁重的工作:msearch 用于密度探针,msearch 用于分类,significant_text 用于标记。对于此语料库(8,495 个文档),5% 的密度探针样本启动 425 个 kNN 探针查询,msearch 将其批量处理为九个 HTTP 调用(批量大小为 50),避免了一次请求的开销。结合 bbq_disk ANN 查找,这使聚类阶段保持快速和可扩展。kNN 查询在聚类通过中使用最小的 num_candidates 值以提高速度;生产搜索查询应使用更高的 num_candidates 值以提高召回率,但这会增加延迟。
聚类具有由每个中心点周围的嵌入空间密度决定的自然大小,而不是硬性的 k 上限。密集的主题区域产生较大的聚类;小众主题产生较小的聚类。
KMeans 假设球形聚类并要求将整个 N×d 矩阵保存在内存中。对于适合内存的语料库,HDBSCAN 是一个很强的替代方案。它处理任意聚类形状,并具有良好的密度语义。
密度探测中心方法针对不同的需求:当您想将存储、检索和聚类整合到一个系统中,或者在规模上使得客户端矩阵操作不切实际时,它会派上用场。它使用 Elasticsearch 的 kNN 作为计算原语,处理任意聚类大小,并且几乎所有计算都在服务器端进行。
1
2
3
4
在 31.6 秒内完成全球索引聚类
总聚类数:82
总噪声: 2420 (28.5%)
密度探针:425 个 kNN 查询通过 9 个 _msearch HTTP 调用
约 28% 的噪声率是有意设计的,而不是故障模式。那些在配置的 similarity_threshold 下不适合任何密集聚类的文档会被留作未分配,而不是强行匹配到不合适的组中。这起到质量门控的作用:意见专栏、短篇文章和一次性故事自然会抵制聚类,因为它们缺乏定义连贯群组的主题密度。
这个阈值是可调的:降低 similarity_threshold 会导致更具侵略性的聚类(更多文档被分配,但聚类更松散),而提高它则会收紧聚类并增加噪声比例。对于这种混合新闻内容的语料库,约 30% 的噪声是一个合理的操作点。生产部署应该根据特定领域的质量标准调整阈值。
现在每个聚类需要一个可读的人类标签。Elasticsearch 的 significant_text 聚合可以找到在前景集(聚类)中相对于背景集(整个语料库)出现频率异常高的术语。
在内部,它使用一种统计启发式(默认是 JLH 分数)来平衡绝对和相对频率的变化,无需机器学习,也无需大语言模型(LLM)调用。一个关于英国政治的聚类可能会浮现出像 starmer、labour、downing 这样的术语,因为这些术语在该聚类中相对于整体新闻语料库来说是不成比例的常见。
对于此全球过程,标签直接在 docs-clustering-all 上计算,因此前景和背景都来自整个月份。在第 2 部分中,标签使用每日索引模式(docs-clustering-*),一个通配符允许查询同时跨越所有匹配的索引,从而为 significant_text 提供更广泛的背景以获得更好的对比。
一个最小的查询结构如下:
1
{ "size": 0, "query": { "term": { "cluster_id": "72" } }, "aggs": { "label_terms": { "significant_text": { "field": "text", "size": 5, "filter_duplicate_text": true } } }}
significant_text 也起到了质量门控的作用:那些没有产生显著术语的聚类没有区别词汇,应该被解散回噪声,而不是给出误导性的标签。
轻量级的确定性清理步骤会去除噪声标签术语(数字 token,通用词)并在必要时回退到代表性标题。这使标签保持 Elasticsearch 原生,同时提高可读性。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
样本聚类标签:
聚类 3 (200 个文档) arsenal | mikel | villa
聚类 1 (198 个文档) volodymyr | ukrainian | kyiv
聚类 0 (196 个文档) hostages | hamas | israeli
聚类 4 (187 个文档) scrum | rugby | borthwick
聚类 52 (185 个文档) fossil | renewable | renewables
聚类 10 (156 个文档) labour | gwynne | mps
聚类 40 (151 个文档) novel | novels | literary
聚类 11 (149 个文档) mewis | sarina | wiegman
聚类 44 (143 个文档) flooding | rainfall | rain
聚类 13 (131 个文档) doge | musk | elon
聚类 12 (128 个文档) murder | insp | knockholt
聚类 5 (124 个文档) putin | backstop | starmer
重新分配 35 个文档从不连贯聚类到噪声
总文档数:8,495
已聚类: 6,040 (71.1%)
噪声: 2,455 (28.9%)
下面的可视化展示了全局聚类过程发现的内容:按日期分解的已聚类文档与噪声文档,整个月份的 UMAP 投影,以及确认聚类反映的是主题而非来源的来源混合图。

2025年2月已聚类与噪声文档的每日分布
2025年2月已聚类与噪声文档的每日分布。

全月份所有文档的 UMAP 投影
全月份 UMAP 投影:每个彩色岛屿是一个主题聚类,灰色点是噪声

仅显示已聚类文档的 UMAP 投影
仅聚类的文档:去除噪声后,主题结构更为清晰

UMAP 投影,突出显示一个聚类
聚焦视图,突出显示一个聚类(英超联赛足球)与其他所有聚类的对比。

按聚类显示的来源混合,显示基于主题的分组
按聚类显示的来源混合:BBC 和 Guardian 在每个主要聚类中都出现,确认是基于主题而非来源的分组。
UMAP 中的每个彩色岛屿代表一个聚类:一组仅通过嵌入相似性发现的关于相同主题的文章。灰色的噪声点是没有清晰地适合任何聚类的文章(通常是短文章、意见文章或一次性故事)。
来源分解图证实,聚类包含来自 BBC 新闻和 The Guardian 的文章。聚类发现的是_主题_,而非_来源_,这正是无监督发现应该产生的结果。
普通的 kNN 返回的是最接近聚类中心(密集核心)的文档。但实际的聚类覆盖子主题。多样化检索器 使用最大边缘相关性(MMR)来展示与中心相关但彼此不同的文档。
关键参数是 λ(lambda):
版本说明: 多样化检索器可用于 Elastic Cloud 无服务器和自我管理的 Elasticsearch 9.3+。较早的版本仍然可以遵循聚类和时间链接部分;只有这个探索步骤需要多样化检索器。
一个最小的检索器请求结构如下:
1
{ "size": 8, "retriever": { "diversify": { "type": "mmr", "field": "embedding", "lambda": 0.5, "query_vector": "<cluster-centroid-vector>", "retriever": { "knn": { "field": "embedding", "query_vector": "<cluster-centroid-vector>", "k": 50, "num_candidates": 100 } } } }}
type、field 和 query_vector 参数在多样化级别是必需的:field 指定 MMR 使用哪个 dense_vector 字段进行结果间相似性计算,而 query_vector 提供相关性评分的参考点。
这让您可以回答:“这个聚类实际上涵盖了什么?”而不仅仅是“它的中心是什么?”
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
探索聚类 52 (185 个文档)
标签:fossil | renewable | renewables
中心点已计算(dim=1024)
========================================================================
普通 kNN(最接近中心点)
========================================================================
1. [0.9738] 绿色活动人士担心部长们准备向德拉克斯电站提供数十亿英镑的新补贴,尽管有强烈的担忧...
2. [0.9710] 十三个石油和天然气许可证可能会被取消,因为在法院的一个里程碑式的裁决之后,部长们决定新的化石燃料提取指导...
3. [0.9699] 专家指责化石燃料行业寻求特殊待遇,因为说客们认为油田的温室气体排放...
4. [0.9681] 烧木头是产生电力的一种糟糕方式。砍树破坏了野生动物的栖息地,种植新树无法...
5. [0.9649] 基尔·斯塔默如果屈从于政治压力并允许开发...
6. [0.9641] 工党将在下周面临严峻的政策选择,这些选择威胁要暴露财政部和...
7. [0.9638] 位于北约克郡塞尔比附近的德拉克斯电站燃烧进口的木颗粒 政府已同意与...
8. [0.9581] 如果您关心我们要传给后代的世界,周四早晨的新闻是戏剧性的。今年一月是...
========================================================================
多样化检索器(MMR,lambda=0.5)
========================================================================
1. [0.9738] 绿色活动人士担心部长们准备向德拉克斯电站提供数十亿英镑的新补贴,尽管有强烈的担忧...
2. [0.9434] 油气利益集团已展开协调行动,阻止禁止新建筑物气体连接的电气化政策...
3. [0.9303] 有趣的是,北海的石油和天然气生产的新许可证因法律行动而延迟...
4. [0.9139] 美国能源部长克里斯·赖特表示,他“很乐意看到澳大利亚参与铀供应,并可能走下去...
5. [0.9077] 瑞秋·里夫斯在周六晚上面临批评,因为她确认她引用的一份报告作为第三...
6. [0.8996] 当玛格丽特·撒切尔在 1990 年开放哈德利气候变化中心时,记者们建议她试图表明自己在...
7. [0.8993] 绝大多数政府可能会错过提交至关重要计划的临界期,这些计划将决定世界是否...
8. [0.8987] 根据一份新报告,欧洲海运天然气进口量去年下降了五分之一,达到自大流行以来的最低水平...
重叠:1/8 个文档同时出现在两个结果集中
平均成对相似度(较低 = 更多样化):
普通 kNN: 0.9057
多样化检索器: 0.6965
普通 kNN 结果围绕主题的一个角度:与中心点和彼此最相似的文档。多样化检索器则展示了同一聚类的不同方面:子主题、不同的来源和不同的观点。
多样性指标定量地确认了这一点:多样化检索器结果的平均成对相似性较低,意味着返回的文档覆盖了更多内容。
这对于以下用途很有帮助:
第 1 部分对整个月进行了全局聚类以进行主题发现。对于时间流动,相同的密度探测中心分类在每日索引上独立运行,然后跨相邻日期链接聚类。请注意,日聚类与第 1 部分的全局聚类无关;每天产生自己的聚类分配和标签,针对当天的内容进行调整。
对于 A 天的每个聚类:
这非常快(每个聚类只查询几个文档,而不是全部),并且使用 Elasticsearch 的原生 kNN,无需外部工具。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
准备每日索引以进行时间链接...
已索引 8,495 个文档到 28 个每日索引
发现时间链接:808 个,耗时 145.4 秒
最强链接:
2025.02.01 'league | arsenal | premier' -> 2025.02.02 'league | season | striker' (100%)
2025.02.03 'league | striker | loan' -> 2025.02.04 'league | striker | season' (100%)
2025.02.03 'score | operator | gedling' -> 2025.02.04 'league | striker | season' (100%)
2025.02.12 'playoff | leg | bayern' -> 2025.02.13 'league | players | injury' (100%)
2025.02.14 'league | injury | football' -> 2025.02.15 'league | premier | football' (100%)
2025.02.18 'russia | ukraine | talks' -> 2025.02.19 'saudi | russia | arabia' (100%)
2025.02.18 'football | league | bayern' -> 2025.02.19 'league | manchester | players' (100%)
2025.02.21 'league | premier | manchester' -> 2025.02.22 'game | players | defeat' (100%)
2025.02.21 'rugby | calcutta | brilliant' -> 2025.02.22 'game | players | defeat' (100%)
2025.02.26 'metals | kyiv | ukrainian' -> 2025.02.27 'ukraine | russia | talks' (100%)
kNN 比例为 100% 意味着源聚类中每个抽样文档都落在同一目标聚类中,这是最强的跨天链接。上面的多数链接与足球相关,这是有道理的:英超联赛的报道每天都有,并且具有很高的主题一致性。
score | operator | gedling → league | striker | season 链接是一个例子,显示了一个小众的本地足球聚类(Gedling 是一个非联赛俱乐部)在第二天被更广泛的英超联赛聚类吸收,这是在不同粒度进行每日重新聚类的自然效果。
一个故事链是跨连续天链接的聚类序列。
个别的成对链接告诉您,周一的“英国政治”聚类连接到周二的。链条揭示了完整的弧线:一个故事从周一开始,经过一周的演变,到周五结束。
链条从 kNN 比例 ≥ 0.4 的链接中贪婪地构建,这意味着源聚类中至少 40% 的抽样文档落在单个目标聚类中。算法从最早的聚类开始,总是跟随最强的出链。
1
2
3
4
5
6
7
8
9
10
强链接(kNN 比例 >= 0.4):244
跨 3 天以上的故事链:18
链 1:'ukrainian | kyiv | eastern'(19 天:2 月 3 日 → 2 月 21 日)
链 2:'playing | opposition'(19 天:2 月 10 日 → 2 月 28 日)
链 3:'tadhg | maro | cadan'(10 天:2 月 1 日 → 2 月 10 日)
链 4:'invade | china | putin'(8 天:2 月 21 日 → 2 月 28 日)
链 5:'elected | labour | leader'(7 天:2 月 12 日 → 2 月 18 日)
链 6:'film | swift | awards'(6 天:2 月 2 日 → 2 月 7 日)
链 7:'amendment | termination | reporting'(6 天:2 月 12 日 → 2 月 17 日)
链 8:'officers | scene | police'(5 天:2 月 1 日 → 2 月 5 日)
最长的链追踪了乌克兰-俄罗斯报道长达 19 天,这在 2025 年 2 月的地缘政治紧张局势下并不意外。第二长的链跟踪了英超联赛足球覆盖了这个月的 19 天。较短的链捕捉了颁奖季(电影/奖项,六天)、六国橄榄球(10 天)和英国政治领导报道(七天)。每条链代表了算法仅通过跨每日索引的嵌入相似性发现的一个故事弧。
Sankey 图是流动可视化,其中链接宽度代表连接强度。在这里,每个垂直带代表一天,每个节点是一个每日聚类(按文档数大小),每条彩色路径追踪一个跨时间的故事链。链接宽度编码了 kNN 重叠强度:较厚的链接表示更多的抽样文档落在目标聚类中。颜色在每个链中保持一致,因此从左到右的单一颜色路径即为一个故事的进程。
例如,乌克兰-俄罗斯的链(可见为较长的路径之一)从二月初持续流向第三周,厚实的链接表明了跨天的强烈主题连续性。

时间故事链跨越 2025 年 2 月
时间故事链跨越 2025 年 2 月。每条彩色路径代表一个持续多天的故事;链接宽度表示 kNN 重叠强度。
本文演示了一个基于 Elasticsearch 的完整无监督文档聚类流程:
msearch kNN 探测密度,选择多样化的高密度种子,将所有文档分类到中心点。Elasticsearch 处理繁重的计算;只有种子选择在客户端运行(约 0.01s)。significant_text 标记:显著性测试无需任何 ML 模型或手动标注即可生成有意义的聚类标签。没有显著术语的聚类是不连贯的,会被降级为噪声——这是一种内置的质量门控。关键要点:
significant_text 快速、可解释且有效地用于自动标记和质量门控。何时使用此方法:
可探索的扩展:
替换为您自己的时间戳文档语料库;任何带日期的文本集合都可以与此流程一起使用。完整的笔记本和支持代码可在相关仓库中找到。
bbq_disk 支持的托管集群。报告问题
📡 更多 Elastic & AI 可观测性干货
关注「点火三周」,第一时间获取最新技术文章