一句话:向量检索按含义而非精确关键词来查找行——这是语义检索和 RAG 的基础。你在每一行旁边存一个向量(embedding)(一串数字),然后按它与查询向量的接近程度对行排序。LingCode Cloud 运行的 Postgres 带有 pgvector 扩展,并且可以替你生成这些向量(托管向量化),所以你永远不必把向量化模型的密钥塞进应用里。加一列 vector(N),用 lingcode.vector.embed() 把文本向量化,再用 lingcode.vector.search() 排序。
如果你只用过 LIKE '%term%' 或全文检索,那么向量检索是一种不同的思路,值得在动手用它之前先理解清楚。关键词检索问的是「哪些行包含这个词?」向量检索问的是「哪些行表达的大致是同一个意思?」正是这个差别,让「找相似」「从我的文档里给出答案」以及「能扛住同义词和拼写错误的检索」成为可能。本页讲清楚向量是什么、它为什么有效,如何配置一个列,你真正要写的两个 SDK 调用,以及一个端到端的小型 RAG 示例。
一个向量(embedding)是一串数字——一个向量——由 AI 模型从一段文本生成。模型经过训练,使得含义相近的文本在这个向量空间里落到彼此靠近的位置,而不相关的文本则相距很远。「我怎么重置密码?」和「我忘了我的登录凭证」几乎没有共同的词,但它们的向量却挨得很近,因为它们表达的是同一个意思。
这就是全部诀窍。一旦每一行都带上了向量,「找出和这个查询相似的行」就变成了「找出向量与查询向量最接近的那些行」。关键词检索做不到这一点——它只能匹配你输入的那些词。向量检索匹配的是意思,所以它能扛住同义词、换种说法和拼写错误,这也正是语义检索和检索增强生成(RAG)背后的原理:先取出最相关的行,再交给模型来撰写答案。
生成一个向量通常意味着调用一个向量化模型,也就意味着要有一个 API 密钥。有了 LingCode Cloud 的托管向量化,lingcode.vector.embed() 会在服务端替你生成向量——你的应用永远不持有向量化模型的密钥,也永远不直接和模型厂商对话。你发文本进去,拿向量出来。(想自带模型?你仍然可以把预先算好的向量直接传给 search。)
向量存在类型为 vector(N) 的列里,其中 N 是向量的维度——模型生成的数字数组的长度(例如 1536)。你在迁移(migration)里添加这个列,方式和添加任何其他列一样。运行迁移的方法见数据库指南。
create table docs (
id bigint generated always as identity primary key,
title text not null,
text text not null,
embedding vector(1536)
);
对于大数据集,加一个合适的向量索引,让相似度查询在表增大时仍然保持快速——没有它,检索就得扫描每一行。
vector(N) 里的 N、你存进去的向量,以及你用来检索的查询向量,三者长度必须相同。混用维度(存 1536 维向量却用 768 维向量来查)是向量检索里最常见的一个 bug。用同一个模型给你存储的文本和查询做向量化,数字才能对得上。
你要做的一切都在 lingcode.vector.* 之下。和每个 SDK 调用一样,这两个都返回 { data, error }。
vector.embed(input) —— 把文本变成向量input 是一个字符串或一个字符串数组。它用在两个地方:在写入时填充你的列,以及在检索时把用户的查询向量化。
const { data, error } = await lingcode.vector.embed('How do I reset my password?');
// data = {
// embedding: number[], // 单个字符串输入对应的向量
// embeddings: number[][], // 数组输入时每项一个向量
// model: string, // 所用的向量化模型
// dimensions: number // 每个向量的长度
// }
vector.search({ table, column, embedding, limit?, metric? }) —— 按接近程度排序传入要检索的表和向量列,再加上查询向量。结果以行的形式返回,按距离排序(最近的在前)。
const { data, error } = await lingcode.vector.search({
table: 'docs',
column: 'embedding',
embedding: data.embedding, // 来自 vector.embed() 的查询向量
limit: 5, // 1–200
metric: 'cosine' // 'cosine'(默认)| 'l2' | 'ip'
});
// data = rows[],按距离排序
结果会和普通查询一样经过行级安全过滤——用户永远只在自己有权看到的行上排序。
两个阶段:写入时向量化一次,查询时再向量化并检索。把向量化放在写入时做,这样检索本身就只是一次快速查找。
当一篇文档到来时,把它的文本向量化,并把向量存进这一行的 embedding 列。
const { data } = await lingcode.vector.embed(doc.text);
await lingcode.from('docs').insert({
title: doc.title,
text: doc.text,
embedding: data.embedding
});
用同样的方式把用户的查询向量化,然后让你的行对它进行排序。
const { data: q } = await lingcode.vector.embed(userQuery);
const { data: hits } = await lingcode.vector.search({
table: 'docs',
column: 'embedding',
embedding: q.embedding,
limit: 5
});
检索增强生成无非就是:把问题向量化 → 检索出最相关的行 → 把这些行喂给模型来撰写答案。检索这一步正是本页所讲的;撰写答案那一步是一次普通的模型调用。
async function answer(userQuery) {
// 1. 把问题向量化。
const { data: q } = await lingcode.vector.embed(userQuery);
// 2. 按含义取出最接近的行。
const { data: hits } = await lingcode.vector.search({
table: 'docs',
column: 'embedding',
embedding: q.embedding,
limit: 5
});
// 3. 把最靠前的行作为上下文交给模型来撰写答案。
const context = hits.map(r => r.text).join('\n\n');
// ...用 `context` + `userQuery` 调用你的答案模型...
return context;
}
因为检索基于含义,用户可以用自己的话来提问,仍然能拉到正确的行——这正是 RAG 答案区别于关键词查找的地方。
metric 决定了两个向量之间的「接近程度」如何衡量。选你的向量化模型推荐的那个;如果拿不准,cosine 是稳妥的默认值。
cosine(默认)—— 比较向量的方向,忽略其长度。文本向量最常用的选择。l2 —— 两点之间的直线(欧几里得)距离。ip —— 内积(点积)。当你的模型文档要求时使用。语义检索擅长含义,但可能会漏掉精确的词元——一个产品 SKU、一个用户名、一个特定的错误码。关键词(全文)检索正相反:对精确词项很准,但对同义词视而不见。混合检索用倒数排名融合(RRF)把两者合在一起,于是像「Aurora Pro 蓝牙连不上」这样的查询,既能匹配字面上的产品名,又能匹配这条抱怨的含义。只要查询里既有一个具体词项又有一段模糊描述,就该用它。
这些 SDK 调用映射到你后端基础 URL https://lingcode.dev/api/cloud/be/<backend-id> 下的这些端点。用你的 anon key 或一个已登录的用户令牌认证;结果会和任何其他查询一样经过行级安全过滤。
POST /vector/embed
{ input }
→ { embedding, embeddings, model, dimensions }
POST /vector/search
{ table, column, embedding, limit?, metric? }
→ ranked rows
POST /search/text
{ table, column, query, is_tsvector?, limit? }
→ matching rows
用倒数排名融合(RRF)把关键词排名和含义排名融合起来。你既要传原始的 query 文本(用于全文),也要传它的 embedding(用于语义);权重用来调节每一边各贡献多少。
POST /search/hybrid
{
table,
text_column,
vector_column,
query,
embedding,
id_column?,
text_is_tsvector?,
metric?,
limit?,
full_text_weight?,
semantic_weight?,
rrf_k?
}
→ fused, ranked rows
N。cosine。