The data that is not a number

The filter that feeds the table

A self-index answers exact queries and nothing else. Approximate matching needs a table with twenty thousand columns in it. The pigeonhole joins them — cut the pattern into k+1 pieces and at least one occurs exactly, and the index that cannot answer the question decides where to ask it.

Two structures, built in this collection, that cannot talk to each other.

The index. A compressed self-index answers how many times does this exact string occur in about two rank operations per pattern character, without comparing a single character and without keeping the text. It answers nothing else. Ask it for a string with one letter wrong and it returns zero, correctly.

The table. A semi-global dynamic programme finds every position where a pattern occurs within kk errors, by filling a rectangle as wide as the text. A pattern of 24 characters against 20,000 of text is 480,000 cells, and the cost does not depend on whether the answer is seven positions or none.

The thing that joins them is a counting argument two sentences long.

The funnel at 3 errors: 20,000 positions to 7 answersA pattern of 24 characters over four symbols, allowed 3 errors. The index proposes 140 starting positions out of 20,000; each is verified by a banded table of at most 24x30 cells; 7 survive. The whole thing computes 28,700 table cells against the 480,000 of filling the rectangle over the whole text — a factor of 16.7. The selectivity, which is the only number that says whether the filter was worth running, is 5.0%.pattern of 24, 3 errors allowedpositions in the text20,000candidates proposed140candidates verified140occurrences7pattern 24 · 3 errors · 28,700 cells against 480,000selectivity 5.0%
Fig. 1 Twenty thousand positions, a hundred and forty candidates, seven answers. Every stage in between is the index doing something it can do in order to avoid the table doing something it cannot afford.

The pigeonhole

Cut the pattern into k+1k+1 pieces. An occurrence with at most kk errors cannot have an error in every piece — there are k+1k+1 pieces and only kk errors to go round — so at least one piece occurs exactly.

That is the whole of it, and it converts a question the index cannot answer into k+1k+1 questions it can.

A pattern of 24 cut into 4 pieces, and where each occurs exactlyAn occurrence within 3 errors cannot have an error in every piece, so at least one piece must occur exactly — which is the whole reason an index that answers only exact queries can be made to answer this one. Each piece is 6 characters and is looked up by backward search with the text withheld. Between them the 4 pieces report 23 exact hits, which widen into 140 candidate starting positions and verify down to 7. Cutting the pattern more finely is always allowed and always shortens the pieces.4 pieces of 6 characters eachpiece 1: gcacag1at pattern offset 0piece 2: gtccgc8at pattern offset 6piece 3: aacgcg6at pattern offset 12piece 4: aacgat8at pattern offset 18pattern of 24 · 3 errors allowed · pieces of 6140 candidates
Fig. 2 A pattern of twenty-four characters cut into four pieces of six, with the number of exact occurrences each piece has in the text. The pieces are looked up by backward search with the text withheld, so the positions come out of the structure rather than out of a copy of the text beside it.

A hit for the piece beginning at pattern offset qq, found at text position pp, places the occurrence’s start near pqp - q. How near is decided by the errors: an occurrence with kk edits can have its start displaced by at most kk, so each hit widens into 2k+12k+1 candidate starting positions.

piece exact occurrences in the text
gcacag at offset 0 1
gtccgc at offset 6 8
aacgcg at offset 12 6
aacgat at offset 18 8

Twenty-three hits, widened and deduplicated into 140 candidate starts, of which 7 survive verification.

How the pieces are cut, and where the spare characters go

Twenty-four characters into four pieces is six each and there is nothing to decide. Twenty-five is not, and the choice matters more than it looks.

The pieces here are m/(k+1)\lfloor m/(k+1)\rfloor characters each, with the last piece taking the remainder. So a pattern of 26 cut into four gives pieces of 6, 6, 6 and 8, and the longest piece is at the end.

