Files
Cyrene_v2/internal/runtime/memory/vector/delete.go
T
AskaEth a017ab6cf8 feat(memory): 临时记忆到期物理删除(有界清扫 + 同步清向量 + 删前审计)
design/23 §19 的第 ② / ③ 步:读侧时效闸(M1)只负责「看不见」,本切片负责
「真删掉」。三条护栏照 §19.3:删除是本包内唯一 DELETE 出口、有界(500 行/批 ×
20 批)、删前留审计。

- internal/runtime/memory/expire.go:扫描集合写死为「expires_at ≠ '' ∧
  expires_at ≤ now ∧ status='active' ∧ 来源非受限」。空 expires_at(长期/日常
  记忆,含 M2 登记的「INSERT 与补写之间中断」窗口)结构性进不了集合;受限来源
  复用 nonOwnerSource(与整理/审查同一处判据)。键集游标 (expires_at, rowid)
  保证被跳过的行只读一次、扫描位置一定前移。审计串 = id + persona_id + scope +
  expires_at + 按 rune 截断(48)的摘要 —— 本仓唯一一处允许携带内容片段的审计。
- internal/runtime/memory/expire_loop.go:清扫循环挂在 TidyService 的生命周期上
  (AttachExpirySweep 由组合根调用),随心跳 ctx 跑、随 Stop 停;一轮只写一行
  汇总日志,逐条的那一行进审计。
- internal/runtime/memory/vector/delete.go:新增可选删除能力 vector.Deleter +
  两个后端实现,删行时同步清向量(M1 reviewer 的 finding:过期向量占 top-K 预算)。
- cmd/cyrene/wire_memory_tidy.go:挂上清扫;顺带把已过时的「这十键」改成「这十一
  键」(M2 加了第十一键 tempTtlSec)。
2026-10-13 21:43:27 +08:00

91 lines
3.8 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package vector
import (
"context"
"fmt"
"strings"
)
// 本文件是**向量删除**(索引的清除侧)。动因是一条实测 finding(切片 M1 的
// 读侧时效闸交下来的):临时记忆到期后**行不在了、向量还在** —— 过期行的向量
// 仍留在 vec_items 里,占掉 idx.Search 的 top-K 预算,于是「语义检索前 K 名」
// 里有一半是永远不会被返回的死条目(读侧闸已挡住那些行),**召回质量静默下降**。
// 到期清扫(internal/runtime/memory/expire.go)在物理删行时同步调这里,把
// 向量一并清掉。
//
// 🔴 **为什么不是 Index 的必需方法**(Deleter 与 Index 并列、由调用方类型断言):
// Index 是本包对外的稠密索引抽象,加一个必需方法会**立刻打破所有既有实现** ——
// 包括别的包里的测试桩(例如 memory 包的 mockVectorIndex)。清除能力因此按
// **可选扩展**建模:两种生产后端(SQLiteIndex / PGVectorIndex)都实现它,
// 只读的桩可以不实现,调用方断言失败时退化为「不清向量」并留一条日志。
// 这与本仓既有的可选能力手法一致(例如 `a.vindex.(*vector.PGVectorIndex)`)。
// Deleter 是 Index 的**可选扩展**:把已不存在的 id 从索引里清掉。id 不存在时
// 视为成功(删不存在的向量本就是空操作),因此重复调用是幂等的。
//
// 调用方(memory 包)在物理删行之后调用它;**不是**读路径的一部分。
type Deleter interface {
Delete(ctx context.Context, ids ...string) error
}
// deleteVecItemsSQLTemplate 是两种后端共用的删除语句骨架:`%s` 是占位串
// (SQLite `?` / pgvector `$1,$2,...`)。表名是编译期常量,绝不来自输入。
const deleteVecItemsSQLTemplate = `DELETE FROM vec_items WHERE id IN (%s)`
// vecDeletePlaceholders 为 ids 生成删除语句的占位串与参数表。`dollar` 为 true
// 时用 Postgres 的 `$n` 形式,否则用 SQLite 的 `?`。
//
// 空白 id 被**跳过**而不是报错:删一个不存在的向量是空操作,不该让整批失败
// (Add 拒绝空 id,是因为那会写出一行不可寻址的数据;删除没有这个问题)。
// 全部为空时返回空占位串,调用方据此直接返回。
func vecDeletePlaceholders(ids []string, dollar bool) (string, []any) {
args := make([]any, 0, len(ids))
for _, id := range ids {
if strings.TrimSpace(id) == "" {
continue
}
args = append(args, id)
}
if len(args) == 0 {
return "", nil
}
var b strings.Builder
for i := range args {
if i > 0 {
b.WriteByte(',')
}
if dollar {
fmt.Fprintf(&b, "$%d", i+1)
} else {
b.WriteByte('?')
}
}
return b.String(), args
}
// Delete 把 ids 对应的向量从 SQLite 索引里删掉。id 不存在时无事发生(幂等);
// 空 ids 是空操作。ctx 被取消时中止。
func (s *SQLiteIndex) Delete(ctx context.Context, ids ...string) error {
placeholders, args := vecDeletePlaceholders(ids, false)
if placeholders == "" {
return nil
}
if _, err := s.db.ExecContext(ctx, fmt.Sprintf(deleteVecItemsSQLTemplate, placeholders), args...); err != nil {
return fmt.Errorf("memory/vector: delete %d sqlite vectors: %w", len(args), err)
}
return nil
}
// Delete 把 ids 对应的向量从 pgvector 索引里删掉。语义与 SQLiteIndex.Delete
// 逐字相同(幂等、空 ids 空操作、ctx 取消即中止),只用 `$n` 占位。
func (p *PGVectorIndex) Delete(ctx context.Context, ids ...string) error {
placeholders, args := vecDeletePlaceholders(ids, true)
if placeholders == "" {
return nil
}
if _, err := p.db.ExecContext(ctx, fmt.Sprintf(deleteVecItemsSQLTemplate, placeholders), args...); err != nil {
return fmt.Errorf("memory/vector: delete %d pg vectors: %w", len(args), err)
}
return nil
}