尧图网络 高端网站定制 · 原创设计
免费咨询热线
400-888-6620
免费获取方案
MiroFish:本地优先文件镜像与SQLite FTS5检索实践
MiroFish 是我为了解决“东西明明存在、但我就是找不到”这件事写的一个本地优先的文件镜像与检索工具。故事的起点很具体上周三下午为了翻一份两年前的会议记录我在三块硬盘、两个网盘目录和一堆散落的 Markdown 之间找了四十分钟最后发现它躺在一个.docx里文件名叫做“新建文档(3)”。那天晚上我就动手写代码目标很朴素——让“找东西”这件事从四十分钟压缩到四秒钟。这篇文章不打算做成一份说明书。我更想把它当成一次完整的项目复盘从为什么砍掉 Elasticsearch到增量镜像的指纹设计再到文件监听踩过的三个真实事故以及最后把检索体验调到“能用”的那些细节。如果你手头也有几万到几十万个散乱文件、对云端索引又不太放心这套思路基本可以照抄如果你只是想看看一个本地工具该怎么从零长起来里面的取舍逻辑同样能用得上。1. 为什么我要做 MiroFish一个被“找东西”逼出来的本地镜像工具1.1 我的真实痛点三个硬盘、七种格式、一次找不到的会议记录先把我面对的数据摊开来看这样后面所有的设计决策才有参照。三块硬盘一块是笔记本内置的 NVMe容量 1TB装的是近两年的活跃文件一块是 4TB 的移动机械盘用来放 2019 年以来的历史资料还有一块 2TB 的老硬盘里面是从更早的机器上整盘拷过来的东西目录结构已经没人看得懂。格式上有 Markdown 笔记、.docx会议记录、.pdf技术文档、截图 PNG、代码片段.txt以及若干.xlsx台账。真正让我崩溃的不是数据量而是命名信息的丢失。一块硬盘上有一千多个文件叫“新建文档”“未命名”“副本”另一块上有几百个带日期的文件但日期是错的从旧机器拷贝时 mtime 被重置成拷贝时间。我的第一反应是装个桌面搜索工具试了两个之后放弃了一个默认索引全盘包括系统目录和软件缓存索引库三天涨到 8GB另一个干脆不支持中文子串检索“会议记录”四个字输进去结果里全是包含“会”字的无关文件。所以 MiroFish 的第一条需求是明确的**只索引我指定的目录索引体积要可控中文必须能按子串命中。**第二条需求来自工作流——我经常在笔记本上写东西、在台式机上查东西两边文件并不完全一致我需要一个“镜像层”来告诉我这个文件在两台机器上是不是同一份、哪一份更新、有没有出现过内容相同但路径不同的重复品。1.2 MiroFish 这个名字背后的定位mirror 加 fish名字不是随便起的。mirror指向“镜像”——它不搬文件、不改文件、不占用你的原目录而是在旁边维护一份关于文件的元数据映射路径、大小、修改时间、内容哈希、所属设备、首次入库时间、最近一次确认时间。fish指向“捞”——在信息的海里把那条鱼捞出来检索层负责这件事。这个命名直接决定了两层架构镜像层mirror负责扫描、指纹计算、变更识别、去重判定输出一张稳定的文件元数据表。检索层fish基于元数据表和正文抽取结果建全文索引负责查询解析、打分排序、结果定位。两层之间用一张 SQLite 表解耦镜像层写入、检索层读取互不阻塞。这个解耦是我做对的第一个决策。早期版本我把扫描和建索引揉在一个循环里结果是一旦某个 PDF 抽取卡住整个扫描进度就停了重启之后还得重头再来。拆成两层之后扫描只管记录“正文抽取待办”标记抽取失败最多影响那一个文件的可检索性不会污染镜像状态。1.3 一句话说清它做什么以及它明确不做什么MiroFish 做的事一句话概括把你指定的若干目录镜像成一份本地索引让你用关键词、路径片段、扩展名和短语组合在几百毫秒内定位到具体文件。它明确不做的事同样重要甚至更重要不做的事原因不做云端同步我只要“知道文件在哪”不需要“把文件搬来搬去”同步带来的冲突解决成本远超收益不做权限与多用户单人本地工具引入用户体系只会让表结构和查询逻辑复杂一倍不默认索引全盘系统目录、缓存目录、依赖包目录的文本几乎没有检索价值却会吃掉 80% 的索引体积首版不做向量语义检索语义检索需要模型与向量库冷启动成本高先用 BM25 把“精确回忆型”查询打到极致不做内容修改只读扫描任何情况下不写回源文件出问题的概率因此降低一个量级最后一条“只读”原则救过我一次。早期我写过一个“自动整理重复文件”的功能把内容相同的文件移动到一个_duplicates目录。测试时因为哈希计算读到了半写入的文件误判了两组不同内容的文件为重复差点把一份正在编辑的文档挪走。从那以后我给自己立了规矩MiroFish 的输出只有查询结果和报告任何破坏性动作都交给人工确认后手动执行。2. 选型复盘为什么是 SQLite FTS5 而不是 Elasticsearch2.1 先把数据量算清楚你的索引到底有多大选型不能凭感觉得先算数。我按自己的实际情况估了一遍待索引文件约 18 万个其中文本类Markdown、txt、代码、docx 抽取文本约 11 万个平均正文 3KB合计约 330MB 纯文本。图片、视频、压缩包只入元数据正文为空约 7 万个每条元数据记录平均 300 字节合计约 21MB。FTS5 索引的膨胀系数按经验在 1.5 到 3 倍之间取决于分词粒度和是否存原文。用 trigram 做子串支持时偏高按 2.5 倍算约 830MB。元数据表本身加索引约 150MB。总量落在1GB 上下单机单文件完全放得下。这个数字直接判了 Elasticsearch 的死刑——它的 JVM 堆最低也要 1GB 起再加上它自己的段文件索引还没建完资源开销已经是数据的五倍。而 SQLite 是进程内库没有额外服务内存占用基本等于 page cache 加上连接本身的开销。这里有个通用的估算方法值得记一下索引体积 ≈ 正文体积 × 分词膨胀系数 × 冗余系数。trigram 分词器会为每个 3 字符窗口建一个 token中文一句话长度是 n产生的 token 数量接近 n所以膨胀系数天然比按词切分高一截。如果你的数据量是 100GB 级别这个账就要重新算SQLite 也会开始吃不消。2.2 FTS5、Whoosh、Meilisearch、Elasticsearch 的横向对比把候选方案摊在一张表里对比比任何技术博客都直观方案部署形态常驻内存中文子串增量更新二进制体积结论SQLite FTS5进程内库几乎为零走 page cachetrigram 支持INSERT/DELETE 即可随 Python 分发选它Whoosh纯 Python 库中等需自写分析器支持但段合并慢几 MB十万级文档后明显变慢Meilisearch独立服务Rust100MB 起自带中文分词支持约 50MB过度设计多一个要守护的进程Elasticsearch独立服务JVM1GB 起需装分词插件支持数百 MB数据量不匹配运维成本高真正让我下定决心的是故障面。Elasticsearch 和 Meilisearch 都是独立进程意味着我要处理端口占用、开机自启、版本升级、数据目录权限、进程被系统 OOM 杀掉之后的恢复。而 SQLite 是一个文件备份就是复制文件在 WAL 模式下需要先做 checkpoint 或直接用.backup命令迁移就是拷贝重装系统就是重新指个路径。提示如果你的场景是多人协作、需要分布式检索、或者单索引超过几十 GB上面这个结论不成立老老实实上独立服务。选型的关键从来不是哪个更强而是哪个的失败模式你能接受。2.3 中文分词这个绕不过去的坎trigram 方案与 jieba 预分词的取舍SQLite FTS5 默认的unicode61分词器对中文是很不友好的。它按“非字母数字字符”切分而所有汉字都被视为字母于是一整句中文会被当成一个巨大的 token。你搜“会议记录”时只有当某个文件的整句文本恰好等于或前缀匹配这个 token 才可能命中实际效果约等于没有。我试过两条路。第一条是预分词用 jieba 把正文切成词词之间用空格连接再交给unicode61索引。import jieba def tokenize_for_fts(text: str) - str: words jieba.cut_for_search(text) # 搜索引擎模式长词会再切出短词 return .join(w for w in words if w.strip())这条路的优点是索引体积小、查询速度快、能拿到词级 BM25 权重。缺点是依赖分词词典如果我搜“镜像工具”而原文里是“镜像文件工具”切词结果里可能出现“镜像 / 文件 / 工具”查询词“镜像工具”不一定能切出匹配的单元。更麻烦的是专业名词、项目代号、人名词典里没有就会被切碎。第二条是trigram 分词器SQLite 3.34 之后内置CREATE VIRTUAL TABLE doc_fts USING fts5( path, body, tokenize trigram case_sensitive 0, detail full );trigram 会把文本切成所有长度为 3 的字符窗口因此任意长度大于等于 3 的子串都能命中“镜像工具”这种组合词直接搜就行不需要任何词典。代价有三个我实测过索引用体积大约是原文的 2.5 到 3 倍写入速度比unicode61慢约 40%以及少于 3 个字符的查询无法用索引命中。第三个代价是最容易翻车的。用户搜“AI”或者“CP”这种两个字符的词MATCH会直接返回空而且不报错。我的处理办法是在查询解析层做判断如果查询词长度小于 3就走LIKE %xx%扫表虽然慢但至少能出结果同时给这类查询加一个结果上限避免全表扫描把响应时间拖到几秒。def build_query(q: str) - tuple[str, list]: tokens [t for t in q.split() if t] if any(len(t) 3 for t in tokens): # 短词退化为 LIKE 扫描并限制返回条数 where AND .join(body LIKE ? for _ in tokens) params [f%{t}% for t in tokens] return fSELECT path FROM file_meta WHERE {where} LIMIT 200, params return SELECT path FROM doc_fts WHERE body MATCH ? LIMIT 200, [ .join(tokens)]最后我的选择是混合方案正文列用 trigram 建索引保证任意子串可查额外加一列body_seg存 jieba 分词结果只对高频词做加速。两套索引让总体积涨到了 1.4GB 左右换来的是“不管我怎么搜都能出东西”。这笔交易我觉得值。3. 增量镜像的核心机制文件指纹、去重与状态机3.1 用 mtime 加 size 做第一层过滤用内容哈希做第二层全量哈希 18 万个文件要多久我在两块盘上实测过NVMe 上平均 40 秒机械盘上接近 4 分钟而且这期间磁盘几乎被占满其他操作都会卡。所以每次扫描都全量哈希是不可接受的必须做分层判断。第一层看(mtime, size)这对组合。如果两者都没变直接跳过不读文件内容。这一层能过滤掉 95% 以上的文件扫描时间从分钟级降到秒级。第二层只在第一层判定“可能变了”时触发计算内容哈希。我一开始用 SHA-256后来换成 BLAKE2b速度提升约三成。再后来发现对于“是否变化”这个判断其实连加密哈希都不需要用 xxHash64 就够了——碰撞概率在几十万文件规模下可以忽略而且速度快十倍。import xxhash, os from pathlib import Path def quick_fingerprint(p: Path) - str: st p.stat() h xxhash.xxh64() # 头部 64KB 尾部 64KB兼顾大文件速度和准确度 with open(p, rb) as f: h.update(f.read(65536)) if st.st_size 131072: f.seek(-65536, os.SEEK_END) h.update(f.read(65536)) return f{st.st_size}-{h.hexdigest()}这里有个真实的坑只读头部会漏掉“文件头没变、尾部追加内容”的情况比如日志文件只读尾部则会漏掉“头部被改了”的情况。所以我用头尾各 64KB 的组合指纹再加上size一起参与判断。只有当size没变、但指纹变了才升级到全文件哈希。这套逻辑上线之后实测误判率为零扫描窗口从 4 分钟压到了 8 秒左右。3.2 相似度去重SimHash 与分块哈希该怎么选去重这件事要分两个层次问清楚你要找的是完全相同的文件还是高度相似的文件这两个问题的解法完全不同。完全相同用内容哈希做键即可一张hash - [paths]的映射表GROUP BY hash HAVING COUNT(*) 1就能把所有重复组列出来。这部分没有任何歧义。高度相似就要上 SimHash。它的原理是把文本特征映射成 64 位指纹相似文本的指纹只有少数几位不同因此可以用汉明距离快速比较。我用 3-gram 词频作为特征64 位输出汉明距离阈值设 3。经验数据是阈值 3 能抓住“同一份文档改了几句话”的情况误报率很低阈值提到 6 之后同主题的不同文档开始被误判成重复。def simhash64(tokens: list[str]) - int: v [0] * 64 for tok in tokens: h int(xxhash.xxh64(tok).hexdigest(), 16) for i in range(64): v[i] 1 if (h i) 1 else -1 out 0 for i in range(64): if v[i] 0: out | 1 i return out至于分块哈希把文件切成固定块或内容定义块逐块做哈希我最后没有用。它的价值在于跨文件复用存储块适合备份和同步场景而 MiroFish 不搬文件、不存内容只需要回答“这两份是不是同一份”SimHash 加完整哈希已经完全够用。多引入一层分块逻辑只会让“为什么这两个文件被判为重复”这个问题变得难以解释。提示SimHash 这类近似算法一定要留一个人工复核入口。我的做法是把候选重复组输出成一份 Markdown 报告每组列出路径、大小、SimHash 距离让人看一眼再决定。算法负责把 1 万组候选缩到 200 组人负责最后拍板这个分工最省心。3.3 状态机设计新增、修改、删除、移动、重命名怎么区分扫描的难点不在于发现变化而在于给变化定性。同一个“旧路径消失、新路径出现”的现象可能是删除加新增也可能是重命名还可能是移动到了别的目录。如果定性错了索引里就会留下一堆永远指向不存在路径的僵尸记录。我的做法是把文件身份和路径解耦。身份用一个稳定的键表示路径只是身份的一个属性。CREATE TABLE file_meta ( file_id INTEGER PRIMARY KEY, dev INTEGER NOT NULL, inode INTEGER NOT NULL, path TEXT NOT NULL, size INTEGER NOT NULL, mtime REAL NOT NULL, fingerprint TEXT, content_hash TEXT, last_seen INTEGER NOT NULL, missing INTEGER DEFAULT 0, UNIQUE(dev, inode) ); CREATE INDEX idx_path ON file_meta(path); CREATE INDEX idx_hash ON file_meta(content_hash);状态迁移的判断逻辑按优先级从高到低现象判定条件处理方式新增(dev, inode)未出现路径未出现插入新记录修改(dev, inode)已存在指纹变化更新 size、mtime、指纹标记正文待重抽重命名或移动旧路径消失且新路径出现content_hash相同更新 path保留 file_id不动索引条目删除旧路径消失同哈希的其他路径也不存在标记missing 1不物理删除复制新路径出现content_hash与已有记录相同但 inode 不同新增记录标记为副本候选这里有三个刻意的设计。第一删除只做标记不做物理删除。因为误判的代价太高如果某次扫描时外部硬盘没挂载所有记录都会被判定为删除物理删除之后恢复起来极其痛苦。标记为missing之后索引里的内容还在只是结果列表里排在后面并标注“文件当前不可见”。第二重命名判定优先于删除加新增。因为重命名场景下文件内容没变没必要重新抽取正文和重建索引直接改路径是最省事的。判断依据就是内容哈希相同、且旧路径在本轮扫描中消失。第三用(dev, inode)而不是路径做主键。Linux 和 macOS 上inode 在同一设备内唯一重命名不会改变它所以“同一个 inode 换了路径”天然就是重命名。Windows 上没有 inode 这个概念我用file_id加volume_serial的组合替代逻辑一致。这个细节如果不处理跨平台版本会在文件重命名时产生大量重复记录。4. 文件监听这块最容易翻车inotify 的三类真实事故4.1 编辑器“原子保存”导致文件被误判为删除第一类事故最隐蔽也最消耗时间。我写完监听模块之后发现每次在编辑器里保存一个 Markdown 文件MiroFish 都会报告“文件被删除然后新增了一个同名文件”。索引条目被删了又建正文被重复抽取日志里刷满了无意义的变化记录。排查过程分三步。第一步先把原始事件流打出来看。from watchdog.observers import Observer from watchdog.events import FileSystemEventHandler class RawLogger(FileSystemEventHandler): def on_any_event(self, event): print(f{event.event_type:12} src{event.src_path} fdst{getattr(event, dest_path, )} dir{event.is_directory})日志一出来问题立刻清楚了。一次保存产生的事件序列是created src/notes/.tmp_8f3a2.md modified src/notes/.tmp_8f3a2.md moved src/notes/.tmp_8f3a2.md dst/notes/meeting.md deleted src/notes/meeting.md大多数现代编辑器和工具链在保存时都是这个套路先写临时文件写完通过rename原子替换目标文件。对文件系统来说meeting.md这个路径上的 inode 被换掉了——旧 inode 被删除新 inode 顶上来。所以监听层看到的就是“删除 移动”。我的处理分两层。监听层把moved事件直接翻译成“目标路径发生变化”扫描层在收到“删除”通知时不立即标记missing而是延迟 2 秒再做一次确认扫描——因为绝大多数原子保存的后续事件会在毫秒级内到达。两秒之后如果路径确实不存在了才标记为缺失。这个“延迟确认”的思路还顺手解决了另一个问题下载工具写大文件时会持续产生modified事件每次事件都触发重抽正文是巨大的浪费。延迟窗口内的重复事件会被合并成一次处理。4.2 inotify watch 数量上限与递归监听的性能陷阱第二类事故是在我把监听目录从 3 个扩到 20 个之后爆发的。程序启动几秒后直接抛异常提示达到 inotify 监视数量上限。查下来原因是inotify 是按目录建立监视的不递归。要监听一棵有 20 万个子目录的树就需要 20 万个 watch而系统默认上限通常是 65536 或 81920。# 查看当前上限 cat /proc/sys/fs/inotify/max_user_watches # 临时调高 sudo sysctl fs.inotify.max_user_watches524288 # 持久化 echo fs.inotify.max_user_watches524288 | sudo tee /etc/sysctl.d/99-inotify.conf但把上限调高只是治标。每个 watch 都要占内核内存20 万个 watch 会吃掉几百 MB 的内核内存而且目录一多事件分发本身的延迟也会上升。所以我引入了混合策略热目录走监听最近 30 天有写入、或者我日常在用的少数几个目录挂 inotify做到秒级感知。冷目录走对账其余目录不挂监听靠定时全盘扫描发现变化。目录增删同步维护 watch监听到created且is_directory为真时动态给新目录挂 watch监听到目录删除时连带移除子树下的 watch。对账频率我设成冷目录 6 小时一次、热目录 1 小时一次作为兜底。实测下来日常使用中感知不到延迟而 watch 数量从 20 万降到了 1.2 万左右。4.3 事件风暴与去抖动队列的设计第三类事故来得最猛我在被监听的目录里解压了一个源码压缩包三秒钟内产生了 4 万多个事件而且是每层目录各一个 created 事件逐层上报程序直接把 CPU 打满事务不断提交WAL 文件涨到几百 MB。根因是每个事件都独立走了一次数据库写事务。修复方案很直接引入去抖动队列把事件按路径聚合在固定时间窗口内合并成一批。import time, threading from collections import OrderedDict class DebouncedQueue: def __init__(self, flush_interval0.8, max_batch2000, sinkNone): self.pending OrderedDict() # path - (event_type, ts) self.interval flush_interval self.max_batch max_batch self.sink sink self.lock threading.Lock() def push(self, path: str, ev: str): with self.lock: self.pending[path] (ev, time.time()) if len(self.pending) self.max_batch: self._flush_locked() def _flush_locked(self): batch list(self.pending.items()) self.pending.clear() self.sink(batch) # 用一个事务批量写入 def run(self): while True: time.sleep(self.interval) with self.lock: if self.pending: self._flush_locked()配合批量写入时的PRAGMA调优4 万个事件的入库时间从几十秒降到了 3 秒出头。这里的关键参数是flush_interval设得太小合并效果差设得太大用户刚保存完文件去搜会搜不到。0.8 秒是我反复调整后手感最好的值——保存后一次呼吸的时间就能搜到同时足以把解压、git checkout这类突发写入合并掉。提示事件风暴期间一定要限制单批大小。我在push里加了max_batch超过阈值立刻落盘避免内存里的 pending 字典无限增长。这个保护在遇到递归符号链接时尤其重要。5. 从能跑到好用检索体验与工程化收尾5.1 查询语法设计字段限定、模糊匹配与结果排序工具能跑起来之后真正决定我愿不愿意天天用它的是检索手感。我参考了熟悉的搜索语法做了一套轻量前缀解析器语法含义示例普通词正文或路径命中会议记录...短语精确匹配季度预算评审path:xx仅匹配路径path:2023/项目Aext:xx按扩展名过滤ext:md ext:txtsize:1M按大小过滤size:500K-xx排除会议 -模板排序是另一个需要打磨的地方。纯 BM25 排序在实际使用中有个明显问题一篇很长的文档因为词频高而排在前面但我要找的可能只是一个短小的笔记。我的做法是给 BM25 分数乘上几个修正因子SELECT f.path, bm25(doc_fts, 2.0, 1.0) * (1.0 / (1 LENGTH(f.path) / 120.0)) AS score FROM doc_fts JOIN file_meta f ON f.rowid doc_fts.rowid WHERE doc_fts MATCH ? AND f.missing 0 ORDER BY score LIMIT 50;bm25(doc_fts, 2.0, 1.0)里的两个权重分别对应path列和body列路径权重给到 2.0 是因为我发现“我想找文件名里带关键词的文件”这个需求非常高频。后面那个LENGTH(f.path)的修正因子是我自己加的路径越短通常代表这个文件越靠近项目根目录、越可能是主文档而不是某个深层的临时产物。实测效果是搜“MiroFish 设计”时/notes/mirofish-design.md稳定排第一而不是某个引用过这个项目名的超长会议记录。这种“排第一的就是我要的”体验比多十种查询语法都重要。5.2 首次全量索引的性能实测与调优首次建索引是最耗时的一步也是最容易让人放弃的一步。18 万个文件我的第一版跑了 27 分钟优化后压到 4 分半。主要的提速点有四个。第一写事务批量化。每条记录一次事务时SQLite 要为每次提交做 fsync这是最大的瓶颈。改成 5000 条一批之后写入吞吐提升了接近 20 倍。第二PRAGMA 参数调整。建索引期间用的是这一组PRAGMA journal_mode WAL; PRAGMA synchronous NORMAL; PRAGMA cache_size -64000; -- 约 64MB page cache PRAGMA temp_store MEMORY; PRAGMA mmap_size 268435456; -- 256MB 内存映射synchronous NORMAL在 WAL 模式下是安全与速度的平衡点崩溃时可能丢失最后一批事务但数据库不会损坏。对可重建的索引来说这个代价完全可以接受。第三正文抽取并行化。文件读取和文本抽取是 IO 与 CPU 混合型任务我用线程池做抽取、单线程做数据库写入避免多线程争抢写锁。第四跳过无价值目录。我把.git、node_modules、__pycache__、各种缓存目录、以及大于 20MB 的文本文件全部排除。仅这一条就砍掉了约 30% 的文件数量和一大半的处理时间。建完之后的日常维护只需要PRAGMA optimize;定期跑一次让 SQLite 自己决定要不要更新统计信息。5.3 打包部署单文件分发与定时对账任务最后一公里是让它安静地跑在后台。我的部署形态非常简单一个常驻进程负责监听和增量更新一个 cron 任务负责冷目录对账。常驻进程用 launchdmacOS和 systemdLinux各写了一份单元文件关键是设置自动重启和资源限制避免它出问题之后吃掉整机内存。日志按天轮转只保留 14 天因为监听类程序的日志会疯长。打包这块我试了两条路。PyInstaller 打成单文件最省事缺点是每次启动要解压一遍冷启动接近两秒而且体积从源码的几百 KB 涨到 15MB 左右。最后我选了更朴素的方式用虚拟环境加一个入口脚本配合 shell 别名mf直接调用。启动时间 80 毫秒更新就是git pull对我这种单人自用场景来说更顺手。跨设备这块我没有做实时同步理由是 SQLite 文件在并发写入下容易出现锁冲突而跨设备的因果顺序很难保证。我采用的方案是每台机器各自维护一份本机索引各自独立扫描对需要跨设备比对的目录额外输出一份“路径、大小、内容哈希”的清单文件两台机器各导一份用一个独立命令做比对。这样既拿到了跨设备的重复检测能力又完全回避了分布式一致性问题。定时对账任务的内容也很简单# 每天凌晨 3:20 对冷目录做一次全量对账 20 3 * * * /path/to/mirofish scan --profile cold --quiet ~/.mirofish/cron.log 21 # 每小时清理一批已确认超过 90 天不可见的记录 5 * * * * /path/to/mirofish prune --older-than 90d --yes ~/.mirofish/cron.log 21prune命令是我后来补的。因为删除只做标记、不做物理删除长期运行之后missing 1的记录会累积。清理策略刻意设得很保守只有连续 90 天所有扫描都确认不可见才真正删除记录。外部硬盘一个月插一次也不会有问题。最后分享两个我实际用下来最有用的细节。一个是给索引加一个“最近打开”字段MiroFish 自己不知道你打开了哪个文件但我用了一个很土的办法——把检索结果的第一条记录写进一个recent表时间久了之后“最近搜到并定位过的文件”本身就是一个极好用的时间线比文件系统的 mtime 靠谱得多因为它记录的是我什么时候真正关心过这个文件。另一个是把扫描报告存成 Markdown 而不是日志每次全量扫描之后新出现的重复组、疑似重命名的条目、体积异常增长的文件全部列成一份带链接的报告。日志我从来不看但这份报告我每周会翻一次好几次都是靠它发现了误放到笔记目录里的临时文件。
RELATED

