What “Fuzzy” Really Ranks

Once fzf has the right records, the central question changes: what makes one matching record deserve to appear before another? This chapter focuses on fzf's search language and ranking model, from query punctuation to the algorithms that score character alignments. The goal is practical judgment: you should be able to express common search intent, choose a scheme that fits the record type, and understand when a faster or upstream search boundary is preferable—without mistaking textual ranking for semantic understanding.

From punctuation to intent

fzf's default search mode is extended search. A query can contain several terms separated by spaces, and each term can express a different relationship to the candidate text. The unadorned term is fuzzy: its characters must appear in order, but they do not need to be adjacent. A leading apostrophe requests an exact occurrence, a leading ^ asks for a prefix, and a trailing $ asks for a suffix. A term surrounded by apostrophes asks for an exact occurrence at word boundaries. A leading ! excludes candidates that satisfy the term. A bar separates alternatives.

These operators are small, but they are not interchangeable decorations. Consider the query ^music .mp3$ sbtrkt !fire. It asks for a candidate that starts with music, ends with .mp3, contains the fuzzy sequence sbtrkt, and does not contain fire. The query is a set of constraints, not a single spelling approximation. A query such as ^core go$ | rb$ | py$ combines a shared prefix with alternatives for the ending. A quoted term is useful when a person knows the exact text that matters and does not want the ranking algorithm to bridge gaps between characters.

The exact mode changes the default. With --exact, ordinary terms are exact matches, while the apostrophe prefix can be used to remove that interpretation for an individual term. This is valuable when the list is large or when a person wants the query to behave like a conventional text filter. Smart-case matching adds another layer: matching is case-insensitive by default, but a query containing uppercase letters becomes case-sensitive. That lets a short lowercase query remain forgiving while allowing capitalization to express intent when it matters.

The query is interpreted against the search scope chosen for the record. If a row contains a path, a line number, and matching text, --nth can restrict matching to the fields that carry the relevant signal. The person may still see the other fields and use them to decide, but they need not compete for ranking attention. This is why candidate shape and search syntax cannot be separated completely. A fuzzy query over an identifier, a description, and a timestamp is asking a different question from the same query over the description alone.

fzf also provides schemes for common ordering problems. The default scheme is general-purpose. The path scheme gives additional preference to characters after path separators and uses pathname-aware tie-breaking, which helps a file name match carry more weight than an equally placed match in a directory name. The history scheme is designed for command history or other input where chronological order matters; it avoids additional positional bonuses and gives more weight to input order. These choices do not change what text is present. They change what “good” means when multiple records satisfy the same query.

Tie-breaking makes the policy more explicit. A score can be equal even when two records have different lengths, matched chunks, path-name locations, or positions in the input. fzf can prefer shorter lines, shorter matched chunks, a match in the pathname, a match closer to the beginning or end, or an item that appeared earlier. The default preference is length, with input order available as a final stabilizer. A path-oriented selector and a history selector can therefore show different first choices without either one being inconsistent. They are applying different priorities to the same basic textual operation.

This is the useful way to think about ranking: it is an interpretation of a decision, not a claim about the object. If a person searches a list of files for cfg, a match at the beginning of a file name may be more useful than the same letters deep in a directory. If they search history, a recent command may be more useful than a shorter old command. If they search process rows, the right field may be a process name or identifier rather than the entire formatted line. fzf can express these preferences because the producer and the query expose the relevant structure.

But the ranking remains textual. It does not know that two differently spelled entries refer to the same concept, that one process is more important than another, or that a line of source code is a better answer to a question merely because its characters align well. A high score means that the candidate fits fzf's configured matching and tie-breaking rules. It is a useful ordering for a human decision, not a semantic verdict.

flowchart LR Q["Typed query"] --> T["Parse terms"] T --> K["Fuzzy, exact, anchored, inverse, or OR"] K --> S["Search scope and input scheme"] S --> C["Compare candidate text"] C --> R["Score and tie-break"] R --> D{"Choose a search boundary"} D --> V2["Optimal matching"] D --> V1["Faster matching"] D --> U["Upstream search, then fzf"]

Quality has a cost

The difference between “a match” and “the best match” is where fzf's two fuzzy algorithms become important. The older V1 algorithm scans for the first fuzzy occurrence and then works backward to find a shorter matching substring. Its behavior is simple and fast, with linear time in the length of the item, but it is not guaranteed to discover the alignment with the highest score. It stops early because early discovery is part of its performance advantage.

