一提到站内搜索,很多人的第一反应是上 Elasticsearch。但对文章几十到几万篇、文档类内容为主的中小站点来说,ES 的部署、运维、中文分词词典维护成本并不低,而数据库配合一张倒排表,完全能做到毫秒级、支持中文、结果还排得有章法。本文给出一套不依赖重型搜索引擎的轻量站内搜索实现思路。
一、为什么 LIKE '%关键词%' 不够用
最朴素的站内搜索是 WHERE title LIKE '%关键词%',它有三个硬伤:
- 走不了索引,前导模糊匹配必然全表扫描,数据量一上来就慢;
- 只能匹配连续子串,搜"权限设计"匹配不到标题为"设计一套权限体系"的文章;
- 无法按相关度排序,命中的结果只能按时间倒序,用户要的那篇往往沉底。
解决思路是引入信息检索里最经典的倒排索引:不直接搜正文,而是预先把每篇文档切成词,建立"词 → 文档列表"的映射,搜索时先查词、再反查文档。
二、三张表搭出倒排索引
-- 词表:每个词一行
CREATE TABLE search_term (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
term VARCHAR(32) NOT NULL,
doc_freq INT NOT NULL DEFAULT 0, -- 出现在多少篇文档(用于IDF)
UNIQUE KEY uk_term (term)
);
-- 倒排表:词与文档的对应关系及词频
CREATE TABLE search_posting (
term_id BIGINT NOT NULL,
doc_id BIGINT NOT NULL,
tf INT NOT NULL DEFAULT 1, -- 该词在该文档出现次数
in_title TINYINT NOT NULL DEFAULT 0, -- 是否出现在标题(加权)
PRIMARY KEY (term_id, doc_id),
KEY idx_doc (doc_id)
);
文档发布或更新时,离线把标题和正文分词,统计每个词的词频,重建该文档对应的倒排记录。搜索时先把用户查询也切成词,到倒排表里聚合:
SELECT p.doc_id,
SUM(p.tf * (1 + p.in_title * 2)
* LOG((SELECT COUNT(*) FROM docs) / NULLIF(t.doc_freq,0))) AS score
FROM search_posting p
JOIN search_term t ON t.id = p.term_id
WHERE t.term IN (?, ?, ?) -- 查询分词结果
GROUP BY p.doc_id
ORDER BY score DESC
LIMIT 20;
这就是一个简化版 TF-IDF 排序:词在某文档出现越多越相关(TF),但这个词越稀有、区分度越高(IDF),出现在标题里额外加权。
三、中文分词:不必追求完美,二元分词就够用
英文按空格切词即可,中文的难点在词之间没有分隔符。轻量方案不必训练复杂模型,实践中两种做法组合最划算:
- 词典最大匹配:维护一份几千词的领域词典(产品名、行业术语、常见词),用前向/双向最大匹配优先切出领域词,保证专有名词不被切碎;
- 二元分词(bigram)兜底:词典没覆盖的句子,按相邻两字切,如"站内搜索实现"切成"站内/内搜/搜索/索实/实现"。二元分词会产生冗余词项,但召回稳、零依赖,对中小站点完全可接受,查询时同样二元化即可对上。
def bigram(text):
text = re.sub(r'[^\u4e00-\u9fa5a-zA-Z0-9]', '', text)
return [text[i:i+2] for i in range(len(text)-1)] if len(text) >= 2 else [text]
停用词(的、了、是、我们、如何)在建索引和查询时都过滤掉,能显著减少噪声词项。
四、让结果排得更合理的几个加权
纯 TF-IDF 只反映词面相关度,实际站内搜索还要叠加业务权重,用户体验会好很多:
- 标题命中权重高于正文命中;
- 较新的内容给轻微时间衰减加权,避免多年前的旧文长期霸榜;
- 高质量、高收藏的文档给静态质量分;
- 命中全部查询词的文档,排在只命中部分词的前面。
可以把这些做成一个可叠加的 score,而不是写死在一条 SQL 里,方便后续调权重。对无结果的查询,记录查询词日志,它既是后续补充内容的选题来源,也能反向扩充领域词典。
五、踩坑清单
- 用前导 LIKE 全表扫描,数据量上来搜索变慢;
- 只匹配连续子串,词序一变就搜不到;
- 只按时间排序,最相关的结果沉底;
- 中文不做分词直接整句查,几乎零召回;
- 倒排索引只在建文档时生成,文档更新/删除后不重建,结果出现死链;
- 不过滤停用词,词表被"的、了"这类高频词撑大且无区分度。
六、工程落地建议
建议先用"词典最大匹配 + 二元分词兜底 + 简化 TF-IDF/标题加权"这套方案把搜索跑起来,把无结果查询和点击行为记成日志,等文档量、查询复杂度真的超过数据库承载(如需要近义词语义检索、千万级文档),再平滑迁移到专业搜索引擎,倒排表的设计思想是通用的。内容型站点在搭建阶段也可以直接借助成型建站能力的内置检索(如基于成型平台的站内搜索,如乔拓云企业网站),自研时优先保证召回与排序的可调性。
七、上线自检清单
- 搜索是否走索引、P95 响应是否在百毫秒内;
- 调换词序、拆成近义词是否仍能召回;
- 标题命中的结果是否排在正文命中之前;
- 文档更新、下架后搜索结果是否同步;
- 无结果查询是否被记录用于优化。
站内搜索不必一上来就堆重型组件,理解倒排、分词、相关度排序这三件事的原理,用数据库也能做出体验合格的搜索;等规模和需求到了再升级,技术决策才不会过度。以上为个人实践分享,具体实现请结合数据规模评估。