Skip to contents

Overview

seek() and seekr() returns matches as a seekr_match vector. For many workflows, you can inspect and refine this vector directly with print(), summary(), and filter_match().

Sometimes, however, it is useful to switch to a tabular workflow. This is especially true when you want to summarize matches by file, filter based on group-level information, or prepare replacements with data frame tools.

This article shows how to:

  • convert a seekr_match vector to a data frame,
  • inspect matches with grouped summaries,
  • filter matches using group-level logic,
  • update replacements with dplyr,
  • convert back to a seekr_match vector with as_match(),
  • apply replacements with replace_files().

The examples use a temporary copy of the example files shipped with seekr, so they are safe to modify.

Find matches

Suppose we want to inspect log messages.

x <- seek("([A-Z]+) : (.+$)", extension = "log")
x
#> <seekr::match[80]> 2 sources
#> Common Path: /tmp/Rtmp1WgjRK/seekr-example/extdata
#> 
#> server1.log [10/40]
#>  [1] ->  1 | 2026-06-30 01:13:45 INFO : Starting process
#>  [2] ->  2 | 2026-06-30 15:35:57 DEBUG : Disk usage high
#>  [3] ->  3 | 2026-06-30 20:36:52 WARNING : Timeout reached
#>  [4] ->  4 | 2026-06-30 12:15:22 WARNING : Loading config
#>  [5] ->  5 | 2026-07-01 00:59:41 INFO : Restart scheduled
#>  [6] ->  6 | 2026-06-30 17:03:42 DEBUG : Connection successful
#>  [7] ->  7 | 2026-07-01 00:54:56 DEBUG : Restart scheduled
#>  [8] ->  8 | 2026-06-30 16:43:17 INFO : Timeout reached
#>  [9] ->  9 | 2026-06-30 21:24:23 INFO : Timeout reached
#> [10] -> 10 | 2026-06-30 22:49:27 INFO : Timeout reached
#> 
#> # ℹ 70 more matches
#> # ℹ Use `print(n = ...)` to see more matches

We can start with a summary of what was found.

summary(x)
#> ── <seekr::match[80]> ────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
#> Common Path: /tmp/Rtmp1WgjRK/seekr-example/extdata
#> 
#> Top sources [2]
#>  • server1.log : 40 (50.0%)
#>  • server2.log : 40 (50.0%)
#> 
#> Top matches [10/31]
#>  • <INFO : Timeout reached>         : 8 (10.0%)
#>  • <DEBUG : Disk usage high>        : 6 ( 7.5%)
#>  • <ERROR : Retrying request>       : 6 ( 7.5%)
#>  • <ERROR : Starting process>       : 5 ( 6.2%)
#>  • <INFO : User login failed>       : 5 ( 6.2%)
#>  • <DEBUG : Failed to authenticate> : 3 ( 3.8%)
#>  • <DEBUG : Loading config>         : 3 ( 3.8%)
#>  • <ERROR : Restart scheduled>      : 3 ( 3.8%)
#>  • <INFO : Connection successful>   : 3 ( 3.8%)
#>  • <WARNING : Disk usage high>      : 3 ( 3.8%)
#> 
#> Top extension [1]
#>  • log : 80 (100.0%)
#> 
#> Top encoding [1]
#>  • UTF-8 : 80 (100.0%)

Convert to a data frame

For simple filtering, filter_match() is usually enough. Let’s keep only the DEBUG lines and convert our vector to a data frame.

df <- 
  x |> 
  filter_match(str_detect(match, "^DEBUG")) |>
  as_tibble()

