Skip to content
grepvine.aiJoin preview

Searching

A search runs one pattern over every searchable document in an index and returns every match, sorted by key, then line, then offset. The examples search acme/monorepo, a git repository kept in step with gv sync, and acme/support, an index of support tickets.

This search finds every HTTP handler under services/billing/ and returns three lines after each one.

import os
import grepvine
from grepvine.models import QueryRequest
client = grepvine.Client(api_key=os.environ["GREPVINE_API_KEY"])
query = QueryRequest(
pattern=r"func \w+Handler\(",
filter_="path STARTSWITH 'services/billing/'",
lines_after=3,
)
for hit in grepvine.paginate(client.search.query, organization="acme", index="monorepo", body=query):
print(f"{hit.key}:{hit.line_number}: {hit.text}")

Each client names the query options a little differently:

API and TypeScript Python QueryRequest gv search Default
pattern pattern the positional PATTERN Required.
mode: "literal" mode=QueryRequestMode.LITERAL -F regex
case_sensitive: false case_sensitive=False -i true
whole_word: true whole_word=True -w false
multiline: true multiline=True -U false
filter filter_ --filter None.
lines_before, lines_after lines_before, lines_after -B, -A, or -C for both 0
amount (page size) amount= on search.query --amount 50, or 200 in gv

Literal mode matches the pattern’s bytes exactly, so it needs no escaping. Use it for identifiers, error messages, URLs and paths.

  • case_sensitive: false matches under simple Unicode case folding.
  • whole_word: true matches only where the characters on either side of the match aren’t word characters: letters, digits or _. It works in literal mode only.
Terminal window
gv search -F acme/support 'upstream timeout after 30000ms'
gv search -F -w acme/monorepo retry_count # skips max_retry_count
gv search -F -i acme/support 'connection reset by peer'

Regex mode, the default, uses RE2 syntax. It has no backreferences and no lookaround; tradeoffs explains why.

  • Character classes. \d, \w, \s and \b are ASCII. For other scripts, use Unicode classes such as \p{L}, \p{Lu} or \p{Han}.
  • Case. case_sensitive: false applies Unicode case folding to the whole pattern. (?i) does the same for part of it: (?i:error) E\d{4}.
  • Escaping. Quote patterns in single quotes in a shell, and use raw strings in Python (r"...") and TypeScript (String.raw`...`).
To find Pattern Index
Go HTTP handlers func \w+Handler\( acme/monorepo
TODOs that name an owner TODO\(\w+\): acme/monorepo
Possible AWS access key IDs AKIA[0-9A-Z]{16} acme/monorepo
Exact pins of requests ^requests==\d+\.\d+\.\d+$ acme/monorepo
Timeouts of 10 seconds or more timeout after \d{5,}ms acme/support
Order IDs pasted into tickets ord_[0-9a-z]{24} acme/support

By default, a pattern matches within one line:

  • ^ and $ mark the start and end of a line.
  • A character class never matches a line break.
  • A pattern that contains a line break is rejected.

With multiline: true, the pattern runs over the whole document:

  • ^ and $ still mark line edges, and \A and \z mark the document’s edges.
  • A match can contain line breaks, and its result covers lines line_number through last_line_number.

Even in multiline mode, . doesn’t match a line break. Use \n or \s to cross one, or start the pattern with (?s) to let . match it.

This finds Go error checks that return nil, which drops the error:

Terminal window
gv search -U acme/monorepo 'if err != nil \{\n\s*return nil\n\s*\}'

A filter restricts a search to documents whose metadata matches an expression:

path STARTSWITH 'services/billing/' AND NOT path STARTSWITH 'services/billing/vendor/'
mtime >= (CURRENT_DATE - INTERVAL '7 days')
product = 'checkout' AND priority <= 2 AND status != 'closed'
opened_at >= TIMESTAMP '2026-10-01T00:00:00Z' AND opened_at < TIMESTAMP '2026-11-01T00:00:00Z'
(lang = 'go' OR lang = 'rust') AND size < 1048576
generated = false
assignee IS NULL
Operator Applies to
=, != Every type.
<, <=, >, >= Strings, integers, numbers and timestamps. Strings compare in byte order.
STARTSWITH Strings.
IS NULL, IS NOT NULL Whether the document has the field at all.

Combine predicates with AND, OR, NOT and parentheses. NOT binds tighter than AND, and AND tighter than OR. Keywords are case-insensitive. ENDSWITH and CONTAINS aren’t supported; best practices shows what to store instead.

Each predicate puts the field on the left, as in size < 1048576. A field name starts with a letter or _ and holds letters, digits, _, . and -.

Type Write it as Examples
string Single quotes. Double a quote to include one. 'checkout', 'O''Brien'
integer Digits, with an optional -. 102400, -1
number Digits with a . or an exponent. 0.5, 1e6
boolean true or false. generated = false
timestamp TIMESTAMP and an RFC 3339 string. TIMESTAMP '2026-10-01T00:00:00Z'

CURRENT_DATE is midnight UTC today. Subtract an interval of days, hours, minutes or seconds from any timestamp: CURRENT_DATE - INTERVAL '36 hours'.

A comparison is true only when the document has the field with a value of the literal’s type. size = 1024.0 never matches an integer size, and priority = '1' never matches an integer priority.

When the field is missing or has another type, the comparison is unknown, and NOT of unknown is still unknown. A document matches only when the whole filter is true. Both status = 'closed' and NOT status = 'closed' therefore skip tickets with no status. To include them, say so:

status != 'closed' OR status IS NULL

A pattern or filter that doesn’t parse is rejected with a 400 and the code invalid_pattern or invalid_filter. A filter error names the byte where parsing stopped. For status = open, it reads filter byte 9: expected a value; quote a string, as in 'open'.

A result is one match, or several matches that share lines. With --json, gv search prints each result in the API’s shape:

{
"key": "services/billing/refund.go",
"revision": "9f2c41d07a3e",
"line_number": 42,
"last_line_number": 42,
"line_start_offset": 1187,
"line_end_offset": 1247,
"text_start_offset": 1187,
"text_line_number": 42,
"text": "func RefundHandler(w http.ResponseWriter, r *http.Request) {",
"matches": [{ "offset": 1187, "length": 19 }],
"text_truncated": false,
"matches_truncated": false,
"lines_after": [
{ "line_number": 43, "text": "\tctx := r.Context()", "text_truncated": false }
]
}
  • Offsets count bytes from the start of the document. A match starts at byte offset - text_start_offset of text.
  • Context lines arrive in lines_before and lines_after, up to 20 each. There are fewer at the start and end of a document, and the field is left out when there are none.
  • Long lines and dense matches are cut, and text_truncated and matches_truncated say so. Guarantees gives the limits.

To read more of the document around a result, request a byte range:

document = client.documents.get("acme", "monorepo", hit.key, range_="bytes=1187-4095")
print(document.payload.read().decode())

paginate and gv search fetch page after page until the last one. To stop early, break out of the loop, or pass --max-results to gv search. If you page by hand, keep going until has_next is false: a page can be empty while more results follow. Guarantees covers page tokens and snapshots.

gv search exits with status 0 when something matched, 1 when nothing did, and 2 on an error.