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 pub model: String,
14 pub dimension: usize,
15 #[serde(default = "text_version")] pub text_version: u32,
16 #[serde(default = "default_encoding")] pub encoding: String,
18}
19#[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 pub text_pending: bool }
26#[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#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
38#[serde(rename_all = "snake_case")]
39pub enum EmbedErrorKind {
40 TooLarge,
42 RateLimited,
44 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#[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
73pub 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#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
86pub struct EmbedderOptions {
87 #[serde(default = "default_max_batch")] pub max_batch: usize,
88 #[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 pub effective_batch: usize,
99 pub embedder: Box<dyn Embedder>,
100}
101
102impl EmbedderEntry {
103 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#[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
131const 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#[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
166fn 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#[inline]
180fn dot_codes(left: &[i8], right: &[i8]) -> i32 {
181 #[cfg(target_arch = "x86_64")]
182 {
183 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#[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
217fn 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
236fn 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
251fn 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#[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 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 fn default_enabled(self) -> bool { !matches!(self, Self::Notes) }
291}
292
293fn vectorize_key(namespace: &str) -> String { format!("vectorize:{}", text::normalized_tag(namespace)) }
295
296fn target_vectorize_key(namespace: &str, target: VectorizeTarget) -> String {
298 format!("{}:{}", vectorize_key(namespace), target.as_str())
299}
300
301pub(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
307pub(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
313pub(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
324pub(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
337fn enabled_kinds_sql() -> String {
340 VectorizeTarget::ALL.iter().map(|target| target_enabled_sql(*target)).collect::<Vec<_>>().join(" OR ")
341}
342
343fn 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
350pub(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 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 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 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 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
419fn embeddable(input: &EmbeddingInput) -> bool { !input.text.trim().is_empty() }
423
424const GAP_PROBE: usize = 256;
426
427fn 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
440fn 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
448pub(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
457fn 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
469pub(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
480fn 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
488enum EmbedOutcome { Vectors(Vec<Vec<f32>>), Shrunk, Failed(String) }
490
491fn 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#[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 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 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 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 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 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 {
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 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 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 {
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 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 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 {
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 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 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 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 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 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 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 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 {
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 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 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 writer.conn.execute("DELETE FROM vector_meta WHERE key GLOB 'vector_ready:*:'||?1||':*'", [id])?;
826 }
827 }
828 self.0.engine.vectors.invalidate();
830 Ok(receipt)
831 }
832}
833
834fn 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
846const VERIFY_ATTEMPTS: usize = 3;
850
851struct VectorRow { key: RecordKey, kind: RecordKind, tags: Vec<String>, note: i64, #[allow(dead_code)] fingerprint: String }
853enum PartitionData {
856 F32(Vec<f32>),
857 Sq8 { codes: Vec<i8>, scales: Vec<f32> },
858}
859pub(crate) struct Partition { dimension: usize, rows: Vec<VectorRow>, data: PartitionData }
861
862struct 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 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 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 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 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 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 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 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 #[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 #[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 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 #[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 assert!((reference - actual).abs() < 1e-3, "reference {reference} vs actual {actual}");
1018 }
1019
1020 #[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 #[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}