文本相关性初筛实战:从关键词到TF-IDF再到语义向量
2026/9/21 19:33:08 网站建设 项目流程

看到一位开发者在 Hacker News 上分享了自己做“创业想法验证”时遇到的现象:他围绕自己的点子收集了 992 篇帖子,最后逐篇查看发现只有 7 篇真正和这个点子有关。这个比例相当低,但在信息收集类需求里很常见。

问题不是“能不能把帖子保存下来”,而是如何在海量文本里把真正有关的内容挑出来。如果只靠人工一篇一篇点开看,992 篇也许还能读完,等数据涨到几万条,就完全没有办法继续维护了。这篇文章从这种筛选需求出发,聊一聊如何用 Python 做“主题相关性初筛”:从关键词匹配到 TF-IDF,再到向量语义匹配。我会给出完整可复现代码和常见坑点,最后补充工程化落地建议。

这类“992 条里只有 7 条相关”的情况并不少见。网上抓来的帖子经常出现标题相似、正文偏离、关键词命中但毫不相关等状态。与其完全交给人工判断,不如先用技术过滤一遍,把高概率相关的内容挑出来,再让人做最终确认。

1. 背景与核心问题

1.1 Show HN 是什么

Show HN 是 Hacker News 上的一个社区标签。用户在 Hacker News 发帖时,如果标题以 Show HN 开头,通常意味着这篇帖子是在展示自己的作品,比如一个开源项目、在线工具、小游戏或创业原型。

对做创投观察、竞品调研和开发者运营的人来说,Show HN 是一个不错的信息来源。但这里有一个很现实的问题:每天有大量新帖子被发布到 Hacker News,靠 RSS 或 API 拉回来的帖子内容五花八门,单纯看标题根本判断不了它是不是和自己的领域相关。

比如目标是用“自动备份”服务,结果拉回来的 992 帖子里,真正围绕这个主题的只有 7 条。剩下的可能是“如何做自动化测试”“推荐几个备份云盘”“SaaS 工具定价经验”。这些内容不是完全没用,但它们并不指向真正的目标用户和核心需求。

1.2 问题的本质:主题相关性判断

把“992 条里找出 7 条”抽象一下,就是这样一个问题:

给一段描述“我的创业想法”的文本,再给一批候选帖子,判断每一条帖子是不是真正和这个想法相关。这本质上是文本主题相关性判断,而不是简单的关键词检索。

关键词检索的逻辑是“帖子里出现了 XX 词就返回”。它速度快,召回也高,但缺陷很明显:

  • 帖子说“我正在做一款数据备份工具”,关键词“备份”命中。
  • 帖子说“我把服务器配置备份到了阿里云盘”,关键词“备份”也命中。
  • 帖子说“不小心删库之后如何恢复”,没有出现“备份”这个词,但内容上和备份服务高度相关。

如果只靠关键词,第一条和第二条都会被选中,第三条反而会被漏掉。这正是为什么需要从“字面匹配”往“语义匹配”上走。

1.3 这类筛选技术的通用场景

这种主题相关性筛选不止用于分析 Show HN 帖子。常见的场景还包括:

  • 竞品监控:从大量新闻稿、社区讨论里找到和自家产品相关的反馈。
  • 需求挖掘:在开源社区收集用户抱怨,定位未被满足的需求。
  • 论文筛选:从搜索引擎结果中判断论文是否真正属于研究方向的范畴。
  • 舆情排查:在海量评论文本中筛选出特定事件的高相关描述。

虽然具体数据来源不同,核心流程却高度相似:采集文本、清洗、相关性打分、人工复核。

2. 技术方案对比与选型

2.1 几种常见实现思路

实现“帖子是否相关”的方案有很多,从简单到复杂可以大致分成四类。

方案原理优点缺点
关键词规则判断词是否出现在文本中实现简单,速度快无法处理同义改写,误报漏报都高
TF-IDF + 余弦相似度根据词频和逆文档频率构建向量,计算相似度可解释性强,能忽略常见词依赖词面重合,同义词仍命中不了
Word2Vec/FastText把词映射为向量,用词向量表示文本能捕捉一部分语义关系对长文本表示粗糙,需要额外训练
Sentence Embedding用预训练模型把整段文本编码为向量语义能力强,适合跨语言和长句依赖模型下载,计算资源要求较高

如果每一步都需要解释为什么,推荐先从小方案做起。绝大多数个人项目和中小团队不需要一上来就上大型模型。

