← 服务端与数据 · 第 5 篇

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

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

学完这一篇,你能做到

定义资料上传、索引与删除生命周期,验证恢复来源、版本与权限不在副本间失效。

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

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

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

对象存储保存按键访问的二进制内容,例如 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。

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、数据库和搜索客户端需要独立集成测试,内存倒排模型只证明词项匹配规则。

资料来源与延伸阅读

资料核验:2026-11-10。本文为原创工程讲解,教学示例不代表线上实测。