← Back to skills
extension
Category: Development & EngineeringAPI key requirement unconfirmed

seekdb 混合检索

在 OceanBase seekdb 里用纯 SQL 做全文检索、向量检索和单语句混合检索(FULLTEXT + HNSW 向量索引 + RRF 融合),含建表、查询、确认走索引和常见报错。

personAuthor: FrankJayhubModelScope

seekdb 混合检索

seekdb 同时提供全文索引(FULLTEXT)和向量索引(HNSW),两种检索都能直接写成 SQL。本技能给出建表、查询、单语句混合检索和结果融合的写法。下面的示例都在 seekdb 嵌入式实例(pylibseekdb)上实际跑通过。

1. 建表:全文索引和向量索引

CREATE TABLE docs (
  id BIGINT PRIMARY KEY,
  title VARCHAR(255) NOT NULL,
  body TEXT NOT NULL,
  embedding VECTOR(4) NOT NULL,
  FULLTEXT INDEX ft_body (body) WITH PARSER ik,
  VECTOR INDEX vec_embedding (embedding) WITH (distance=l2, type=hnsw, lib=vsag)
);

要点:

  • VECTOR(n) 的维度必须和 embedding 模型的输出维度一致。示例用 4 维,实际常见的是 768 或 1024。
  • 分词器选择:
    • 中文或中英混排用 WITH PARSER ik;
    • 已经预先用空格切好词的文本用 WITH PARSER space;
    • 英文文本可以用 beng。
  • 向量索引的 distance 要和查询里用的距离函数对应:l2 对应 l2_distance,cosine 对应 cosine_distance。
  • 已有的表可以单独建索引:
CREATE FULLTEXT INDEX ft_txt ON notes (txt) WITH PARSER ik;
CREATE VECTOR INDEX vec_emb ON notes (emb) WITH (distance=cosine, type=hnsw, lib=vsag);
  • 写入向量时用 JSON 数组字面量,例如 '[0.9,0.1,0.0,0.0]'。

2. 全文检索

SELECT id, title, MATCH (body) AGAINST ('全文检索') AS score
FROM docs
WHERE MATCH (body) AGAINST ('全文检索')
ORDER BY score DESC
LIMIT 10;
  • WHERE 里必须写 MATCH ... AGAINST,这样才会走全文索引;SELECT 里的同一个表达式用来取相关度分数。
  • 布尔模式:MATCH (body) AGAINST ('+向量 -全文' IN BOOLEAN MODE),其中 + 表示必须出现,- 表示必须不出现。
  • 召回不符合预期时,先用 SELECT TOKENIZE('要检索的句子', 'ik') 看分词结果。查询词和文档的分词结果对不上,就不会命中。

3. 向量检索

SELECT id, title, l2_distance(embedding, '[0.8,0.2,0.0,0.0]') AS dist
FROM docs
ORDER BY l2_distance(embedding, '[0.8,0.2,0.0,0.0]') APPROXIMATE
LIMIT 10;
  • ORDER BY <距离函数> APPROXIMATE LIMIT k 走 HNSW 近似检索。距离函数要和索引的 distance 参数一致,并且必须带 LIMIT。
  • 带额外 WHERE 过滤条件时,近似检索先取 top-k 再过滤,可能拿不够 k 条。过滤后的结果必须完整时,去掉 APPROXIMATE 改成精确排序:
SELECT id, cosine_distance(embedding, '[0.8,0.2,0.0,0.0]') AS dist
FROM docs WHERE id > 1 ORDER BY dist LIMIT 10;

4. 用一条 SQL 做混合检索

用 UNION ALL 把两路候选放进同一条语句,一次往返就能拿到两路结果,用 channel 列区分来源:

(SELECT 'fts' AS channel, id, MATCH (body) AGAINST ('检索') AS score, NULL AS dist
   FROM docs WHERE MATCH (body) AGAINST ('检索')
   ORDER BY score DESC LIMIT 20)
UNION ALL
(SELECT 'vector' AS channel, id, NULL AS score,
        l2_distance(embedding, '[0.5,0.5,0.1,0.0]') AS dist
   FROM docs ORDER BY l2_distance(embedding, '[0.5,0.5,0.1,0.0]') APPROXIMATE LIMIT 20)
  • 每个分支都要用括号包起来,各自带 ORDER BY ... LIMIT。
  • 每一路的候选数量(这里是 20)应该大于最终要返回的数量,给融合留出余量。
  • 想确认两路都用上了索引,在语句前加 EXPLAIN,计划里应该同时出现 TEXT RETRIEVAL SCAN 和 VECTOR INDEX。只出现其中一个,说明另一路退化成了全表扫描。

5. 融合两路结果(RRF)

全文分数和向量距离的量纲不同,不能直接相加。可以按排名做倒数排名融合(Reciprocal Rank Fusion):

def rrf(rows, k=60, limit=10):
    """rows 是第 4 节那条 SQL 的结果:(channel, id, score, dist)。"""
    fts = sorted((r for r in rows if r[0] == "fts"), key=lambda r: (-r[2], r[1]))
    vec = sorted((r for r in rows if r[0] == "vector"), key=lambda r: (r[3], r[1]))
    fused = {}
    for ranked in (fts, vec):
        for rank, row in enumerate(ranked, start=1):
            fused[row[1]] = fused.get(row[1], 0.0) + 1.0 / (k + rank)
    return sorted(fused, key=lambda doc_id: (-fused[doc_id], doc_id))[:limit]
  • 两路各自先排序,并用 id 打破并列,保证结果稳定、可复现。
  • 两路都命中的文档会累加两份分数,自然排在前面。

6. 常见问题

| 现象 | 原因与处理 | |---|---| | 全文检索没有命中 | 分词器和文本语言不匹配,用 TOKENIZE 检查分词结果 | | 向量检索很慢 | 查询漏了 APPROXIMATE 或 LIMIT,或者距离函数和索引的 distance 不一致,用 EXPLAIN 检查 | | 带过滤条件后结果变少 | 近似检索是先取 top-k 再过滤,改成精确排序,或者把候选数量调大 | | 插入向量时报维度错误 | 写入的向量维度和 VECTOR(n) 定义的不一致 |

<!-- OB-S:RRqqJNUWGFWmv5qeGvun1A -->