The index that replaces the text

An index larger than what it indexes

A suffix array over 16,384 characters is 229,376 bits, and it cannot answer a single question without the 81,920 bits of text beside it. Nearly four times the text, to search the text. Every index on this site had been weighed at zero until somebody put one on a scale.

Building a suffix array is a good trick and this collection made a lot of it: every suffix of a text sorted without examining a single character, a search answered in ninety-one character comparisons where a scan costs 1,472, and the whole thing paying for itself at six queries.

Every number in that account is about time. Not one of them is about what the structure weighs, and the structure weighs a great deal.

Sixteen thousand three hundred and eighty-four characters of English-like text, at five bits a character because the alphabet has twenty-one symbols in it, is 81,920 bits. The suffix array over it is sixteen thousand three hundred and eighty-four integers, each of which must address a position in the text, so fourteen bits each: 229,376 bits. And the array on its own answers nothing, because a binary search over it compares the pattern against a suffix, and a suffix is a window onto a text the array does not contain.

Three hundred and eleven thousand two hundred and ninety-six bits, all in, to index eighty-one thousand nine hundred and twenty. Three and eight-tenths times the text, and two and a third times the file it came from.

Four ways to index 16,384 characters, weighedEach bar is what the structure retains, computed from its own shape rather than from a serialisation, on English-like of 16,384 characters. The dashed rule is the packed text at 81,920 bits. All four return the same occurrences for the same pattern; they differ in size by 147 times. One of them cannot answer at all unless the text is kept beside it, and its bar includes that text.suffix array + text311,29619.00 b/chcounter array per symbol5,407,710330.06 b/chFM-index, plain100,9476.16 b/chFM-index, compressed36,8042.25 b/chthe packed textone bar shaded darker needs the text · English-likesigma 21, sample 64
Fig. 1 Four structures that answer the same question about the same text with the same answer, weighed. The dashed rule is the packed text. Only one of them has to keep a copy of it, and that one’s bar includes it; the largest and smallest differ by a factor of a hundred and forty-seven.

Weighed how

Everything on this page is bits computed from the shape of a structure, not bytes measured off a serialisation.

That is a deliberate choice and it is the same one this collection makes when it counts a summary’s state or a table’s cells. A serialised size is a fact about a file format, a language runtime and whichever library did the writing; it is not reproducible across any two of those, and it is not the quantity anybody designing a structure is trying to control. An array of nn values each needing log2n\lceil \log_2 n\rceil bits is nlog2nn\lceil \log_2 n\rceil bits, whether the language that holds it uses thirty-two-bit integers or sixty-four, and that is the number that decides whether the design is possible.

So: a counter array is its length times the bits one counter needs. A bit vector is its bits plus its directory. A table of character offsets is the alphabet size times the length times the bits an offset needs. Nothing is rounded up to a machine word, nothing is compressed by a library, and every number here can be recomputed with a pencil from the structure’s description.

suffix array, with the text: 311,296 bits, and where they goThe same index the other plates weigh, opened up. The payload is 229,376 bits, 74% 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 26% of the index.suffix array229,37674%text81,92026%one unit = one bit · 16,384 characters, sigma = 2119.00 bits/char
Fig. 2 The suffix array, opened up. Two parts, and the second is the text — which is where this whole field starts, because it is the part every account of the structure leaves out.

The text is not an implementation detail

It is worth being precise about why the text has to be there, because “the text is needed as well” sounds like a footnote and is not one.

A search over a suffix array is a binary search over the sorted suffixes. At each step it must decide whether the pattern is before or after the suffix beginning at position SA[i]\mathrm{SA}[i] — and the array holds SA[i]\mathrm{SA}[i], which is a number. To compare, it has to read characters at that position. On the sixteen-thousand-character text above a six-character pattern costs ninety-one character comparisons, which is ninety-one reads of a text the structure does not hold.

This collection now enforces that rather than mentioning it. A structure is handed its text while it is being built and the text is taken away before any query runs; every structure declares in advance whether it needs one; and a structure that declared it does not and then reads a character is stopped at the read, with a distinct error saying so.

