Command-line search: a ranked grep for a folder¶
Installing whoosh3 also installs a small whoosh command. It turns any
folder of notes, docs, or source files into a fast, ranked, stemmed
full-text search index you query from the terminal — a pure-Python alternative
to grep when you want relevance ranking and query operators instead of a
flat line match. There is no server to run, no port to open, and no native
build step.
Note
The command is a thin, copy-pasteable wrapper over Whoosh’s public API. If you want to build your own tool, read or fork src/whoosh/cli.py.
Install¶
pip install whoosh3
The import package is still whoosh (so it is a drop-in for existing code),
and the installed console command is whoosh:
$ whoosh --help
usage: whoosh [-h] [-V] {index,search,stats} ...
Check your installed version at any time with whoosh --version or
whoosh -V:
$ whoosh --version
whoosh 3.18.0 (https://github.com/priya-sundaram-dev/whoosh)
Index a folder¶
Build a search index for a directory. The index is stored in a
.whoosh_index subfolder, so it is easy to find, back up, or delete:
$ whoosh index ~/notes
Indexed /home/you/notes
128 added -> 128 docs total in 0.42s
index stored at /home/you/notes/.whoosh_index
By default, common text and source extensions are indexed. Limit which files
are picked up with --ext (comma-separated):
$ whoosh index ~/notes --ext .md,.txt,.rst
You can exclude specific folders or files using –exclude with glob patterns. This can be specified multiple times:
$ whoosh index ~/notes --exclude "build/*" --exclude "*.min.js"
Skip large files with --max-size. The limit is a whole number of bytes,
optionally followed by a k, m, or g suffix (case-insensitive, an
optional trailing b is allowed, so 2m, 2M, and 2MB are
equivalent). Suffixes are powers of 1024, so 500k is 512000 bytes and
2MB is 2097152 bytes. Files above the limit are skipped before their
contents are read, which keeps indexing fast and memory-friendly on trees that
contain the occasional huge log, dump, or binary blob. There is no size limit
by default:
$ whoosh index ~/notes --max-size 2MB
Preview which files would be indexed — under the current --ext and
--exclude filters — without building anything, using --dry-run. It
prints one relative path per line to stdout (easy to pipe or grep) and a
short summary count to stderr, then exits without creating, clearing, or
writing the .whoosh_index directory:
$ whoosh index ~/notes --dry-run --exclude "build/*"
ideas.md
todo.txt
Would index 2 files under /home/you/notes
Re-index incrementally with --update. Only files whose modification time
changed are re-read, and files that were deleted are dropped from the index —
so keeping a large tree fresh is cheap:
$ whoosh index ~/notes --update
By default Whoosh does not descend into symlinked directories. Pass
--follow-symlinks to index files reachable only through a symlink (for
example a docs/ folder linked in from elsewhere). It is off by default for
safety — following symlinks can pull in unexpected trees or, with cyclic links,
walk in circles:
$ whoosh index ~/notes --follow-symlinks
Search a folder¶
Query the index. Results are ranked with BM25 (best matches first) and show a short highlighted snippet of the surrounding text:
$ whoosh search "full text search" ~/notes
3 matches for 'full text search':
1. search/design.md (score 4.21)
... a pure-Python FULL TEXT SEARCH library that ships as one pip install ...
A brief summary line indicating how many matches were found is also printed to stderr.
Because matching is stemmed, a search for search also matches
searching and searched — something a literal grep will not do.
The query supports Whoosh’s full query language:
boolean operators:
python AND search,index OR store,search NOT sqliteexact phrases:
"full text search"field terms:
title:readme(documents are indexed withtitle,path, andbodyfields;titleis boosted so filename matches rank higher)
Useful options:
$ whoosh search "index writer" ~/notes --limit 20 # show up to 20 hits
$ whoosh search "index writer" ~/notes --limit 20 --page 2 # show the next page
$ whoosh search "index writer" ~/notes --min-score 1.5 # drop weak matches
$ whoosh search "index writer" ~/notes --html # <mark>...</mark> snippets
$ whoosh search "index writer" ~/notes --color always # ANSI-colored matches
$ whoosh search "index writer" ~/notes --no-highlight # plain, grep-friendly snippets
$ whoosh search "index writer" ~/notes --snippet-chars 80 # shorter snippets
$ whoosh search "index writer" ~/notes --json # JSON array output
$ whoosh search "index writer" ~/notes --jsonl # JSON Lines output
$ whoosh search "index writer" ~/notes --count # output just the number of matches
$ whoosh search "index writer" ~/notes -l # just the matching file paths
$ whoosh search "index writer" ~/notes --sort-by mtime # newest files first
$ whoosh search "index writer" ~/notes --field title # search titles only
$ whoosh search "index writer" ~/notes --or # match ANY term, not all
--page N selects a 1-based page of results, with --limit as the page
size. Page 1 is the default. Human-readable output includes page metadata after
the first page; JSON, JSON Lines, and count output remain machine-friendly.
--min-score FLOAT keeps only hits whose relevance score is at or above the
given floor, trimming the long tail of weak matches a broad query can surface.
It composes with every output mode — default text, --json/--jsonl,
--count, and -l/--files-with-matches — and applies to the score even
when results are ordered by --sort-by mtime. When the floor removes every
hit, the command behaves like a no-match search: it prints No matches ...
(or an empty [] for --json) and exits with status 1. There is no
floor by default.
By default a multi-term query such as index writer requires both terms
to appear in a document (index AND writer). Pass --or to match documents
containing any of the terms (index OR writer) for broader, more
exploratory searches; documents matching more of the terms still rank higher:
$ whoosh search "index writer" ~/notes --or # index OR writer
Repeat --field to search more than one selected field with equal weighting.
When it is omitted, Whoosh searches title and body with the usual title
boost:
$ whoosh search "index writer" ~/notes --field title --field body
--field controls which fields are searched. The similarly named
--fields option accepts a comma-separated list of stored fields to include
in the output instead.
--html emits <mark>...</mark> around matched terms instead of the
default UPPERCASE highlighting, which is handy when piping results into a web
page or a note-taking tool.
--color {auto,always,never} colorizes matched terms in the default text
output using ANSI escape codes (bold yellow) instead of UPPERCASE. Because it
plugs into the same highlighting pipeline as --html, it emphasises the
actual matched tokens — including stemmed or expanded matches (a search for
jumping highlights jumps). The default, auto, colorizes only when
standard output is a terminal and honors the NO_COLOR
and FORCE_COLOR environment variables; always and never force it on
or off (useful for piping colored output into a pager such as less -R):
$ whoosh search "index writer" ~/notes --color always | less -R
--no-highlight prints a plain, whitespace-collapsed leading slice of the
document body with no match markup at all. This keeps output readable and
grep-friendly when piping into other tools where the UPPERCASED match
tokens get in the way.
--snippet-chars N sets the maximum number of characters shown per snippet
(default 200). It applies to the default text output, --no-highlight, and
the snippet field of --json and --jsonl output.
--json emits a machine-readable JSON array of matches, making it easy to
parse results with tools like jq.
--jsonl (alias --ndjson) emits newline-delimited JSON (JSON Lines):
one standalone object per match, with the same fields as an element of the
--json array. There are no surrounding brackets or trailing commas, so
line-oriented tools can process each match as soon as it is written. No matches
produces no output and exits with status 1:
$ whoosh search "install guide" --jsonl | jq -c 'select(.score > 1.5)'
-l / --files-with-matches prints just the matching file paths, one per
line, with no scores, snippets, or numbering — the same idea as grep -l. It
honors --limit/--page and, like --jsonl, produces no output and
exits with status 1 when there are no matches, which makes it ideal for
piping into other tools:
$ whoosh search "index writer" ~/notes -l | xargs wc -l
Use -0 / --null with -l to terminate each path with a NUL byte
instead of a newline. This safely handles spaces, newlines, and other unusual
characters in file names when piping to tools that support NUL-separated
input:
$ whoosh search "index writer" ~/notes -l -0 | xargs -0 wc -l
--null requires -l / --files-with-matches.
The output-style flags (--html, --no-highlight, --json,
--jsonl/--ndjson, --count and -l/--files-with-matches) are
mutually exclusive.
--count prints only the total number of matching documents as a single integer
and exits, which is great for shell pipelines. As an output-style flag, it
cannot be combined with the other modes above.
Inspect an index¶
whoosh stats prints a quick summary of an existing index without running a
query — handy for confirming an index built correctly, or for wiring index
health into a script:
$ whoosh stats ~/notes
Index: /home/you/notes/.whoosh_index
documents: 128
fields: 4
- body (TEXT)
- mtime (NUMERIC)
- path (ID)
- title (TEXT)
size on disk: 2.1 MB (7 files)
last updated: 2026-07-15 12:56:46
Add --json for machine-readable output (document count, fields with their
types, size in bytes, and last-modified timestamp), which parses cleanly with
tools like jq:
$ whoosh stats ~/notes --json
To see what a field actually contains, --top-terms FIELD lists that field’s
most frequent indexed terms, most-frequent-first, with their total
frequencies. Use --top N to cap the list (default 10):
$ whoosh stats ~/notes --top-terms body --top 5
...
Top terms in 'body':
312 the
190 and
143 index
98 search
71 python
This is a quick way to eyeball a corpus or sanity-check your analyzer. Naming a
field that does not exist, or a field type that has no text terms to rank
(such as NUMERIC or DATETIME), prints a short, clear error to stderr
and exits 2 rather than a traceback:
$ whoosh stats ~/notes --top-terms mtime
error: field 'mtime' (NUMERIC) does not store text terms, so it has no top terms to list; try a TEXT field
The term listing is human-readable output only — the --json payload is
unchanged.
Exit codes¶
The command uses conventional exit codes so it composes well in scripts:
Exit code |
Meaning |
|---|---|
|
success (index built, or at least one match found) |
|
the search ran but found no matches |
|
a usage/setup error (missing directory, or no index yet —
run |
How it works¶
whoosh index defines a small schema (title, path, body), walks
the directory, and writes each file into a Whoosh index using the same
How to index documents and Designing a schema APIs documented here. whoosh search
opens that index and runs a MultifieldParser query across the
selected --field values (title and body by default), then renders
highlighted snippets.
whoosh stats opens the index read-only and reports counts and metadata from
the reader and the on-disk files.
Everything the command does is achievable directly from the library — the CLI just wires the pieces together with sensible defaults. If you outgrow it (custom analyzers, extra fields, faceting, incremental writers in a long-running process), reach for the API directly; the Quick start is the place to start.
This documentation is maintained by Priya Sundaram, an AI software agent maintaining the Whoosh project. A human is looped in for anything that needs one.