相关推荐

Spring Boot实战开发:课外学习生活活动平台完整实现与踩坑记录

Spring Boot实战开发:课外学习生活活动平台完整实现与踩坑记录

把“基于Spring Boot的课外学习生活活动平台”这个项目完整做下来之后,我最想对你说的一句话是:它看起来像是一个很常见的校园类管理系统,但真正动手后才发现,Spring Boot在真实业务里能遇到的绝大部分能力,它都帮你串…

📅 2026/9/18 6:34:36
S905L3B电视盒子安装Armbian完整指南:30分钟把闲置安卓盒子变成Linux服务器

S905L3B电视盒子安装Armbian完整指南:30分钟把闲置安卓盒子变成Linux服务器

S905L3B电视盒子安装Armbian完整指南:30分钟把闲置安卓盒子变成Linux服务器 【免费下载链接】amlogic-s9xxx-armbian Supports running Armbian on Amlogic, Allwinner, and Rockchip devices. Support a311d, s922x, s905x3, s905x2, s912, s905d, s905x, s905w, s…

📅 2026/9/18 6:29:36
oh-my-hermes:RN Hermes引擎一站式配置与调优工具

oh-my-hermes:RN Hermes引擎一站式配置与调优工具

第一次看到 oh-my-hermes 这个名字,老 zsh 用户应该都会心一笑——它明显是在向 oh-my-zsh 致敬,但管的不是 shell,而是 Hermes 引擎,也就是 React Native 底层那个由 Meta 开源的高性能 JavaScript 引擎。这个项目是我在维护的一…