The declaration alone would be worthless. The cheapest way to make a search fast is to keep the text around for a verification step, and nothing in the occurrences returned would ever reveal it — the answers would be correct, the counts would be correct, and the size claim would be false by however many bits the text takes. That is exactly the class of failure that survives every check a person thinks to write, which is why the check here is behavioural rather than declarative.

Measured under it: the suffix array reads 91 characters during a query, and the compressed index in the same plate reads zero.

The obvious way to make it faster is enormous

Before the good answer, the instructive bad one.

There is a way to search a text that needs no character comparisons at all and answers in time proportional to the pattern alone: build, for every symbol of the alphabet, a running count of how many times that symbol occurs before each position. Then any question of the form how many cc before position ii is one array read.

It works, it is the fastest thing here, and it is σnlog2n\sigma \cdot n \cdot \lceil \log_2 n\rceil bits.

transform + one counter array per symbol: 5,407,710 bits, and where they goThe same index the other plates weigh, opened up. The payload is 5,407,380 bits, 100% 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 0% of the index.occurrence table5,407,380100%C table3300%one unit = one bit · 16,384 characters, sigma = 21330.06 bits/char
Fig. 3 Twenty-one counter arrays over sixteen thousand positions, at fifteen bits a counter: 5,407,710 bits, which is sixty-six times the packed text and forty-one times the file. It answers correctly and it answers instantly, and nobody would build it.

It earns its place in this collection for the same reason a naive scan does: it is the structure a first implementation reaches for, and without it the clever structure looks like an implementation detail rather than the thing that makes the idea possible at all.

The index grows faster than the text

A suffix array is often described as taking “nn integers”, which sounds linear. It is nlog2nn \lceil \log_2 n \rceil bits, and the logarithm is not decoration.

characters packed text suffix array + text bits per character
1,024 5,120 15,360 15.00
4,096 20,480 69,632 17.00
16,384 81,920 311,296 19.00
65,536 327,680 1,376,256 21.00
262,144 1,310,720 6,029,312 23.00

Two bits a character per fourfold increase, exactly as the formula says, and no sign of stopping. On a text of a billion characters it is thirty-five bits per character of text — seven times the text, to index the text.

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 175. 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⁵10⁶10⁷characters of textbits heldsuffix array + textcounter array per symbolFM-index, plainFM-index, compresseddashed: the packed text at one code word a characterEnglish-like
Fig. 4 All four structures against the length of the text, both axes logarithmic. Every series is straight, so every structure is linear in n to within the logarithm, and what separates them is the constant — which is the quantity the notation drops and the only quantity that decides whether an index fits in the machine it has to fit in.

The comparison in the last two rows of the table is worth pausing on. The suffix array’s slope is steeper than the text’s, so the ratio between index and text gets worse without limit. There is no size at which the overhead stops mattering. That is a different situation from a structure with a constant factor, and it is invisible in a description that says “nn integers”.

What the alphabet does to it

Widening the alphabet makes the text cheaper and the index no cheaper at all, so the ratio gets worse.

Four ways to index 16,384 characters, weighedEach bar is what the structure retains, computed from its own shape rather than from a serialisation, on four symbols, uniform of 16,384 characters. The dashed rule is the packed text at 32,768 bits. All four return the same occurrences for the same pattern; they differ in size by 29 times. One of them cannot answer at all unless the text is kept beside it, and its bar includes that text.suffix array + text262,14416.00 b/chcounter array per symbol1,229,02575.01 b/chFM-index, plain67,1164.10 b/chFM-index, compressed42,3932.59 b/chthe packed textone bar shaded darker needs the text · four symbols, uniformsigma 4, sample 64
Fig. 5 The same four structures over four symbols. The text packs to two bits a character, so it is smaller — and the suffix array is exactly the same size it was, because its entries address positions rather than symbols. Eight times the packed text rather than four.

The counter table moves in the opposite direction: fewer symbols means fewer arrays, so it falls from sixty-six times the text to thirty-eight — and it falls by less than the text shrank, which is the same effect from the other side. It is still the largest thing on the plate by two orders of magnitude.

