Skip to main content

p_memory/
embeddings.rs

1use crate::{storage::{self, KnowledgeBase}, text, types::*, Error, Result};
2use parking_lot::Mutex;
3use rusqlite::{params, params_from_iter, types::Value as SqlValue, Connection, OptionalExtension};
4use serde::{Deserialize, Serialize};
5use std::{cmp::Ordering, collections::{BTreeMap, BinaryHeap, HashMap, HashSet}, sync::Arc};
6
7fn text_version() -> u32 { 1 }
8fn default_encoding() -> String { "sq8".into() }
9#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
10pub struct EmbeddingSpace {
11    pub id: String,
12    /// Include provider/model revision in this identity when it changes.
13    pub model: String,
14    pub dimension: usize,
15    #[serde(default = "text_version")] pub text_version: u32,
16    /// On-disk vector encoding: "f32" (4 bytes/dim) or "sq8" (1 byte/dim, lossy).
17    #[serde(default = "default_encoding")] pub encoding: String,
18}
19/// 待嵌入文本,只在库内部流转(`sync` 三段式循环的中间产物)。
20#[derive(Debug, Clone)]
21pub(crate) struct EmbeddingInput { pub key: RecordKey, pub text: String, pub fingerprint: String,
22    pub namespace: String, pub scope: String, pub kind: RecordKind, pub tags: Vec<String>, pub note_id: i64,
23    /// 正文应来自索引、但此刻索引里还没有(写入侧尚未提交这批切片)。
24    /// 它既不是「可嵌入的缺口」,也不代表这一档就绪——与「正文本就为空」必须区分开。
25    pub text_pending: bool }
26/// 一批算好的向量,只由库自己产出并写回。
27#[derive(Debug, Clone)]
28pub(crate) struct EmbeddingWrite { pub key: RecordKey, pub fingerprint: String, pub values: Vec<f32>,
29    pub namespace: String, pub scope: String, pub kind: RecordKind, pub tags: Vec<String>, pub note_id: i64 }
30
31#[derive(Clone)]
32pub struct EmbeddingStore(pub(crate) KnowledgeBase);
33
34// ── 宿主回调:错误分类、约束声明与注册表 ─────────────────────────────
35
36/// 回调失败的类别。类别必须由宿主显式给出,库不解析错误文案去猜。
37#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
38#[serde(rename_all = "snake_case")]
39pub enum EmbedErrorKind {
40    /// 单次请求条数超上限:库把 batch 减半后重试,减半值进程内持久。
41    TooLarge,
42    /// 限流 / 配额:退避后重试有限次。
43    RateLimited,
44    /// 其它(网络、鉴权、模型不存在):不重试,直接降级。
45    Other,
46}
47
48impl EmbedErrorKind {
49    pub fn code(self) -> &'static str {
50        match self { Self::TooLarge => "too_large", Self::RateLimited => "rate_limited", Self::Other => "other" }
51    }
52    pub fn from_code(code: &str) -> Option<Self> {
53        match code { "too_large" => Some(Self::TooLarge), "rate_limited" => Some(Self::RateLimited), "other" => Some(Self::Other), _ => None }
54    }
55}
56
57/// 嵌入回调返回的错误,带类别。
58#[derive(Debug, Clone)]
59pub struct EmbedCallbackError { pub kind: EmbedErrorKind, pub message: String }
60
61impl EmbedCallbackError {
62    pub fn new(kind: EmbedErrorKind, message: impl Into<String>) -> Self { Self { kind, message: message.into() } }
63    pub fn too_large(message: impl Into<String>) -> Self { Self::new(EmbedErrorKind::TooLarge, message) }
64    pub fn rate_limited(message: impl Into<String>) -> Self { Self::new(EmbedErrorKind::RateLimited, message) }
65    pub fn other(message: impl Into<String>) -> Self { Self::new(EmbedErrorKind::Other, message) }
66}
67
68impl std::fmt::Display for EmbedCallbackError {
69    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { write!(f, "{}: {}", self.kind.code(), self.message) }
70}
71impl std::error::Error for EmbedCallbackError {}
72
73/// 嵌入回调:`[文本] -> [向量]`,条数必须与输入一致。Rust 侧直接收闭包。
74pub trait Embedder: Send {
75    fn embed(&mut self, texts: &[String]) -> std::result::Result<Vec<Vec<f32>>, EmbedCallbackError>;
76}
77
78impl<F> Embedder for F
79where F: FnMut(&[String]) -> std::result::Result<Vec<Vec<f32>>, EmbedCallbackError> + Send {
80    fn embed(&mut self, texts: &[String]) -> std::result::Result<Vec<Vec<f32>>, EmbedCallbackError> { self(texts) }
81}
82
83fn default_max_batch() -> usize { 32 }
84/// 宿主注册嵌入回调时一并声明的批次与截断约束;库负责不越界。
85#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
86pub struct EmbedderOptions {
87    #[serde(default = "default_max_batch")] pub max_batch: usize,
88    /// 单条文本的 token 预算,`None` 表示不截断。
89    #[serde(default)] pub max_tokens_per_text: Option<usize>,
90}
91impl Default for EmbedderOptions {
92    fn default() -> Self { Self { max_batch: default_max_batch(), max_tokens_per_text: None } }
93}
94
95pub(crate) struct EmbedderEntry {
96    pub options: EmbedderOptions,
97    /// 当前生效的批次上限。命中「批次过大」后减半,并在进程内持久。
98    pub effective_batch: usize,
99    pub embedder: Box<dyn Embedder>,
100}
101
102impl EmbedderEntry {
103    /// 按声明强制截断后调用回调:库永远不把超出声明的文本送出去。
104    pub(crate) fn embed(&mut self, texts: &[String]) -> std::result::Result<Vec<Vec<f32>>, EmbedCallbackError> {
105        match self.options.max_tokens_per_text {
106            Some(budget) => {
107                let budgeted: Vec<String> = texts.iter().map(|value| text::truncate_to_tokens(value, budget)).collect();
108                self.embedder.embed(&budgeted)
109            }
110            None => self.embedder.embed(texts),
111        }
112    }
113}
114
115/// 进程内的回调表:一个向量空间最多绑一个回调。`Arc` 让调用方拿出去后立刻放掉注册表锁。
116#[derive(Default)]
117pub(crate) struct EmbedderRegistry { entries: Mutex<HashMap<String, Arc<Mutex<EmbedderEntry>>>> }
118
119impl EmbedderRegistry {
120    pub fn new() -> Self { Self::default() }
121    pub fn space_ids(&self) -> Vec<String> {
122        let mut ids: Vec<String> = self.entries.lock().keys().cloned().collect();
123        ids.sort();
124        ids
125    }
126    pub fn get(&self, space_id: &str) -> Option<Arc<Mutex<EmbedderEntry>>> { self.entries.lock().get(space_id).cloned() }
127    pub fn register(&self, space_id: String, entry: EmbedderEntry) { self.entries.lock().insert(space_id, Arc::new(Mutex::new(entry))); }
128    pub fn remove(&self, space_id: &str) -> bool { self.entries.lock().remove(space_id).is_some() }
129}
130
131/// 注册校验用的短样本:真实走一遍回调,按空间契约逐项检查产出。
132const SAMPLE_TEXTS: [&str; 3] = ["样本一 sample", "样本二 sample", "样本三 sample"];
133
134const RATE_LIMIT_ATTEMPTS: u32 = 3;
135const RATE_LIMIT_BACKOFF_MS: u64 = 20;
136
137pub(crate) fn get_space(conn: &Connection, id: &str) -> Result<EmbeddingSpace> {
138    conn.query_row("SELECT id,model,dimension,text_version,encoding FROM embedding_spaces WHERE id=?1", [id], |r|
139        Ok(EmbeddingSpace { id: r.get(0)?, model: r.get(1)?, dimension: r.get::<_, u32>(2)? as usize, text_version: r.get(3)?, encoding: r.get(4)? })).optional()?
140        .ok_or_else(|| Error::NotFound(format!("embedding space {id}")))
141}
142
143pub(crate) fn normalize(values: &[f32], dimension: usize) -> Result<Vec<f32>> {
144    if values.len() != dimension { return Err(Error::InvalidVector(format!("expected dimension {dimension}, received {}", values.len()))); }
145    if values.iter().any(|v| !v.is_finite()) { return Err(Error::InvalidVector("values must be finite".into())); }
146    let norm = values.iter().map(|v| f64::from(*v).powi(2)).sum::<f64>().sqrt();
147    if norm == 0.0 || !norm.is_finite() { return Err(Error::InvalidVector("zero or invalid vector norm".into())); }
148    Ok(values.iter().map(|v| (f64::from(*v) / norm) as f32).collect())
149}
150
151/// Dot product of two equal-length f32 slices, unrolled by 8 so the compiler
152/// can vectorize it. Inputs are normalized, so the result is within [-1, 1].
153#[inline]
154fn dot(a: &[f32], b: &[f32]) -> f32 {
155    let mut acc = [0f32; 8];
156    let chunks = a.len() / 8;
157    for c in 0..chunks {
158        let o = c * 8;
159        for k in 0..8 { acc[k] += a[o + k] * b[o + k]; }
160    }
161    let mut sum = acc.iter().sum::<f32>();
162    for i in chunks * 8..a.len() { sum += a[i] * b[i]; }
163    sum
164}
165
166/// Quantize a normalized query into i8 codes plus its scale, using the same
167/// symmetric rule as the stored sq8 encoding. Quantizing the query lets both
168/// sides of the dot product stay as i8, so the hot loop never expands to f32.
169fn encode_query_sq8(query: &[f32]) -> (Vec<i8>, f32) {
170    let max = query.iter().fold(0f32, |m, v| m.max(v.abs()));
171    let scale = if max == 0.0 { 1.0 } else { max / 127.0 };
172    let codes = query.iter().map(|v| (v / scale).round().clamp(-127.0, 127.0) as i8).collect();
173    (codes, scale)
174}
175
176/// Integer dot product of two i8 rows. The hot loop runs on AVX2 when the
177/// running CPU has it, otherwise it falls back to the scalar path, so a build
178/// compiled for the generic target still gets the vector path on new machines.
179#[inline]
180fn dot_codes(left: &[i8], right: &[i8]) -> i32 {
181    #[cfg(target_arch = "x86_64")]
182    {
183        // Safety: the branch only runs after the CPU is confirmed to have avx2.
184        if std::is_x86_feature_detected!("avx2") { return unsafe { dot_codes_avx2(left, right) }; }
185    }
186    dot_codes_scalar(left, right)
187}
188
189fn dot_codes_scalar(left: &[i8], right: &[i8]) -> i32 {
190    left.iter().zip(right).map(|(a, b)| i32::from(*a) * i32::from(*b)).sum()
191}
192
193/// 16 codes per iteration: widen to i16, multiply-add adjacent pairs into i32.
194/// Each accumulator lane stays well inside i32 even at 65536 dimensions.
195#[cfg(target_arch = "x86_64")]
196#[target_feature(enable = "avx2")]
197unsafe fn dot_codes_avx2(left: &[i8], right: &[i8]) -> i32 {
198    use std::arch::x86_64::*;
199    let mut acc = _mm256_setzero_si256();
200    let chunks = left.len() / 16;
201    for c in 0..chunks {
202        let o = c * 16;
203        let a = _mm_loadu_si128(left.as_ptr().add(o) as *const __m128i);
204        let b = _mm_loadu_si128(right.as_ptr().add(o) as *const __m128i);
205        acc = _mm256_add_epi32(acc, _mm256_madd_epi16(_mm256_cvtepi8_epi16(a), _mm256_cvtepi8_epi16(b)));
206    }
207    let mut total = {
208        let sum = _mm_add_epi32(_mm256_castsi256_si128(acc), _mm256_extracti128_si256(acc, 1));
209        let sum = _mm_add_epi32(sum, _mm_shuffle_epi32(sum, 0b01_00_11_10));
210        let sum = _mm_add_epi32(sum, _mm_shuffle_epi32(sum, 0b10_11_00_01));
211        _mm_cvtsi128_si32(sum)
212    };
213    for i in chunks * 16..left.len() { total += i32::from(left[i]) * i32::from(right[i]); }
214    total
215}
216
217/// Encode a normalized vector for storage. `f32` is lossless; `sq8` is a
218/// per-vector symmetric quantization (one f32 scale, then one i8 per dimension).
219fn encode_values(normalized: &[f32], encoding: &str) -> Result<Vec<u8>> {
220    match encoding {
221        "f32" => Ok(normalized.iter().flat_map(|v| v.to_le_bytes()).collect()),
222        "sq8" => {
223            let max = normalized.iter().fold(0f32, |m, v| m.max(v.abs()));
224            let scale = if max == 0.0 { 1.0 } else { max / 127.0 };
225            let mut out = Vec::with_capacity(4 + normalized.len());
226            out.extend_from_slice(&scale.to_le_bytes());
227            for v in normalized {
228                out.push((v / scale).round().clamp(-127.0, 127.0) as i8 as u8);
229            }
230            Ok(out)
231        }
232        other => Err(Error::Validation(format!("unknown encoding {other}"))),
233    }
234}
235
236/// Decode a stored vector; the payload length is checked against the encoding.
237fn decode_values(bytes: &[u8], dimension: usize, encoding: &str) -> Result<Vec<f32>> {
238    match encoding {
239        "f32" => {
240            if bytes.len() != dimension * 4 { return Err(Error::InvalidVector("stored vector dimension mismatch".into())); }
241            Ok(bytes.chunks_exact(4).map(|b| f32::from_le_bytes([b[0], b[1], b[2], b[3]])).collect())
242        }
243        "sq8" => {
244            let (scale, codes) = decode_sq8(bytes, dimension)?;
245            Ok(codes.into_iter().map(|c| f32::from(c) * scale).collect())
246        }
247        other => Err(Error::Validation(format!("unknown encoding {other}"))),
248    }
249}
250
251/// Split an sq8 payload into its per-vector scale and raw i8 codes, without
252/// expanding them back to f32.
253fn decode_sq8(bytes: &[u8], dimension: usize) -> Result<(f32, Vec<i8>)> {
254    if bytes.len() != dimension + 4 { return Err(Error::InvalidVector("stored vector dimension mismatch".into())); }
255    let scale = f32::from_le_bytes([bytes[0], bytes[1], bytes[2], bytes[3]]);
256    Ok((scale, bytes[4..].iter().map(|b| *b as i8).collect()))
257}
258
259// ── 向量化开关与默认值 ───────────────────────────────────────────────
260
261/// 一个领域下的三个独立开关:记忆、图谱、笔记。
262///
263/// 图谱这一档同时管住实体、关系、事件三类:它们同进同出,本次不细分。
264/// 笔记这一档落在切片上——笔记记录自己没有正文,给它算向量等于算空文本。
265#[derive(Debug, Clone, Copy, PartialEq, Eq)]
266pub(crate) enum VectorizeTarget { Memory, Graph, Notes }
267
268impl VectorizeTarget {
269    pub(crate) const ALL: [Self; 3] = [Self::Memory, Self::Graph, Self::Notes];
270
271    pub(crate) fn as_str(self) -> &'static str {
272        match self { Self::Memory => "memory", Self::Graph => "graph", Self::Notes => "notes" }
273    }
274
275    pub(crate) fn parse(value: &str) -> Result<Self> {
276        Self::ALL.into_iter().find(|target| target.as_str() == value)
277            .ok_or_else(|| Error::Validation(format!("vectorize target must be memory, graph or notes, got {value}")))
278    }
279
280    /// 该档覆盖的记录类型。
281    pub(crate) fn kinds(self) -> &'static [RecordKind] {
282        match self {
283            Self::Memory => &[RecordKind::Memory],
284            Self::Graph => &[RecordKind::Entity, RecordKind::Relation, RecordKind::Event],
285            Self::Notes => &[RecordKind::Chunk],
286        }
287    }
288
289    /// 没被显式设置过时的内置默认:记忆与图谱开、笔记关,新装库因此与旧行为一致。
290    fn default_enabled(self) -> bool { !matches!(self, Self::Notes) }
291}
292
293/// 领域级总闸的键。缺省启用;开关由库落盘,宿主配置一次即生效。
294fn vectorize_key(namespace: &str) -> String { format!("vectorize:{}", text::normalized_tag(namespace)) }
295
296/// 某档开关的键:`vectorize:<领域>:<档位>`。
297fn target_vectorize_key(namespace: &str, target: VectorizeTarget) -> String {
298    format!("{}:{}", vectorize_key(namespace), target.as_str())
299}
300
301/// 该 namespace 的领域级总闸。
302pub(crate) fn namespace_vectorization(conn: &Connection, namespace: &str) -> Result<bool> {
303    let value: Option<i64> = conn.query_row("SELECT value FROM meta WHERE key=?1", [vectorize_key(namespace)], |r| r.get(0)).optional()?;
304    Ok(value != Some(0))
305}
306
307/// 某档位在该 namespace 下的取值;没设置过就落到这一档的内置默认。
308pub(crate) fn target_vectorization(conn: &Connection, namespace: &str, target: VectorizeTarget) -> Result<bool> {
309    let value: Option<i64> = conn.query_row("SELECT value FROM meta WHERE key=?1", [target_vectorize_key(namespace, target)], |r| r.get(0)).optional()?;
310    Ok(value.map_or_else(|| target.default_enabled(), |value| value != 0))
311}
312
313/// 该 namespace 下真正启用向量化的记录类型;总闸关闭时为空。
314/// 生成侧与检索侧共用这一份判定,不各写一套。
315pub(crate) fn enabled_kinds(conn: &Connection, namespace: &str) -> Result<Vec<RecordKind>> {
316    if !namespace_vectorization(conn, namespace)? { return Ok(Vec::new()); }
317    let mut kinds = Vec::new();
318    for target in VectorizeTarget::ALL {
319        if target_vectorization(conn, namespace, target)? { kinds.extend_from_slice(target.kinds()); }
320    }
321    Ok(kinds)
322}
323
324/// 该 namespace 下「已启用、且该档已经补齐」的记录类型。
325/// 检索侧只让这些类型的存量向量参与打分:某档没补完,它就先不走向量。
326pub(crate) fn ready_kinds(conn: &Connection, namespace: &str, space_id: &str) -> Result<Vec<RecordKind>> {
327    if !namespace_vectorization(conn, namespace)? { return Ok(Vec::new()); }
328    let mut kinds = Vec::new();
329    for target in VectorizeTarget::ALL {
330        if target_vectorization(conn, namespace, target)? && vector_ready(conn, namespace, space_id, target)? {
331            kinds.extend_from_slice(target.kinds());
332        }
333    }
334    Ok(kinds)
335}
336
337/// `pending_candidates` 的类型判定:按记录自己所属的 namespace 逐档放行。
338/// 档位覆盖范围与默认值都取自 `VectorizeTarget`,不在 SQL 里另写一遍。
339fn enabled_kinds_sql() -> String {
340    VectorizeTarget::ALL.iter().map(|target| target_enabled_sql(*target)).collect::<Vec<_>>().join(" OR ")
341}
342
343/// 单档的放行条件。核对某个档的缺口时只用它,别的档在不在都不影响这一档的判定。
344fn target_enabled_sql(target: VectorizeTarget) -> String {
345    let codes = target.kinds().iter().map(|kind| kind.code().to_string()).collect::<Vec<_>>().join(",");
346    format!("(r.kind IN ({codes}) AND COALESCE((SELECT value FROM meta WHERE key='vectorize:'||n.text||':{}'),{}) = 1)",
347        target.as_str(), i64::from(target.default_enabled()))
348}
349
350// ── 待嵌入批次(库内部) ─────────────────────────────────────────────
351
352/// 取一批「已启用、已声明、且缺当前指纹向量」的记录与它们的正文。
353///
354/// 合格判定全部落在 SQL 里:领域、领域总闸、档位开关、指纹是否已补齐。
355/// 这样游标推进永远不会跳过仍需处理的记录,也不会把不合格记录反复取回来。
356/// 正文取不到的(切片文档还没进索引)在这里就是空串,由使用方决定要不要跳过。
357pub(crate) fn pending_candidates(conn: &Connection, index: &crate::index::TextIndex, space_id: &str, namespace: Option<&str>,
358    target: Option<VectorizeTarget>, limit: usize, after: Option<i64>, ids: Option<&[i64]>) -> Result<Vec<EmbeddingInput>> {
359    let mut sql = String::from("SELECT r.id,r.kind,r.payload_json,r.fingerprint,n.text,s.text FROM records r \
360        JOIN strings n ON n.id=r.namespace_id JOIN strings s ON s.id=r.scope_id WHERE 1=1");
361    let mut values: Vec<SqlValue> = Vec::new();
362    if let Some(namespace) = namespace {
363        sql.push_str(" AND n.text=?");
364        values.push(SqlValue::Text(text::normalized_tag(namespace)));
365    }
366    sql.push_str(&format!(" AND ({})", match target { Some(target) => target_enabled_sql(target), None => enabled_kinds_sql() }));
367    sql.push_str(" AND COALESCE((SELECT value FROM meta WHERE key='vectorize:'||n.text),1)=1");
368    sql.push_str(&format!(" AND NOT EXISTS(SELECT 1 FROM vectors.embeddings e WHERE e.space_id=? AND e.record_id=r.id AND e.fingerprint=r.fingerprint)"));
369    values.push(SqlValue::Text(space_id.into()));
370    if let Some(ids) = ids {
371        if ids.is_empty() { return Ok(Vec::new()); }
372        sql.push_str(&format!(" AND r.id IN ({})", vec!["?"; ids.len()].join(",")));
373        values.extend(ids.iter().map(|id| SqlValue::Integer(*id)));
374    }
375    if let Some(cursor) = after {
376        sql.push_str(" AND r.id>?");
377        values.push(SqlValue::Integer(cursor));
378    }
379    sql.push_str(" ORDER BY r.id LIMIT ?");
380    values.push(SqlValue::Integer(limit as i64));
381    let mut stmt = conn.prepare(&sql)?;
382    let mut items = Vec::new();
383    let mut candidates: Vec<(i64, i64, String, String, String, String)> = Vec::new();
384    for row in stmt.query_map(params_from_iter(values), |r| Ok((r.get::<_, i64>(0)?, r.get::<_, i64>(1)?, r.get::<_, String>(2)?, r.get::<_, String>(3)?, r.get::<_, String>(4)?, r.get::<_, String>(5)?)))? {
385        candidates.push(row?);
386    }
387    // 切片正文唯一的副本在索引里,按记录 ID 取回;其余记录的正文由 payload 现算,不碰文件。
388    let chunk_ids: Vec<i64> = candidates.iter().filter(|(_, kind, _, _, _, _)| *kind == RecordKind::Chunk.code()).map(|(id, _, _, _, _, _)| *id).collect();
389    let bodies = if chunk_ids.is_empty() { BTreeMap::new() } else { index.bodies(&chunk_ids)? };
390    for (id, kind_code, payload_json, fingerprint, namespace, scope) in candidates {
391        let kind = RecordKind::from_code(kind_code).ok_or_else(|| Error::Validation("invalid stored record kind".into()))?;
392        let payload: Option<serde_json::Value> = serde_json::from_str(&payload_json).ok();
393        // 切片正文只存在于索引;索引里查不到这条切片,说明写入侧还没提交这批索引,
394        // 不是「它没有正文」。这个区别由 `text_pending` 带给缺口核对。
395        let (body, text_pending) = match kind {
396            RecordKind::Chunk => match bodies.get(&id) { Some(body) => (body.clone(), false), None => (String::new(), true) },
397            _ => (payload.as_ref().map(|payload| storage::record_text(kind, payload)).unwrap_or_default(), false),
398        };
399        // 规范名不进正文列(见 record_text),但它对实体的语义不可或缺,向量这边单独补回去。
400        let name = match (kind, &payload) {
401            (RecordKind::Entity, Some(payload)) => payload.get("name").and_then(serde_json::Value::as_str).unwrap_or("").to_string(),
402            _ => String::new(),
403        };
404        let tags = record_tags(conn, id)?;
405        // 记忆是短句,标签是它的另一半特征,两者一起送进向量;其余记录用正文,实体的规范名补在正文前。
406        let text = match (kind, &tags) {
407            (RecordKind::Memory, tags) if !tags.is_empty() => format!("{body}\n{}", tags.join(" ")),
408            (RecordKind::Entity, _) if !name.is_empty() => format!("{name}\n{body}"),
409            _ => body,
410        };
411        let note_id = if kind == RecordKind::Chunk {
412            conn.query_row("SELECT note_id FROM chunks WHERE record_id=?1", [id], |r| r.get(0)).optional()?.unwrap_or(0)
413        } else { 0 };
414        items.push(EmbeddingInput { key: RecordKey { id }, text, fingerprint, namespace, scope, kind, tags, note_id, text_pending });
415    }
416    Ok(items)
417}
418
419/// 一条记录该不该送进向量:正文为空的没有可嵌入的内容,硬凑一个向量没有意义。
420/// 切片正文还在索引里排队(`text_pending`)时也走这条判定被跳过——绝不用空文本凑一个向量;
421/// 但那种情况不算「无内容可嵌」,缺口核对仍会把它记成缺口,该档不会因此就绪。
422fn embeddable(input: &EmbeddingInput) -> bool { !input.text.trim().is_empty() }
423
424/// 缺口核对时一次取多少候选。命中缺口立刻返回,取满说明后面还有。
425const GAP_PROBE: usize = 256;
426
427/// 某一档还有没有缺口:还有「该档启用、且有正文可嵌入」的记录缺当前指纹的向量。
428/// 与 `pending_candidates` 共用同一份候选判定,只是这里不调模型、不写任何东西。
429/// 正文仍在索引里排队(`text_pending`)的切片也算缺口:它这轮补不上,不代表这一档就绪。
430fn has_gap(conn: &Connection, index: &crate::index::TextIndex, space_id: &str, namespace: &str, target: VectorizeTarget) -> Result<bool> {
431    let mut cursor: Option<i64> = None;
432    loop {
433        let candidates = pending_candidates(conn, index, space_id, Some(namespace), Some(target), GAP_PROBE, cursor, None)?;
434        let Some(last) = candidates.last() else { return Ok(false) };
435        if candidates.iter().any(|input| embeddable(input) || input.text_pending) { return Ok(true); }
436        cursor = Some(last.key.id);
437    }
438}
439
440// ── 就绪标记 ──────────────────────────────────────────────────────────
441
442/// 就绪标记的键:`vector_ready:<领域>:<空间>:<档位>`。键在即就绪,值恒为 1。
443/// 记忆、图谱、笔记各记一份:三档的向量各自补齐、各自放行,互不牵连。
444fn vector_ready_key(namespace: &str, space_id: &str, target: VectorizeTarget) -> String {
445    format!("vector_ready:{}:{}:{}", text::normalized_tag(namespace), space_id, target.as_str())
446}
447
448/// 某领域的某一档在该空间下是否已补齐。读不到标记就是没就绪。
449/// 就绪标记住在向量库的 `vector_meta`,与向量行同库。
450pub(crate) fn vector_ready(conn: &Connection, namespace: &str, space_id: &str, target: VectorizeTarget) -> Result<bool> {
451    let key = vector_ready_key(namespace, space_id, target);
452    let value: Option<String> = conn.query_row("SELECT value FROM vectors.vector_meta WHERE key=?1",
453        [&key], |r| r.get(0)).optional()?;
454    Ok(value.as_deref() == Some("1"))
455}
456
457/// 记下或撤掉某领域某一档在该空间上的就绪标记,写进向量库自己的 meta。
458fn set_vector_ready(conn: &Connection, namespace: &str, space_id: &str, target: VectorizeTarget, ready: bool) -> Result<()> {
459    let key = vector_ready_key(namespace, space_id, target);
460    if ready {
461        conn.execute("INSERT INTO vector_meta(key,value) VALUES (?1,'1') ON CONFLICT(key) DO UPDATE SET value='1'",
462            [&key])?;
463    } else {
464        conn.execute("DELETE FROM vector_meta WHERE key=?1", [&key])?;
465    }
466    Ok(())
467}
468
469/// 作废某领域的全部就绪标记。记录或档位一变,「应向量化集合」就变了,
470/// 必须等补齐核对过再重新标;按前缀精确比对,不依赖 LIKE 的转义规则。
471/// 标记在向量库自己的 meta 里,撤它不动主库。
472/// `conn` 是向量外挂库的写连接。
473pub(crate) fn clear_vector_ready(conn: &Connection, namespace: &str) -> Result<()> {
474    let prefix = format!("vector_ready:{}:", text::normalized_tag(namespace));
475    conn.execute("DELETE FROM vector_meta WHERE substr(key,1,?1)=?2",
476        params![prefix.chars().count() as i64, prefix])?;
477    Ok(())
478}
479
480/// 一条记录的标签文本(按字典序,与写入时 `normalize_tags` 的顺序一致)。
481fn record_tags(conn: &Connection, id: i64) -> Result<Vec<String>> {
482    let mut stmt = conn.prepare("SELECT t.text FROM record_tags rt JOIN strings t ON t.id=rt.tag_id WHERE rt.record_id=?1 ORDER BY t.text")?;
483    let mut tags = Vec::new();
484    for row in stmt.query_map([id], |r| r.get::<_, String>(0))? { tags.push(row?); }
485    Ok(tags)
486}
487
488/// 单批回调的结果。
489enum EmbedOutcome { Vectors(Vec<Vec<f32>>), Shrunk, Failed(String) }
490
491/// 调回调并按类别处理:批次过大减半、限流退避重试、其它直接失败。
492/// 减半值写在 `entry.effective_batch` 上,因此在本进程内持久生效。
493fn embed_with_retry(entry: &mut EmbedderEntry, texts: &[String]) -> EmbedOutcome {
494    let mut attempts = 0u32;
495    loop {
496        match entry.embed(texts) {
497            Ok(values) => return EmbedOutcome::Vectors(values),
498            Err(error) if error.kind == EmbedErrorKind::TooLarge => {
499                if entry.effective_batch <= 1 { return EmbedOutcome::Failed(format!("batch size 1 was still rejected: {}", error.message)); }
500                entry.effective_batch /= 2;
501                return EmbedOutcome::Shrunk;
502            }
503            Err(error) if error.kind == EmbedErrorKind::RateLimited && attempts < RATE_LIMIT_ATTEMPTS => {
504                attempts += 1;
505                std::thread::sleep(std::time::Duration::from_millis(RATE_LIMIT_BACKOFF_MS * u64::from(attempts)));
506            }
507            Err(error) => return EmbedOutcome::Failed(error.message),
508        }
509    }
510}
511
512/// 一次对账的实际完成量。中断时已写回的批次保留,未跑的批次不写。
513#[derive(Debug, Clone, Default, Serialize, Deserialize)]
514pub struct SyncReport { pub scanned: usize, pub written: usize, pub batches: usize, pub deleted: usize, pub interrupted: Option<String> }
515
516impl EmbeddingStore {
517    pub fn register_space(&self, space: EmbeddingSpace) -> Result<WriteReceipt<EmbeddingSpace>> {
518        storage::validate_identity("space id", &space.id)?;
519        storage::validate_identity("model", &space.model)?;
520        if !(1..=65_536).contains(&space.dimension) || space.text_version != 1 { return Err(Error::Validation("dimension must be 1..65536 and text_version must be 1".into())); }
521        if space.encoding != "f32" && space.encoding != "sq8" { return Err(Error::Validation("encoding must be \"f32\" or \"sq8\"".into())); }
522        self.0.mutate_meta(|tx| {
523            match get_space(tx, &space.id) {
524                Ok(old) if old == space => return Ok(old),
525                Ok(_) => return Err(Error::Conflict("embedding space is immutable; register a new ID for a new model or dimension".into())),
526                Err(Error::NotFound(_)) => (),
527                Err(err) => return Err(err),
528            }
529            tx.execute("INSERT INTO embedding_spaces(id,model,dimension,text_version,encoding) VALUES (?1,?2,?3,?4,?5)", params![space.id, space.model, space.dimension as i64, space.text_version, space.encoding])?;
530            Ok(space)
531        })
532    }
533    pub fn spaces(&self) -> Result<Vec<EmbeddingSpace>> {
534        let state = self.0.read()?;
535        let mut stmt = state.conn().prepare("SELECT id,model,dimension,text_version,encoding FROM embedding_spaces ORDER BY id")?;
536        let rows = stmt.query_map([], |r| Ok(EmbeddingSpace { id: r.get(0)?, model: r.get(1)?, dimension: r.get::<_, u32>(2)? as usize, text_version: r.get(3)?, encoding: r.get(4)? }))?;
537        Ok(rows.collect::<std::result::Result<Vec<_>, _>>()?)
538    }
539
540    /// 把一个嵌入回调绑到一个向量空间。一个向量模型 = 一个向量空间。
541    pub fn register_embedder<F: Embedder + 'static>(&self, space_id: &str, embedder: F) -> Result<()> {
542        self.register_embedder_with(space_id, embedder, EmbedderOptions::default())
543    }
544
545    /// 注册即校验:用样本真跑一遍完整链路,产出不符该空间契约就拒绝绑定。
546    /// 这样能挡住「回调绑错空间」「换模型后忘改 dimension」——它们若不在这里拒掉,
547    /// 要等第一次写回时才报 `invalid_vector`,那时空间已经建好、离原因很远。
548    pub fn register_embedder_with<F: Embedder + 'static>(&self, space_id: &str, embedder: F, options: EmbedderOptions) -> Result<()> {
549        storage::validate_identity("space id", space_id)?;
550        if !(1..=10_000).contains(&options.max_batch) { return Err(Error::Validation("max_batch must be between 1 and 10000".into())); }
551        if options.max_tokens_per_text == Some(0) { return Err(Error::Validation("max_tokens_per_text must be positive".into())); }
552        let space = { let state = self.0.read()?; get_space(state.conn(), space_id)? };
553        let mut entry = EmbedderEntry { options, effective_batch: options.max_batch, embedder: Box::new(embedder) };
554        let samples: Vec<String> = SAMPLE_TEXTS.iter().take(3.min(entry.effective_batch)).map(|sample| (*sample).to_string()).collect();
555        // 校验调用发生在注册表之外:此刻还没有任何锁被持有。
556        let produced = entry.embed(&samples)
557            .map_err(|error| Error::Validation(format!("embedder failed during registration ({}): {}", error.kind.code(), error.message)))?;
558        validate_vectors(&produced, samples.len(), &space)?;
559        self.0.engine.embedders.register(space_id.to_string(), entry);
560        Ok(())
561    }
562
563    /// 该空间的定义;从未注册过该空间时返回 `None`。
564    pub fn embedder_space(&self, space_id: &str) -> Result<Option<EmbeddingSpace>> {
565        let state = self.0.read()?;
566        match get_space(state.conn(), space_id) { Ok(space) => Ok(Some(space)), Err(Error::NotFound(_)) => Ok(None), Err(error) => Err(error) }
567    }
568
569    pub fn unregister_embedder(&self, space_id: &str) -> Result<bool> { Ok(self.0.engine.embedders.remove(space_id)) }
570
571    /// 该 namespace 是否启用向量化。开关落盘,配置一次即生效。
572    pub fn namespace_vectorization(&self, namespace: &str) -> Result<bool> {
573        storage::validate_identity("namespace", namespace)?;
574        let state = self.0.read()?;
575        namespace_vectorization(state.conn(), namespace)
576    }
577
578    pub fn set_namespace_vectorization(&self, namespace: &str, enabled: bool) -> Result<WriteReceipt<bool>> {
579        storage::validate_identity("namespace", namespace)?;
580        let receipt = self.0.mutate_meta(|tx| {
581            tx.execute("INSERT INTO meta(key,value) VALUES (?1,?2) ON CONFLICT(key) DO UPDATE SET value=excluded.value",
582                params![vectorize_key(namespace), i64::from(enabled)])?;
583            Ok(enabled)
584        })?;
585        // 总闸一变,「应向量化集合」就变了:已记的就绪状态当场作废。
586        {
587            let mut guard = self.0.engine.vector_writer.lock();
588            if let Some(writer) = guard.as_mut() {
589                clear_vector_ready(&writer.conn, namespace)?;
590            }
591        }
592        Ok(receipt)
593    }
594
595    /// 某档开关在该领域下的取值;没设置过时给出该档的内置默认。
596    /// `target` 取 `memory` / `graph` / `notes`。
597    pub fn vectorization(&self, namespace: &str, target: &str) -> Result<bool> {
598        storage::validate_identity("namespace", namespace)?;
599        let target = VectorizeTarget::parse(target)?;
600        let state = self.0.read()?;
601        target_vectorization(state.conn(), namespace, target)
602    }
603
604    /// 设置某档开关。只影响以后是否生成向量,已有向量保留在库里。
605    pub fn set_vectorization(&self, namespace: &str, target: &str, enabled: bool) -> Result<WriteReceipt<bool>> {
606        storage::validate_identity("namespace", namespace)?;
607        let target = VectorizeTarget::parse(target)?;
608        let receipt = self.0.mutate_meta(|tx| {
609            tx.execute("INSERT INTO meta(key,value) VALUES (?1,?2) ON CONFLICT(key) DO UPDATE SET value=excluded.value",
610                params![target_vectorize_key(namespace, target), i64::from(enabled)])?;
611            Ok(enabled)
612        })?;
613        // 档位一变,「应向量化集合」就变了:已记的就绪状态当场作废。
614        {
615            let mut guard = self.0.engine.vector_writer.lock();
616            if let Some(writer) = guard.as_mut() {
617                clear_vector_ready(&writer.conn, namespace)?;
618            }
619        }
620        Ok(receipt)
621    }
622
623    /// 某领域的某一档在该空间下是否已补齐;`target` 取 `memory` / `graph` / `notes`。
624    /// 未就绪的那一档,检索不让它的向量参与打分。
625    pub fn vector_ready(&self, namespace: &str, space_id: &str, target: &str) -> Result<bool> {
626        storage::validate_identity("namespace", namespace)?;
627        storage::validate_identity("space id", space_id)?;
628        let target = VectorizeTarget::parse(target)?;
629        let state = self.0.read()?;
630        vector_ready(state.conn(), namespace, space_id, target)
631    }
632
633    /// 向量对账(对外写入口之一,显式调用):取消就绪 → 读向量库、读主库 → 算差异 →
634    /// 先删后生成 → 核对并标就绪。它不提交索引、不碰主库写锁:切片的正文存在索引里,
635    /// 索引尚未被写入侧提交时这批切片这轮取不到正文、跳过,等调用方 `update_index`
636    /// 之后再调一次。模型调用是网络往返,绝不持有任何库锁:循环严格三段式——
637    /// 读快照取一批文本(随即放锁)→ 调回调(不持任何库锁)→ 短事务写回这一批。
638    pub fn sync(&self, space_id: &str, batch: usize) -> Result<WriteReceipt<SyncReport>> {
639        storage::validate_limit(batch)?;
640        let Some(entry) = self.0.engine.embedders.get(space_id) else {
641            return Err(Error::Validation(format!("no embedder registered for space {space_id}")));
642        };
643        // 1. 取消就绪:本空间的就绪标记全部先撤,差异补齐核对过再重标。
644        {
645            let mut guard = self.0.engine.vector_writer.lock();
646            let writer = guard.as_mut().ok_or(Error::Closed)?;
647            writer.conn.execute("DELETE FROM vector_meta WHERE key GLOB 'vector_ready:*:'||?1||':*'", [space_id])?;
648        }
649        // 2. 读两边、算差异:需要删的 = 向量库里记录已不在主库的孤儿行
650        //    (删除流程跨库非原子,断电可能留下它们)。指纹不符的不用删——
651        //    主键就是 (space, record),生成阶段原地覆盖。
652        let orphans: Vec<(i64, String)> = {
653            let main_ids: HashSet<i64> = {
654                let state = self.0.read()?;
655                let mut stmt = state.conn().prepare("SELECT id FROM records")?;
656                let rows = stmt.query_map([], |r| r.get::<_, i64>(0))?;
657                rows.collect::<std::result::Result<HashSet<_>, _>>()?
658            };
659            let mut guard = self.0.engine.vector_writer.lock();
660            let writer = guard.as_mut().ok_or(Error::Closed)?;
661            let mut stmt = writer.conn.prepare("SELECT record_id,namespace FROM embeddings WHERE space_id=?1")?;
662            let mut rows = stmt.query(params![space_id])?;
663            let mut out = Vec::new();
664            while let Some(row) = rows.next()? {
665                let (record_id, namespace): (i64, String) = (row.get(0)?, row.get(1)?);
666                if !main_ids.contains(&record_id) { out.push((record_id, namespace)); }
667            }
668            out
669        };
670        let mut report = SyncReport::default();
671        // 3. 先删:孤儿向量行一次清掉。
672        if !orphans.is_empty() {
673            let mut guard = self.0.engine.vector_writer.lock();
674            let writer = guard.as_mut().ok_or(Error::Closed)?;
675            let tx = writer.conn.transaction_with_behavior(rusqlite::TransactionBehavior::Immediate)?;
676            for (record_id, _) in &orphans {
677                tx.execute("DELETE FROM embeddings WHERE space_id=?1 AND record_id=?2", params![space_id, record_id])?;
678            }
679            tx.commit()?;
680            report.deleted = orphans.len();
681            let touched: HashSet<String> = orphans.iter().map(|(_, namespace)| namespace.clone()).collect();
682            self.0.engine.vectors.invalidate_namespaces(&touched);
683        }
684        // 4. 后生成:缺向量与指纹不符的记录分批补齐。
685        let filled = self.drain(space_id, &entry, batch)?;
686        report.scanned = filled.scanned;
687        report.written = filled.written;
688        report.batches = filled.batches;
689        report.interrupted = filled.interrupted;
690        if report.interrupted.is_some() { self.0.note_degrade(Degrade::EmbedFailed); }
691        // 5. 逐档核对缺口,把补齐的标成就绪。
692        self.verify_and_mark(space_id)?;
693        let state = self.0.read()?;
694        Ok(WriteReceipt { value: report, revision: storage::current_revision(state.conn())? })
695    }
696
697    /// 逐档核对缺口并把结果写成就绪标记(该档缺口为 0 才算就绪)。
698    ///
699    /// 核对在**读连接**上做,写锁只用于最后落标记:核对要扫一遍记录,
700    /// 若整段扣着写锁,写入与读取的自愈都会被它挡住——读路径拿不到写锁就不再等,
701    /// 刚写完的记录于是短暂查不到。核对期间有人写过就重来(最多几次);
702    /// 一直有人在写就不标:宁可停在未就绪,也不给「补了半个领域」的假象。
703    fn verify_and_mark(&self, space_id: &str) -> Result<()> {
704        for _attempt in 0..VERIFY_ATTEMPTS {
705            let (revision, marks) = {
706                let state = self.0.read()?;
707                let index = self.0.index()?;
708                let conn = state.conn();
709                let revision = storage::current_revision(conn)?;
710                let mut marks = Vec::new();
711                for namespace in storage::record_namespaces(conn)? {
712                    for target in VectorizeTarget::ALL {
713                        let enabled = target_vectorization(conn, &namespace, target)?;
714                        let gap = has_gap(conn, &index, space_id, &namespace, target)?;
715                        let ready = enabled && !gap;
716                        marks.push((namespace.clone(), target, ready));
717                    }
718                }
719                (revision, marks)
720            };
721            {
722                let mut guard = self.0.engine.vector_writer.lock();
723                let writer = guard.as_mut().ok_or(Error::Closed)?;
724                let current_rev = { let state = self.0.read()?; storage::current_revision(state.conn())? };
725                if current_rev != revision {
726                    continue;
727                }
728                for (namespace, target, ready) in &marks {
729                    set_vector_ready(&writer.conn, namespace, space_id, *target, *ready)?;
730                }
731                return Ok(());
732            }
733        }
734        Ok(())
735    }
736
737    /// 分段补齐的实际循环。
738    fn drain(&self, space_id: &str, entry: &Arc<Mutex<EmbedderEntry>>, batch: usize) -> Result<SyncReport> {
739        let mut report = SyncReport::default();
740        let mut guard = entry.lock();
741        let mut cursor: Option<i64> = None;
742        loop {
743            let limit = batch.min(guard.effective_batch).max(1);
744            let candidates = {
745                let state = self.0.read()?;
746                let index = self.0.index()?;
747                pending_candidates(state.conn(), &index, space_id, None, None, limit, cursor, None)?
748            };
749            let Some(last) = candidates.last().map(|input| input.key.id) else { break };
750            let pending: Vec<EmbeddingInput> = candidates.into_iter().filter(embeddable).collect();
751            if pending.is_empty() { cursor = Some(last); continue; }
752            report.scanned += pending.len();
753            let texts: Vec<String> = pending.iter().map(|input| input.text.clone()).collect();
754            let values = match embed_with_retry(&mut guard, &texts) {
755                EmbedOutcome::Shrunk => continue,
756                EmbedOutcome::Failed(message) => { report.interrupted = Some(message); break; }
757                EmbedOutcome::Vectors(values) => values,
758            };
759            if values.len() != pending.len() {
760                report.interrupted = Some(format!("embedder returned {} vectors for {} inputs", values.len(), pending.len()));
761                break;
762            }
763            let writes: Vec<EmbeddingWrite> = pending.iter().zip(values).map(|(input, vector)|
764                EmbeddingWrite { key: input.key, fingerprint: input.fingerprint.clone(), values: vector,
765                    namespace: input.namespace.clone(), scope: input.scope.clone(), kind: input.kind,
766                    tags: input.tags.clone(), note_id: input.note_id }).collect();
767            match self.put(space_id, &writes) {
768                Ok(receipt) => { report.written += receipt.value; report.batches += 1; }
769                Err(Error::StaleRevision(_)) => {}
770                Err(error) => return Err(error),
771            }
772            cursor = Some(last);
773        }
774        Ok(report)
775    }
776
777    /// 一批向量落进外挂库:指纹在主库读连接上核,写入只走向量库自己的写连接。
778    /// 回调期间主库里的记录被改写过就整批作废——它会在下一轮以新指纹重新出现。
779    pub(crate) fn put(&self, space_id: &str, writes: &[EmbeddingWrite]) -> Result<WriteReceipt<usize>> {
780        let space = { let state = self.0.read()?; get_space(state.conn(), space_id)? };
781        // 指纹核对在主库读连接上做:不拿主库写锁,也不为核对去写主库。
782        {
783            let state = self.0.read()?;
784            for write in writes {
785                let actual: Option<String> = state.conn().query_row("SELECT fingerprint FROM records WHERE id=?1", [write.key.id], |r| r.get(0)).optional()?;
786                let actual = actual.ok_or_else(|| Error::NotFound(write.key.id.to_string()))?;
787                if actual != write.fingerprint { return Err(Error::StaleRevision(write.key.id.to_string())); }
788            }
789        }
790        let mut guard = self.0.engine.vector_writer.lock();
791        let writer = guard.as_mut().ok_or(Error::Closed)?;
792        let tx = writer.conn.transaction_with_behavior(rusqlite::TransactionBehavior::Immediate)?;
793        for write in writes {
794            let normalized = normalize(&write.values, space.dimension)?;
795            let bytes = encode_values(&normalized, &space.encoding)?;
796            tx.execute("INSERT INTO embeddings(space_id,record_id,namespace,scope,kind,tags_json,note_id,fingerprint,vector)
797                VALUES (?1,?2,?3,?4,?5,?6,?7,?8,?9)
798                ON CONFLICT(space_id,record_id) DO UPDATE SET
799                namespace=excluded.namespace,scope=excluded.scope,kind=excluded.kind,
800                tags_json=excluded.tags_json,note_id=excluded.note_id,
801                fingerprint=excluded.fingerprint,vector=excluded.vector",
802                params![space_id, write.key.id, write.namespace, write.scope, write.kind.code(),
803                    serde_json::to_string(&write.tags)?, write.note_id, write.fingerprint, bytes])?;
804        }
805        tx.commit()?;
806        // 向量一落盘,这些记录所属领域的向量分区就变了。
807        let touched: std::collections::HashSet<String> = writes.iter().map(|w| w.namespace.clone()).collect();
808        self.0.engine.vectors.invalidate_namespaces(&touched);
809        Ok(WriteReceipt { value: writes.len(), revision: 0 })
810    }
811
812    /// 删掉一个向量空间:定义在主库,向量与就绪标记在外挂库,两边一起清。
813    /// 外挂库没有跨库外键,主库删行不会级联,这里显式补两条删除。
814    pub fn delete_space(&self, id: &str) -> Result<WriteReceipt<bool>> {
815        let receipt = self.0.mutate_meta(|tx| {
816            let deleted = tx.execute("DELETE FROM embedding_spaces WHERE id=?1", [id])? > 0;
817            Ok(deleted)
818        })?;
819        if receipt.value {
820            let mut guard = self.0.engine.vector_writer.lock();
821            if let Some(writer) = guard.as_mut() {
822                writer.conn.execute("DELETE FROM embeddings WHERE space_id=?1", [id])?;
823                // 就绪键形如 vector_ready:<领域>:<空间>:<档位>。空间 id 是第二段:
824                // 找到第一个冒号之后、到第二个冒号之间的子串,精确比对。
825                writer.conn.execute("DELETE FROM vector_meta WHERE key GLOB 'vector_ready:*:'||?1||':*'", [id])?;
826            }
827        }
828        // 该空间所有分区条目一并作废:它们指向的向量行已经没了。
829        self.0.engine.vectors.invalidate();
830        Ok(receipt)
831    }
832}
833
834/// 注册校验:条数一致,且逐条满足维度、有限性、非零范数。
835fn validate_vectors(produced: &[Vec<f32>], expected: usize, space: &EmbeddingSpace) -> Result<()> {
836    if produced.len() != expected {
837        return Err(Error::InvalidVector(format!("embedder returned {} vectors for {expected} inputs", produced.len())));
838    }
839    for values in produced {
840        normalize(values, space.dimension)
841            .map_err(|error| Error::InvalidVector(format!("embedder output does not satisfy space {}: {error}", space.id)))?;
842    }
843    Ok(())
844}
845
846// ── 向量分区与就近检索 ────────────────────────────────────────────────
847
848/// 就绪核对最多重试几次:核对期间有人写就重来,几次都有人在写就先不标。
849const VERIFY_ATTEMPTS: usize = 3;
850
851/// 一行常驻向量的元信息。`note` 是它所属笔记的记录 id,只有 chunk 有归属,其余为 0。
852struct VectorRow { key: RecordKey, kind: RecordKind, tags: Vec<String>, note: i64, #[allow(dead_code)] fingerprint: String }
853/// 分区内的向量以**存储形态**常驻:sq8 空间保留原始 i8 码与逐条 scale,
854/// 不再展开成 f32,因此常驻内存与磁盘体积同量级(1024 维约 1KB/条,而非 4KB/条)。
855enum PartitionData {
856    F32(Vec<f32>),
857    Sq8 { codes: Vec<i8>, scales: Vec<f32> },
858}
859/// 一个 `(space, namespace, scope)` 分区的向量:按需载入,一次查询只碰自己这块。
860pub(crate) struct Partition { dimension: usize, rows: Vec<VectorRow>, data: PartitionData }
861
862// Reverse score ordering makes the heap root the worst retained candidate.
863struct Candidate { score: f64, key: RecordKey }
864impl PartialEq for Candidate { fn eq(&self, other: &Self) -> bool { self.score == other.score && self.key == other.key } }
865impl Eq for Candidate {}
866impl PartialOrd for Candidate { fn partial_cmp(&self, other: &Self) -> Option<Ordering> { Some(self.cmp(other)) } }
867impl Ord for Candidate {
868    fn cmp(&self, other: &Self) -> Ordering { other.score.total_cmp(&self.score).then_with(|| self.key.cmp(&other.key)) }
869}
870
871impl Partition {
872    /// 载入指定 `(space, namespace, scope)` 的向量。该范围内没有向量时返回 `None`,
873    /// 空结果也会被缓存,避免每次查询都回库。
874    pub fn load(conn: &Connection, space: &EmbeddingSpace, namespace: &str, scope: &str) -> Result<Option<Self>> {
875        let namespace = text::normalized_tag(namespace);
876        let scope = text::normalized_tag(scope);
877        // 不要 `ORDER BY r.id`:行序对结果没有意义(并列名次由 `retain` 显式按 key 比,
878        // 行只按序号取自己那段向量),而排序会让 SQLite 把整行搬进临时 B 树再读回来——
879        // 一万行 1KB 的向量就是几十 MB 白走两遍。索引本身按 (space_id, record_id) 走,
880        // 行序天然是 id 升序。
881        // 向量外挂库自包含路由:namespace/scope/kind/tags/note_id 全在向量行里,
882        // 加载分区纯读 vectors.sqlite3,不回主库 join。
883        let mut stmt = conn.prepare("SELECT record_id,kind,vector,tags_json,note_id,fingerprint
884            FROM embeddings WHERE space_id=?1 AND namespace=?2 AND scope=?3")?;
885        let sq8 = space.encoding == "sq8";
886        let mut partition = Self {
887            dimension: space.dimension,
888            rows: vec![],
889            data: if sq8 { PartitionData::Sq8 { codes: vec![], scales: vec![] } } else { PartitionData::F32(vec![]) },
890        };
891        let mut rows = stmt.query(params![space.id, namespace, scope])?;
892        while let Some(row) = rows.next()? {
893            let key = RecordKey { id: row.get(0)? };
894            let kind = RecordKind::from_code(row.get::<_, i64>(1)?).ok_or_else(|| Error::InvalidVector("invalid stored record kind".into()))?;
895            let bytes: Vec<u8> = row.get(2)?;
896            let tags: Vec<String> = serde_json::from_str(&row.get::<_, String>(3)?)?;
897            let note: i64 = row.get(4)?;
898            // 指纹带上,检索时跟主库核对,主库改了这条就作废。
899            let fingerprint: String = row.get(5)?;
900            match &mut partition.data {
901                PartitionData::F32(values) => {
902                    let decoded = decode_values(&bytes, space.dimension, "f32")?;
903                    if decoded.iter().any(|v| !v.is_finite()) { return Err(Error::InvalidVector("stored vector contains nonfinite values".into())); }
904                    values.extend(decoded);
905                }
906                PartitionData::Sq8 { codes, scales } => {
907                    let (scale, decoded) = decode_sq8(&bytes, space.dimension)?;
908                    if !scale.is_finite() { return Err(Error::InvalidVector("stored vector contains nonfinite values".into())); }
909                    scales.push(scale);
910                    codes.extend(decoded);
911                }
912            }
913            partition.rows.push(VectorRow { key, kind, tags, note, fingerprint });
914        }
915        Ok(if partition.rows.is_empty() { None } else { Some(partition) })
916    }
917
918    /// 行是否参与本次打分:白名单、笔记、类型、标签四重过滤。
919    /// `note_ids` 为空表示不按笔记限定;非 chunk 行的笔记归属是 0,给了限定就必然出局。
920    fn matches(&self, row: &VectorRow, kinds: &[RecordKind], tags: &[String], note_ids: &[i64], allowed: Option<&HashSet<i64>>) -> bool {
921        if allowed.is_some_and(|set| !set.contains(&row.key.id)) { return false; }
922        if !note_ids.is_empty() && !note_ids.contains(&row.note) { return false; }
923        (kinds.is_empty() || kinds.contains(&row.kind)) && tags.iter().all(|t| row.tags.contains(t))
924    }
925
926    /// 维护 top-`limit` 的最小堆:满员后只在分数更好或分数相同但 key 更小时替换。
927    fn retain(heap: &mut BinaryHeap<Candidate>, key: RecordKey, scored: f64, limit: usize) {
928        let score = scored.clamp(-1.0, 1.0);
929        if heap.len() < limit { heap.push(Candidate { score, key }); }
930        else if let Some(worst) = heap.peek() {
931            if score > worst.score || (score == worst.score && key < worst.key) {
932                heap.pop(); heap.push(Candidate { score, key });
933            }
934        }
935    }
936
937    /// 分区内精确打分,返回本分区 top-`limit`(按分数、key 排序)。
938    /// `kinds` / `tags` / `note_ids` / `allowed` 均由调用方归一化并传入;`allowed` 为 `Some` 时只给这批 record_id 打分。
939    pub fn search(&self, query: &[f32], kinds: &[RecordKind], tags: &[String], note_ids: &[i64], limit: usize, allowed: Option<&HashSet<i64>>) -> Result<Vec<(RecordKey, f64)>> {
940        let query = normalize(query, self.dimension)?;
941        let mut heap = BinaryHeap::<Candidate>::new();
942        match &self.data {
943            PartitionData::F32(values) => {
944                for (i, row) in self.rows.iter().enumerate() {
945                    if !self.matches(row, kinds, tags, note_ids, allowed) { continue; }
946                    let offset = i * self.dimension;
947                    Self::retain(&mut heap, row.key, f64::from(dot(&query, &values[offset..offset + self.dimension])), limit);
948                }
949            }
950            PartitionData::Sq8 { codes, scales } => {
951                // 查询也量化一次,整条打分链路保持 i8;查询的 scale 对本分区所有行一致,不影响排序。
952                let (query_codes, query_scale) = encode_query_sq8(&query);
953                for (i, row) in self.rows.iter().enumerate() {
954                    if !self.matches(row, kinds, tags, note_ids, allowed) { continue; }
955                    let offset = i * self.dimension;
956                    let raw = query_scale * scales[i] * dot_codes(&query_codes, &codes[offset..offset + self.dimension]) as f32;
957                    Self::retain(&mut heap, row.key, f64::from(raw), limit);
958                }
959            }
960        }
961        let mut result: Vec<_> = heap.into_iter().map(|c| (c.key, c.score)).collect();
962        result.sort_by(|a,b| b.1.total_cmp(&a.1).then_with(|| a.0.cmp(&b.0)));
963        Ok(result)
964    }
965}
966
967#[cfg(test)]
968mod tests {
969    use super::*;
970    use std::sync::atomic::{AtomicUsize, Ordering as AtomicOrdering};
971
972    /// AVX2 内核与标量内核必须逐位一致,包括长度不是 16 倍数的尾部。
973    #[test]
974    fn integer_kernel_matches_scalar() {
975        for dimension in [1usize, 15, 16, 17, 250, 1024] {
976            let left: Vec<i8> = (0..dimension).map(|i| ((i * 37 % 255) as i32 - 127) as i8).collect();
977            let right: Vec<i8> = (0..dimension).map(|i| ((i * 91 % 255) as i32 - 127) as i8).collect();
978            assert_eq!(dot_codes(&left, &right), dot_codes_scalar(&left, &right), "dimension {dimension}");
979        }
980    }
981
982    /// 并列名次按 key 升序,与向量行的载入顺序无关。
983    /// 载入顺序由 SQLite 的执行计划决定(索引怎么走、要不要临时排序),结果若依赖它,
984    /// 就等于把契约交给了计划;载入 SQL 因此不必排序。这里用相反的两种行序各查一次。
985    #[test]
986    fn tied_scores_break_by_key_whatever_the_row_order() {
987        let dimension = 4usize;
988        let query = vec![1.0f32, 0.0, 0.0, 0.0];
989        // 四行向量都与查询同向:分数并列,只能按 key 决出名次。
990        let build = |order: &[i64]| Partition {
991            dimension,
992            rows: order.iter().map(|id| VectorRow { key: RecordKey { id: *id }, kind: RecordKind::Memory, tags: vec![], note: 0, fingerprint: String::new() }).collect(),
993            data: PartitionData::F32(order.iter().flat_map(|_| [1.0f32, 0.0, 0.0, 0.0]).collect()),
994        };
995        let top_two = |partition: &Partition| -> Vec<i64> {
996            partition.search(&query, &[], &[], &[], 2, None).unwrap().into_iter().map(|(key, _)| key.id).collect()
997        };
998        assert_eq!(top_two(&build(&[1, 2, 3, 4])), vec![1, 2], "并列时取 key 最小的两条");
999        assert_eq!(top_two(&build(&[4, 3, 2, 1])), vec![1, 2], "换个行序,结果必须一样");
1000    }
1001
1002    /// sq8 常驻 i8、查询也量化后,分数必须贴合「解码成 f32 再点积」的参考值。
1003    #[test]
1004    fn quantized_query_tracks_decoded_f32_kernel() {
1005        let dimension = 256usize;
1006        let stored_raw: Vec<f32> = (0..dimension).map(|i| (i as f32 * 0.37).sin() + 0.25).collect();
1007        let query_raw: Vec<f32> = (0..dimension).map(|i| (i as f32 * 0.11).cos() - 0.1).collect();
1008        let stored = normalize(&stored_raw, dimension).unwrap();
1009        let query = normalize(&query_raw, dimension).unwrap();
1010        let encoded = encode_values(&stored, "sq8").unwrap();
1011        let (scale, codes) = decode_sq8(&encoded, dimension).unwrap();
1012        let decoded = decode_values(&encoded, dimension, "sq8").unwrap();
1013        let reference = dot(&query, &decoded);
1014        let (query_codes, query_scale) = encode_query_sq8(&query);
1015        let actual = query_scale * scale * dot_codes(&query_codes, &codes) as f32;
1016        // 差异只来自查询那一次的 sq8 量化,量级应在千分之一以内。
1017        assert!((reference - actual).abs() < 1e-3, "reference {reference} vs actual {actual}");
1018    }
1019
1020    /// 批次过大时减半重试,减半值落在 entry 上(进程内持久),且下一批按新上限切分。
1021    #[test]
1022    fn oversized_batches_shrink_and_persist() {
1023        let lengths = std::sync::Arc::new(Mutex::new(Vec::<usize>::new()));
1024        let observed = lengths.clone();
1025        let mut entry = EmbedderEntry {
1026            options: EmbedderOptions { max_batch: 8, max_tokens_per_text: None },
1027            effective_batch: 8,
1028            embedder: Box::new(move |texts: &[String]| {
1029                observed.lock().push(texts.len());
1030                if texts.len() > 4 { return Err(EmbedCallbackError::too_large("too many texts")); }
1031                Ok(texts.iter().map(|_| vec![1.0f32, 0.0]).collect())
1032            }),
1033        };
1034        let texts: Vec<String> = (0..8).map(|i| format!("文本 {i}")).collect();
1035        assert!(matches!(embed_with_retry(&mut entry, &texts), EmbedOutcome::Shrunk));
1036        assert_eq!(entry.effective_batch, 4, "减半值应当写在 entry 上并持久");
1037        assert!(matches!(embed_with_retry(&mut entry, &texts[..4]), EmbedOutcome::Vectors(_)));
1038        assert_eq!(*lengths.lock(), vec![8, 4]);
1039    }
1040
1041    /// 减半到 1 仍被拒绝即视为模型不可用;其它类别不重试、也不减半。
1042    #[test]
1043    fn callback_errors_are_classified() {
1044        let mut broken = EmbedderEntry {
1045            options: EmbedderOptions::default(), effective_batch: 1,
1046            embedder: Box::new(|_: &[String]| Err(EmbedCallbackError::too_large("still too large"))),
1047        };
1048        assert!(matches!(embed_with_retry(&mut broken, &["a".to_string()]), EmbedOutcome::Failed(_)));
1049        assert_eq!(broken.effective_batch, 1);
1050
1051        let attempts = std::sync::Arc::new(AtomicUsize::new(0));
1052        let counter = attempts.clone();
1053        let mut throttled = EmbedderEntry {
1054            options: EmbedderOptions::default(), effective_batch: 4,
1055            embedder: Box::new(move |_: &[String]| {
1056                let attempt = counter.fetch_add(1, AtomicOrdering::SeqCst);
1057                if attempt < 2 { Err(EmbedCallbackError::rate_limited("slow down")) } else { Ok(vec![vec![1.0f32, 0.0]]) }
1058            }),
1059        };
1060        assert!(matches!(embed_with_retry(&mut throttled, &["a".to_string()]), EmbedOutcome::Vectors(_)));
1061        assert_eq!(attempts.load(AtomicOrdering::SeqCst), 3, "限流应当退避重试后成功");
1062        assert_eq!(throttled.effective_batch, 4, "限流不触发减半");
1063    }
1064}