The alternative — spreading the remainder over the first pieces — gives 7, 7, 6, 6, and it is worse for a reason worth naming: the filter’s candidate count is dominated by the shortest seed, because a seed of length ss is expected to occur n/σsn/\sigma^s times and that number is exponential in ss. Cutting 6, 6, 6, 8 leaves three seeds of six; cutting 7, 7, 6, 6 leaves two of six. Two shortest seeds are better than three, so the second cut is in fact the better one — and neither is what the arithmetic actually wants, which is pieces as equal as possible.

The version here takes the simpler rule and states it, because on the lengths measured the remainder is zero and nothing depends on it. What the rule cannot be is unequal by design: a filter that cut a pattern into one long piece and kk short ones would still be correct by the pigeonhole and would propose candidates by the thousand, and its correctness would be no defence at all.

Verification is the table, made small

Each candidate is checked by the same recurrence the whole-text table uses, on a window of m+km + k characters instead of on the text. And it is banded: a cell whose row and column differ by more than kk cannot lie on a path of cost kk or less, so it is never computed.

cells per candidatem(2k+3)\text{cells per candidate} \le m \cdot (2k+3)

At m=24m = 24 and k=3k = 3 that is about 216 cells, against 480,000 for the rectangle.

cells computed
the whole table 480,000
140 verifications 28,700
ratio 16.7×
The last row of the table: "measured" against 156 charactersCell (m, j) of a table whose top row is zero holds the cost of the best alignment of the whole pattern ending at position j of the text. So the curve is the text scored, position by position, and every local minimum is a place the pattern nearly occurs. The rule at k = 3 is the threshold: 13 positions are at or under it, and the best is 0 at position 150. Filled under every substitution costs 1, every gap character costs 1.048best: 0 at 150position in the textcost of the best match ending here, in editsdashed: the threshold k = 3 · unit cost13 positions under it
Fig. 3 The row the whole-text table reads its answers off, from the essay that built it. Every value in it is a distance from the pattern to a text position, and the filter’s job is to decide which columns of that row are worth computing.

The saving is not the ratio of the answer to the text. It is the ratio of the candidates to the text, and the whole design lives in the gap between those two numbers.

What the filter costs on its own side

The index queries are not free either, and it would be a poor accounting that charged the table and not the structure that replaced it.

Locating the twenty-three seed occurrences performed 1,749 rank operations on bit vectors. Reading the 140 candidate windows touched 660 distinct text positions — 3.3% of the text — which is what the verification stage costs in the unit the matching field charges a scan in.

The two stages are counted separately and never summed, because they are different acts on different structures: a rank on a bit vector is a directory read and a mask, a table cell is three additions and a minimum, and a number adding them would belong to no level of anything. What can be said is that both are small, and that the second is the one the whole-table alternative pays 480,000 of.

The stages are checked against each other

An approximate matcher is easy to make fast by making it wrong, and there are two ways to do it here. Both are checked on every build.

Missing an occurrence. The filter’s answers are compared against the whole table’s, at every kk in the sweep, on the same text. Every end position the table reports must be covered by some start the filter reports.

The reconciliation is not trivial and is worth a sentence, because the two stages answer different questions. The table reads its answers off its last row, so it reports end positions. The filter proposes start positions and verifies windows. An occurrence starting at ss with at most kk errors ends somewhere in [s+mk, s+m+k][s+m-k,\ s+m+k], so the check expands each start into its possible ends and requires the table’s set to be contained. At k=7k=7 the filter reports 75 starts against the table’s 48 ends, and neither number is wrong: several starts share an end when the alignment can absorb the difference in a gap.

Reporting a candidate as an answer. This is the interesting one, because a filter that skips verification returns the right answers whenever its candidates happen to be right, and is enormously faster.

The text is handed to the verification stage through an object with a single accessor, which records every read at the position it happened. Afterwards, one condition:

Every character of every occurrence reported must have been read.

A genuine verification passes it, because verifying a window means reading the window. A filter that reports its candidates directly reads only the seed — six characters of twenty-four — and is stopped at the first occurrence whose seventh character nobody looked at. The check is the same device this field uses to stop an index quietly reading the text it claims to have replaced, applied to a stage that is allowed to read the text and must be shown to have done so.