What is not counted here, and why

Three things are outside the number on every plate above, and naming them is part of the measurement.

Construction space. Building a suffix array by prefix doubling needs rank arrays alongside the array itself, and at the moment of peak use that is several times the finished structure. Every index has a build-time footprint larger than its resting one, they differ between construction algorithms by large factors, and none of it survives into the structure that gets kept. What is weighed here is what a query needs, which is what has to be resident.

The queries’ own working space. A binary search needs a few integers; a backward search needs a few more. Both are constant and neither is interesting.

Any decoration. No structure here carries an auxiliary array it does not need for the queries in this collection. A suffix array in practice is usually accompanied by its longest-common-prefix array, which is another nlog2nn\lceil\log_2 n\rceil bits and would nearly double the first bar — and it is not counted, because the searches measured here do not use it. A plate that included it would be weighing a structure nobody on this page built.

The third of those is the one worth being careful about in both directions. Leaving it out understates what a deployed suffix array costs; putting it in would overstate what the search measured here needs. The honest position is to weigh exactly what answers the question being asked and to say which question that is, which is what the captions do.

Two break-evens, and only one of them arrives

The case for an index is usually made as a break-even: preprocessing costs something once, each query saves something, and after enough queries the saving has covered the cost.

That argument is about time and it is sound. On the text above, a scan costs 1,472 character comparisons a query and the array costs 91, so the build pays for itself after a handful of searches, and every search after that is free profit.

The same argument about space does not exist. There is no number of queries after which the index stops taking 229,376 bits. The cost is paid once and kept forever, it grows superlinearly, and the only thing that can change it is a different structure.

Scan every time, or build the index onceA text of 4,096 characters over 4 symbols. Building the suffix array by prefix doubling and its LCP array by Kasai's method costs 8,188 character comparisons and 85,248 integer comparisons, paid once. After that a query costs 89 characters against a scan's 1739. The lines cross at 5 queries, which is the whole of the decision.11010010⁴10⁵queries answeredcharacter comparisons, cumulative5 queriesScan each timeIndex, then queryone unit = one character comparison · 4 doubling roundsbreak-even at 5 queries
Fig. 6 The time break-even, drawn: the build’s cost against the accumulated saving from queries, crossing where it crosses. Nothing on this plot is measured in bits, and reading it as the whole case for an index is the omission this field opened to repair.
"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 And the reason “smaller than the text” is a slippery claim even before any structure is proposed: one text has at least five defensible sizes, from eight bits a byte down to its order-3 entropy, and they differ by a factor of twenty-eight. A comparison that does not name which is being used is a comparison to nothing.

What a better answer would have to do

Reading the plate at the top from the right, the requirements assemble themselves.

It has to hold no text, which means it must be able to reproduce any part of the text on demand — otherwise the text has to be kept and the whole comparison is a fiction.

It has to hold no positions, or almost none, since nlog2nn\lceil\log_2 n\rceil bits of positions is the suffix array’s whole problem.

And it still has to find the occurrences, which the positions were there for.

Those three sound incompatible, and the structure that satisfies all three exists. It is built out of a transform that emits nothing and compresses nothing, searched by a procedure that runs the pattern backwards and never compares two characters, and it is smaller than the text it indexes on any text that has any structure at all.

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. 8 Where that structure’s bits go, for comparison with the two plates above. No text, almost no positions, and a third of it is directories — which is a story of its own and the reason “plus a lower-order term” is a phrase worth distrusting.

The unit is the bit, and that is a decision too

One more thing to settle before the field can proceed, because it decides how every comparison here reads.

An index could be measured in objects — states, nodes, entries — and the suffix automaton essay does exactly that, because its bound is a theorem about states. That is the right unit when the question is whether a structure is within its own theoretical bound, and it is the wrong unit the moment two different kinds of structure are compared: an automaton state and a counter array entry are not the same thing, and a table with one column per symbol has no objects in it at all.

