The index that replaces the text

The sampling that follows the runs

A run-length index over thirty-two copies of one text spends 11,286 bits on its suffix-array sampling and 5,605 on the transform it was built to compress. Sample at the run boundaries instead and the sampling is 10,942 bits that stop moving — two values per run, and a function that fills in everything between them.

Two phases ago this collection built an index whose transform is stored as runs, measured it across a collection of thirty-two identical copies, and reported the finding that mattered rather than the one that looked good: the parts that follow the run count grew 1.45 times and the sampling grew 18.3 times, from seven per cent of the structure to half of it.

The structure had been described as proportional to rr. Half of it was proportional to nn.

An r-index over 32 copies, in partsThe structure over 16,385 characters with 233 runs in its transform. Every part is a function of r: the two suffix-array values kept per run are 6,990 bits and the predecessor structure that makes phi computable is 3,952, so the whole sampling is 10,942 bits — 64% of the structure and containing no term in n but the width each value is written in. The parts are checked to sum to the reported total on the plate.sampled positions6,99041%phi predecessor3,95223%run starts2,09712%run lengths per symbol1,97012%run heads1,5389%C table5003%English-like · n = 16,385 · r = 233the sampling is the two shaded rowsone unit = one bit17,047 bits · 1.04 bits/char
Fig. 1 An r-index over sixteen thousand characters whose transform has 233 runs. Every part is a function of that number, including the two shaded rows, which are the sampling.

What the sampling is for, and why it cannot simply be dropped

An index answers three questions and they are three different costs. How many is answered by backward search, which narrows an interval of rows one pattern character at a time and never touches a suffix-array value. Where is answered by turning a row into a text position, and that is what a sampling is for. What — produce the text itself — is answered by walking the transform backwards.

The second question is the one with a dial on it. A row does not know its own position; the array that would say is nlog2nn\lceil \log_2 n\rceil bits, which is larger than the text and larger than the index. So a regular sampling keeps one value in every ss of them and walks: from an unsampled row, take LF steps until a sampled row is reached, and add the number of steps taken. At most ss steps, one value in ss kept, and the product of the two is fixed.

The sampling that goes the other way measured the same dial for extraction rather than for location, and an index larger than what it indexes is where this collection first weighed the thing the dial is spending.

Where a run-length index's bits go at 32 copies16,384 characters whose transform has 224 runs — the same 224 it had at two copies. The parts proportional to r total 5,989 bits, 1.5x what they were at two copies. The two marked parts are the sampling, which is proportional to n: 11,286 bits, 19.0x what they were, and 65% of the structure. A structure whose size is a function of the run count still has an n in it, and the n is the answer to "where", not to "how many" — which is what the run-boundary sampling of an r-index removes and this structure does not.sampled positions7,695grows with nsample marks3,591grows with nrun starts2,016grows with rrun lengths per symbol1,885grows with rrun heads1,563grows with rC table525grows with r17,275 bits in total16,384 characters · r = 224 · sample one in 3265% is the sampling
Fig. 2 The structure this one replaces, at the same point in the same collection. The two sampling rows are the largest in the table, and they are the two that have nothing to do with the run count.

Two values per run

The replacement is one sentence long. Keep the suffix-array value at the first row of every run and at the last row of every run, and keep nothing else.

That is 2r2r values. There is no marking vector over the nn rows, because the rows that carry a value are not scattered through the array — they are the run boundaries, which the structure already knows where to find. There is no rate to choose, because the sampling is not a rate: it is a consequence of the transform’s shape.

On the collection above, r=233r = 233 and n=16,385n = 16{,}385. Four hundred and sixty-six values at fifteen bits each is 6,990 bits. The regular sampling at one value in thirty-two is 512 values, a marking vector over 16,385 rows, and 11,286 bits between them.

The sampling that stops growing, and the one that does notEnglish-like of 512 characters repeated up to 32 times, identically. The upper curve is the run-length index's regular sampling at one value in 32; the lower is the r-index's two values per run. Across the sweep the first grows 39.05x and the second 1.49x, and the second's growth is entirely the ⌈log₂ n⌉ each value is written in — r itself does not move once there is a second copy. Both axes are logarithmic.1,00010,00010³10⁴bits12481632characters in the collection · copies aboveregular, one in 32run boundaries, 2rEnglish-like · divergence 0r = 233 at n = 16,385
Fig. 3 The two sampling policies across a collection that grows by repeating itself. The upper line is one value in thirty-two; the lower is two values per run. The collection grows sixteenfold and the second grows 1.34 times.

The 1.34 is not noise, and it is not zero either

Across the sweep the regular sampling grows by a factor of nineteen and the run-boundary sampling by a factor of 1.34. The second number deserves a sentence, because a claim of “no term in nn” would be false and the difference is worth being exact about.

