首页
/ ripgrep 的行式搜索核心:深入 grep-searcher 的 Searcher、Sink 与二进制检测机制

ripgrep 的行式搜索核心:深入 grep-searcher 的 Searcher、Sink 与二进制检测机制

2026-09-04 16:12:32作者:温玫谨Lighthearted

crates/searcher/README.md 介绍了 ripgrep 仓库中的 grep-searcher crate——一个用于执行"快速行式搜索"的高层库,负责上下文行报告、行数计数、搜索反转、二进制数据检测、自动 UTF-16 转码以及内存映射(mmap)策略决策等关键行为。本文以该文档为主体,逐条展开其描述的每项能力,并结合仓库中 crates/searcher 的源码实现与示例,帮助你既能在自己的 Rust 项目中使用它,也能理解 ripgrep 搜索管线的底层工作原理。

一、定位:行式搜索的高层库

grep-searcher 的官方描述(见 crates/searcher/Cargo.toml)是 "Fast line oriented regex searching as a library",即"作为库提供的快速行式正则搜索"。README 明确列出了它统一承担的职责:

  • 报告上下文行(contextual lines);
  • 计数行号(counting lines);
  • 反转搜索(inverting a search,对应 grep -v 的语义);
  • 检测二进制数据(detecting binary data);
  • 自动 UTF-16 转码(automatic UTF-16 transcoding);
  • 决定是否使用内存映射(deciding whether or not to use memory maps)。

这些能力全部收敛在 Searcher 这一核心类型上。从 crates/searcher/src/lib.rs 的 crate 文档可以看到完整的心智模型:Searcher 从某个数据源(如文件)读取字节,使用 Matcher(如正则表达式)对字节执行搜索,并把结果报告给 Sink(如 stdout)。Matcher 本身定义在 grep-matcher crate 中,接口与正则表达式非常相似;grep-regexcrates/regex)提供了基于 Rust regex crate 的实现。

README 还给出了一条重要建议:不要直接依赖 grep-searcher,而应优先使用门面(facade)crate grep——它位于 crates/grep/src/lib.rs,对外统一暴露 MatcherSearcherSink 及相关配置类型,屏蔽底层各 crate 的细节。

二、快速上手:依赖声明与最小示例

2.1 声明依赖

继承自 README 的用法部分,在你的项目 Cargo.toml 中添加:

[dependencies]
grep-searcher = "0.1"

当前仓库中该 crate 的实际版本为 0.1.17(见 crates/searcher/Cargo.toml),0.1 这一 semver 简写可以解析到它。其运行依赖包括 memchrencoding_rsencoding_rs_iobstrmemmap2 等(见 crates/searcher/Cargo.toml)。

2.2 最小搜索示例

crates/searcher/src/lib.rs 中的文档示例演示了"执行搜索并用 UTF8 sink 收集结果"的完整流程:

use {
    grep_matcher::Matcher,
    grep_regex::RegexMatcher,
    grep_searcher::Searcher,
    grep_searcher::sinks::UTF8,
};

const SHERLOCK: &'static [u8] = b"\
For the Doctor Watsons of this world, as opposed to the Sherlock
Holmeses, success in the province of detective work must always
be, to a very large extent, the result of luck. Sherlock Holmes
can extract a clew from a wisp of straw or a flake of cigar ash;
but Doctor Watson has to have it taken out for him and dusted,
and exhibited clearly, with a label attached.
";

let matcher = RegexMatcher::new(r"Doctor \w+")?;
let mut matches: Vec<(u64, String)> = vec![];
Searcher::new().search_slice(&matcher, SHERLOCK, UTF8(|lnum, line| {
    // We are guaranteed to find a match, so the unwrap is OK.
    let mymatch = matcher.find(line.as_bytes())?.unwrap();
    matches.push((lnum, line[mymatch].to_string()));
    Ok(true)
}))?;

assert_eq!(matches.len(), 2);
assert_eq!(matches[0], (1, "Doctor Watsons".to_string()));
assert_eq!(matches[1], (5, "Doctor Watson".to_string()));

要点:

  1. Searcher::new() 使用默认配置构建搜索器(等价于 SearcherBuilder::new().build(),见 crates/searcher/src/searcher/mod.rs);
  2. search_slice 直接对内存字节切片执行搜索;
  3. sinks::UTF8sink.rssinks 子模块提供的闭包式便捷实现,回调收到行号 lnum 与行内容,返回 Ok(true) 继续搜索、Ok(false) 停止搜索。

