1.数据库建立索引
第一步:做"精华笔记" (ALTER TABLE)
SQL
ALTER TABLE papers
ADD COLUMN search_vector tsvector
GENERATED ALWAYS AS ( ... ) STORED;
这一步是在干什么?
普通的 title 和 abstract 是给人看的"原文",里面有很多没用的词(比如 "the", "is", "a", "of")。
这一步是让数据库自动生成一个新列(叫 search_vector),专门存"给机器看的精华笔记"。
-
自动分词 (to_tsvector):
它会自动把 "The batteries are charging" 变成 battery, charge 这种词根。它不仅分词,还去掉了废话。
-
划分等级 (
setweight):-
setweight(..., 'A'):把标题 里的词标记为 A级(最高级)。 -
setweight(..., 'B'):把摘要 里的词标记为 B级(次级)。 -
为什么要这么做? 以后算分的时候,如果关键词出现在标题里,分数会加倍;出现在摘要里,分数正常。这就实现了"标题党"优先。
-
-
自动化 (GENERATED ALWAYS ... STORED):
意思是:你以后不需要管这个列。
当你修改了某篇论文的标题,数据库会自动重新计算这个"精华笔记"并存好。你只管存数据,它负责整理笔记。
第二步:编"字典目录" (CREATE INDEX ... GIN)
SQL
CREATE INDEX idx_papers_search ON papers USING GIN(search_vector);
这一步是速度提升几千倍的核心。
没有索引时(你之前的 Python 代码):
你要找 "Lithium"。数据库必须拿起第1本书,翻一遍看有没有 "Lithium";放下,拿起第2本书... 直到看完 20万本。这叫"全表扫描"。
有了 GIN 索引后:
PostgreSQL 创建了一个类似书后索引(Inverted Index) 的结构。它大概长这样:
| 关键词 (Key) | 出现在哪些书里 (Value / IDs) |
|---|---|
| Battery | ID: 1, 5, 8, 102, ... |
| Lithium | ID: 3, 5, 99, 2000, ... |
| NCM | ID: 5, 8, 12, ... |
当你搜 "Lithium" 时:
数据库不用去翻那 20 万本书。它直接看这个目录,找到 Lithium 这一行,瞬间就知道:"哦,第 3, 5, 99... 页有这个词。"
然后它直接把这几页抓出来给你。这就是为什么它能从 几秒钟 变成 几毫秒。
总结
-
第一段代码:把人类读的文章,翻译成机器读的、带权重的"关键词列表",并自动存起来。
-
第二段代码:给这些关键词列表编一个"超级目录",查词的时候直接查目录,不用翻书。
2.匹配算法
没问题,这条 SQL 语句虽然短,但是信息量极大。它是 PostgreSQL 全文检索(Full Text Search)的核心。
我们把它拆解成 3 个核心步骤 来理解:"把词变成概念" -> "匹配" -> "打分"。
这是你代码里的那段 SQL:
SQL
SELECT
title, abstract, ...
ts_rank_cd(search_vector, query) AS score -- 3. 打分
FROM
papers,
to_tsquery('english', %s) query -- 1. 把你的关键词变成“查询对象”
WHERE
search_vector @@ query -- 2. 匹配:看文章里有没有这些词
ORDER BY
score DESC -- 4. 排序:分高的在前
LIMIT %s;
第一步:准备子弹 (to_tsquery)
SQL
to_tsquery('english', 'energy | density | 300')
这里的 %s 被我们 Python 代码里的字符串替换了(比如 'energy | density | 300')。
-
作用 :它不仅仅是把字符串传进去,它会做自然语言处理。
-
例子:如果你搜 "running",它会自动转换成词根 "run"。如果你搜 "batteries",它会变成 "batteri"。
-
核心符号
|:我们在 Python 里加的竖线|代表 OR(或) 。意思是:"只要包含 energy,或者 包含 density,或者包含 300,都算匹配成功"。
第二步:射击命中 (WHERE search_vector @@ query)
这是整句 SQL 最关键的地方。
-
search_vector:这是我们在数据库里预先算好存起来的那一列(就是上一条 SQL 建的那个)。它不是原本的句子,而是一个"词袋(Bag of Words)"。-
原文:"The high energy density."
-
Search Vector :
'densit':4 'energi':3 'high':2(它记住了词根,和词出现的位置)。
-
-
@@:这是 Postgres 专用的全文检索匹配符。-
它不像 SQL 里的
LIKE '%word%'那样傻傻地逐个字母比对。 -
它利用 GIN 索引(倒排索引),直接去查字典:"'energy' 这个词在哪几篇文章里出现过?" ------ 瞬间定位到这 20 万篇文章里的几百篇。
-
人话翻译:从 20 万篇文章里,瞬间通过目录找到所有包含 "energy"、"density" 或 "300" 的文章 ID。
第三步:计算得分 (ts_rank_cd)
SQL
ts_rank_cd(search_vector, query) AS score
现在我们找到了一堆文章,但谁排第一呢?这就需要打分。
-
ts_rank_cd:这是 Postgres 内置的高级打分算法(Cover Density Ranking)。 -
它怎么算分?
-
命中次数:关键词出现的次数越多,分越高。
-
距离(关键) :如果你的关键词是 "energy density",文章里这两个词是挨在一起的("high energy density"),分就很高;如果一个在开头,一个在结尾,分就低。
-
权重 :还记得我们在建表时设置的
setweight(title, 'A')吗?如果关键词出现在标题里,这里的算法会给它更高的分数加成。
-
总结
这段 SQL 的执行流程是这样的:
-
to_tsquery:把你输入的乱七八糟的词("energy", "density")标准化成数据库能懂的查询指令。 -
WHERE ... @@:利用 GIN 索引 ,以毫秒级速度直接过滤掉 19.9 万篇无关文档,只把包含关键词的文档挑出来。 -
ts_rank_cd:对挑出来的这些文档进行"考试评分",看谁跟你的关键词最亲密。 -
ORDER BY:按分数从高到低排队,把前 10 名给你。
这就是为什么它比 Python 里的 BM25 快几千倍的原因:它是在查字典(索引),而不是在读文章(遍历)。