rr does not move: 233 at two copies and 233 at thirty-two. What moves is the width each value is written in. A position in a text of 1,025 characters is eleven bits and a position in a text of 16,385 is fifteen, so 466 values cost 5,126 bits at one end of the sweep and 6,990 at the other. The predecessor structure grows for the same reason and one more: its low parts are log2(n/r)\lceil\log_2(n/r)\rceil bits each, and with rr fixed that width grows with the collection too.

So the honest statement is that the sampling is O(rlogn)O(r\log n) and the previous one was O((n/s)logn)O((n/s)\log n), and across a sixteenfold sweep the first grew by a third and the second by nineteen. That is the difference between a logarithm and a linear term, measured rather than asserted, which is what a limit is not a prediction asks of every claim of this shape here.

What share of each structure is its samplingThe regular sampling's share of the run-length index climbs from 7% to 65% across a collection that is the same text repeated, because the sampling is n/32 values while everything else follows r. The number beside each bar is the same share for the r-index, which does not move: its sampling is 2r values whatever n is. Both structures hold the same transform.1 copy7%r-index 67%2 copies12%r-index 66%4 copies21%r-index 66%8 copies33%r-index 65%16 copies49%r-index 65%32 copies65%r-index 64%English-like · base 512 · sample 32share of the structure's own bitsn = 16,385
Fig. 4 The share of each structure that is its sampling, across the same sweep. The bars are the run-length index’s, climbing to two thirds; the number beside each is the r-index’s own share, which does not climb.

Why two values per run are enough for all nn positions

They are not, on their own. Two values per run are enough because of a function, and the function is the subject of a function with r pieces, which is the essay this one hands off to.

The short version, because the size claim here is empty without it: φ(i)\varphi(i) is the text position that precedes ii in suffix order. It is piecewise linear with slope one, it breaks only at positions whose row begins a run, and the value at each break is the suffix-array value of the row above — which is the last row of the previous run, which is already stored. So φ\varphi needs a predecessor structure over rr anchors and no values of its own, and applying it repeatedly walks an interval of rows without ever taking an LF step.

The other half is the toehold: one occurrence has to be known before φ\varphi can produce the rest, and it is produced during the backward search rather than afterwards. That is the occurrence carried through the search, and it is what makes the run-end values load-bearing rather than decorative.

What the parts look like when they are all one measure

The plate at the top of this essay is the structure opened up, and it is worth reading as an accounting rather than as a picture. Six parts, and every one of them is rr times a width:

The run starts are a sparse bit vector with rr ones over nn positions — 2,097 bits. The run heads are a wavelet tree over rr symbols — 1,538. The run lengths, one sparse vector per symbol, are 1,970. The sampled positions are 2r2r values — 6,990. The predecessor structure for φ\varphi is 3,952. The CC table is 500.

The parts sum to 17,047, and the plate asserts that they do. That check is cheap and it is not decorative: an index larger than what it indexes records that the commonest way a compressed structure is reported smaller than it is, is a part left out of the sum.

Two compressed indexes over one growing collectionEnglish-like of 512 characters repeated up to 32 times, identically. The upper curve is the run-length index's regular sampling at one value in 32; the lower is the r-index's two values per run. Across the sweep the first grows 4.42x and the second 1.55x, and the second's growth is entirely the ⌈log₂ n⌉ each value is written in — r itself does not move once there is a second copy. Both axes are logarithmic.1,00010,00010⁴bits12481632characters in the collection · copies aboverun-length, sample 32r-indexEnglish-like · divergence 0r = 233 at n = 16,385
Fig. 5 Both whole structures across the collection. They cross: the r-index is larger while the collection is short and smaller once it has repeated itself enough times, and at thirty-two copies they are within two per cent.

The two structures are nearly the same size, and that is not the finding

At thirty-two copies the r-index is 17,047 bits and the run-length index at one value in thirty-two is 17,391. Two per cent apart. A plate that stopped there would have shown a great deal of machinery bought for nothing.

The size is not the finding because the two structures do not answer the second question at the same price. The regular sampling walks up to thirty-two LF steps for every occurrence it locates; the r-index takes one predecessor query and no LF step at all. To match that, the regular sampling has to raise its rate, and raising the rate is what its size is made of.

Put the other way round: the comparison that means something is at equal locate cost, not at equal size, and it is the whole of every occurrence at the same price. The short answer is that a regular sampling reaching one step per occurrence on this collection costs 69,649 bits against the r-index’s 10,244.

Where it loses, stated here rather than left for the reader to find

