#RAG#Next.js

实战:给内容站加语义搜索

关键词搜索答不了"模糊提问"和"近义词",语义搜索补上这块。本文用 Next.js + pgvector,给一个内容站加完整的语义搜索:建索引、查询、混合检索、前端高亮,以及增量更新——一套能直接用进生产的方案。

本文要点

  • 主题聚焦:RAG、Next.js
  • 适合人群:独立开发者、全栈工程师、AI 应用开发者
  • 核心价值:快速理解背景、实践路径、常见误区和工程建议
作者 @admin·发布于 2026年1月12日·更新于 2026年7月23日

你的博客/文档站/社区,搜索框多半还是关键词匹配。它有个老问题:用户搜"如何部署 Next.js",但标题写的是"上线应用",关键词对不上就搜不到。语义搜索解决这个——它理解"部署 ≈ 上线"、"Next.js 应用 ≈ Next.js"。

这篇用 Next.js + pgvector 给一个内容站加完整语义搜索,一套能上生产的方案。

一、为什么选 pgvector

如果你的站已经在用 PostgreSQL,pgvector 是首选:不用再起一个向量服务,一个库同时存业务数据和向量,能 JOIN、能带条件过滤("只搜已发布的、某分类的"),中小规模(百万级以内)完全够用。

sql
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 列。

ts
// 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、错误码),所以混合:

ts
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 准但慢,只跑在召回的少量候选上。

ts
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 接后端:

tsx
// 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 + 增量更新。

关键几条:

  1. 已有 PG → pgvector,别平白起一个向量服务。
  2. 混合检索是标配(语义+关键词),纯向量解决不了精确词。
  3. 向量化标题+摘要,比纯正文质量高。
  4. 增量同步 embedding,否则新内容搜不到。

这套搭下来,你的搜索框从"必须打对关键词"升级成"懂人话",体验提升立竿见影——而且全程不用离开你熟悉的 PostgreSQL。

Related

相关推荐