实战:给内容站加语义搜索
关键词搜索答不了"模糊提问"和"近义词",语义搜索补上这块。本文用 Next.js + pgvector,给一个内容站加完整的语义搜索:建索引、查询、混合检索、前端高亮,以及增量更新——一套能直接用进生产的方案。
本文要点
- 主题聚焦:RAG、Next.js
- 适合人群:独立开发者、全栈工程师、AI 应用开发者
- 核心价值:快速理解背景、实践路径、常见误区和工程建议
你的博客/文档站/社区,搜索框多半还是关键词匹配。它有个老问题:用户搜"如何部署 Next.js",但标题写的是"上线应用",关键词对不上就搜不到。语义搜索解决这个——它理解"部署 ≈ 上线"、"Next.js 应用 ≈ Next.js"。
这篇用 Next.js + pgvector 给一个内容站加完整语义搜索,一套能上生产的方案。
一、为什么选 pgvector
如果你的站已经在用 PostgreSQL,pgvector 是首选:不用再起一个向量服务,一个库同时存业务数据和向量,能 JOIN、能带条件过滤("只搜已发布的、某分类的"),中小规模(百万级以内)完全够用。
CREATE EXTENSION IF NOT EXISTS vector;
ALTER TABLE articles ADD COLUMN embedding vector(1536);
CREATE INDEX ON articles USING hnsw (embedding vector_cosine_ops);
1536 维是 OpenAI text-embedding-3-small 的维度;换模型换维度即可。HNSW 索引让向量检索快(百万级毫秒)。
二、建索引:把内容向量化
入库时(文章创建/更新),把正文向量化存入 embedding 列。
// app/admin/actions.ts 或后台脚本
import OpenAI from "openai";
const openai = new OpenAI();
async function embed(text: string): Promise<number[]> {
const res = await openai.embeddings.create({
model: "text-embedding-3-small",
input: text.slice(0, 8000), // 截断防超限
});
return res.data[0].embedding;
}
// 文章保存时
const embedding = await embed(`${article.title}\n${article.summary}\n${article.bodyMd}`);
await db.article.update({ where: { id }, data: { embedding } });
几个要点:
- 向量化"什么文本"很关键。标题 + 摘要 + 正文(或正文摘要)比纯正文好——标题权重高、噪声少。
- 截断。长文截断到模型上限(OpenAI 8192 token),超出部分要么分段要么先摘要。
- 成本。embedding 便宜(比生成便宜两个数量级),全量索引一次开销可控。
三、查询:语义 + 关键词混合
用户查询时,把 query 向量化,在库里做余弦相似度 top-k。但纯向量弱关键词(搜精确 slug、错误码),所以混合:
async function semanticSearch(query: string, limit = 10) {
const [qEmbedding] = await Promise.all([
embed(query),
// 用 prisma 执行原生 SQL 做混合检索
]);
// pgvector 语义检索 + PG 全文检索,RRF 融合
const results = await db.$queryRaw`
WITH semantic AS (
SELECT id, row_number() OVER (ORDER BY embedding <=> ${qEmbedding}::vector) AS rank
FROM articles WHERE status = 'PUBLISHED'
),
keyword AS (
SELECT id, row_number() OVER (
ORDER BY ts_rank(search_vector, websearch_to_tsquery('simple', ${query})) DESC
) AS rank
FROM articles WHERE status = 'PUBLISHED'
AND search_vector @@ websearch_to_tsquery('simple', ${query})
)
SELECT a.id, a.title, a.summary, a.slug,
COALESCE(1.0/(s.rank+60), 0) + COALESCE(1.0/(k.rank+60), 0) AS score
FROM articles a
LEFT JOIN semantic s ON s.id = a.id
LEFT JOIN keyword k ON k.id = a.id
WHERE s.id IS NOT NULL OR k.id IS NOT NULL
ORDER BY score DESC
LIMIT ${limit};
`;
return results;
}
<=> 是 pgvector 的余弦距离算子。RRF(1/(rank+60))融合语义和关键词两个排名。这一套是生产级语义搜索的标准配方:准、可过滤、一个库搞定。
四、Rerank(可选,质量上一档)
混合检索召回的 top-10,如果想要更准,加一道 rerank(cross-encoder 重排),把最相关的顶到前几。rerank 准但慢,只跑在召回的少量候选上。
import { CohereClient } from "cohere-ai";
const cohere = new CohereClient({ token: process.env.COHERE_KEY });
const reranked = await cohere.rerank({
model: "rerank-multilingual-v3.0",
query, documents: results.map(r => `${r.title} ${r.summary}`),
topN: 5,
});
要不要加 rerank,看你对精度的要求——社区/博客搜索,混合检索通常够;企业知识库,建议加。
五、前端:流式高亮
前端是个搜索框 + 结果列表,用 Server Action 或 route handler 接后端:
// app/search/page.tsx
"use client";
import { useState } from "react";
export default function SearchPage() {
const [q, setQ] = useState("");
const [results, setResults] = useState<Hit[]>([]);
async function onSearch(e: React.FormEvent) {
e.preventDefault();
const res = await fetch(`/api/search?q=${encodeURIComponent(q)}`).then(r => r.json());
setResults(res.hits);
}
return (
<form onSubmit={onSearch}>
<input value={q} onChange={e => setQ(e.target.value)} placeholder="语义搜索…" />
{results.map(h => (
<a key={h.id} href={`/articles/${h.slug}`}>
<h3>{h.title}</h3>
<p>{h.summary}</p>
</a>
))}
</form>
);
}
体验点:
- 防抖(debounce 300ms)再请求,别每键一次。
- 高亮命中关键词(关键词分支的结果可高亮;语义分支没精确词,不高亮也行)。
- 空状态/无结果给引导("试试更宽泛的词")。
六、增量更新
新文章发布时,同步生成 embedding,否则搜不到。在文章保存的 hook/action 里调 embed + update。对历史内容,写一个一次性脚本全量回填。
注意:embedding 模型别中途换。换了维度变了,所有向量要重算。锁定一个 embedding 模型版本长期用。
七、成本与性能
- embedding 成本低:
text-embedding-3-small约 $0.02/百万 token,1000 篇文章全量索引几毛钱。 - 查询延迟:HNSW 索引下,百万级向量余弦检索是毫秒级;加 rerank 多 100–300ms。
- 索引构建:HNSW 索引构建对大表较慢,可在低峰期建。
小结
给内容站加语义搜索,一套生产方案 = pgvector 存向量 + HNSW 索引 + 混合检索(向量+关键词 RRF)+ 可选 rerank + 增量更新。
关键几条:
- 已有 PG → pgvector,别平白起一个向量服务。
- 混合检索是标配(语义+关键词),纯向量解决不了精确词。
- 向量化标题+摘要,比纯正文质量高。
- 增量同步 embedding,否则新内容搜不到。
这套搭下来,你的搜索框从"必须打对关键词"升级成"懂人话",体验提升立竿见影——而且全程不用离开你熟悉的 PostgreSQL。
Related