At one copy — an ordinary English text of 512 characters, nothing repeated — the r-index is 10,974 bits and the run-length index at one in thirty-two is 3,939. Nearly three times larger, and the reason is arithmetic: rr is 229 on that text, so 2r2r is 458 values against the regular sampling’s 16.

The condition is 2r<n/s2r < n/s, which is a fact about the text. On English rr is 0.29 runs per character and on a four-symbol text it is 0.75, so a sampling proportional to rr is proportional to nn with a constant of a half or one and a half. A sampling that costs more than the array is that measurement in full, including the case where the sampling is twice the size of the whole suffix array it was invented to avoid storing.

A sampling in r against a sampling that makes a locate freeThe run-boundary sampling's bits divided by the bits of a sampling at one value in one — which is every suffix-array value kept, and the only regular rate that locates an occurrence in no steps at all. Both policies then answer a locate in constant work per occurrence, so the ratio is a like-for-like size comparison. On a collection of copies it is a twentieth; on ordinary English it is 0.80; on four symbols, uniform it is over one, because 0.75 runs a character means 2r is one and a half times n and the structure that removed the term in n has more values than there are positions.English-like, 32 copies0.05x0.017 runs/charEnglish-like, 8 copies0.12x0.045 runs/chara text that repeats itself0.08x0.029 runs/charEnglish-like0.80x0.292 runs/charfour symbols, uniform2.07x0.753 runs/charthe whole suffix arraysampling bits, run boundaries / every valuen = 8,192
Fig. 6 The run-boundary sampling against a sampling that makes a locate free, on five collections of about eight thousand characters. Under one it is the smaller policy; the last row is over two.

The crossing has a formula, and the formula has somebody’s parameter in it

The condition for the run-boundary sampling to be the smaller of the two is 2r<n/s2r < n/s, and left in that form it reads as a property of the text. Solved for the collection instead, it says something sharper.

A collection of cc copies of a base text of length bb has n=cbn = cb, and rr does not move with cc — that is the whole premise of a run-length index. So the condition becomes

c  >  2rsbc \;>\; \frac{2rs}{b}

and every quantity on the right is known before the collection is built. On the collection measured here, r=229r = 229 on the base text, s=32s = 32 and b=512b = 512, which puts the crossing at 28.6 copies. The measurement agrees: at thirty-two copies the two structures are within two per cent of each other and the r-index is on the smaller side of the crossing it was predicted to have just passed. The whole-structure crossing sits slightly later than the sampling’s own, because outside the sampling the r-index is paying for a wavelet tree and a predecessor structure that the other one does not have.

The formula is worth having for the term it contains rather than for the number it produced. The crossing is linear in ss, and ss belongs to the other structure. A deployment sampling one row in eight moves the crossing to seven copies; one sampling one in a hundred and twenty-eight moves it to a hundred and fifteen. The question “which index is smaller” therefore has no answer that is a property of the two designs. It is a question about a number somebody typed, and a threshold somebody chose is exactly the kind of quantity a comparison must not be allowed to rest on.

That is the arithmetic behind the refusal above to treat the two-per-cent size gap as the finding. A comparison whose outcome is linear in the loser’s free parameter is not a comparison of structures; the parameter’s owner can put the crossing wherever the argument needs it. Fixing the locate cost first and asking what each structure spends to achieve it removes the dial, which is why that is the measurement every occurrence at the same price makes.

One consequence of the same inequality is worth stating because it closes off a hope. Divide through by nn: the run-boundary policy wins when the run density r/nr/n is below 1/(2s)1/(2s). English runs at 0.29 boundaries a character before any copying, which would require ss under 1.7 — a sampling rate that keeps more than one row in two. On a four-symbol text at 0.75 the requirement is impossible outright. So on a text that does not repeat, no choice of rate makes the run-boundary sampling the smaller policy, and the crossing is not something a parameter can reach. It has to be earned by the collection.

What a run boundary knows that a rate does not

There is a reason a sampling at the run boundaries can be smaller than a regular one and answer faster, and it is not that somebody found a cleverer rate. The two policies are sampling different things.

A regular sampling picks positions in the text: every thirty-second position, whichever rows they happen to land in. It knows nothing about the transform, so a row’s distance from the nearest sampled row is arbitrary and is bounded only by the rate. That bound is the whole of its guarantee, and it is why the rate appears in both of its costs.

A run boundary is a position in the transform, and the transform is the thing the structure is built out of. A run is a maximal stretch of rows carrying the same character, so a boundary is exactly a place where LF stops being order-preserving — which is exactly where a piecewise-linear function of suffix-array values has to break. The sampling and the function are the same set of rr places, seen from two sides.

That is why nothing has to be tuned. There is no rate here that a deployment could get wrong, and no measurement in this essay sweeps one, because the structure has no parameter of that kind at all. What it has instead is a dependence on the text: the whole cost is rr, and rr is not something an implementation chooses.