2.2 推荐的分层漏斗流程

我的建议是把筛选做成一道漏斗,而不是用单一算法一把梭。

第一层是粗筛。用关键词和简单规则,快速把完全不相关的帖子排除掉。比如目标中没有“数据库”“恢复”等词,可以先删掉一半候选。

第二层是精排。对剩下的帖子做 TF-IDF 向量化,计算它们与“想法描述文本”的余弦相似度。这一步会把候选帖子按相关程度排序。

第三层是语义复核。对排序靠前的部分,用 sentence-transformer 之类的预训练模型重新打分,降低关键词不同但意思相近带来的漏报。

为什么不能一步到位用语义模型?因为向量模型需要把每条文本都编码一遍,1000 条无所谓,10 万条就有耗时和成本压力。先用关键词和 TF-IDF 缩小范围,再做语义计算,性价比更高。

2.3 环境准备

本文示例使用 Python 实现,基础环境如下:

  • Python 3.8+
  • jieba:用于中文分词
  • scikit-learn:用于 TF-IDF 和余弦相似度计算
  • pandas:用于结果展示
  • sentence-transformers:可选,用于语义向量计算

可以执行下面的安装命令:

pip install jieba scikit-learn pandas sentence-transformers

版本不需要完全一致,按你本地的实际情况调整。如果离线环境无法下载 sentence-transformers 模型,可以只保留关键词匹配和 TF-IDF 部分。

3. 完整实战:从 992 条候选中筛出真相关内容

为了说明完整过程,我会用模拟数据代替真实的 Hacker News 帖子。原因有两个,第一是避免把某个特定平台的大量真实数据直接贴进文章,第二是模拟数据可以让代码更短,方便你直接复制运行。

3.1 需求定义

假设我的“创业想法”描述如下:

我想做一个给独立开发者使用的自动化备份工具:定时把项目代码、数据库和重要配置自动备份到云盘,备份完成后生成变更记录,出问题时可以从最近一次备份快速恢复。

模拟数据里准备了 12 条帖子,其中有明确相关的、有擦边的、也有完全不相关的。真实场景中的 992 条数据只是规模更大,处理逻辑完全一致。

3.2 完整代码

下面是完整的 Python 脚本。这个脚本会依次做三件事:

  1. 用关键词对所有帖子进行初筛。
  2. 用 TF-IDF 和余弦相似度对帖子打分。
  3. 输出排序结果,并额外展示可选的语义模型打分步骤。
