# 文件、索引和记录需要不同的存储策略

原文: https://yishto.com/learn/service-data/storage-search-and-data-models/

作者: [Yishto](https://yishto.com/about/)

将学习资料原文、所有权记录与检索索引分开，设计上传恢复、签名权限、模型选择和删除一致性。

首次发布: 2026-11-10
内容更新: 2026-11-10

一份学习资料同时有原始文件、所有权记录与搜索结果。把所有内容塞进同一种存储，会让上传、查找和权限纠缠在一起；把搜索索引当唯一事实来源，则在重建或删除时难以恢复原文。

案例是一座合成资料库：原文按对象键保存，数据库记录所属用户与版本，搜索索引用于找候选。代码只建立内存词项索引，不上传文件，不生成向量，也不连接真实搜索引擎。

## 原文、元信息和索引各有责任

对象存储保存按键访问的二进制内容，例如 PDF 或图片；数据库记录资料 ID、对象键、所有者、版本、状态与校验信息。索引从原文与元信息派生，用于查询，不应成为唯一保留内容的地方。

对象键不等于授权。知道键或资料 ID 之后，仍要检查是否允许读取。不要以用户提交的任意对象路径决定可下载范围，后端应从已授权记录取得相应键。版本与内容摘要有助于确认索引处理的是哪份原文。

原文修改、元信息修改和删除各自触发对应更新。明确哪个版本已进入索引，允许多长更新延迟，重建时从哪份事实恢复。否则界面查到旧标题，却打开新内容，或已删除资料仍持续出现在候选里。

## 上传与记录提交需要协调

文件上传成功与数据库提交不是一个自动事务。可以先创建 pending 记录，给受限上传机会，校验实际对象与尺寸后标为 ready，再安排索引。数据库失败后已上传对象需要清理；对象失败后记录要有可重试或失败状态。

恢复任务应核对过期 pending、孤立对象和失效引用，采用有时间窗口的清理策略。不要刚看到对象没有 ready 记录就立即删除，上传流程可能还在进行。清理本身也需要范围、重试与审计，避免移除仍有效内容。

R2 的对象读、写、删除和列举具有强一致性；IAM 权限传播是另一边界。CDN 缓存可能仍呈现旧资源，数据库与对象之间也没有自动原子提交。不能把这些不同现象都概括为“对象存储最终一致”。

## 下载凭据要按持有者权限理解

Presigned URL 允许持有者在有效期内对特定对象执行限定操作，是 bearer 凭据，不会自动证明下载者实名身份。签发前由应用验证权限，避免把带签名的地址写进公开日志、页面分析或长期共享资料。

有效期限制以后使用，不会收回已经下载的内容。需要严格访问审计或即时策略时，要评估代理下载或额外鉴权。CORS 影响浏览器是否允许特定跨源交互，不是内容所有权验证，也不能保护一个本来公开可读的对象。

上传签名也应限制对象范围和操作，应用仍需核验实际内容与大小。文件扩展名和客户端 Content-Type 不足以证明格式安全。生成缩略图或提取文本时，外部文件是待验证输入，不是可信代码。

## 数据模型按查询方式选择

关系型模型适合约束、关联和事务；文档或 JSON 字段适合部分灵活结构，但仍需 schema 与查询策略。图模型表达节点与关系，适合关系遍历；不因为数据有“关系”就必须换图数据库。

时间序列按时间与维度组织采样，聚合需要时间窗口、缺失值与单位。Prometheus 标签组成序列身份，使用无限增长的用户或请求 ID 作为标签会增加基数，不能当成普通日志字段随意使用。

向量表示可以用于相似性候选，全文索引则查词项。两者都需要权限过滤、质量评估与版本管理，向量相似不等于事实正确。选择一种模型时同时考虑写入、恢复和查询要求，而不是列一个通用优劣排名。

## 用倒排索引查共同词项

倒排索引从词映射到包含它的资料集合。查询 agent retry 时，取两个集合的交集，所以只有同时包含它们的 D-1 命中。每篇资料先用 set 去掉重复词，避免同一词多次添加同一 ID。

```python
documents = {"D-1": "agent state retry", "D-2": "image queue retry", "D-3": "agent tools"}
index = {}
for doc_id, text in documents.items():
    for term in set(text.split()):
        index.setdefault(term, set()).add(doc_id)
def search(query):
    groups = [index.get(term, set()) for term in query.split()]
    return sorted(set.intersection(*groups)) if groups else []
assert search("agent retry") == ["D-1"]
assert search("unknown") == []
print(search("agent retry"))
```

输出为 `['D-1']`。未知词返回空结果，空查询也返回空列表。这个模型按空格分词、区分大小写，只做词项 AND 匹配；没有词形归并、中文分词、相关性排序、向量或权限判断。

SQLite FTS5 提供更完整的全文能力，但分词规则仍要针对语言与数据验证。将中文整段文本简单按空格拆分，通常不能满足中文检索。检索结果还要回到权威记录确认版本、状态和当前访问权限。

## 权限与删除覆盖所有副本

索引里的私有标题、摘要和词项也可能泄露内容，不能只在下载时检查。检索应按用户可访问范围取候选，并在返回展示前检查当前权限；具体过滤位置依引擎能力与需求设计，不能先泄露再补一句无权访问。

删除流程要使记录不可见、撤销可用访问、更新索引与处理对象，并记录重试。备份、缓存和签名地址也有各自保留边界。旧索引未清除时，最终读取检查可以减少泄露，但不能替代索引清理与保留策略。

搜索质量测试包含应命中、应无结果、相似但不相关和无权限样本。候选数量多不是质量好，准确召回也不证明授权正确。保留合成评测数据与可重建流程，避免把用户真实文档当公开测试集。

查询协议也要说明词项组合。AND、OR 和短语查询返回不同候选，空查询不应自动放开全部私有资料。把规则写清，再用同一套合成样本比较结果。

## 练习：完成资料生命周期

定义上传 pending 到 ready、索引状态、修改版本和删除状态。模拟对象成功但数据库失败、记录存在但对象失败、索引落后一版本及权限撤销。对每种情况说明恢复来源和清理条件。

验收要求：原文与元信息可恢复；旧版本索引可检测；未授权资料不展示标题或下载；签名地址只在约定范围有效；删除覆盖索引与对象；未知词和空查询明确无结果。实际 R2、数据库和搜索客户端需要独立集成测试，内存倒排模型只证明词项匹配规则。

## 资料来源与延伸阅读

- [SQLite full-text search](https://www.sqlite.org/fts5.html)
- [PostgreSQL JSON types](https://www.postgresql.org/docs/current/datatype-json.html)
- [Graph database concepts](https://neo4j.com/docs/getting-started/appendix/graphdb-concepts/)
- [Object storage concepts](https://developers.cloudflare.com/r2/)
- [Time-series data model](https://prometheus.io/docs/concepts/data_model/)

资料核验：2026-11-10。教学示例不代表线上实测。

## 相关文章

- [RAG 的最小闭环：检索、证据、回答](https://yishto.com/learn/rag/retrieval-pipeline/)
- [项目：文档处理任务编排器](https://yishto.com/learn/project-labs/lab-document-workflow-editor/)