Bits per character, as the same text is repeatedEnglish-like of 512 characters, repeated up to 32 times at divergence 0, indexed three ways at a sampling rate of one in 32. The plain FM-index is proportional to the length. The compressed one follows the entropy, which does not fall, so it is proportional to the length too. The run-length index is 1.1x the compressed one at a single copy and 0.5x it at 32, and per character it falls from 7.67 to 1.05 bits while the other two are flat. Both axes are logarithmic.1,00010,0001characters in the collectionbits per characterFM-index, plainFM-index, compressedrun-length indexsample one in 32 · divergence 0 · r = 224per character
Fig. 7 Bits per character for the structure this replaces, across the same collection. The falling line is what a run-length representation buys; the sampling is the part of it that would not fall.

The term that dominates is the one the structure is not about

This is the third time in four fields, and the pattern is worth naming because it has now been found from three different directions.

The compressed FM-index’s o(n)o(n) term — the rank directories, the thing every description calls negligible — is a third of that structure. A windowed estimator’s timestamps, the bits that say when rather than what, are the part that grows. And a run-length index’s sampling, which has nothing to do with runs, becomes half of a structure sized by runs.

In every case the part that eventually dominates is the part the structure is not named after, and in every case it dominates precisely where the structure is at its best. A collection that repeats itself thirty-two times is where the transform’s representation is cheapest, so it is where anything not following the transform is most visible. The better the structure gets at what it is for, the larger the share of it that is doing something else.

What is stored, and what is derived

One detail of the accounting is worth pulling out because it is the reason the sampling is 2r2r rather than 3r3r.

The predecessor structure for φ\varphi needs, at each anchor, the value φ\varphi takes there. That value is the suffix-array value of the row above a run’s first row — which is the last row of the previous run, whose value is already in the run-end array. So the anchors carry a run index rather than a position, and the position is read from a table that exists for the toehold.

The two halves of the sampling would each be rr values on their own and they share one of the two arrays. If they did not, the structure would be half as big again, and the essay’s headline number would be 10,942 rather than 6,990 for the same information. Which array a value is read from is not an implementation detail when the structure’s whole claim is a size.

An r-index over 32 copies, in partsThe structure over 16,385 characters with 95 runs in its transform. Every part is a function of r: the two suffix-array values kept per run are 2,850 bits and the predecessor structure that makes phi computable is 1,605, so the whole sampling is 4,455 bits — 58% of the structure and containing no term in n but the width each value is written in. The parts are checked to sum to the reported total on the plate.sampled positions2,85037%phi predecessor1,60521%run starts95012%run lengths per symbol91312%run heads89912%C table4756%a text that repeats itself · n = 16,385 · r = 95the sampling is the two shaded rowsone unit = one bit7,692 bits · 0.47 bits/char
Fig. 8 The same accounting on a text that repeats internally rather than by being copied. The shape is the same and the run count is different, which is the only thing that moves any row in the table.

The check this leaves behind

The claim is two-sided and is checked as one: across a collection that grows sixteenfold the run-boundary sampling must grow by less than half, and the regular sampling must grow by at least eight. Either half alone passes on something that is not the claim.

A check that only required the flat line would pass on a structure that had quietly stopped storing samples at all — which is not a hypothetical failure, because a structure with no sampling still answers how many correctly and only fails at where. A check that only required the growing line would pass on any pair of structures at all.

The second check is that the three structures agree on every occurrence of every pattern: the r-index, the regularly sampled index, and exhaustive search. That is the one that catches an off-by-one in the toehold, and it caught two.

The honest limit

Everything above is measured on collections of a few thousand to a few tens of thousands of characters, and the constants at that scale are large. At one copy the r-index is 21 bits a character; at thirty-two it is one. Neither number is what a real index over a real repetitive corpus achieves, and the sweep is short enough that a term growing like logn\log n and a term growing like log2n\log^2 n would be hard to separate.

What the measurement does establish is the shape: one policy’s size is a function of a property of the text and the other’s is a function of the text’s length, and on a collection that repeats, those are different functions. The size claim is checked at every point of the sweep and the correctness claim — that the smaller sampling answers the same question — is checked against exhaustion rather than against the other index.

What is not measured here is a real corpus with document boundaries, where rr behaves differently because each document’s transform contributions interleave. That is named in the direction and not built, as it was when the run-length index was first weighed.

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

Every essay whose body links to this one.

The objects this essay names

Each one links to every other essay that touches it.

Burrows-wheeler transformCompressibilityFM-indexIndex sizeLocateMeasurementR-indexRepetitionRun boundary samplingRun-lengthSamplingSelf-indexSpace overheadToeholdTrade off