将文档嵌入为向量,存储到 Pinecone,按语义相似度查询。这是 RAG、推荐系统、语义搜索以及"查找相似文档"功能的基础。
text-embedding-3-small 输出 1536 维)。语义相近 → 向量距离近。控制台 → Create Index,需要做以下选择:
text-embedding-3-small = 1536,text-embedding-3-large = 3072,Cohere embed-v3 = 1024。创建后无法更改 — 选错了只能重建索引。cosine(默认,归一化相似度)、euclidean 或 dotproduct。除非嵌入模型文档另有说明,否则使用 cosine。复制索引名称和 host URL。在 API Keys 中复制密钥。
PINECONE_API_KEY=…
PINECONE_INDEX=my-docs
OPENAI_API_KEY=… # for embeddings
你需要一个能生成向量的服务。OpenAI 的嵌入 API 是默认选择;Cohere、Voyage 以及通过 sentence-transformers 使用开源模型也是可行的替代方案。
npm install openai @pinecone-database/pinecone
import OpenAI from "openai";
const openai = new OpenAI();
async function embed(text) {
const r = await openai.embeddings.create({
model: "text-embedding-3-small",
input: text,
});
return r.data[0].embedding; // float[1536]
}
尽量批量处理 — 一次调用嵌入 100 条字符串比分 100 次调用便宜约 30 倍:
const r = await openai.embeddings.create({
model: "text-embedding-3-small",
input: chunks, // array of strings, max 2048 inputs per call
});
const vectors = r.data.map(d => d.embedding);
不要把一份 50 页的 PDF 作为单个向量嵌入 — 这样语义信息会被平均成一锅粥。应将其拆分为约 500 token 的片段,并保留约 50 token 的重叠(避免上下文在句子中间被截断)。
function chunkText(text, size = 1500, overlap = 150) {
const chunks = [];
let i = 0;
while (i < text.length) {
chunks.push(text.slice(i, i + size));
i += size - overlap;
}
return chunks;
}
在生产环境中若需要尊重段落和句子边界的精细分块,可使用 LangChain 的 RecursiveCharacterTextSplitter 或 LlamaIndex 节点解析器。
import { Pinecone } from "@pinecone-database/pinecone";
const pc = new Pinecone({ apiKey: process.env.PINECONE_API_KEY });
const index = pc.index(process.env.PINECONE_INDEX);
async function ingestDocument(docId, text) {
const chunks = chunkText(text);
const vectors = await embedBatch(chunks);
await index.upsert(
chunks.map((chunk, i) => ({
id: `${docId}#${i}`, // deterministic ID — re-ingesting overwrites
values: vectors[i],
metadata: {
docId,
text: chunk, // store the chunk for retrieval
chunkIndex: i,
createdAt: Date.now(),
},
})),
);
}
元数据使用建议:
userId、workspaceId、documentType、tags: ["draft", "public"]。Pinecone 在服务端对元数据做过滤,开销很小。async function search(query, userId) {
const queryVec = await embed(query);
const result = await index.query({
vector: queryVec,
topK: 5,
includeMetadata: true,
filter: { userId: { $eq: userId } }, // multi-tenant guard
});
return result.matches.map(m => ({
text: m.metadata.text,
docId: m.metadata.docId,
score: m.score, // 0..1 cosine similarity
}));
}
过滤运算符:$eq、$ne、$gt、$lt、$in、$nin、$and、$or。完整参考:Pinecone 元数据过滤。
检索增强生成(RAG):搜索 → 将结果塞入提示词 → 询问大模型。
async function answer(question, userId) {
const docs = await search(question, userId);
const context = docs.map(d => `[${d.docId}]\n${d.text}`).join("\n\n---\n\n");
const r = await openai.chat.completions.create({
model: "gpt-4o-mini",
messages: [
{
role: "system",
content: "Answer using only the context below. Cite [docId] inline. If the context doesn't contain the answer, say so.",
},
{ role: "user", content: `Context:\n${context}\n\nQuestion: ${question}` },
],
});
return r.choices[0].message.content;
}
若需要更复杂的 RAG 流程(查询改写、多跳推理、重排序),可使用框架:LangChain 或 LlamaIndex。对于上面这种简单的"搜索+填充"模式,引入框架反而是额外负担。
纯向量搜索会漏掉精确匹配的查询。例如"ZeroMQ"嵌入后与"RabbitMQ"向量接近,但用户查找 ZeroMQ 文档时需要的就是 ZeroMQ。混合搜索结合 BM25(关键词)和余弦相似度(语义)。
Pinecone 通过 稀疏-稠密向量 在 serverless 索引上支持混合搜索。可使用 Pinecone Inference 或 rank-bm25 生成稀疏向量。
如果需求比较简单,也可以并行跑两种搜索,再合并结果 — 互惠排名融合(reciprocal rank fusion)不超过 20 行代码,效果不错。
topK: 100 几乎没用,5–10 是 RAG 的最佳区间。如果对质量有更高要求,可以用较大的 topK 配合 Cohere Rerank 重排序。${docId}#${i})配合 upsert 覆盖写入,同时删除超出新分块数量的旧块。index.query。text-embedding-3-small 为 $0.02/百万 token;嵌入 100 万篇文档(平均 500 token)= $10。重新嵌入所有数据才是贵的部分。定价详情:pinecone.io/pricing。
cosine(query, every_vector) 比请求 Pinecone 还快,而且免费。向量数据库在规模化后才物有所值 — 但大多数应用根本到不了那个量级。