Chapter 07

向量搜索与 RAG

libSQL 2.0 开始原生支持向量列和最近邻搜索——Turso 变成了"能跑 SQL 的轻量级 Pinecone"。本章跑通完整 RAG 小项目。

7.1 为什么数据库要懂向量

RAG(Retrieval-Augmented Generation)的核心:把文档切块 → 每块转成 embedding 向量 → 用户提问 → 把问题也转成向量 → 数据库里找最相似的文档块 → 送给 LLM 生成答案。

以前常见做法:Postgres + pgvector,或者单独用 Pinecone/Qdrant。libSQL 把向量搜索内置进 SQLite,RAG 应用从此不需要多装一个服务。

7.2 向量列类型:F32_BLOB

libSQL 加了一个新列类型:F32_BLOB(N)——定长 32 位浮点数组(blob 底层是 4*N 字节)。

CREATE TABLE documents (
  id         INTEGER PRIMARY KEY,
  title      TEXT,
  content    TEXT,
  embedding  F32_BLOB(1536)    -- OpenAI text-embedding-3-small 的维度
);

7.3 写入向量

libSQL 提供 vector32() 函数把 JSON 数组转成 blob:

INSERT INTO documents (title, content, embedding) VALUES (
  'libSQL 简介',
  'libSQL 是 Turso fork...',
  vector32('[0.01, -0.32, 0.77, ...1536 个浮点]')
);

JS 端写入:

import OpenAI from 'openai';
const openai = new OpenAI();

async function embed(text: string) {
  const r = await openai.embeddings.create({
    model: 'text-embedding-3-small',
    input: text,
  });
  return r.data[0].embedding;   // number[] 长度 1536
}

const vec = await embed('libSQL 简介正文...');
await db.execute({
  sql: `INSERT INTO documents (title, content, embedding)
        VALUES (?, ?, vector32(?))`,
  args: ['libSQL 简介', '正文...', JSON.stringify(vec)],
});

7.4 建索引:libsql_vector_idx

没有索引就是全表扫描——千行以内可接受。规模大要建近似最近邻索引:

CREATE INDEX documents_embedding_idx
  ON documents (libsql_vector_idx(embedding));

这会生成一个DiskANN(磁盘友好的 ANN 算法)索引——不是精确,而是 recall > 0.9x 的近似。百万向量下毫秒级查询。

7.5 查询:vector_top_k

SELECT d.id, d.title,
       vector_distance_cos(d.embedding, vector32(?)) AS dist
FROM vector_top_k('documents_embedding_idx', vector32(?), 10) AS t
JOIN documents d ON d.rowid = t.id
ORDER BY dist;

vector_top_k(索引名, 查询向量, K) 返回 rowid 与距离,通常 JOIN 回主表取字段。

7.6 完整 RAG 小项目

Step 1 — Schema

-- migrations/001_chunks.sql
CREATE TABLE chunks (
  id         INTEGER PRIMARY KEY AUTOINCREMENT,
  source     TEXT NOT NULL,
  content    TEXT NOT NULL,
  embedding  F32_BLOB(1536) NOT NULL
);
CREATE INDEX chunks_emb_idx ON chunks (libsql_vector_idx(embedding));

Step 2 — 灌入

import fs from 'node:fs';
import { db } from './db';

async function ingest(filePath: string) {
  const text = fs.readFileSync(filePath, 'utf8');
  const chunks = splitIntoChunks(text, 500); // 500 字符一块
  for (const c of chunks) {
    const vec = await embed(c);
    await db.execute({
      sql: `INSERT INTO chunks(source, content, embedding)
            VALUES(?, ?, vector32(?))`,
      args: [filePath, c, JSON.stringify(vec)],
    });
  }
}

Step 3 — 检索 + 生成

async function ask(question: string) {
  const qvec = await embed(question);
  const { rows: hits } = await db.execute({
    sql: `SELECT c.content, vector_distance_cos(c.embedding, vector32(?1)) d
          FROM vector_top_k('chunks_emb_idx', vector32(?1), 5) t
          JOIN chunks c ON c.rowid = t.id
          ORDER BY d`,
    args: [JSON.stringify(qvec)],
  });

  const context = hits.map((h: any) => h.content).join('\n---\n');
  const answer = await openai.chat.completions.create({
    model: 'gpt-4o-mini',
    messages: [
      { role: 'system', content: '基于下面上下文作答:\n' + context },
      { role: 'user', content: question },
    ],
  });
  return answer.choices[0].message.content;
}

7.7 距离函数

vector_distance_cos(a, b)
余弦距离(1 - cos 相似度)。推荐给 OpenAI embedding——它们已单位归一化。
vector_distance_l2(a, b)
L2 欧氏距离。部分模型(BGE、部分 BERT)用这个更合适。

索引构建时参数 metric= 决定用哪种:

CREATE INDEX idx ON t(
  libsql_vector_idx(emb, 'metric=cosine', 'max_neighbors=20')
);

7.8 混合检索:向量 + SQL 过滤

SQL 的优势在于:关键词 + 元数据 + 向量可以一条语句混合:

SELECT c.content
FROM vector_top_k('chunks_emb_idx', vector32(?), 20) t
JOIN chunks c ON c.rowid = t.id
WHERE c.source LIKE 'docs/%'         -- 只看文档
  AND c.created_at > 1700000000     -- 最近的
LIMIT 5;
向量检索 + SQL 元数据 = 专用向量库做不到的事

Pinecone 虽有 metadata filter,但 join 三张表、分组统计这类 SQL 能力是缺失的。Turso 的卖点就是"一条 SQL 搞定 RAG 复合查询"。

7.9 规模建议

向量数推荐策略
< 10K不建索引,全表扫描最快
10K – 1Mlibsql_vector_idx + 默认参数
1M – 10M参数调优:max_neighbors=30, efConstruction=200
> 10M分表 / 分库 / 专业向量库(Qdrant)

7.10 Drizzle 里用向量

Drizzle 目前把 F32_BLOB 当成普通 blob——可以用 sql 模板访问:

import { sql } from 'drizzle-orm';

const qvec = JSON.stringify(await embed(question));

const hits = await db.all(sql`
  SELECT c.content,
         vector_distance_cos(c.embedding, vector32(${qvec})) as d
  FROM vector_top_k('chunks_emb_idx', vector32(${qvec}), 5) t
  JOIN chunks c ON c.rowid = t.id
  ORDER BY d
`);

小结

libSQL 把向量搜索做成一等公民——F32_BLOB 列、libsql_vector_idx 索引、vector_top_k 查询。对中小型 RAG(< 1M 向量)来说,Turso 一个库就够,省掉了专门的向量数据库。下一章讲 schema 迁移与版本管理。