Bits are the only unit all of them share. A state that stores three transitions over an alphabet of twenty-one is some number of bits; a counter that must reach sixteen thousand is fifteen; a bit vector is its bits. Once everything is in bits, structures that have nothing in common structurally can be put on one axis honestly, and the axis is the one a machine actually has a fixed amount of.

The price of that choice is that a bit count depends on details a state count does not: how a transition is stored, whether an entry is addressed absolutely or relatively, what the alphabet is. So every plate here names the text and the alphabet it was measured on, in the same slot where the summary plates name their state and the table plates name their model. A bit count without those is a number belonging to a structure nobody described.

An index contains no information

There is a way of stating the plate at the top that makes the factor of a hundred and forty-seven less surprising and the whole field’s project sharper, and it takes one observation.

Every structure here is a function of the text. The suffix array is determined by the text; so is the counter table; so is the transform and everything built on it. Hand two people the same text and they compute the same index, with no choices to make and nothing to record. So the pair (text, index) carries exactly as much information as the text alone, and every bit of the index is redundant by construction.

That is not a criticism and it is not an argument that the bits are wasted. It settles what kind of quantity an index’s size is. It is not a cost of representing anything; it is a cost of arranging something. The 229,376 bits of a suffix array say nothing the 81,920 bits of text do not already say, and they are there so that a question can be answered by looking rather than by computing.

Read the plate again with that in mind and the ordering means something new. The counter table is sixty-six times the text in pure redundancy, and it buys a rank in one array read. The suffix array is 2.8 times the text in redundancy, and it buys a search in a logarithmic number of comparisons — but it cannot stand alone, so its bar carries the text as well. The compressed index is under the text and needs no text beside it, which means it has crossed a line the other two have not: it is not the text plus an arrangement, it is an arrangement that is also the text.

That is the entire ambition of a self-index stated in one sentence, and it explains why the phrase is worth a category of its own rather than being one more compressed structure. The interesting question stops being how much overhead does the index add and becomes how far below the text can a structure go while still answering, and the second question has a floor made of the text’s entropy rather than of anything about indexing.

What the redundancy is buying

If none of an index’s bits carry information, the honest way to price them is against what they save, and the saving is in a different currency every time.

The counter table’s bits buy one read instead of a walk. The suffix array’s buy a logarithm instead of a scan — 91 character comparisons against 1,472. The compressed index’s buy the same thing at a different exchange rate, in ranks rather than comparisons, and buy it while occupying less than the text.

None of those exchange rates is comparable with the others, which is why this field weighs structures in bits and counts queries in each structure’s own primitive rather than trying to produce a single figure of merit. A ratio of bits to operations saved would be a number in units of bits per comparison-avoided, and there is no such quantity.

What can be said, and is worth saying because it is the shape of every decision in this field: an index’s size is a choice about how much redundancy to keep, and its speed is what the redundancy was arranged into. A structure that keeps none is the text itself, answering by scanning. A structure that keeps sixty-six times the text answers by looking once. Everything anybody builds is between those two, and the plate at the top of this page is four points on that line with their prices attached.

The measurement this field is for

One sentence, and it is the reason the field exists rather than a summary of the essay.

Four structures on this page answer the same question about the same text and return the same occurrences, and they differ in size by a factor of a hundred and forty-seven. Nothing in the description of any of them — “a sorted array of positions”, “a running count per symbol”, “a transform with a rank structure” — carries that number, and none of the descriptions is wrong. The number is a property of the structure that only appears when somebody weighs it, and until this field opened nobody here had.

It is worth saying what follows from that, because it is not “indexes are bad”. Every structure on the plate is a reasonable answer to a question somebody has: the counter table is what to build when the alphabet is tiny and the queries are relentless, the suffix array is what to build when the text has to be there anyway and the code has to be simple. What does not survive is the habit of choosing between them on the strength of a description. Two of these descriptions differ by four words and the structures they describe differ by two orders of magnitude, and the only way to know that is to put both on a scale.

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

The objects this essay names

Each one links to every other essay that touches it.

Honest limitIndex sizeMeasurementPattern matchingPreprocessingSelf-indexSpace lower boundState bitsSuffix arrayTrade offUnit of cost