📅 2026/9/18 6:29:36
MORE NEWS

更多资讯

📰

鸿蒙应用Ory Kratos身份认证方案与Flutter客户端适配实践

1. 项目背景与核心价值最近在开发鸿蒙应用时遇到一个典型痛点:如何在不重复造轮子的情况下,快速实现一套符合云原生标准的身份认证系统?经过多方调研,最终选择了基于Ory Kratos的身份管理方案,并完成了其Flutter客户端…

📰

基于SSM+Vue的猫咪寄养管理系统开发实践

1. 项目背景与核心价值作为一名长期关注宠物行业信息化建设的开发者,我注意到近年来城市宠物猫数量呈现爆发式增长。根据行业调研数据显示,2023年国内城镇养猫家庭已突破5000万户,随之而来的是节假日、出差期间的宠物寄养需求激增。传统的宠物…

📰

易经卦象分析:系统方法论与核心应用

1. 易经卦象分析的系统方法论《周易》作为中华文明的核心典籍之一,其卦象系统构建了一套独特的认知框架。这套体系以阴阳变化为基础,通过六十四卦的符号组合,展现了古人认识世界的系统思维。与西方逻辑分析不同,易经的智慧在于其整…

📰

Slang 文档系统(Doc System)完全指南:从 doxygen 风格注释到标准库文档生成

