The index that replaces the text

The index that is smaller than the text

The Burrows–Wheeler transform is a permutation, so it changes no symbol frequency and a plain index over it is the same size whether the text has deep structure or none — 6.29 bits a character against 6.16, on texts whose third-order entropies differ fourfold. What the transform changed was the runs, and a structure that charges one bit per bit cannot see a run.

Build the index from the last essay over five different texts of sixteen thousand characters each and weigh it. Two of the five are English-like over the same twenty-one symbols; one of them is a text that repeats itself with occasional mutations, so its third-order entropy is 0.234 bits a character, and the other is ordinary prose at 0.975 — four times as much structure in the first as in the second.

The index is 6.29 bits a character on the first and 6.16 on the second.

A gap of two per cent, across a fourfold difference in how compressible the texts are. Whatever this structure is doing, it is not exploiting the thing the transform was introduced to expose.

The transform is a permutation, so a plain index cannot see itFive texts of 16,384 characters, each indexed twice: once with plain bit vectors and once with bit vectors stored as a class and an offset per block. The bars are the index's size in bits per character of text. Rank the five by their zeroth-order entropy and the plain index's sizes come out in exactly that order; rank them by their third-order entropy and the compressed index's do. A wavelet tree of plain vectors costs the sequence's zeroth-order entropy, and the Burrows-Wheeler transform rearranges a text without changing its symbol frequencies, so the plain index cannot see the structure the transform exposed.a text that repeats itselfH0 4.01 · H3 0.236.291.48an order-1 sourceH0 3.00 · H3 0.895.151.67English-likeH0 3.89 · H3 0.976.162.25four symbols, uniformH0 2.00 · H3 1.994.102.59eight symbols, uniformH0 3.00 · H3 2.835.143.65bits per character of textupper bar: plain bit vectors · lower bar: compressed16,384 characters each · sample rate 64sigma 21, 8, 21, 4, 8
Fig. 1 Five texts, each indexed twice. Rank the five by their zeroth-order entropy and the plain index’s sizes come out in exactly that order. Rank them by their third-order entropy and the compressed index’s do. The plain index does not follow the third-order entropy at all — which is the whole content of this essay.

Why not, in one sentence

The transform is a permutation. It outputs exactly the characters it was given in a different order, which is how the compression field measured it — the zeroth-order entropy of the output is identical to the input’s to fifteen decimal places, and by that measure the transform does nothing at all.

A wavelet tree built of plain bit vectors costs the zeroth-order entropy of what it holds, near enough: each symbol contributes one bit per level of its code, the Huffman-shaped codes are within a bit of the entropy, so the whole tree is about nH0n H_0 bits.

Put those two sentences together. H0H_0 of the transform equals H0H_0 of the text; the tree costs nH0n H_0 of the transform; therefore the index costs nH0n H_0 of the text, and no amount of order-3 structure in the text can change it, because the transform threw the order away when it rearranged the symbols and H0H_0 never knew about it in the first place.

The order is not gone. It is sitting in the output in a different form: as runs. A text where th is usually followed by e produces, in the transform, a stretch of the last column that is mostly t. That is what the transform converts context into, and it is exactly what a structure charging one bit per bit cannot see.

Class and offset

The repair is to store the bit vectors in a form whose size depends on their contents.

Cut a vector into blocks of bb bits. For each block store two things:

  • its class — the number of ones in it — which takes log2(b+1)\lceil \log_2 (b+1) \rceil bits;
  • its offset — which of the (bclass)\binom{b}{\text{class}} blocks with that many ones it happens to be — which takes log2(bclass)\lceil \log_2 \binom{b}{\text{class}} \rceil bits.

The second number is the whole idea. An all-zeros block has (b0)=1\binom{b}{0} = 1 possibility and costs zero bits of offset. So does an all-ones block. A block that is half ones costs nearly bb bits, which is nothing saved, and that is honest: a balanced block genuinely carries a bit of information per bit.

A run therefore costs almost nothing and a coin-flip sequence costs everything. Which is precisely the property needed, because runs are what the transform produces.

