# Rust API
核心 crate 名 `p-memory`,公开入口是 `KnowledgeBase`。以下签名省略 `pub`,`Result<T>` 即 `p_memory::Result<T>`。
## KnowledgeBase
```rust
impl KnowledgeBase {
fn open(directory: impl AsRef<Path>) -> Result<Self>;
fn directory(&self) -> &Path;
fn close(&self) -> Result<()>;
fn memories(&self) -> MemoryStore;
fn graph(&self) -> GraphStore;
fn notes(&self) -> NoteStore;
fn embeddings(&self) -> EmbeddingStore;
fn search(&self, request: &SearchRequest) -> Result<SearchResult>;
fn search_with_context(&self, request: &SearchRequest, limit: usize) -> Result<Vec<ContextualHit>>;
fn search_preset(&self, request: &PresetRequest) -> Result<PresetResult>;
fn register_reranker<F: Reranker + 'static>(&self, reranker: F) -> Result<()>;
fn register_reranker_with<F: Reranker + 'static>(&self, reranker: F, options: RerankerOptions) -> Result<()>;
fn unregister_reranker(&self) -> bool;
fn reranker_registered(&self) -> bool;
fn register_event_sink<F: Fn(&LogEvent) + Send + Sync + 'static>(&self, sink: F);
fn unregister_event_sink(&self) -> bool;
fn event_sink_registered(&self) -> bool;
fn health(&self) -> Result<HealthReport>;
fn rebuild_indexes(&self) -> Result<HealthReport>;
fn backup(&self, target: impl AsRef<Path>) -> Result<()>;
fn restore(snapshot: impl AsRef<Path>, directory: impl AsRef<Path>) -> Result<Self>;
}
```
### 打开与关闭
- `open` 创建目录、抢占 `writer.lock` 独占锁、初始化 schema、打开并恢复全文索引。锁被占用返回 `locked`。
- `KnowledgeBase` 是 `Clone` 的:克隆体**共享同一进程内引擎**——同一个写连接、同一个索引句柄与写锁。
读取从空闲池取一条只读连接,池空则新建、读完归还(见 [architecture](architecture.md))。
- `close()` 先同步索引再释放;关闭后任意操作返回 `closed`。
### 并发语义
- **一个数据目录在同一时刻只允许一个进程持有写锁**,同进程内可通过 `Clone` 共享句柄。
- 所有写入走 `mutate`,事务失败即整体回滚;写入不就地索引,由 `update_index()` 一趟追平(见 [architecture](architecture.md))。
- 写入提交后会清空向量缓存,保证后续检索看到最新向量。
- **写是串行的,读是并发的**:所有写入共用一把写锁、按到达顺序排队;读取不经过写锁,
每次从空闲池取一条只读连接(池空则新建),所以同一句柄上的多个读可以真正并行执行。
跨线程共享句柄请用 `Clone`。
### 备份与恢复
- `backup(target)`:SQLite 在线备份,包含向量。目标文件已存在时拒绝覆盖(先 `create_new` 占位)。
- `restore(snapshot, directory)`:只读打开快照,校验 `application_id` 与 `user_version` 后复制到**新目录**并打开;全文索引从数据重建。目标目录已存在会失败。
### 健康检查
- `health()` 返回 `HealthReport`:schema 版本、`revision` 与 `indexed_revision`、`pending_index_updates`、索引文档数、`PRAGMA quick_check`、外键错误数、各类型记录计数,以及 `embedder_spaces` / `reranker_registered` / `last_degraded`。
- `update_index()` 追平写入累积的待办、只提交一次,返回新的健康报告。写入路径不调用它;批量导入后调用一次即可,期间读取走自愈兜底。
- `rebuild_indexes()` 强制重建全文索引并清空向量缓存,随后返回新的健康报告。重建按 record id 分页流式进行、逐批提交,内存不随语料规模增长;进程中途被杀后重开库会从持久游标续跑。
- `rebuild_progress()` 返回一次重建进度快照 `RebuildProgressReport { active, processed, total }`,可在重建进行时从另一线程轮询。
### 注册模型回调
- `register_reranker` / `register_reranker_with` 注册重排回调:进程内单例,不绑定向量空间。库在调用前按 `RerankerOptions` 的条数上限与 token 预算强制截断候选与文本(两者是与门)。注册时用样本真跑一遍校验产出条数与有限性;回调当场不可用时无从校验形状,允许绑定,可用性留到检索时降级。
- 嵌入回调在 `EmbeddingStore` 上注册(见下)。
### 事件回调
- `register_event_sink` 注册事件接收回调:进程内单例,库在关键执行点(一次检索、一次索引重建)产出一条 `LogEvent` 交给它。落盘、轮转、保留多久都由宿主负责,库不打开日志文件。
- **回调必须非阻塞**:库在检索线程里同步调用它,做同步 IO 或网络上报会把检索拖住。回调 panic 被捕获,只丢这一条事件,检索照常返回。
- 不注册就完全不产出事件,全程不构造、不格式化。
- 事件形状与各阶段口径见 [search](search.md#事件流)。
### 预设检索
`search_preset` 是预先配好的搜索方法,调用方按名字取用:
```rust
enum SearchPreset { Memory, Graph, Notes, Rag, Broad } // "memory"/"graph"/"notes"/"rag"/"broad"
impl SearchPreset {
const ALL: [Self; 5];
fn as_str(self) -> &'static str;
fn parse(value: &str) -> Result<Self>; // 别的名字报 validation
fn uses_memory(self) -> bool; // memory / rag / broad
fn uses_graph(self) -> bool; // graph / rag / broad
fn uses_notes(self) -> bool; // notes / broad
}
struct PresetRequest {
preset: SearchPreset, // 默认 Rag
query: String, // 必填,空即 validation
filter: ReadFilter,
embed_space: Option<String>,
text: bool, vector: bool, rerank: bool, // 默认全真
budget: PresetBudget,
candidate_limit: usize, // 默认 64,不得为 0
}
struct PresetBudget { // 都可被调用方覆盖
memory_chars: usize, // 默认 2000
notes_chars: usize, // 默认 3000
seed_entities: usize, // 默认 4,不得为 0;唯一按个数的阈值
graph_relations_chars: usize, // 默认 1000
graph_context_chars: usize, // 默认 2000
note_titles: usize, // 默认 5,书名块想要的条数,也是路径兜底的触发线
}
struct PresetResult {
preset: SearchPreset,
memories: Vec<SearchHit>, // 记忆那一路
graph: GraphSection, // 图谱那一路
notes: NoteSection, // 笔记那一路
revision: i64,
indexed_revision: i64,
diagnostics: SearchDiagnostics,
}
struct NoteSection { // 笔记那一路:书名块 / 内容块 / 路径兜底同时返回
titles: Vec<SearchHit>, // 文件名命中(name 列),纯全文
contents: Vec<SearchHit>, // 正文命中(text 列);同一篇只留最高的一片,note_chunks 报出这一篇的片段总数、top_chunks 给出这一篇在候选窗口内的几片
paths: Vec<SearchHit>, // 书名块不足时用目录段(path 列)补的,排最后
}
struct GraphSection {
entities: Vec<Entity>, // 第一步的种子,按分排,带别名
relations: Vec<Relation>, // 第二步命中的关系,按相关度排
context_relations: Vec<Relation>, // 第三步铺开的关系,不筛,按 id 升序
context_events: Vec<Event>, // 第三步铺开的事件,不筛,按 id 升序
}
```
- 三个字段各自独立排序、各自按字符数封顶,互不挤占;这次没走的那一路是空数组,图谱字段全空。
- 记忆、笔记两路复用 `search`(全文 + 向量 + 可选重排),按字符数截断后返回;图谱那一路按三步流程走,见 [search](search.md#预设检索)。
- 预设带的默认阈值只是起点,实例化时按场景覆盖即可。
## MemoryStore
```rust
impl MemoryStore {
fn upsert(&self, input: MemoryInput) -> Result<WriteReceipt<Memory>>;
fn upsert_many(&self, inputs: &[MemoryInput]) -> Result<WriteReceipt<Vec<Memory>>>;
fn upsert_by_judgment(&self, input: MemoryInput) -> Result<WriteReceipt<Memory>>;
fn get(&self, id: i64, filter: &ReadFilter) -> Result<Memory>;
fn list(&self, request: &PageRequest) -> Result<Page<Memory>>;
fn delete(&self, id: i64, filter: &ReadFilter) -> Result<WriteReceipt<bool>>;
fn delete_by_filter(&self, filter: &ReadFilter) -> Result<WriteReceipt<usize>>;
fn feedback(&self, request: &FeedbackRequest) -> Result<WriteReceipt<FeedbackReport>>;
fn decay(&self, filter: &ReadFilter, policy: &DecayPolicy, at: Option<i64>) -> Result<WriteReceipt<DecayReport>>;
}
```
- `upsert`:按 `id` 更新;未给 `id` 则新建。`state = None` 时保留既有状态。
- `upsert_many`:批内**全成功或全回滚**。
- `upsert_by_judgment`:在同一 `namespace`+`scope` 内按归一化论断去重。命中多条同一论断时报 `conflict`(要求改用按 ID 更新);命中唯一记录时合并 metadata 与 evidence。
- `delete_by_filter`:删除过滤条件命中的全部记忆,返回删除条数。过滤条件里的 `namespace` 决定删哪个域,`scopes`/`tags` 收窄范围;空命中返回 0,不是错误。记忆没有子表,单条删除与批量删除走同一条删除路径。
- `feedback`:`useful_ids` 必须是 `recalled_ids` 的子集。有用项提升强度/计数/有用分;**非固定保留且处于 T1(`tier0 <= score < tier1`)的未命中项减 1 强度**。返回 `FeedbackReport { recalled, boosted, penalized }`。
- `decay`:对命中的记忆按策略衰减,返回 `DecayReport { decayed, retirement_candidates }`。固定保留的记忆不衰减。详见 [lifecycle](lifecycle.md)。
## GraphStore
```rust
impl GraphStore {
fn apply_batch(&self, batch: &GraphBatch) -> Result<WriteReceipt<GraphBatchResult>>;
fn get(&self, kind: RecordKind, id: i64, filter: &ReadFilter) -> Result<Value>;
fn list(&self, kind: RecordKind, page: &PageRequest) -> Result<Page<Value>>;
fn resolve(&self, name: &str, filter: &ReadFilter, limit: usize) -> Result<Vec<Entity>>;
fn neighbors(&self, id: i64, filter: &ReadFilter, limit: usize) -> Result<Neighborhood>;
fn events_for_entity(&self, id: i64, filter: &ReadFilter, limit: usize) -> Result<Vec<Event>>;
fn delete(&self, kind: RecordKind, id: i64, filter: &ReadFilter) -> Result<WriteReceipt<bool>>;
fn delete_by_filter(&self, filter: &ReadFilter) -> Result<WriteReceipt<usize>>;
}
```
- `apply_batch` 是唯一的图谱写入入口,**单事务**,顺序为实体 → 关系 → 事件,允许批内互相引用。
- 写入单个实体后,系统会刷新引用它的关系/事件的检索文本(因为文中含实体名);文本变化会同步失效对应向量。
- `resolve` 按归一化别名查实体,返回候选列表(不自动合并)。
- `neighbors` 返回一跳关系与对端实体;标签过滤只约束关系,作用域同时约束端点。
- `get`/`list`/`delete` 只接受 `Entity`/`Relation`/`Event`,其他类型报 `validation`。
- 删除被引用的实体或事件参与者关系,由外键 `RESTRICT` 报 `conflict`;调用方须先解除引用。
- `delete_by_filter`:删除过滤条件命中的全部图记录(实体、关系、事件),返回删除条数。删除顺序固定为**关系 → 事件 → 实体**,级联顺序由库内部保证,调用方不必知道外键怎么连。若某个待删实体仍被过滤条件之外的关系或事件引用,删除被外键拦下、报 `conflict`,整个事务回滚,不做部分删除。
## 图搜索
`GraphStore` 提供**内存态图搜索**(算法由 [petgraph](https://github.com/petgraph/petgraph) 提供),随库内置,无需额外开关。
```rust
impl GraphStore {
fn build_graph(&self, filter: &ReadFilter) -> Result<GraphView>;
fn ego(&self, root: i64, depth: usize, filter: &ReadFilter, limit: usize) -> Result<Vec<Entity>>;
fn path(&self, from: i64, to: i64, filter: &ReadFilter) -> Result<Option<Vec<Entity>>>;
fn strongly_connected(&self, filter: &ReadFilter) -> Result<Vec<Vec<i64>>>;
fn set_predicate_rule(&self, predicate: &str, inverse: Option<&str>, symmetric: bool) -> Result<WriteReceipt<()>>;
fn set_predicate_equivalents(&self, namespace: &str, groups: &[Vec<String>]) -> Result<WriteReceipt<usize>>;
fn predicate_equivalents(&self, namespace: &str) -> Result<Vec<Vec<String>>>;
fn expand_query(&self, namespace: &str, text: &str) -> Result<Vec<String>>;
}
// p_memory::graph_search::GraphView
impl GraphView {
fn node_count(&self) -> usize;
fn edge_count(&self) -> usize;
fn ego_ids(&self, root: i64, depth: usize, limit: usize) -> Vec<i64>;
fn path_ids(&self, from: i64, to: i64) -> Option<Vec<i64>>;
fn component_count(&self) -> usize;
}
```
- `build_graph(filter)`:读一次快照(`filter` 范围内的实体 + 关系),建**有向图**。节点 = 实体记录(含无关系的孤立实体),边 = 关系记录;范围外记录不进图,多命名空间不串。有向边只走出边,反向查询靠 `predicate_rules` 的对称/逆谓词在内存补出虚拟边打通(物理表只存单向真实边)。互为 `sys:same_as` 的实体在查询期由并查集缩点成同一节点,`ego_ids` / `path_ids` 的入参会先折算成代表 id。每次调用重建,无缓存。
- `ego_ids` / `ego`:从 `root` 出发 `depth` 跳内的实体,`limit` 截断。`ego_ids` 返回 `record_id`,`ego` 返回 `Entity`(带 name / aliases / attributes)。
- `path_ids` / `path`:`from` → `to` 的**最短桥接**(`astar`,各边等价)。不连通返回 `None`;`path` 返回路径上的实体(含两端)。
- `component_count`:连通分量数量(`connected_components`,按无向方式算)。
- `strongly_connected`:**有向**强连通环(`tarjan_scc`),只返回大小 > 1 的分量,元素为 `record_id`。
- `set_predicate_rule`:登记谓词元规则——`symmetric` 声明对称谓词(反向即自身),`inverse` 声明逆谓词(反向补一条对偶边,如 `父亲` 的逆是 `子女`),二者互斥。规则只影响后续建图的内存补边。内置 `sys:same_as` 为对称关系。
- `set_predicate_equivalents`:按知识领域登记谓词等价组(如 `[["alpha","beta","gamma"]]`),组内第一个是规范词。表由上游提供、库不内置领域数据,登记后持久化;同一个词重复登记会改写它的归属。只管同义,与 `set_predicate_rule` 的方向规则是不同维度。
- `predicate_equivalents`:列出某领域已登记的等价组(每组按文本排序)。
- `expand_query`:查询期扩散——找出 `text` 里出现的登记词,返回它们所在等价组的全部同义词(`text` 里没有登记词就返回空)。全文路与预设检索图谱路内部用同一套扩散;单次返回词元有上限(`EXPAND_QUERY_LIMIT = 64`)。
- 图搜索只服务「要在图上走一步以上」的查询;一层邻接(`neighbors`)仍走 SQL。
> 定位、能力边界与实测提醒见 [graph-search](graph-search.md)。
## NoteStore
```rust
impl NoteStore {
fn upsert_file(&self, input: NoteFileInput) -> Result<WriteReceipt<Note>>;
fn set_root(&self, namespace: &str, root: &str) -> Result<()>;
fn root(&self, namespace: &str) -> Result<Option<String>>;
fn get(&self, id: i64, filter: &ReadFilter) -> Result<Note>;
fn list(&self, page: &PageRequest) -> Result<Page<Note>>;
fn get_chunk(&self, id: i64, filter: &ReadFilter) -> Result<Chunk>;
fn chunks(&self, note_id: i64, filter: &ReadFilter) -> Result<Vec<Chunk>>;
fn delete(&self, id: i64, filter: &ReadFilter) -> Result<WriteReceipt<bool>>;
fn delete_by_filter(&self, filter: &ReadFilter) -> Result<WriteReceipt<usize>>;
}
```
- `upsert_file` 按给定文件路径同步一篇笔记:库读文件,正文取文件原文,标题取文件名(去扩展名);路径即身份,同一路径定位到同一笔记。监听与对账在使用方,库只处理给到的这一个文件。
- `set_root` 登记该领域的笔记根目录(必须是一个已存在的目录),`root` 读回来;落了 `namespace_roots` 表。**根目录是写入笔记的前提**:没登记就写入直接报 `validation`;登记之后写入的路径必须是它的子路径,否则同样报 `validation`,库里存减掉根目录的相对路径,`Note.source` 取回时再拼回绝对路径。
- **相对路径拆成标签**:目录段原样、文件名去扩展名,与调用方给的标签合并去重,写到这一篇的每一条切片上(库里 `record_tags`,索引里标签文本与标签 id 各一列)。
- **正文不进库**:笔记 payload 只留切片粒度,切片正文在写入时切好、随文档进全文索引。**笔记记录不进索引**,要文件列表按库里的标签翻笔记。
- 正文替换在**同一事务**内重建切片,删除失效切片及其向量。
- 切片 payload 只存 `note_id` 与行区间;`get_chunk` / `chunks` 返回的 `Chunk.content` 按切片 ID 从索引取回(索引还没提交就先提交一次)。写入时文件缺失直接报错;索引重建时文件缺失只让那批切片正文为空。
- `delete` 先删切片再删笔记。
- `delete_by_filter`:删除过滤条件命中的全部笔记,返回删除条数(只计笔记本身,随笔记一起删掉的切片是级联产物、不单独计数)。空命中返回 0。每篇都先删切片再删笔记。
- `chunk_text(content, target)` 是公开辅助函数,可脱离数据库单独调用。
## EmbeddingStore
```rust
impl EmbeddingStore {
fn register_space(&self, space: EmbeddingSpace) -> Result<WriteReceipt<EmbeddingSpace>>;
fn spaces(&self) -> Result<Vec<EmbeddingSpace>>;
fn register_embedder<F: Embedder + 'static>(&self, space_id: &str, embedder: F) -> Result<()>;
fn register_embedder_with<F: Embedder + 'static>(&self, space_id: &str, embedder: F, options: EmbedderOptions) -> Result<()>;
fn unregister_embedder(&self, space_id: &str) -> Result<bool>;
fn embedder_space(&self, space_id: &str) -> Result<Option<EmbeddingSpace>>;
fn namespace_vectorization(&self, namespace: &str) -> Result<bool>;
fn set_namespace_vectorization(&self, namespace: &str, enabled: bool) -> Result<WriteReceipt<bool>>;
fn vectorization(&self, namespace: &str, target: &str) -> Result<bool>;
fn set_vectorization(&self, namespace: &str, target: &str, enabled: bool) -> Result<WriteReceipt<bool>>;
fn vector_ready(&self, namespace: &str, space_id: &str, target: &str) -> Result<bool>;
fn sync(&self, space_id: &str, batch: usize) -> Result<WriteReceipt<SyncReport>>;
fn delete_space(&self, id: &str) -> Result<WriteReceipt<bool>>;
}
```
- 空间定义不可变;重复注册相同定义幂等。
- `register_embedder_with` 把回调绑到一个空间,**注册即用样本真跑一遍校验**(维度、有限性、非零范数、条数),不符即拒绝绑定并报 `invalid_vector`。一个向量模型对应一个向量空间。
- `sync(space_id, batch)` 是**批次结束后的补齐**,宿主不参与向量计算。它按固定顺序走完三件事:先追平索引(切片正文只存在索引里,不追平就取不到文本)→ 再按缺口分批补齐 → 最后逐档核对缺口,把缺口为 0 的档标成**就绪**。补齐循环严格三段式——取文本放锁 → 调回调不持锁 → 短事务写回;失败即中断,已写回的批次保留、未跑的批次不写。中断即未就绪。
- `vector_ready(namespace, space_id, target)` 读的是「领域 × 向量空间 × 档位」的就绪标记(落盘在 `meta`),`target` 取 `memory` / `graph` / `notes`,三档各自独立记、各自放行:记忆补完了不表示图谱也补完了。未就绪的档在检索里被剔出向量路,其余已就绪的档照常走向量;三档全被剔才记 `vector_not_ready`。写入、删除记录、改总闸或改档位都会让标记当场作废,要重新走一次 `sync` 核对过才会再标。
- 写入路径**不产生向量**:`upsert` 一条记忆或笔记只做两件事——入库与把文档写进索引 writer。一行向量都不算、一次模型都不调,所以写入耗时与模型无关;向量统一留到批次结束调 `sync`。落库后没有人调用 `sync` 的情形由库内后台线程兜底。
- 后台线程在开库时启动、关库时停下并等它收尾。它是**纯事件触发**,不轮询、不设定时:开库、注册或切换向量模型、改档位、写入提交后各叫它一次;一轮补齐跑完若还写出过向量,就接着再跑一轮收掉漏网的。线程没被叫时一直睡在条件变量上。
- `namespace_vectorization` / `set_namespace_vectorization` 是某知识域的**总闸**;`vectorization(ns, target)` / `set_vectorization(ns, target, enabled)` 是域内的**档位开关**,`target` 取 `memory` / `graph` / `notes`,三者互不牵连。读数时没设置过的档位落到内置默认(记忆开、图谱开、笔记关),非法档位名报 `validation`。四者都落盘在 `meta` 表。
- 开关只决定是否生成向量:已有向量保留,检索时按启用档位过滤;总闸关闭时该域既不生成向量、也不走向量路。
## 类型与常量
```rust
type Metadata = Map<String, Value>;
fn default_namespace() -> String; // "default"
fn public_scope() -> String; // "public"
```
`SearchRequest`、`SearchHit`、`SearchResult`、`SearchDiagnostics`、`MatchField`、`Degrade`、`ContextualHit`、`GraphPrune`、`NoteSection`、`EmbedderOptions`、`RerankerOptions`、`LogEvent`、`SyncReport`、`DecayPolicy`、`FeedbackRequest`、`Page`/`PageRequest`/`ReadFilter` 等见 [data-model](data-model.md);检索流程见 [search](search.md)。
## 错误处理
所有接口返回 `p_memory::Error`(`thiserror`),`Error::code()` 给出稳定字符串码(见 [data-model](data-model.md#错误码)),适合跨语言映射。