# -*- coding: utf-8 -*- """ 从大量帖子中筛选与“创业想法”相关的文本 运行环境:Python 3.8+ 依赖库:jieba, pandas, scikit-learn 可选依赖:sentence-transformers """ import re import jieba import pandas as pd from sklearn.feature_extraction.text import TfidfVectorizer from sklearn.metrics.pairwise import cosine_similarity # -------------------------------- # 1. 数据准备 # -------------------------------- # 创业想法描述 idea_text = """ 我想做一个给独立开发者使用的自动化备份工具: 定时把项目代码、数据库和重要配置自动备份到云盘, 备份完成后生成变更记录,出问题时可以从最近一次备份快速恢复。 """ # 模拟帖子数据:每个元素是 (id, 标题, 正文) posts = [ (1, "我写了一个自动备份脚本", "会把整个项目目录压缩后上传到对象存储,支持每天凌晨执行。"), (2, "独立开发者如何选择云服务器", "从预算、地区、线路稳定性几个方面分享了选型经验。"), (3, "用 Python 监听数据库变化", "实现了基于 binlog 的变更捕获,可以做增量同步。"), (4, "分享我的创业点子生成器", "输入关键词自动生成一些看起来很厉害的产品方向。"), (5, "MySQL 误删数据恢复经验", "没有开启 binlog,最后通过快照恢复到了前一天状态。"), (6, "内容农场如何批量生产文章", "主要讲 SEO 和 AI 生成内容的配合方法,不涉及工程工具。"), (7, "2025 年开发者工具趋势清单", "汇集了几个值得关注的方向,包含 CI/CD、云原生和可观测性。"), (8, "开源项目维护者的备份策略", "每天把仓库推到多个远端,用裸仓库避免本机故障。"), (9, "我想做一个定时备份工具", "目标用户是独立开发者,准备支持数据库和配置文件的自动备份。"), (10, "给独立开发者推荐笔记软件", "整理了几款支持 Markdown 和同步的笔记工具。"), (11, "聊聊 SaaS 定价策略", "从订阅制、一次性买断、按量计费三个角度对比。"), (12, "Docker 数据卷备份与迁移方案", "介绍了使用 tar 打包数据卷并在另一台机器恢复的流程。"), ] # -------------------------------- # 2. 文本清洗与分词 # -------------------------------- STOP_WORDS = set( [ "一个", "我们", "可以", "这个", "那个", "怎么", "如何", "什么", "自己", "进行", "使用", "某个", "很多", "这样", "已经", "觉得", "就是", "可能", "会", "做了", "一种", ] ) def clean_text(text): """清洗文本并做 jieba 分词,返回空格分隔的词序列""" if not isinstance(text, str): text = str(text) # 去掉 URL text = re.sub(r"https?://\S+", " ", text) # 只保留中文、英文字母、数字,其余换成空格 text = re.sub(r"[^0-9A-Za-z\u4e00-\u9fa5]", " ", text) # 分词 words = jieba.lcut(text) # 去掉空串、停用词和单字词 tokens = [] for word in words: word = word.strip() if not word: continue if word in STOP_WORDS: continue if len(word) < 2: continue tokens.append(word) return " ".join(tokens) print("清理后的想法描述:") print(clean_text(idea_text)) print("-" * 60) # -------------------------------- # 3. 关键词粗筛 # -------------------------------- CORE_KEYWORDS = ["备份", "恢复", "数据库", "云盘", "定时", "容灾", "快照"] def keyword_match(title, body): """返回命中的关键词列表,没命中返回空列表""" text = f"{title} {body}" hits = [word for word in CORE_KEYWORDS if word in text] return hits print("关键词初筛结果:") for post_id, title, body in posts: hits = keyword_match(title, body) if hits: print(f"post {post_id}: {title},命中 {hits}") print("-" * 60) # -------------------------------- # 4. TF-IDF 相似度排序 # -------------------------------- # 清洗全部帖子标题和正文 corpus = [] for _, title, body in posts: content = f"{title}。{body}" corpus.append(clean_text(content)) # 把想法描述作为第一份文档 idea_clean = clean_text(idea_text) all_docs = [idea_clean] + corpus # 训练 TF-IDF 向量 vectorizer = TfidfVectorizer() tfidf_matrix = vectorizer.fit_transform(all_docs) # 计算第 0 篇文档与其余文档的余弦相似度 similarities = cosine_similarity(tfidf_matrix[0:1], tfidf_matrix[1:]).flatten() print("TF-IDF 余弦相似度排序结果:") # 按相似度从高到低排序 sorted_index = similarities.argsort()[::-1] for rank, idx in enumerate(sorted_index, start=1): post_id, title, body = posts[idx] hits = keyword_match(title, body) print( f"第 {rank:>2} 名 | post {post_id} | 相似度 {similarities[idx]:.4f} | " f"关键词 {hits} | {title}" ) print("-" * 60) # -------------------------------- # 5. 输出候选表 # -------------------------------- result_rows = [] for idx in sorted_index: post_id, title, body = posts[idx] result_rows.append( { "帖子ID": post_id, "标题": title, "正文摘要": body[:40], "TFIDF相似度": round(float(similarities[idx]), 4), "是否命中关键词": bool(keyword_match(title, body)), } ) df = pd.DataFrame(result_rows) print("候选帖子汇总表:") print(df.to_string(index=False))

这段代码的运行逻辑很清楚:先把想法描述和所有帖子转换为 TF-IDF 向量,再计算想法描述和每一条帖子之间的余弦相似度,最后输出带排序的表格。

3.3 可选:语义向量评分

TF-IDF 有一个短板:如果帖子里通篇没有“备份”“云盘”这类词,但在语义上是相关的,相似度分数就会偏低。这时可以引入预训练向量模型。

下面是可选参考代码:

# 可选步骤:如果安装并下载了 sentence-transformers,可以执行这段 from sentence_transformers import SentenceTransformer # 加载多语言模型,支持中文文本 model = SentenceTransformer("paraphrase-multilingual-MiniLM-L12-v2") # 把所有文档转成向量 all_texts = [idea_text] + [f"{title}。{body}" for title, body in posts] embeddings = model.encode(all_texts, normalize_embeddings=True) # 计算向量余弦相似度 sem_scores = cosine_similarity([embeddings[0]], embeddings[1:]).flatten() print("语义相似度排序结果:") for rank, idx in enumerate(sem_scores.argsort()[::-1], start=1): post_id, title, body = posts[idx] print( f"第 {rank:>2} 名 | post {post_id} | 语义相似度 {sem_scores[idx]:.4f} | {title}" )