FM-index, compressed bit vectors: 36,804 bits, and where they goThe same index the other plates weigh, opened up. The payload is 21,452 bits, 58% of the structure, and the rest is directories, sample marks and sampled positions. That is the o(n) which every statement of the form "n times the entropy, plus o(n)" carries and no statement of it quantifies. At n = 16,384 it is 42% of the index.wavelet tree21,45258%rank directories9,50626%sampled positions3,85510%sample marks1,4414%C table5501%one unit = one bit · 16,384 characters, sigma = 212.25 bits/char
Fig. 2 The compressed index, opened up. The payload — the offsets, the part that shrinks — is 21,452 bits over sixteen thousand characters. The rest is class arrays, superblock directories, the marks that say which rows are sampled, and the sampled positions themselves.
FM-index, plain bit vectors: 100,947 bits, and where they goThe same index the other plates weigh, opened up. The payload is 64,619 bits, 64% of the structure, and the rest is directories, sample marks and sampled positions. That is the o(n) which every statement of the form "n times the entropy, plus o(n)" carries and no statement of it quantifies. At n = 16,384 it is 36% of the index.wavelet tree64,61964%sample marks16,38516%rank directories15,53815%sampled positions3,8554%C table5501%one unit = one bit · 16,384 characters, sigma = 216.16 bits/char
Fig. 3 And the same index with plain vectors, for comparison. The payload is three times larger — 64,619 bits against 21,452 — over exactly the same text, with exactly the same transform, answering exactly the same queries.

What a run is worth, exactly

The arithmetic is small enough to do by hand on one block, and doing it is what makes the mechanism concrete rather than plausible.

Take a block of fifteen bits. If it is all zeros, its class is 0 and (150)=1\binom{15}{0} = 1, so the offset needs zero bits and the block costs four bits of class and nothing else — a fifteen-to-four saving. If it has one bit set, (151)=15\binom{15}{1} = 15 and the offset needs four bits, so the block costs eight. Two bits set: (152)=105\binom{15}{2} = 105, seven bits of offset, eleven in total. And at seven or eight bits set, (157)=6435\binom{15}{7} = 6435, thirteen bits of offset, seventeen in total — more than the fifteen bits it replaced.

That last case is why the compressed index over an incompressible text is larger than the plain one, which the five-text table below shows without apology. The encoding is not a compressor with a floor of zero; it is an encoding whose cost tracks the entropy of the block classes, and on a sequence of balanced blocks that entropy is the whole thing plus a class array.

The whole structure is that arithmetic repeated over every node of the wavelet tree, and the reason the transform matters is that it makes the blocks unbalanced.

Measured, on five texts

text σ\sigma H0H_0 H3H_3 plain, bits/char compressed, bits/char
repeats itself 21 4.007 0.234 6.29 1.48
order-1 source 8 3.000 0.895 5.15 1.67
English-like 21 3.892 0.975 6.16 2.25
four symbols, uniform 4 2.000 1.991 4.10 2.59
eight symbols, uniform 8 3.000 2.829 5.14 3.65

Sort the table by H0H_0 and the fourth column is in order. Sort it by H3H_3 and the fifth column is in order. Neither is a coincidence and both are asserted on every build — including the third statement, which is that the plain index does not follow H3H_3, because a check that only confirmed the two positive claims would pass an implementation in which the compression had quietly stopped working.

The first and third rows are the pair to look at. Same alphabet, nearly the same H0H_0, four times the difference in H3H_3: 6.29 against 6.16 plain, and 1.48 against 2.25 compressed.

The block length is a number somebody types

The block cannot be very short, because every block costs a class whatever is in it. It cannot be very long, because log2(bc)\lceil\log_2\binom{b}{c}\rceil approaches bb as the block lengthens and a long block is rarely uniform.

So there is an interior minimum, and it is not where the textbook parameter suggests.

The block length of a compressed vector: cheapest at 63The whole index over 16,384 characters, rebuilt at five block lengths. Short blocks: one class each and a great many of them, so the directory dominates. Long blocks: rarely all ones or all zeros, so the offsets approach the bits they replace and the payload dominates. The minimum is at 63 bits, at 2.25 bits per character against 3.71 at the short end and 2.54 at the long one — a spread of 1.65 times, decided by one number in a header.block of 7 bits3.71 b/ch72% directoryblock of 15 bits2.79 b/ch58% directoryblock of 31 bits2.35 b/ch41% directoryblock of 63 bits2.25 b/ch26% directoryblock of 127 bits2.54 b/ch14% directoryEnglish-like · sample rate 64cheapest at 63
Fig. 4 The whole index rebuilt at five block lengths. 3.71 bits a character at seven, 2.25 at sixty-three, 2.54 at a hundred and twenty-seven. The two halves move in opposite directions — payload up, directory down — and the total has a minimum at 63 that is 1.65 times better than the short end.

