按文件名搜索很有用,但它只能找到你还记得名字的文件。真实场景常常反过来:「我知道某份合同里提到过某个条款,但不记得文件叫什么了。」这时候需要的是搜内容——在一堆文件里 grep。

Nebula 现在支持在云端文件夹里做内容搜索:文本类文件与 PDF 都能搜,命中后给一段带上下文的片段。

复用已有能力,不重复造轮子

内容搜索听起来是个新功能,但它需要的两块能力 Nebula 早就有了:

  • 读云端字节:provider.read 已经在下载、预览、同步里用了无数次;
  • 从 PDF 里抽文本:做 PDF 编辑器时写的 nebula_pdf::extract_text 正好拿来用。

于是内容搜索的核心就很短:走文件夹收集候选文件,逐个读字节,按类型抽成文本,再匹配关键字。

let text = if ext_of(&entry.name) == "pdf" {
    // PDF:复用编辑器那套文本提取
    nebula_pdf::extract_text(&bytes)?.join("\n")
} else {
    // 文本类:UTF-8 有损解码(遇到非法字节用 � 代替,不报错)
    String::from_utf8_lossy(&bytes).into_owned()
};
if let Some(snippet) = make_snippet(&text, &needle) {
    hits.push(ContentHit { entry, snippet });
}

文本文件用 from_utf8_lossy 而不是严格解码,是因为日志、CSV 这类文件里偶尔有非法字节很正常,不该让整份文件搜不了——用 � 替换掉继续搜即可。

哪些文件「值得搜」由扩展名白名单决定:常见的 .txt / .md / .json / .log / 各种源码,加上 .pdf。图片、视频、压缩包这些没有可搜文本的,直接跳过——既省下载,也避免把二进制乱码当文本匹配。

必须自带成本上限

这个功能和别的搜索有个本质区别:它要下载文件。按文件名搜只需列目录(便宜),按内容搜却要把每个候选文件的字节拉下来解析(费流量、费请求数、费时间)。所以它必须自带明确的上限,否则对着一个上万文件的桶点一下就是一场灾难:

  • 单文件大小上限(16 MB):超过的直接跳过,不下载巨物;
  • 扫描文件数上限(60):最多解析这么多候选,到顶就停并标记「已截断」;
  • 命中数上限:够用就停。

而且它是显式切换的——搜索结果区有个「按名称 / 按内容」开关,默认按名称。内容搜索是你主动选择付出下载代价去做的事,不会在你只想按名字找东西时偷偷下载一堆文件。

这些上限让功能「够用且安全」:在一个文档目录里找一句话完全胜任,又不会因为手滑点在一个海量桶上而失控。

片段生成:一个纯函数

搜到了还得让你一眼看出为什么命中,所以每个结果带一段上下文片段——匹配词前后各约 60 个字符,折叠掉多余空白,首尾按需补省略号。

这段逻辑写成一个不碰 IO 的纯函数,这样能脱离网络单测,把「中文按字符而非字节切窗口」「大小写不敏感」「长文本首尾加 …」这些边界都锁死:

pub fn make_snippet(text: &str, needle_lower: &str) -> Option<String> {
    let pos = text.to_lowercase().find(needle_lower)?;   // 大小写不敏感
    let chars: Vec<char> = text.chars().collect();
    let char_idx = text[..pos].chars().count();          // 字节位置 → 字符索引
    let start = char_idx.saturating_sub(60);
    let end = (char_idx + needle_lower.chars().count() + 60).min(chars.len());
    // 按字符边界取窗口,折叠空白,首尾按需加 …
    ...
}

有个容易忽略的坑:str::find 返回的是字节位置,而中文一个字符占多个字节。直接拿字节位置去 text[start..end] 切片,一旦切在多字节字符中间就会 panic。所以先把字节位置换算成字符索引,再在 Vec<char> 上按字符取窗口——中文文档也不会崩。

小结

  • 复用:读字节 + PDF 文本提取都是现成的,内容搜索只是把它们串起来;
  • 成本自觉:自带大小 / 数量上限,且显式切换,绝不偷偷下载;
  • 看得懂:带上下文片段,纯函数生成、按字符安全切窗、大小写不敏感。

按文件名搜是「我知道它叫什么」,按内容搜是「我知道它里面有什么」——两个都有,才算真的能在云盘里找东西。