需要注意,模型文件名需要以实际下载情况为准。如果网络受限,可以放在本地离线环境,也可以换成更适合中文场景的向量模型。使用不同模型时,唯一要调整的是模型名称,后续计算方法完全相同。

4. 运行结果与对比分析

4.1 预期输出形态

TF-IDF 部分运行后,你会看到类似下面的表格:

帖子ID标题TFIDF相似度是否命中关键词
9我想做一个定时备份工具0.3451True
1我写了一个自动备份脚本0.2987True
12Docker 数据卷备份与迁移方案0.2214True
5MySQL 误删数据恢复经验0.1980True
8开源项目维护者的备份策略0.1756True
3用 Python 监听数据库变化0.1022True
72025 年开发者工具趋势清单0.0561False
2独立开发者如何选择云服务器0.0488False
11聊聊 SaaS 定价策略0.0310False
4分享我的创业点子生成器0.0214False
10给独立开发者推荐笔记软件0.0172False
6内容农场如何批量生产文章0.0099False

以上数值是模拟展示。真实数据中,同样的逻辑会输出一版分数不同的结果,但大体趋势是相关的帖子排在前面。

4.2 为什么不同方法会给出不同结果

关键词初筛阶段,帖子 3 命中了“数据库”,它会被保留。但从帖子标题“用 Python 监听数据库变化”来看,它更像是数据库同步工具,而不是备份工具。TF-IDF 会给一个中等的分数,因为它和“数据库”相关,但不一定和“自动备份到云盘”相关。

帖子 5 在关键词阶段也会被选中,因为它有“恢复”“数据库”。从内容上讲,“MySQL 误删恢复经验”确实是备份价值的一种体现,所以被排到中间位置是合理的。

帖子 4 虽然含有“创业点子”,但正文讨论的是内容农场和 SEO,和独立开发者备份工具没有关系。由于它没有命中关键词,TF-IDF 分数也比较低,可以安全忽略。

这个例子说明,在真实筛选中,不要期待某一个指标能完美区分所有内容。关键词负责召回,向量分数负责排序,最后人工复核责任边界清晰。

4.3 如果只有 7 条真实相关,如何定阈值

992 条选 7 条意味着绝大多数内容都不相关。这时候如果用单一阈值,很容易出现两种情况:

  • 阈值太高,相关帖子被丢掉。
  • 阈值太低,几十条无关内容混进来。

更稳妥的做法是看 Top K。先把相似度靠前的帖子输出到表格或 CSV,比如输出前 50 条,再人工抽看。真实相关的通常集中在前 20 名。如果只想保留几条,可以再观察前 10 名的分数差距,用“断崖式下跌”的地方作为自然分界。

5. 常见问题与排查思路

5.1 启动时提示 jieba 找不到词,分词结果很碎

现象是“自动化备份”被切成了“自动”“备份”或更碎。

可能原因是默认词库没有覆盖某些领域词汇。

解决办法是为领域词补充词典:

jieba.add_word("自动化备份") jieba.add_word("独立开发者") jieba.add_word("变更记录")

也可以把行业术语写进一个 userdict.txt,用 jieba.load_userdict 加载。

5.2 TF-IDF 算出的相似度普遍很低

TF-IDF 严重依赖两个文本之间的字面公共词。如果想法描述只有一句话,而帖子正文很长,公共词占比本身就不高,结果分数自然偏低。

可以在文本清洗阶段把帖子标题和正文拼接起来,并在向量的相似度计算前先去除停用词。如果分数还是低,说明帖子确实缺少核心关键词,这时需要靠语义模型补充召回。

问题现象常见原因解决思路
相似度普遍很低文本清洗不足,长文本稀释了公共词权重拼接标题与正文,去掉停用词和单字词
无关帖子命中关键词关键词太泛增加组合规则,比如必须同时出现两到三个关键词
相关帖子未命中任何关键词目标描述与帖子表述不一致增加语义模型做二次复核
运行时间太长文本量太大或模型太慢先关键词粗筛,再对少量文本做语义打分

5.3 关键词命中的帖子顺序很怪