The default here is sixty-three, and it is sixty-three because that sweep says so rather than because it is a round number. It is worth being explicit that this makes the plates on this page tuned: a compressed index reported at the wrong block length is off by up to 65%, which is larger than most of the differences anybody argues about, and a size quoted without its block length is not reproducible.

One implementation detail is worth a sentence because it nearly went wrong. At a block of sixty-three the middle binomial is (6331)9.16×1017\binom{63}{31} \approx 9.16 \times 10^{17}, which is past what a double-precision float holds exactly. That number is the payload of the structure, so computing it approximately would mean computing the index’s size approximately. The binomials here are exact integers of arbitrary precision, and the bit widths are read off their binary representations.

Half the index is bookkeeping, and the block length decides how much

Here is the finding this essay would rather not have.

The standard statement about a compressed self-index is that it occupies nHk+o(n)n H_k + o(n) bits. Both halves of that are true of the structure measured here. The question is what the second half weighs, and the answer depends on a number in a header.

At the block length the sweep chose, on English-like text:

characters H3H_3 payload, bits/char directory, bits/char index, bits/char directory’s share
4,096 0.907 2.44 0.71 3.57 20%
16,384 0.975 1.31 0.58 2.25 26%
65,536 0.994 0.98 0.55 1.89 29%
262,144 0.995 0.89 0.55 1.83 30%

Two things in the last row.

The payload behaves better than advertised. At a quarter of a million characters it is 0.89 bits a character against an H3H_3 of 0.995 — under the floor it is supposed to approach. That is not a contradiction: H3H_3 is a floor for a coder that conditions on three preceding symbols, and the class-and-offset encoding of the transform’s runs is not one. It conditions on however much context the run it sits in was produced by, which on this text is more than three symbols’ worth.

And the index is twice the payload. Directory 30%, sampled positions 16%, sample marks 3%, the CC table a rounding error — half of the structure is accounting, and the accounting does not shrink when the text becomes compressible.

Run the same sweep at the block length the textbook parameter suggests, fifteen, and the split moves a long way:

characters payload, bits/char directory, bits/char index, bits/char directory’s share
4,096 1.16 1.67 3.22 52%
16,384 0.86 1.61 2.79 58%
65,536 0.78 1.61 2.73 59%
262,144 0.76 1.64 2.76 59%

The payload is smaller — shorter blocks are more often uniform — and the directory is nearly three times larger, because there are four times as many blocks each needing a class. Net, the index is bigger. At one block length a quarter of the structure is bookkeeping and at another three-fifths of it is, and the choice between them is one integer.

Both directories are o(n)o(n) in the sense the notation means: the rate is Θ(nloglogn/logn)\Theta(n \log\log n / \log n), which does go to zero relative to nn. It goes there through eleven decades of nobody’s data. This is a fourth instance of the shape what O-notation does not say collects, and the sharpest of them, because the discarded term is not merely non-negligible — at the parameter most descriptions imply, it is the majority of the object.

Four indexes over the same texts, in bitsEvery structure here answers the same question with the same answer, and at n = 32,768 they differ by a factor of 10. The dashed line is the packed text. Both axes are logarithmic, so every series being straight and parallel is the statement that all four are linear in n and differ only in the constant, which is the quantity the notation drops and the only quantity that decides whether an index fits.10,00010⁴10⁵characters of textbits heldsuffix array + textFM-index, plainFM-index, compresseddashed: the packed text at one code word a characterEnglish-like
Fig. 5 The three structures against the length of the text. The compressed series is the only one whose bits per character falls as the text grows, and it flattens rather than continuing — which is the two tables above drawn.
The block length of a compressed vector: cheapest at 63The whole index over 16,384 characters, rebuilt at five block lengths. Short blocks: one class each and a great many of them, so the directory dominates. Long blocks: rarely all ones or all zeros, so the offsets approach the bits they replace and the payload dominates. The minimum is at 63 bits, at 1.48 bits per character against 3.08 at the short end and 1.74 at the long one — a spread of 2.09 times, decided by one number in a header.block of 7 bits3.08 b/ch87% directoryblock of 15 bits2.10 b/ch77% directoryblock of 31 bits1.60 b/ch61% directoryblock of 63 bits1.48 b/ch40% directoryblock of 127 bits1.74 b/ch21% directorya text that repeats itself · sample rate 64cheapest at 63
Fig. 6 The block sweep again on the text that repeats itself, where the payload is nearly free and the choice of block length is therefore almost entirely a choice about how much directory to pay. The minimum sits in the same place, which is worth knowing: the parameter does not have to be retuned per text.