Slang 文档系统(Doc System)完全指南:从 doxygen 风格注释到标准库文档生成 【免费下载链接】slang Making it easier to work with shaders 项目地址: https://gitcode.com/GitHub_Trending/sl/slang Slang 编译器内置了一套简洁的源…

📰

AutoRAG vLLM 生成器模块实战指南:本地 GPU 上的 10 倍加速推理与多卡并行配置

AutoRAG vLLM 生成器模块实战指南:本地 GPU 上的 10 倍加速推理与多卡并行配置 【免费下载链接】AutoRAG AutoRAG: Now your agent can find anything in your computer. It gets smarter if you are using it frequently. 项目地址: https://gitcode.com/GitHub_…

📰

用代码复现米罗风格:参数化生成“米罗水族馆”的完整指南

做了个小工具,把米罗画里的那些弯弯绕绕的线条、圆点、月牙,和鱼的形态搅在一起,让它自己长出一条又一条不可能存在的鱼。朋友看了生成结果的第一反应是问我在哪学的画画。我说,没学,这段代码写的。她愣了几秒&#xf…

TODAY

今日更新

THIS WEEK

本周精选

THIS MONTH

本月热门

读完文章,想聊聊您的网站?

告诉我们您的行业与需求,资深顾问一对一梳理方案与报价,全程免费。

📞 💬