V2 takes the opposite approach. It is a modified form of the Smith–Waterman dynamic-programming algorithm, adapted so that every character in the pattern must be matched. Rather than settling for the first plausible alignment, it examines alternatives within the candidate text and chooses the alignment with the highest score under fzf's rules. When a match is found, the general calculation can take time proportional to the product of item length and pattern length; when there is no match, it can finish in linear time. The manual exposes the choice as --algo=v2 for quality and --algo=v1 for performance, with V2 as the default.

The score is not just a count of matched characters. fzf gives bonuses to special positions, such as the beginning of a word or a capital letter in a camelCase name. That makes an acronym-like alignment recognizable. It also gives the first query character extra weight, because the beginning of a typed pattern often carries more intent than later characters. Those bonuses are balanced by penalties for gaps between matched characters. Otherwise, a long acronym could always defeat a compact ordinary match, even when the compact match is more useful.

The balance can be seen in the kinds of examples the algorithm documents. For a query such as ff, a match at the beginnings of words can beat a shorter-looking alignment because those positions are informative, but the advantage eventually disappears as the gap grows. For a query such as foob, consecutive characters receive an additional bonus so that foobar is not unfairly ranked below foo-bar merely because a separator creates a boundary bonus. The scoring system is therefore trying to combine two intuitions: special positions help a person recognize a result, while compact consecutive text usually remains preferable.

That is why “fuzzy” should not be reduced to “characters occur in order.” The order constraint determines whether a candidate can match. The scoring model determines which alignment inside the candidate is considered most useful. A file name can match through a directory prefix, a camelCase boundary, or a compact run of letters; the result depends on which combination of bonuses and gap penalties wins. When the user changes from the default scheme to a path or history scheme, the same character positions are interpreted through a different purpose.

V2 also contains an important admission about cost. Its score matrix is not allowed to grow without bound. If the input and pattern would require a matrix larger than the available workspace, or if the pattern exceeds the implementation's safe length limit, fzf falls back to V1. Single-character and two-character ASCII patterns use specialized paths that avoid the general matrix as well. These paths are not a promise that every query is equally cheap; they are examples of the implementation spending sophistication where it can change the result and using simpler work where the general calculation would not pay for itself.

The same cost reasoning appears outside the matcher. Processing ANSI color codes requires fzf to parse the input before scanning it. Limiting search with --nth requires tokenizing each line. A regular-expression delimiter costs more than a plain delimiter, and --with-nth can require fzf to tokenize and reassemble every record. These options may be exactly right for a structured workflow, but they are not free. A feature that improves the human view can move work into ingestion or transformation, where it affects every candidate rather than only the few records eventually selected.

Corpus size changes the decision again. fzf is designed to filter a list quickly, but the project's advanced examples draw a line around searching inside many large files. In that situation, ripgrep can be the primary filter that searches file contents, while fzf becomes a secondary interactive filter over the matching lines. A different variant reloads ripgrep whenever the query changes and disables fzf's own search, trading fuzzy ranking for a search boundary that can perform better on a larger project and use less memory as the result narrows.

That trade-off is not a defeat for fzf's algorithm. It follows from responsibility. An upstream content-search tool knows how to traverse and inspect a corpus; fzf knows how to rank and select the records it receives. If fzf is asked to scan every long line from every large file, its general-purpose matcher is operating on a problem the producer could reduce earlier. If the producer returns a manageable set of meaningful rows, V2 can spend its extra work improving the human ordering of those rows.

There are also cases where speed is the purpose. A continuously changing process or log view may prefer input order over relevance, and a user may deliberately disable sorting. A completion widget may value a quick first response over a marginally better ranking. An exact query may remove the need for fuzzy scoring altogether. Conversely, a small but ambiguous file list may benefit from V2 because a slightly better ordering prevents repeated manual scanning. The right choice depends on the cost of a mistaken or delayed selection, not on an abstract preference for one algorithm.

A good diagnostic sequence is therefore: first ask whether the producer has already narrowed the corpus; then ask which fields should be searched; then choose the query semantics and scheme; and only after that consider algorithm speed. Changing --algo cannot compensate for a producer that emits the wrong records or a query that searches irrelevant fields. Nor can a high score compensate for a stale list or a consumer that expects a different output form.

The most important boundary is the one fzf does not cross. Its score is evidence that a textual alignment satisfies a configured policy. It is not evidence that the candidate is correct in the domain, safe to execute, current, or more valuable in a broader sense. Human selection remains part of the system because the final judgment may require context that character-level scoring cannot represent.

That distinction leads to the next operating question. Ranking tells fzf what result it wants once a query and candidate set are known. It does not explain how fzf keeps reading input, rescanning records, cancelling obsolete work, and updating the terminal while the person is still typing. The next layer is the runtime that turns a ranking function into a responsive interaction.

References