What “smaller than the text” means

The claim in this essay’s title is now measurable, and it is measurable in at least five ways — which is the difficulty rather than an incidental detail. The text is not one quantity: it is the file on disk, the same characters packed into as many bits as the alphabet needs, and three different entropies each computed under a different model of what a character depends on. An index can be smaller than some of those and larger than others at the same moment, and it is.

"The size of the text" names four numbers, on English-likeA text of 16,384 characters over an alphabet of 21, measured five ways. The dashed rule is a compressed FM-index over the same text at 36,804 bits: under the file, under the packed text, and above every entropy below the zeroth order. Which of these five numbers a claim of "smaller than the text" is measured against decides whether it is true, and the claim is usually made without naming one.the file, 8 bits a byte131,072packed, ceil(log2 21) bits81,920n·H₀63,7593.89 b/chn·H₁36,7272.24 b/chn·H₂19,3091.18 b/chn·H₃15,9670.97 b/chthe index: 36,804 bitsone unit = one bit · text of 16,384 characterssigma = 21
Fig. 7 One text, five sizes: the file at eight bits a byte, the packed text at five, and the three entropies below it. They differ by a factor of twenty-eight. The dashed rule is the index.

On English-like text at sixteen thousand characters the compressed index is 2.25 bits per character. So:

  • smaller than the file (8 bits/char) by 3.6 times — true;
  • smaller than the packed text (5 bits/char) by 2.2 times — true;
  • smaller than nH0n H_0 (3.89) — true;
  • larger than nH1n H_1 (2.24), by a hair;
  • larger than nH3n H_3 (0.975) by 2.3 times.

Every one of those is correct. A claim of “smaller than the text” that does not say which is a claim about nothing, and the honest form of the sentence is the last three lines together: the index is smaller than the text at any encoding a person would actually store it in, and larger than the entropy it is named after.

Both integers, then, are in a header somewhere, neither appears in any description of the structure, and between them they move the index by more than a factor of two in size and by an unbounded factor in locate time. A reported index size is a point in that two-dimensional space and not a property of the algorithm, which is the same complaint this collection makes about every constant it has had to go and measure.

What is not measured here

Two things, and both would move the numbers.

Query cost. A compressed vector’s rank is not free — it reads a class, decodes an offset and counts within a block, where a plain vector reads a directory entry and popcounts. The structures here are counted in rank queries, which charges both the same, and that is the right unit for comparing an index against a suffix array and the wrong unit for comparing plain against compressed. Nothing on this page says the compressed index is as fast; it says it is smaller, and it says so in bits.

Run-length encoding the transform directly. Storing the transform as (symbol, run length) pairs is the other route to the same structure, gets closer to the run-length entropy, and is what the indexes built for highly repetitive collections actually do. It is a different structure with a different rank mechanism and it is not built here — so this collection has no measurement of it, and quoting one would be quoting.

FM-index, compressed bit vectors: 24,192 bits, and where they goThe same index the other plates weigh, opened up. The payload is 9,042 bits, 37% of the structure, and the rest is directories, sample marks and sampled positions. That is the o(n) which every statement of the form "n times the entropy, plus o(n)" carries and no statement of it quantifies. At n = 16,384 it is 63% of the index.rank directories9,63840%wavelet tree9,04237%sampled positions3,85516%sample marks1,1075%C table5502%one unit = one bit · 16,384 characters, sigma = 211.48 bits/char
Fig. 8 The same index over the text that repeats itself. The payload falls to 0.51 bits a character and the directory becomes 40% of the structure — and at a quarter of a million characters of the same source the payload is 0.10 and the directory and samples are nine-tenths of what is left. When the thing being compressed goes to almost nothing, the accounting that was a footnote becomes the object.