顺序怪通常是因为 TF-IDF 对“数据库”这类常见词并不敏感。它在所有文档里出现频率高,IDF 权重就低。如果需求里“数据库”很重要,应该把核心词加入自定义词典,或把它从停用词风格处理中排除。

更好的做法是给规则增加优先级。比如:

  • 命中“备份”且“云盘”,优先级 A。
  • 仅命中“数据库”,优先级 B。

规则越明确,粗筛越有效。

5.4 模型下载失败或无法连接外网

sentence-transformers 在第一次加载模型时,会去模型仓库下载权重。如果服务器无法连接外部网络,这一步会卡住。

解决办法是先在本地开发环境把模型下载好,再把模型目录拷贝到服务器,通过本地路径加载:

model = SentenceTransformer("/data/models/paraphrase-multilingual-MiniLM-L12-v2")

如果没有办法部署向量模型,只做 TF-IDF 或 BM25 也能解决大部分排序问题,只是对同义改写文本的召回会弱一些。

6. 工程化落地与最佳实践

6.1 筛选流程不要做成单层黑盒

很多人容易犯一个错误:找到一种不错的相似度算法,就指望它一次性解决所有筛选问题,最后在真实数据上“翻车”。

实际上,过滤环节的前后拆分很重要:

  1. 采集时保留元数据:URL、发布时间、作者、点赞数等。之后人工复核时可以快速点进原帖。
  2. 用关键词排除噪声:删除明显不相关的技术标签、营销内容。
  3. 用排序模型打分:把候选集从几千条压缩到几十条。
  4. 人工确认或大模型复核:最后一步做最终判断。

6.2 把想法描述写具体

很多筛选结果差,不是因为算法不好,而是因为输入描述太短。比如“我想做备份工具”和“给独立开发者提供数据库自动备份与快速恢复的云服务”,表达的侧重点完全不同。

把想法描述扩展成一个 3 到 5 句的文档,效果会好很多。这样 TF-IDF 和向量模型都能提取出更多有价值的特征词。

6.3 用多个检索结果交叉观察

可以把关键词命中、TF-IDF 得分、语义模型得分放到同一张结果表里。最终判断时参考以下逻辑:

  • 三项都命中的,优先看。
  • 关键词没命中但 TF-IDF 和语义得分都高的,值得人工确认。
  • 关键词和 TF-IDF 都没有命中的,大概率不相关。

这种方式可以帮助发现“想法描述里的词没有出现,但帖子讲的正是这件事”的情况。

6.4 数据采集的注意事项

采集社区帖子时,需要先确认目标平台的访问规则和 API 使用条款。合法合规地进行抓取,控制请求频率,不绕过登录和防爬机制,不把数据用于侵权场景。最小化采集范围,只取和分析相关的标题、正文摘要、发布时间、链接,不要过度采集用户隐私数据。

6.5 数据量变大以后的优化方向

当候选帖子从 1000 条量级增长到 100 万条量级,sklearn 的 TfidfVectorizer 会变得吃力。可以考虑:

  • 先按关键词和章节做分区,减少全量计算的次数。
  • 使用 Elasticsearch 内置的 BM25 检索做第一轮召回。
  • 对向量做索引,比如 faiss,用近似最近邻加速语义检索。
  • 把离线批量任务和在线查询任务分开,避免影响主业务。

6.6 用标注集校验效果

在实际项目中,建议准备一份 100 条到 300 条的小型标注集,人工标记“相关”和“不相关”。每次调整关键词或阈值后,在这个标注集上重跑一遍,看准确率和召回率变化。

没有标注集,优化的方向感很容易丢失。

7. 回到帖子筛选本身

回到开头那个“992 条只有 7 条相关”的案例。它说明了一个普遍现象:在开放社区里,“关键词相同”不等于“主题相关”。先靠关键词过滤大范围排除,再用 TF-IDF 排序让高价值帖子靠前,最后结合语义模型或人工复核做确认,这是成本最低也最容易持续维护的路径。

如果以后你也要处理类似需求,可以先从一份采集样本出发,用 100 条数据跑通整套流程,再逐步扩大到完整数据集。你会发现,筛选准确率提升的关键往往不在更高级的模型,而在文本清洗、关键词配置和最后的人工复核环节。

想继续深入的话,可以研究一下 Elasticsearch 内置检索、BM25 排序公式,以及向量召回模型在长文档上的效果差异。每次做一步小改进,用标注集验证一步,最终形成的流程会比直接套一个大模型更稳定。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询