algonotes_rag/docs/STORE.md

8.4 KiB
Raw Permalink Blame History

🗄️ 三层存储详解

本文档详细描述 AlgoNotes RAG 的三层存储结构设计,包括文件层、关系层和向量层的职责、数据格式与关键设计决策。


📊 总览

graph LR
    A["笔记.md"] --> B["FileStore"]
    B -- 原始文件 --> C["📁 data/files/"]
    B -- 读取内容 --> D["MarkdownHeaderTextSplitter"]
    D -- 分块 + chunk_count --> E["SQLStore.insert()"]
    E --> F["🗄️ data/sql_db/notes.db"]
    D -- 分块 + note_id --> G["VectorStore.insert()"]
    G --> H["🧬 data/chroma_db/"]
层级 存储后端 物理路径 存储内容
文件层 文件系统 data/files/ 原始 .md 笔记文件
关系层 SQLite data/sql_db/notes.db 笔记元数据索引
向量层 Chroma data/chroma_db/ 文本分块 + Embedding 向量

📄 文件层FileStore

职责

  • 笔记原始.md 文件的保存、读取、更新、删除
  • 文件系统操作的安全防护

文件平铺结构

data/files/
├── fenwick.md                     # 普通笔记
├── segment-tree.md
├── fenwick_20260616_143022.md     # 文件名冲突时自动添加时间戳后缀
├── dijkstra.md
└── ...

所有笔记文件平铺在同一目录下,无子文件夹,文件名即唯一标识。

文件名冲突策略

save("fenwick.md") 时如果文件已存在,自动在 stem 后追加 _YYYYMMDD_HHMMSS 后缀:

fenwick.md  (已存在)
        ↓ save("fenwick.md", new_content)
fenwick_20260616_143022.md  (自动重命名后保存)

安全防护

_resolve() 方法对每个传入的文件名做路径穿越检测:

def _resolve(self, filename: str) -> Path:
    path = (self._dir / filename).resolve()
    if not str(path).startswith(str(self._dir.resolve())):
        raise ValueError(f"Path traversal detected: {filename}")
    return path

阻止 ../../etc/passwd 或绝对路径绕过。

API 一览

方法 参数 返回值 说明
save(filename, content) 文件名, 内容字符串 str 绝对路径 保存新文件,冲突时自动重命名
update(filename, content) 文件名, 内容字符串 str 绝对路径 覆盖写入,不存在则创建
read(filename) 文件名 str | None 读取内容,不存在返回 None
delete(filename) 文件名 bool 删除文件
list_files() list[str] 列出所有文件名
exists(filename) 文件名 bool 检查文件是否存在
compute_hash(filename) 文件名 str 计算 SHA256 hex digest

🗃️ 关系层SQLStore

职责

  • 存储笔记的结构化元数据(文件名、标题、标签、时间戳等)
  • 提供按条件检索笔记的能力

数据库配置