The part of the index that does not compress

The two tables above are usually read as a warning about a badly chosen block length. They say something stronger, and it survives every choice of block length: there is a term in this structure that does not know what text it is indexing, and below it no amount of compressibility helps.

Sort the index into the parts that depend on the text and the parts that do not.

The payload — the offsets — is the only part that reads the data. It falls with the runs, it fell to 0.51 bits a character on the text that repeats itself, and at a quarter of a million characters of the same source it fell to 0.10. Nothing here is a floor; a more repetitive text makes it smaller without limit.

The class array is a function of the number of blocks, which is a function of the text’s length and the block length and nothing else. The superblock directory is the same. The sample marks are one bit per sampled row. The sampled positions are log2n\lceil\log_2 n\rceil bits each, times however many rows are sampled. Not one of those four consults a single character of the text.

So the structure has an asymptote. On English-like text at the tuned block length the accounting is a quarter of the index and looks like overhead; on the repetitive text it is nine-tenths of it and is the index. The reason is not that the accounting grew — it is identical in both cases — but that the thing it was accounting for went away.

That reframes the title’s claim once more. The index is smaller than the text, and how much smaller has a ceiling that is set by the bookkeeping rather than by the entropy: past a certain compressibility, indexing a more repetitive text produces an index of nearly the same size. A collection of a thousand near-identical genomes does not index a thousand times more cheaply than one genome; it indexes at the rate its directories charge.

This is exactly the gap the run-length route named at the end of the previous section is built to close, and it closes it by attacking the right term. Storing the transform as runs makes the number of blocks fall with the runs rather than with the length, so the accounting shrinks alongside the payload instead of sitting underneath it. That is a different structure and it is not built here — but the reason it exists is legible from these tables alone, which is worth more than a quoted figure would be.

The sample rate is the other integer

The block length is the parameter this essay swept. There is a second one in the same structure and it is doing something quite different, so it is worth separating them before either is defended.

A backward search returns a range of rows, not positions in the text. Turning a row into a position means walking backwards through the transform until a row is reached whose position was stored, and the sampling rate ss decides how many are stored: one row in ss, at log2n\lceil\log_2 n\rceil bits each, plus a bit vector marking which.

Both halves move with 1/s1/s. Halving the rate halves that part of the index and doubles the expected number of steps a locate performs, and the expected walk is about s/2s/2 steps because a row is on average half a sample away from one. So this dial trades index size against query time, where the block length traded index size against index size. They look alike in a size table and they are not comparable at all: one has an interior optimum that a sweep can find, and the other has no optimum, only a rate at which a reader is prepared to wait.

That is why the sweep in this essay holds the sampling fixed. A size quoted with the sampling rate left out is not merely unreproducible in the way a block length left out is — it is a size that can be made arbitrarily small by making locates arbitrarily slow, down to the limit where nothing is sampled and every occurrence costs a walk the length of the text.

The counting rows in the tables here are unaffected by any of this, and that is the cleanest way to see the split. Counting how many times a pattern occurs needs the transform and its rank structure and no samples whatever, which is why the text does not have to be kept is a statement about counting and why locating is where an index starts paying rent again. A structure sized for counting and then asked to locate is the shape of a great many disappointing benchmarks.

The sentence to keep

The transform emits nothing, compresses nothing, and changes no symbol’s frequency. Every measurement in the compression field said so.

What it does is convert context into adjacency, and whether that is worth anything depends entirely on whether the structure reading it charges by the bit or by the block. Plain vectors charge by the bit and see nothing. Class-and-offset vectors charge by the block and see everything, at 1.48 bits a character on a text whose third-order entropy is 0.234.

The transform did not become useful. The thing reading it did.

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 19 that link here.

The objects this essay names

Each one links to every other essay that touches it.

Backward searchBit vectorBurrows-wheeler transformCompressionEntropyHonest limitIndex sizeMeasurementRank querySelf-indexState bitsTrade offWavelet tree