df
#> # A tibble: 20 × 14
#>    path                                          start_line end_line start   end start_col end_col match replacement before line  after encoding hash 
#>    <chr>                                              <int>    <int> <int> <int>     <int>   <int> <chr> <chr>       <chr>  <chr> <chr> <chr>    <chr>
#>  1 /tmp/Rtmp1WgjRK/seekr-example/extdata/server…          2        2    65    87        21      43 DEBU… NA          "2026… 2026… "202… UTF-8    88aa…
#>  2 /tmp/Rtmp1WgjRK/seekr-example/extdata/server…          6        6   245   273        21      49 DEBU… NA          "2026… 2026… "202… UTF-8    88aa…
#>  3 /tmp/Rtmp1WgjRK/seekr-example/extdata/server…          7        7   295   319        21      45 DEBU… NA          "2026… 2026… "202… UTF-8    88aa…
#>  4 /tmp/Rtmp1WgjRK/seekr-example/extdata/server…         12       12   514   536        21      43 DEBU… NA          "2026… 2026… "202… UTF-8    88aa…
#>  5 /tmp/Rtmp1WgjRK/seekr-example/extdata/server…         14       14   602   624        21      43 DEBU… NA          "2026… 2026… "202… UTF-8    88aa…
#>  6 /tmp/Rtmp1WgjRK/seekr-example/extdata/server…         19       19   831   852        21      42 DEBU… NA          "2026… 2026… "202… UTF-8    88aa…
#>  7 /tmp/Rtmp1WgjRK/seekr-example/extdata/server…         20       20   874   896        21      43 DEBU… NA          "2026… 2026… "202… UTF-8    88aa…
#>  8 /tmp/Rtmp1WgjRK/seekr-example/extdata/server…         24       24  1055  1079        21      45 DEBU… NA          "2026… 2026… "202… UTF-8    88aa…
#>  9 /tmp/Rtmp1WgjRK/seekr-example/extdata/server…         25       25  1101  1123        21      43 DEBU… NA          "2026… 2026… "202… UTF-8    88aa…
#> 10 /tmp/Rtmp1WgjRK/seekr-example/extdata/server…         28       28  1234  1256        21      43 DEBU… NA          "2026… 2026… "202… UTF-8    88aa…
#> 11 /tmp/Rtmp1WgjRK/seekr-example/extdata/server…         31       31  1366  1387        21      42 DEBU… NA          "2026… 2026… "202… UTF-8    88aa…
#> 12 /tmp/Rtmp1WgjRK/seekr-example/extdata/server…         38       38  1686  1709        21      44 DEBU… NA          "2026… 2026… "202… UTF-8    88aa…
#> 13 /tmp/Rtmp1WgjRK/seekr-example/extdata/server…          5        5   211   234        21      44 DEBU… NA          "2026… 2026… "202… UTF-8    cb05…
#> 14 /tmp/Rtmp1WgjRK/seekr-example/extdata/server…          7        7   299   328        21      50 DEBU… NA          "2026… 2026… "202… UTF-8    cb05…
#> 15 /tmp/Rtmp1WgjRK/seekr-example/extdata/server…         10       10   442   463        21      42 DEBU… NA          "2026… 2026… "202… UTF-8    cb05…
#> 16 /tmp/Rtmp1WgjRK/seekr-example/extdata/server…         20       20   903   932        21      50 DEBU… NA          "2026… 2026… "202… UTF-8    cb05…
#> 17 /tmp/Rtmp1WgjRK/seekr-example/extdata/server…         30       30  1364  1386        21      43 DEBU… NA          "2026… 2026… "202… UTF-8    cb05…
#> 18 /tmp/Rtmp1WgjRK/seekr-example/extdata/server…         33       33  1496  1519        21      44 DEBU… NA          "2026… 2026… "202… UTF-8    cb05…
#> 19 /tmp/Rtmp1WgjRK/seekr-example/extdata/server…         35       35  1585  1614        21      50 DEBU… NA          "2026… 2026… "202… UTF-8    cb05…
#> 20 /tmp/Rtmp1WgjRK/seekr-example/extdata/server…         36       36  1636  1664        21      49 DEBU… NA          "2026… 2026… "202… UTF-8    cb05…

This gives one row per match and one column per field, which makes grouped summaries easy.

For example, we can summarize the DEBUG matches in each file by counting the total and distinct messages and recording the first and last matching lines.

df |>
  summarise(
    n_matches = n(),
    n_distinct_matches = n_distinct(match),
    first_line = min(start_line),
    last_line = max(end_line),
    .by = path
  )
#> # A tibble: 2 × 5
#>   path                                              n_matches n_distinct_matches first_line last_line
#>   <chr>                                                 <int>              <int>      <int>     <int>
#> 1 /tmp/Rtmp1WgjRK/seekr-example/extdata/server1.log        12                  6          2        38
#> 2 /tmp/Rtmp1WgjRK/seekr-example/extdata/server2.log         8                  5          5        36

This kind of question is often easier to answer with a data frame than with the match vector directly.

Filter with group-level information

A tabular workflow is also useful when filtering depends on group-level information.

For example, suppose we only want to keep the latest occurrence of each distinct DEBUG log message in each file.

latest <-
  df |>
  mutate(
    match_rank_from_end = row_number(desc(start_line)),
    .by = c(path, match)
  ) |>
  filter(match_rank_from_end == 1L)

latest
#> # A tibble: 11 × 15
#>    path                      start_line end_line start   end start_col end_col match replacement before line  after encoding hash  match_rank_from_end
#>    <chr>                          <int>    <int> <int> <int>     <int>   <int> <chr> <chr>       <chr>  <chr> <chr> <chr>    <chr>               <int>
#>  1 /tmp/Rtmp1WgjRK/seekr-ex…          6        6   245   273        21      49 DEBU… NA          "2026… 2026… "202… UTF-8    88aa…                   1
#>  2 /tmp/Rtmp1WgjRK/seekr-ex…         12       12   514   536        21      43 DEBU… NA          "2026… 2026… "202… UTF-8    88aa…                   1
#>  3 /tmp/Rtmp1WgjRK/seekr-ex…         24       24  1055  1079        21      45 DEBU… NA          "2026… 2026… "202… UTF-8    88aa…                   1
#>  4 /tmp/Rtmp1WgjRK/seekr-ex…         28       28  1234  1256        21      43 DEBU… NA          "2026… 2026… "202… UTF-8    88aa…                   1
#>  5 /tmp/Rtmp1WgjRK/seekr-ex…         31       31  1366  1387        21      42 DEBU… NA          "2026… 2026… "202… UTF-8    88aa…                   1
#>  6 /tmp/Rtmp1WgjRK/seekr-ex…         38       38  1686  1709        21      44 DEBU… NA          "2026… 2026… "202… UTF-8    88aa…                   1
#>  7 /tmp/Rtmp1WgjRK/seekr-ex…         10       10   442   463        21      42 DEBU… NA          "2026… 2026… "202… UTF-8    cb05…                   1
#>  8 /tmp/Rtmp1WgjRK/seekr-ex…         30       30  1364  1386        21      43 DEBU… NA          "2026… 2026… "202… UTF-8    cb05…                   1
#>  9 /tmp/Rtmp1WgjRK/seekr-ex…         33       33  1496  1519        21      44 DEBU… NA          "2026… 2026… "202… UTF-8    cb05…                   1
#> 10 /tmp/Rtmp1WgjRK/seekr-ex…         35       35  1585  1614        21      50 DEBU… NA          "2026… 2026… "202… UTF-8    cb05…                   1
#> 11 /tmp/Rtmp1WgjRK/seekr-ex…         36       36  1636  1664        21      49 DEBU… NA          "2026… 2026… "202… UTF-8    cb05…                   1