Where the widening comes from, and why it is 2k+1

The step between “the seed occurs here” and “an occurrence might start here” is the one place the construction can be quietly wrong, and it is worth doing the arithmetic rather than asserting the constant.

A piece beginning at pattern offset qq is found at text position pp. If the occurrence were exact, its start would be pqp - q exactly. It is not exact: there are up to kk edits, and any of them lying before the piece shifts the piece’s position relative to the pattern’s start. An insertion moves it right by one, a deletion moves it left by one, and a substitution does not move it at all.

So the start lies in [pqk, pq+k][p - q - k,\ p - q + k]2k+12k+1 positions — and no fewer, because kk insertions before the seed is a real occurrence and so is kk deletions.

Two consequences follow, and both are measurable.

The candidate count is the number of seed hits times 2k+12k+1, minus whatever collides. At a real occurrence all k+1k+1 pieces point at the same start, so a genuine occurrence contributes exactly 2k+12k+1 candidates however finely the pattern was cut. At a chance hit only one piece fires, so it also contributes 2k+12k+1. The two are indistinguishable at this stage — which is the point of a filter, and the reason the next stage exists.

And the window verified is m+km + k characters, not mm. An occurrence with kk deletions in the pattern is m+km + k characters of text. Verifying mm would find the shorter alignments and miss the longer ones, and would find every occurrence in a test whose edits happened to be substitutions — which is exactly the shape of test somebody writes first.

The selectivity is the only number that matters

A filter is worth having exactly when the candidates it proposes are few. That ratio has a name here and it goes on every plate in this family.

kk seeds of candidates answers selectivity table cells saved
0 24 3 3 100% 2,286×
1 12 9 9 100% 460×
2 8 20 15 75% 149×
3 6 154 21 13.6% 15.2×
4 4 2,679 27 1.0% 0.7×
What the filter computes, against what it was avoidingThe flat line is the whole table: 480,000 cells, a pattern of 24 against 20,000 characters, and it does not move with k. The rising curve is what the filter computes — a banded table at every candidate — and it crosses the flat line at k = 4, where the seeds have fallen to 4 characters. Below that crossing the filter saves up to 2285.71x; above it, running the filter and then verifying its candidates costs more than filling the rectangle would have. The k axis is drawn as k+1 so that k = 0 has a place on a logarithmic scale.010³10⁴10⁵10⁶errors allowed, k+1table cells computedthe whole table: 480,0002412864433k = 4filter and verifynumbers above the points are the seed lengthscrossing at k = 4
Fig. 4 What the filter computes against what it was avoiding. The flat line is the rectangle, which does not move with k; the rising curve is filter-plus-verify, which crosses it at four errors.

At zero errors the filter is the index answering the question directly and the table is not needed at all, which is a saving of three orders of magnitude. At four errors, running the filter and then verifying its candidates costs more than filling the whole rectangle would have.

That collapse has an essay of its own, because the mechanism is arithmetic rather than luck and there are two thresholds in it, both computable before anything is measured.

Why this is what every practical matcher does

Nothing above is a trick. It is the shape of every approximate matcher that runs on data large enough to care about, and the shape is forced by three facts that have all been measured in this collection.

The exact question is cheap and the approximate one is not. Counting occurrences of an exact string is two ranks per character and independent of the text length. Approximate matching is nmnm cells, and under a standard conjecture that cannot be improved to n2ϵn^{2-\epsilon} in the worst case.

The bound is about the worst case and about exactness, so it forbids a general algorithm and forbids nothing about a filter. A filter does not compute edit distance faster; it computes it in fewer places, which the bound says nothing about.

And the index is already there. A search system that supports exact queries has built the structure; the filter is a loop over k+1k+1 backward searches on top of it. The marginal cost of approximate matching, given an index, is the verification — and the verification is what the selectivity decides.

The pigeonhole is checked, not quoted

An argument this short is easy to state and easy to state slightly wrong — with kk pieces instead of k+1k+1, or with the pieces overlapping, or with the last piece taking the remainder in a way that leaves it shorter than the others.

