seekr is not a fast text search tool. It is a structured
text search and replacement workflow for R. These are different goals,
and it helps to be clear about the trade-off before working with large
file sets.
Why seekr is slower than CLI tools
When seekr searches a file, it does more than check
whether a pattern matches.
For every match it finds, it stores the source path, the absolute
character positions, the line and column positions, the matched text,
the staged replacement if any, the line containing the match,
surrounding context lines, the encoding, and a hash of the searched text
that replace_files() uses later to verify the file has not
changed.
This is what makes the following workflow possible:
library(seekr)
x <- seekr("foo|bar", toupper)
summary(x)
print(x, context = 2)
x <- filter_match(x, !grepl("/generated/", path))
replace_files(x)The cost is real. seekr reads each file as a complete
string, builds a structured R object for every match, and does all of
this sequentially in a single R process. For most source code projects
this is not a problem. For very large repositories, it can be.
The main cases where performance is likely to matter:
- Large repositories with tens of thousands of files.
- Large individual files, because each file must fit in memory as text.
-
Files with very large lines, because the match line
is stored and by default some context lines too, minified files such as
JavaScript, generated JSON, and one-line data dumps are common examples
for which
seekrmight not be the best fit. The default exclude functions remove some of these by default, but not all. -
Workloads where you only need file names, not
structured match objects. For this, a CLI tool such as
ripgrepthat will stop as soon as it finds a match is a better fit.
Practical strategies for larger workloads
Only list files tracked by git
Before using an external search tool, one simple option for Git repositories is to start with Git-aware file discovery:
files <- list_files(use_git = TRUE)Pre-filter with ripgrep
ripgrep is
not managed by seekr: installing it, making it available on
your system PATH, and dealing with platform-specific
command-line details are left to the user. The goal here is only to show
a possible pattern for advanced users with heavy workloads and willing
to use ripgrep
for performance.
If the main bottleneck is the number of files or volume of data, you
can use ripgrep to
identify which files contain the pattern, then pass only those files to
match_files().
# requires ripgrep to be installed and accessible to PATH
pattern <- "function"
path <- system.file("extdata", package = "seekr")
files_with_a_match <- system2(
command = "rg",
args = c("-l", shQuote(pattern), shQuote(path)),
stdout = TRUE
)
x <- match_files(files_with_a_match, pattern, toupper)
xThis keeps seekr’s structured match objects while
offloading the initial scan to a tool optimized for raw throughput.
Parallelize with mirai
If you want to parallelize the search itself, you can split the file
vector into chunks and run match_files() on each chunk with
mirai.
The idea:
- List and filter files as usual.
- Split the file vector into
Nroughly equal chunks. - Start
Nmiraidaemons. - Call
match_files()on each chunk in parallel. - Combine the resulting
seekr_matchvectors.
library(seekr)
library(mirai)
files <-
list_files() |>
filter_files(extension = "R")
# Sequential version, useful as a reference
x <- match_files(files, "foo", "bar")
attr(x, "exclusions") <- NULL
N <- 8L
mirai::daemons(N)
mirai::everywhere(library(seekr))
split_files <- unname(split(files, cut(seq_along(files), N)))
matches_list <- mirai::mirai_map(
split_files,
\(files) match_files(files, "foo", "bar", .progress = FALSE)
)[]
mirai::daemons(0)
y <- Reduce(c, matches_list, init = new_seekr_match())
identical(x, y)
#> TRUEAs a rough example: searching a simple pattern across about 48,000 R files (roughly 500 MB of text) took about 80 seconds sequentially and about 20 seconds with 8 daemons on one machine. Files were likely cached by the OS at that point. This is a single data point, not a benchmark. Actual results depend on the storage device, number of cores, file sizes, pattern complexity, and how many matches need to be materialized.
For small projects, the overhead of starting workers and combining results is probably not worth the added code. The sequential workflow is simpler and usually fast enough.