The extra column match_rank_from_end is useful for now, but it is not part of a seekr_match vector. as_match() will ignore extra columns and validate the required match fields when converting back.

Update replacements

Finally, it is also easy to prepare some replacements. Here, we create different replacements depending on the content of each match.

latest <-
  latest |>
  mutate(
    replacement = case_when(
      str_detect(match, "Disk") ~ str_to_upper(match),
      str_detect(match, "config") ~ str_to_lower(match),
      .default = str_to_title(match)
    )
  )

latest |>
  count(match, replacement)
#> # A tibble: 8 × 3
#>   match                          replacement                        n
#>   <chr>                          <chr>                          <int>
#> 1 DEBUG : Connection successful  Debug : Connection Successful      2
#> 2 DEBUG : Disk usage high        DEBUG : DISK USAGE HIGH            2
#> 3 DEBUG : Failed to authenticate Debug : Failed To Authenticate     1
#> 4 DEBUG : Loading config         debug : loading config             2
#> 5 DEBUG : Restart scheduled      Debug : Restart Scheduled          1
#> 6 DEBUG : Retrying request       Debug : Retrying Request           1
#> 7 DEBUG : Starting process       Debug : Starting Process           1
#> 8 DEBUG : Timeout reached        Debug : Timeout Reached            1

Convert back to a seekr_match vector

Before replacing files, convert the data frame back to a seekr_match vector. The result can be printed again with replacement previews now that some replacements have been set.

to_replace <- as_match(latest)
to_replace
#> <seekr::match[11]> 2 sources
#> Common Path: /tmp/Rtmp1WgjRK/seekr-example/extdata
#> 
#> server1.log [6]
#>  [1] --  6 | 2026-06-30 17:03:42 DEBUG : Connection successful
#>      ++  6 | 2026-06-30 17:03:42 Debug : Connection Successful
#>  [2] -- 12 | 2026-06-30 17:34:23 DEBUG : Timeout reached
#>      ++ 12 | 2026-06-30 17:34:23 Debug : Timeout Reached
#>  [3] -- 24 | 2026-06-30 07:41:37 DEBUG : Restart scheduled
#>      ++ 24 | 2026-06-30 07:41:37 Debug : Restart Scheduled
#>  [4] -- 28 | 2026-06-29 22:58:20 DEBUG : Disk usage high
#>      ++ 28 | 2026-06-29 22:58:20 DEBUG : DISK USAGE HIGH
#>  [5] -- 31 | 2026-06-30 03:04:51 DEBUG : Loading config
#>      ++ 31 | 2026-06-30 03:04:51 debug : loading config
#>  [6] -- 38 | 2026-06-30 19:08:53 DEBUG : Starting process
#>      ++ 38 | 2026-06-30 19:08:53 Debug : Starting Process
#> 
#> server2.log [5]
#>  [7] -- 10 | 2026-06-30 02:19:35 DEBUG : Loading config
#>      ++ 10 | 2026-06-30 02:19:35 debug : loading config
#>  [8] -- 30 | 2026-06-30 00:15:00 DEBUG : Disk usage high
#>      ++ 30 | 2026-06-30 00:15:00 DEBUG : DISK USAGE HIGH
#>  [9] -- 33 | 2026-06-30 11:03:55 DEBUG : Retrying request
#>      ++ 33 | 2026-06-30 11:03:55 Debug : Retrying Request
#> [10] -- 35 | 2026-06-30 04:43:43 DEBUG : Failed to authenticate
#>      ++ 35 | 2026-06-30 04:43:43 Debug : Failed To Authenticate
#> [11] -- 36 | 2026-06-30 16:12:44 DEBUG : Connection successful
#>      ++ 36 | 2026-06-30 16:12:44 Debug : Connection Successful

Finally, our seekr_match vector can be used to replace only the selected matches in the files with their corresponding replacements.

replace_files(to_replace)