M15(2026-10-13)登记的两条放大源落在记忆层,本次一并收口: 1. 整理轮/入库审查**只嵌增量**:新加 vector.Index.Vectors(SQLite/PG 实现, 按 id 取已存向量、维度不符当未命中),Store.embedIncremental 先复用索引里的 向量、只嵌缺的那些,并把手算出的向量回填索引 ⇒ 同一分区第二轮起零请求。 判据写明为「向量存在性 + 维度一致」(正文在本仓不可就地改写,故无需哈希)。 维度与当前 embedder 不一致时禁用复用并 WARN(不混两种表示)。 2. 嵌入链对单个慢分片有韧性:embedIsolated 整批失败后逐条隔离,坏的那几条 记缺口、其余照常返回;一条都没拿到才报错降级。请求数上界 1+B(B=8 ⇒ 9), 服务整体挂死时一次即收手,不用「请求放大」换韧性。 3. 缺口/复用/回填条数进日志(缺口走 WARN),review 的被审条目拿不到向量时 如实返回错误 ⇒ 走既有词项余弦降级(不把「算不出来」说成「没有重复」)。 请求数(单测内假 OpenAI 兼容服务端计数,生产 API embedder + 真 SQLite 索引): 24 条存量:修复前每轮 3 → 第一轮 3、第二轮 0 12 条分区整理轮:第一轮 2 → 第二轮 0 20 条候选的入库审查:26(201÷8)→ 第一条 3、第二条 0 一条坏正文:保住 7/8 条,代价 9 个请求;整体性故障 9 个请求后收手
69 lines
2.1 KiB
Go
69 lines
2.1 KiB
Go
package vector
|
||
|
||
import (
|
||
"strconv"
|
||
"strings"
|
||
)
|
||
|
||
// 本文件是「按 id 取已存向量」的共用件:增量向量化(memory 包的
|
||
// embedIncremental)靠它跳过已经算过的行,从而不再每轮把整个分区重新嵌一遍
|
||
// ——那是 M15(2026-10-13)登记的请求放大源之一。
|
||
//
|
||
// 只读、参数化:id 一律走绑定参数,查询里不出现任何拼接的取值。
|
||
|
||
// vectorsQueryChunk 是单条 IN 查询携带的 id 上限。SQLite 的绑定变量上限
|
||
// (历史默认 999)之下留出余量,使 2000 条的分区不会因为一条查询太长而
|
||
// 失败;超过它的 id 分多条查询,结果仍按入参顺序对齐。
|
||
const vectorsQueryChunk = 256
|
||
|
||
// placeholders 返回 n 个占位符:encode 决定形式(SQLite 的 "?"、PostgreSQL
|
||
// 的 "$i")。它只拼占位符,不拼任何取值。
|
||
func placeholders(n int, encode func(i int) string) string {
|
||
var b strings.Builder
|
||
for i := 0; i < n; i++ {
|
||
if i > 0 {
|
||
b.WriteByte(',')
|
||
}
|
||
b.WriteString(encode(i))
|
||
}
|
||
return b.String()
|
||
}
|
||
|
||
// sqlitePlaceholders 返回 "?,?,?"。
|
||
func sqlitePlaceholders(n int) string {
|
||
return placeholders(n, func(int) string { return "?" })
|
||
}
|
||
|
||
// pgPlaceholders 返回 "$1,$2,..."。
|
||
func pgPlaceholders(n int) string {
|
||
return placeholders(n, func(i int) string { return "$" + strconv.Itoa(i+1) })
|
||
}
|
||
|
||
// idArgs 把 ids 转成绑定参数([]any 是 database/sql 要求的入参形态)。
|
||
func idArgs(ids []string) []any {
|
||
args := make([]any, len(ids))
|
||
for i, id := range ids {
|
||
args[i] = id
|
||
}
|
||
return args
|
||
}
|
||
|
||
// alignVectors 把「id → 向量」按入参顺序摊开:未命中的位置(含同一 id 在
|
||
// 入参里出现多次时)一律为 nil。因此调用方拿到的切片长度恒等于 ids。
|
||
func alignVectors(ids []string, found map[string]Vector) []Vector {
|
||
out := make([]Vector, len(ids))
|
||
if len(found) == 0 {
|
||
return out
|
||
}
|
||
pos := make(map[string][]int, len(ids))
|
||
for i, id := range ids {
|
||
pos[id] = append(pos[id], i)
|
||
}
|
||
for id, vec := range found {
|
||
for _, i := range pos[id] {
|
||
out[i] = vec
|
||
}
|
||
}
|
||
return out
|
||
}
|