仓库还提供了命令行示例 crates/searcher/examples/search-stdin.rs:从标准输入读取数据,用命令行参数作为正则模式,通过 search_reader 执行搜索并打印 行号:行内容,是 search_reader API 的最短可用样板。

三、三大抽象:Searcher、Matcher、Sink

3.1 Searcher:搜索的执行者

Searcher(定义于 crates/searcher/src/searcher/mod.rs)内部持有四部分状态:

字段 作用
config: Config 全部搜索配置(行终止符、上下文、mmap 策略等)
decode_builder / decode_buffer 转码流构建器与转码临时缓冲区;无需转码时字节零开销直通
line_buffer: RefCell<LineBuffer> 行式搜索使用的滚动缓冲(见 line_buffer.rs
multi_line_buffer 多行搜索时的整段内容缓冲

对外的四个搜索入口按数据源区分:

从源码结构看,搜索策略的选择逻辑很清晰:文件搜索会先尝试 mmap(search_file_maybe_pathmod.rs#L678-L714),mmap 不可用时若启用了多行搜索则把整个文件预读到堆上(MultiLine 策略),否则回退到通用的逐行滚动缓冲搜索(ReadByLine)。而 search_slice 在无需转码时走最快的 SliceByLine 路径,否则委托给 search_reader

3.2 Sink:结果的"推"式接收器

grep-searcher 采用"push(推)"执行模型:搜索器驱动执行,把结果推给调用方提供的 Sink 实现,而不是由调用方拉取结果(见 crates/searcher/src/sink.rs 的 trait 文档)。Sink trait(sink.rs#L102-L223)的方法及其默认行为:

方法 何时被调用 默认行为
matched 发现匹配时 必须实现
context 发现上下文行时 忽略,返回 Ok(true)
context_break 上下文行组之间出现间隔时 忽略
binary_data 启用二进制检测且发现二进制数据时 忽略
begin 搜索开始前 什么都不做
finish 搜索成功完成后 什么都不做

每个方法返回 Ok(false) 时搜索立即停止(随后调用 finish),返回错误时搜索立即停止且不再调用 finish,错误上抛。错误类型由伴随的 SinkError trait(sink.rs#L18-L60)描述,std::io::ErrorBox<dyn std::error::Error> 都开箱即用地实现了它,文档建议一般直接用 std::io::Error 即可。

匹配结果的载体是 SinkMatchsink.rs#L366-L426):

  • bytes():匹配行的完整字节(含行终止符);
  • lines():行迭代器——多行搜索时可能跨越多行;
  • absolute_byte_offset():匹配起点在整个输入中的绝对字节偏移(不能当作内存切片下标使用);
  • line_number():首行行号,仅当构建器开启了行号计数时才有值;
  • buffer()bytes_range_in_buffer():暴露底层搜索缓冲及其对应区间,供需要"窗口"信息的实现者使用。

搜索结束时的汇总信息是 SinkFinishsink.rs#L331-L362),提供 byte_count()(共搜索了多少字节)和 binary_byte_offset()(首个二进制字节的绝对偏移)。

四、SearcherBuilder:逐项解析全部配置

README 概括的六项能力,在实现层面正是 SearcherBuilder 的一组链式配置方法。内部配置结构体 Config 及其默认值定义在 crates/searcher/src/searcher/mod.rs,逐条对应如下:

Builder 方法 配置字段 默认值 说明
line_terminator line_term b'\n' 行终止符;matcher 若自行指定行终止符必须与之一致,否则构建时报 ConfigError::MismatchedLineTerminators
invert_match invert_match false 反转匹配:报告不匹配的行
line_number line_number true 是否计算行号;关闭可省掉一点性能开销
after_context after_context 0 每个匹配后报告的上下文行数
before_context before_context 0 每个匹配前报告的上下文行数
passthru passthru false 直通模式:把全部不匹配行都当作上下文行,相当于无界前后上下文;启用时 before_context/after_context 被强制置 0(见 build()mod.rs#L315-L320
heap_limit heap_limit None 堆内存上限;设为 0 时仅允许 mmap 策略可用,否则立即报错
memory_map mmap Never mmap 策略,见下节
binary_detection binary BinaryDetection::none() 二进制检测策略,见下节
encoding encoding None 显式指定源数据编码,无条件转码为 UTF-8
bom_sniffing bom_sniffing true 基于 BOM 的自动转码
multi_line multi_line false 允许多行匹配;代价是必须整体载入内容
stop_on_nonmatch stop_on_nonmatch false 在"匹配行之后出现不匹配行"时停止搜索,适合匹配项集中在相邻行的有序文件
max_matches max_matches None 最多产出的匹配数;0 是合法值,意味着立即停止

几个值得展开的默认值设计:

  • mmap 默认关闭Config::default()mmap: MmapChoice::default(),而 MmapChoiceDefault 实现是 Nevermmap.rs#L21-L25)。Builder 文档直言"与常规直觉相反,mmap 并不总能带来更快搜索"(mod.rs#L495-L501);
  • 行号默认开启:这是与"搜索库"直觉相反但贴合 grep 语义的选择;
  • heap_limit 的细分行为:限制固定缓冲搜索时滚动缓冲的容量上限(单行超长则报错);多行搜索时约束整段内容的堆占用。当限制设为 0 且 mmap 不可用时,构建结果会体现为 ConfigError::SearchUnavailablemod.rs#L244-L262)。

构建搜索器时,build()mod.rs#L315-L337)还会根据 encoding/bom_sniffing 组装 DecodeReaderBytesBuilder,并预留 8KB 的转码临时缓冲区;文档同时建议"构建后的 Searcher 应尽量复用"。

五、二进制检测:三种策略与两种搜索路径的差异

README 提到的 "detecting binary data" 由 BinaryDetection 实现(crates/searcher/src/searcher/mod.rs),共三种策略:

构造器 行为
BinaryDetection::none()(默认) 不做检测,Sink 可能收到任意字节
BinaryDetection::quit(byte) 检测到指定字节(ripgrep 场景中通常是 NUL)即停止搜索,如同到达 EOF
BinaryDetection::convert(byte) 把指定字节替换为行终止符(CRLF 模式下替换为 LF),调用方保证不会观察到该字节;仅在固定缓冲搜索下生效

quit_byte() / convert_byte() 两个访问器允许 Sink 实现按策略做差异化处理。

源码注释(mod.rs#L43-L53)特别解释了二进制检测在两类搜索路径下的差异

  1. 固定缓冲搜索:检测应用于缓冲内容的填充过程——因为二进制文件可能完全没有行终止符,若不在缓冲层直接检测,可能导致内存暴涨;
  2. mmap/堆上搜索:检测只保证覆盖"匹配所在部分";启用 Quit 时会先扫描开头前几个 KB,任何后续匹配(或上下文)行中一旦检测到二进制数据,搜索同样按 EOF 处理。

quit 策略触发时,Sinkbinary_data 回调会收到首个二进制字节的绝对偏移,最终也体现在 SinkFinish::binary_byte_offset() 中——这就是 ripgrep 对二进制文件输出 "Binary file ... matches" 之类提示的底层数据来源之一。

六、内存映射策略:默认 Never,谨慎 Auto

MmapChoicecrates/searcher/src/searcher/mmap.rs)只有两个选项:

  • MmapChoice::never():默认,永不使用内存映射;多行搜索时改为把全部内容读入堆;
  • unsafe fn MmapChoice::auto():由搜索器按文件大小、平台等启发式决定是否启用 mmap。之所以是 unsafe 构造器,是因为"文件在映射期间不被修改"这一契约无法在所有平台上封装进安全 API——调用方要自行承担文件被截断时进程收到 SIGBUS 的风险(mod.rs#L492-L494)。

MmapChoice::open 的实现(mmap.rs#L65-L115)还能看到两个实现细节:

  • 在 macOS 上直接放弃 mmap(源码注释指出 macOS 的 mmap 表现不佳,并引用了上游 issue 讨论);
  • Unix 平台成功映射后会调用 madvise(Sequential) 提示内核顺序读取,失败仅记录 debug 日志,不影响搜索。

Builder 文档给出的经验结论(mod.rs#L465-L490):搜索大型目录时,mmap 的管理开销可能反而比普通 read 更慢;仅在"搜索已驻留内存的超大单文件"这类场景才可能略快。官方建议"不确定就不要开"。

七、编码与 BOM 嗅探:自动 UTF-16 转码

README 提到的 "automatic UTF-16 transcoding" 对应 Encodingbom_sniffing 两项配置(mod.rs#L129-L146mod.rs#L518-L556):

  • Encoding::new(label) 按 WHATWG Encoding Standard 的标签表解析编码,未知标签返回 ConfigError::UnknownEncoding
  • 显式设置编码:源数据被无条件转码为 UTF-8;若存在 BOM,则以 BOM 声明的编码优先。转码错误字节替换为 Unicode 替换码点(U+FFFD),搜索不会因坏字节中断;
  • 未设置编码(默认):开启 BOM 嗅探时,UTF-16(含 BOM)文件会被无缝识别并转码后搜索;找不到 BOM 时则按"当作 UTF-8"处理——只要数据至少是 ASCII 兼容的,搜索仍能产出有用结果。

search_slice 中的 slice_needs_transcoding 分支(mod.rs#L782-L787)体现了这一设计:只有需要转码时才退回通用 reader 路径,否则享受切片直搜的快路径。

八、与 ripgrep 主程序的关系

在 ripgrep 的 crate 分层中,grep-searcher 处于"搜索引擎"层:

  • crates/grep 是门面 crate,re-export 各底层类型,是外部程序推荐的唯一入口(与 README 的 NOTE 一致);
  • crates/printer 负责把搜索结果变成人类/机器可读的输出:grep-searcherSink 分别被 standard.rs(标准 grep 风格输出,含上下文行合并)、json.rs(JSON/JSONLines 输出)、summary.rs-c 计数与文件摘要)、util.rs 等模块实现为各自复杂的 Sink——lib.rs 文档中所说的"Sink 实现可以非常复杂,如 grep-printer 中的 Standard printer"指的就是这条链路。

因此可以这样理解整条管线:grep-matcher(模式层)→ grep-searcher(行式搜索 + 上下文/二进制/编码/mmap 策略)→ grep-printer(输出层)rg 命令行通过 grep 门面把它们串起来。

九、许可证与文档入口

十、实践清单

结合以上源码证据,使用该库时的建议:

  1. 优先依赖门面 crate grep,而非直接依赖 grep-searcher
  2. 构建一次 Searcher 后在多次搜索间复用;
  3. 按数据源选择入口:能拿到路径/File 时优先 search_path / search_file(保留 mmap 可能性),纯流式数据用 search_reader
  4. 需要上下文或计数输出时,实现完整的 Sink(至少处理 contextcontext_breakfinish),并参考 crates/printer/src/standard.rs 的成熟实现;
  5. 对含二进制的数据源启用 BinaryDetection::quitconvert,并在 binary_data 回调中做提示;
  6. 除非有明确的单大文件场景与安全性评估,保持默认的 MmapChoice::never()
登录后查看全文
热门项目推荐
相关项目推荐

项目优选

收起
kernelkernel
deepin linux kernel
C
33
18
ops-transformerops-transformer
本项目是CANN提供的transformer类大模型算子库,实现网络在NPU上加速计算。
C++
1.12 K
2.72 K
kernelkernel
openEuler内核是openEuler操作系统的核心,既是系统性能与稳定性的基石,也是连接处理器、设备与服务的桥梁。
C
528
590
ops-nnops-nn
本项目是CANN提供的神经网络类计算算子库,实现网络在NPU上加速计算。
C++
904
1.82 K
pytorchpytorch
作为 Ascend for PyTorch 社区的核心组件,TorchNPU 是昇腾专为 PyTorch 打造的深度学习适配插件,使 PyTorch 框架能够直接调用昇腾 NPU,为开发者提供昇腾 AI 处理器的超强算力。
Python
854
1.34 K
docsdocs
暂无描述
Markdown
889
5.78 K
jiuwenswarmjiuwenswarm
JiuwenSwarm 是一款基于openJiuwen开发的智能AI Agent,它能够将大语言模型的强大能力,通过你日常使用的各类通讯应用,直接延伸至你的指尖。
Python
3.52 K
1.01 K
ops-mathops-math
本项目是CANN提供的数学类基础计算算子库,实现网络在NPU上加速计算。
C++
1.33 K
1.45 K
cann-learning-hubcann-learning-hub
CANN 学习中心仓,支持在线互动运行、边学边练,提供教程、示例与优化方案,一站式助力昇腾开发者快速上手。
Jupyter Notebook
983
503
AscendNPU-IRAscendNPU-IR
AscendNPU-IR是基于MLIR(Multi-Level Intermediate Representation)构建的,面向昇腾亲和算子编译时使用的中间表示,提供昇腾完备表达能力,通过编译优化提升昇腾AI处理器计算效率,支持通过生态框架使能昇腾AI处理器与深度调优
C++
540
384