So it is performed. Eight hundred strings within kk substitutions of a pattern are constructed at k=1,2,3,4k = 1, 2, 3, 4, and each is required to contain at least one of its pattern’s k+1k+1 pieces exactly. None fails.

The construction is deliberately the easy case — substitutions only — and the check that covers insertions and deletions is the one above, which compares against the table’s own answers on text containing genuine occurrences of both kinds. Two checks, because the short one proves the counting argument and the long one proves the implementation of it.

A pattern of 24 cut into 3 pieces, and where each occurs exactlyAn occurrence within 2 errors cannot have an error in every piece, so at least one piece must occur exactly — which is the whole reason an index that answers only exact queries can be made to answer this one. Each piece is 8 characters and is looked up by backward search with the text withheld. Between them the 3 pieces report 4 exact hits, which widen into 10 candidate starting positions and verify down to 5. Cutting the pattern more finely is always allowed and always shortens the pieces.3 pieces of 8 characters eachpiece 1: gcacaggt1at pattern offset 0piece 2: ccgcaacg1at pattern offset 8piece 3: cgaacgat2at pattern offset 16pattern of 24 · 2 errors allowed · pieces of 810 candidates
Fig. 5 The same pattern cut into three pieces instead of four. Longer pieces occur less often by chance, so the same text yields a fifth of the candidates — and cutting into fewer pieces than the errors require is not allowed, which is what makes this dial one-sided.
The funnel at 1 errors: 20,000 positions to 3 answersA pattern of 24 characters over four symbols, allowed 1 errors. The index proposes 3 starting positions out of 20,000; each is verified by a banded table of at most 24x26 cells; 3 survive. The whole thing computes 348 table cells against the 480,000 of filling the rectangle over the whole text — a factor of 1379.3. The selectivity, which is the only number that says whether the filter was worth running, is 100.0%.pattern of 24, 1 errors allowedpositions in the text20,000candidates proposed3candidates verified3occurrences3pattern 24 · 1 errors · 348 cells against 480,000selectivity 100.0%
Fig. 6 The funnel at one error, where the two pieces of twelve characters are long enough to occur nowhere by chance and every candidate is an answer. This is what a filter looks like when it is working perfectly, and it is the left-hand end of every sweep in the next essay.

More pieces than errors, and the filter gets sharper

The pigeonhole gives a floor on the number of pieces and no rule, which the section above records as an open choice. It is worth working out, because the arithmetic points somewhere the simple version does not go.

Cut the pattern into pp pieces rather than k+1k+1. Each error spoils at most one piece, so at least pkp - k pieces occur exactly. With p=k+1p = k+1 that is one, which is the version built here. With p=k+2p = k+2 it is two, and with p=k+jp = k+j it is jj.

That changes what a candidate has to look like. At p=k+1p = k+1 a single seed hit is enough to propose a start. At p=k+2p = k+2 a start is only worth proposing if two different pieces both point at it, and requiring agreement between two independent chance events is a much stronger filter than requiring one.

The two effects pull against each other and both are computable. Shorter pieces occur more often by chance — the expected hits for a piece of length m/pm/p are n/σm/pn/\sigma^{m/p}, exponential in the piece length, so raising pp raises the hit count sharply. But the probability that two chance hits from different pieces land on the same start is the product of two small numbers rather than one, and a product falls faster than the individual terms rise.

Where the balance lands depends on mm, kk, σ\sigma and nn — the same four parameters that decide everything else in this family. The important point is structural: going from one required seed to two converts the candidate count from something linear in the chance-hit rate to something quadratic in it, and quadratic in a small number is the direction worth going.

It also costs nothing new to implement. The seeds are already located; what changes is the bookkeeping that turns hits into candidates — count how many distinct pieces vote for each start, and propose only the starts with at least pkp - k votes. That is a tally over the same hits rather than an extra index query.