项目
数据库引擎 SQLite 3
存储路径 data/sql_db/notes.db
Journal 模式 WAL写不阻塞读
Row 工厂 sqlite3.Row(查询结果可转 dict

表结构

CREATE TABLE IF NOT EXISTS notes (
    id              INTEGER PRIMARY KEY AUTOINCREMENT,
    filename        TEXT    NOT NULL UNIQUE,
    filepath        TEXT    NOT NULL,
    title           TEXT,
    tags            TEXT,
    source_url      TEXT,
    type            TEXT    DEFAULT 'note',
    author          TEXT,
    content_hash    TEXT    NOT NULL,
    ingested_at     TEXT    NOT NULL,
    updated_at      TEXT,
    chunk_count     INTEGER DEFAULT 0,
    file_size       INTEGER
);

字段说明

字段 类型 约束 说明
id INTEGER PK, AUTOINCREMENT 自增主键
filename TEXT NOT NULL, UNIQUE 原始文件名(如 fenwick.md
filepath TEXT NOT NULL 文件系统实际路径(含冲突后缀)
title TEXT 从 Markdown 提取的标题
tags TEXT 逗号分隔的标签(如 "树状数组,模板")
source_url TEXT 来源 URL从网页导入时
type TEXT DEFAULT 'note' 笔记类型note/solution/template
author TEXT 笔记作者
content_hash TEXT NOT NULL SHA256 文件内容哈希
ingested_at TEXT NOT NULL 入库时间(YYYY-MM-DD HH:MM:SS
updated_at TEXT 最后更新时间
chunk_count INTEGER DEFAULT 0 分块数(向量化后回填)
file_size INTEGER 文件字节大小

内容哈希策略

  • 哈希在 insert()update() 内部自动计算,调用方无需传入
  • 通过 FileStore.compute_hash(filename) 委托文件层读取
  • update() 中如果更新了 filepath,自动从相同 filename 重算哈希

关键设计决策

  1. update() 白名单机制:只允许更新 {filepath, title, tags, source_url, chunk_count, file_size},防止意外修改核心字段
  2. 动态 SET 子句:只更新实际传入的字段,而不是全字段覆盖
  3. 先查后改update()delete() 先检查记录是否存在,返回 bool 表示是否实际生效

API 一览

方法 参数 返回值 说明
insert(filename, filepath, ...) 文件名, 路径, 可选元数据 int id 插入新笔记记录
get(note_id) 主键 ID dict | None 按 ID 查询
get_by_filename(filename) 文件名 dict | None 按文件名查询
update(note_id, **fields) 主键 ID, 字段键值 bool 更新指定字段
update_metadata(note_id, ...) 主键 ID, 元数据字段 bool 轻量更新元数据(不重算 hash
delete(note_id) 主键 ID bool 删除记录
list_all() list[dict] 列出所有笔记(按入库时间倒序)
search_by_tags(keyword) 关键词 list[dict] 按标签模糊搜索LIKE
close() 关闭数据库连接

标签搜索说明

tags 字段以逗号分隔的字符串存储,搜索使用 SQL LIKE

SELECT * FROM notes WHERE tags LIKE '%并查集%';

MVP 折中:简单易实现,但无法做精确标签匹配。后续可改为 JSON 数组字段或关联表。


🧬 向量层VectorStore

职责

  • 笔记文本分块的 Embedding 与存储
  • 语义相似度检索

设计要点

项目 规划
向量库 Chromalangchain_chroma.Chroma
持久化 data/chroma_db/persist_directory
Embedding 模型 Qwen3-Embedding-4B via Gitee.AI
Collection 名称 algonotes
Metadata 约束 仅支持 str / int / float / bool必须扁平

分块策略对存储的影响

摄入管线使用 MarkdownHeaderTextSplitter 按标题层级分块,每个 Chunk 独立存入 Chromametadata 自动注入:

metadata = {
    "source": filename,        # str — 源文件名
    "ingested_at": timestamp,  # str — 入库时间
    "chunk_index": i,          # int — 块序号
    "tags": "树状数组,数据结构", # str — 逗号分隔标签(可选)
    "header_h1": "...",        # str — 一级标题(如有)
    "header_h2": "...",        # str — 二级标题(如有)
}

Chroma 注意事项

  1. 持久化路径:统一使用相对路径 data/chroma_db/,由 config.tomlstore.chroma_dir 指定
  2. 重启加载Chroma(persist_directory="./data/chroma_db") 自动加载已有数据
  3. 维度一致性:更换 Embedding 模型时,必须先删除旧 collection 再重建,否则会因维度冲突报错
  4. 不可直接操作:禁止直接修改 chroma_db/ 目录内容,必须通过 Chroma API

🔗 跨层数据流

摄入流程详见上方 总览 部分的流程图。 查询流程详见 RAG 查询管线


⚙️ 配置参考

[store]
chroma_dir = "data/chroma_db"    # Chroma 持久化目录
files_dir  = "data/files"         # 原始笔记存储目录
sqlite_path = "data/sql_db/notes.db"  # SQLite 数据库路径

三个路径均相对于项目根目录(或可配绝对路径)。


🔐 安全与边界

层级 防护措施
文件层 _resolve() 路径穿越检测,阻止 ../ 逃逸
关系层 update() 白名单字段,阻止修改 id/filename/ingested_at
向量层 通过 Chroma API 操作,禁止直接修改目录