Nothing above is measured here, and the reason is worth stating rather than skipping. The version in this collection is the p=k+1p = k+1 one because its correctness argument is a single sentence, and adding a second parameter to a filter whose selectivity already has an essay’s worth of behaviour would mean measuring a surface rather than a curve. What is offered here is the arithmetic that says which direction the surface slopes.

Why the floor is a floor and not a target

One thing follows from the same counting that is worth stating explicitly, because it is the reason the dial is one-sided.

Cutting into fewer than k+1k+1 pieces is not a worse filter; it is not a filter at all. With pkp \le k the errors can spoil every piece, no piece is guaranteed to occur exactly, and an occurrence can be missed with no indication. The algorithm would still run, still verify whatever candidates it found, and still report answers — just not all of them.

That is the same class of failure the verification check in this essay exists to catch, arriving from the other end: a filter that reports fewer answers than there are looks exactly like a query that had fewer answers. The pigeonhole is not a heuristic that gets better with more pieces; it is a correctness condition with a hard floor and a free parameter above it, and the two halves of that sentence want completely different treatment.

What is not measured here

A clock, and the two stages are not comparable without one. The plate reports rank operations on one side and table cells on the other, and which of them a processor charges more for is a question a count cannot answer.

Other filters. The pigeonhole partition is the simplest correct filter and not the only one. Counting filters, qq-gram filters and the backtracking search that walks the index itself with an error budget are each used in practice, and none of them is built here. What is built is the one whose correctness argument fits in a sentence.

The optimal number of pieces. Cutting into more than k+1k+1 pieces is always allowed and always shortens them, so there is a choice here and nothing above makes it. The pigeonhole gives a floor on the piece count, not a rule.

Weighted models. Everything above is unit cost — every substitution and every gap character costs one — and the pigeonhole argument depends on it: an occurrence “within kk” has to mean at most kk discrete edits for the counting to work. Under a cost model with real-valued entries there is no such argument, and the filters that exist for that case are a different subject.

The last row of the table: "measured" against 156 charactersCell (m, j) of a table whose top row is zero holds the cost of the best alignment of the whole pattern ending at position j of the text. So the curve is the text scored, position by position, and every local minimum is a place the pattern nearly occurs. The rule at k = 2 is the threshold: 9 positions are at or under it, and the best is 0 at position 150. Filled under every substitution costs 1, every gap character costs 1.048best: 0 at 150position in the textcost of the best match ending here, in editsdashed: the threshold k = 2 · unit cost9 positions under it
Fig. 7 How many places a pattern is found as the error budget grows, from the essay that built the table. The rising curve there is the numerator of the selectivity here; the denominator rises much faster, and that is the next essay.
A pattern of 32 cut into 4 pieces, and where each occurs exactlyAn occurrence within 3 errors cannot have an error in every piece, so at least one piece must occur exactly — which is the whole reason an index that answers only exact queries can be made to answer this one. Each piece is 8 characters and is looked up by backward search with the text withheld. Between them the 4 pieces report 5 exact hits, which widen into 14 candidate starting positions and verify down to 7. Cutting the pattern more finely is always allowed and always shortens the pieces.4 pieces of 8 characters eachpiece 1: gcacaggt1at pattern offset 0piece 2: ccgcaacg1at pattern offset 8piece 3: cgaacgat2at pattern offset 16piece 4: ggtcatca1at pattern offset 24pattern of 32 · 3 errors allowed · pieces of 814 candidates
Fig. 8 A longer pattern at the same error count: four pieces of eight instead of four of six. Two extra characters of seed reduce the chance hits by a factor of sixteen, because a seed’s expected occurrences fall as the alphabet to the power of its length.

What this makes readable

Essays that name this one as a prerequisite.

Named alongside this one

Essays reaching for the same objects. Nobody chose these; they are what the concept index makes visible.

What links here

The 8 essays that link to this one and share the most of its objects, of 14 that link here.

The objects this essay names

Each one links to every other essay that touches it.

Approximate matchingBanded dpDynamic programmingEdit distanceFalse-positiveFilteringFM-indexMeasurementPigeonholeSeed-and-extendSelf-indexVerification