The index that replaces the text

Three savings on one structure

A factor of eleven on an extension, a factor of seven on a branching search, and a sixth of the bits. Applied to one bidirectional index they do not give a factor of seventy-seven, and the reason is that two of the three are the same saving.

Three changes to a bidirectional index have been measured here, each on its own, each against the same published baseline.

The compound walk. A wavelet tree can return a symbol’s rank and the count of smaller symbols before a position from one descent, at the price of the rank alone. An extension that had been summing σ separate ranks becomes one walk. The count that was already there: 139.7 bit-vector ranks against 10.0 at a twenty-six letter alphabet.

The interval enumeration. One descent returns every symbol present in an interval with the interval each occupies, so a search never attempts an extension that finds nothing. The branches that find nothing: 168,000 ranks against 24,000 on a one-error search over twenty symbols.

The sparse marks. An index’s mark vector holds n/s ones in n positions and is stored as n bits; represented as an Elias–Fano array it is a fifth of that. A position split in two: 15% of a whole index at one sampled position in thirty-two.

A reader with those three numbers has a question, and the answer is not the product.

Three ways to extend an interval, on the same searchA search for 8-character patterns within 1 error over 8,192 characters of a 21-symbol alphabet, 6 patterns, counted in bit-vector ranks. The published shape asks about every character of the alphabet at every node: 1,478,400. The compound walk makes each of those questions cheaper by returning the rank and the smaller-count from one descent, which is 11x. Enumerating the interval's symbols removes the questions instead, which is 78x — and the 55.8% of extensions that found nothing are what it removed. All three visit the same live intervals in the same order and report the same occurrences; that is the check, and it is what makes this a plate about cost.a loop over the alphabet1,478,400the compound walk, inside the loop134,40011xone descent per node18,91678x6 patterns · 1 error · sigma 2155.8% of the extensions were dead
Fig. 1 Three ways to extend an interval on the same search: a loop over the alphabet, the compound walk inside that loop, and one descent per node.

What the third one composes with

The mark representation is a size change and the other two are operation changes, so the first question is easy.

Measured directly: the same branching search over the same index costs 12,486 bit-vector ranks whether the marks are plain or sparse. Exactly the same number, because a search never touches the marks — it reads the wavelet tree, and the marks are read only when an interval is turned into text positions.

And the sparser-marked index is smaller: 102,154 bits against 106,160 at the rates compared.

So the size saving is orthogonal to both operation savings, in the strong sense that one of them does not change the other’s number at all. That is what “composes” means and it is worth checking rather than assuming, because a size change that slowed the search would compose differently.

The orthogonality check is worth describing because a weaker version of it would have been easy to accept. The obvious check is that the two indexes report the same occurrences, which they do — but that says nothing about cost, and a change that made the search slower while keeping the answers would pass it.

The check that was written requires the rank counts to be identical, not close: 12,486 against 12,486. An inequality with a tolerance would pass on a change that added a few per cent, and the claim being made is that the marks are not read at all during a search, which is an exact statement.

That is the general shape of an orthogonality claim: not “these do not interfere much” but “this one does not touch that one”, and the check for it is an equality.

What the first two do not compose with

The two operation savings both act on the same thing — what an extension costs — and the measurement of them together is not the product.

On a one-error search over a twenty-symbol alphabet:

The published loop: 1,478,400 ranks. The compound walk inside the loop: 134,400. A factor of 11.0. The enumeration: 18,916. A factor of 78.2.

An index with both would be expected to cost 1,478,400 / (11.0 × 78.2) = 1,719 ranks. It costs 18,916.

The shortfall is a factor of 11.0 — which is the compound walk’s own factor, exactly.

That is not a coincidence and it is not an interaction cost. Two factors that do not multiply is the arithmetic: one descent over an interval returns each present symbol’s sub-interval, which is the compound walk’s rank; and running-summing the counts in leaf order gives the walk’s smaller-count for every symbol at once. Taking both is taking the second one twice.

Two savings that do not multiply, and the amount by which they do notThe compound walk is 11x and the enumeration is 78x against the same baseline, so a reader with both numbers computes 860x for an index that has both. The measured figure is 78x, and the shortfall is 11x — which is the compound walk's own factor, exactly. It is not an interaction, an overhead or a rounding: one descent over an interval returns each present symbol's sub-interval, which IS the walk's rank, and running-summing the counts gives the walk's smaller-count for every symbol at once. Taking both is taking the second one twice.factor against the published loopthe compound walk11xthe enumeration78xthe two, multiplied860xnothing measures thisan index with both78xthe shortfall is 11x, and the walk's factor is 11x6 patterns · 1 error · sigma 21the larger of the two, not the product
Fig. 2 The product a reader with two factors would compute, drawn against the measurement: the shortfall is the compound walk’s own factor, exactly.

That the shortfall is exactly the walk’s factor rather than approximately it is what turns this from an observation into a claim. An interaction, an overhead or a shared constant would produce a shortfall of some number nobody could predict; a shortfall equal to one of the two factors says that one of them contributed nothing.

The check requires it: the measured combined factor must fall short of the product, and the shortfall must equal the walk’s factor to within two per cent. Both hold at 11.0 against 11.0.

Which is which

Since two of the three turn out to be one, it is worth restating what the three actually are.

A size saving, in the marks, which composes with everything.

An operation saving for a search that knows its character — the compound walk. One extension by one named symbol, at a fixed cost instead of σ ranks.

An operation saving for a search that does not — the enumeration. Every present symbol at once, which subsumes the walk on that kind of search.

So there are two operation savings and they apply to two different query shapes, rather than one applying on top of the other.

An exact search branches nowhere and wants the walk. A backtracking search branches everywhere and wants the enumeration. The saving that is a loss measures the enumeration on an exact search and finds it costing about twice the walk — so the split is not a preference, it is a crossing.

On an exact search the enumeration is the more expensive of the twoThe same three machines on a search that knows which character it wants at every step: 16 exact patterns of 12 characters, in bit-vector ranks per step. The loop costs 107.9, the compound walk 10.0 — 11x, and one walk per step is all an exact search needs. The descent costs 19.0, which is 1.90x the walk, because it reports every symbol present in order to hand back one of them. So the two operation savings are a choice rather than a stack, and what chooses is the shape of the search: a branch wants the descent and a known character wants the walk.a loop over the alphabet107.9the compound walk10.0the cheapest hereone descent per step19.016 exact patterns of 12ranks per step · sigma 21
Fig. 3 The same three machines on a search that knows which character it wants at every step: the enumeration is the more expensive of the two.
One saving is flat in the budget and the other is notThe two savings against the error budget, on 8-character patterns over 8,192 characters of a protein alphabet. The compound walk is flat at 11x — it makes each extension cheaper by a factor that is a property of the alphabet, and the budget does not change the alphabet. The enumeration moves with the budget, because what it removes is the extensions that find nothing and the share of those rises from 64.5% at no errors to 74.7% at 2. The two lines never cross here, and that is the finding rather than a limitation: on a branching search the descent wins at every budget, and the crossing is on the other axis entirely — whether the search branches at all.025507510000.50011.502errors permittedfactor against the published loop64.5%55.8%74.7%the walk: 11xthe descentthe label is the shareof dead extensions6 patterns · sigma 2198x to 105x
Fig. 4 The two operation savings against the error budget: one flat in the budget and one not, and no crossing between them on a branching search.

The two operation savings behave differently as the search gets harder, which is another way of seeing that they are not the same kind of thing.

The compound walk is flat at 11.0 across the whole budget sweep. It makes each extension cheaper by a factor that is a property of the alphabet, and the budget does not change the alphabet.

The enumeration moves: 98.1 at no errors, 78.2 at one, 104.6 at two. Its factor follows the share of extensions that find nothing, which rises with the budget.

So a reader choosing between them on a branching search does not have to: the enumeration is ahead at every budget, by between seven and nine times. The choice is on the other axis entirely — whether the search branches at all.

What an index with all three looks like

The structure this strand builds takes all three and it is worth describing because the combination is not obvious from the parts.

Its forward half locates, with Elias–Fano marks.

Its reverse half counts only — no marks, no sampled positions — which is a fourth change, from a different strand, and composes with the marks because it removes them from one half and shrinks them in the other.

Its extensions by a named symbol use the compound walk.

Its branching uses the enumeration, which requires the trees to be order-preserving — so it refuses a Huffman shape in its constructor, the same guard the compound index has for the same reason.

That last is the one interaction between the operation savings that is not about cost: both need an ordered tree, for the same reason, and an index doing either has the same constraint.

What the strand had to build to answer it

The measurement above needed an index with all three changes in it, and building one turned out to need two things that neither of the original strands had.

One index rather than three. The compound walk lives in one class, the enumeration in another, and the mark representation in a third. An index with all three has to be a subclass of the first with the second’s method and the third’s field — so the code exists to make the comparison rather than because anybody wanted such an object.

One counter across all of them. Every measurement here is bit-vector ranks on the forward half’s wavelet tree, charged by a counter the index owns. Comparing three machines means running them against the same counter on the same tree, because a rank on one tree and a rank on another are not necessarily the same amount of work.

That second constraint is the reason the comparison is between traversals rather than between implementations. All three machines share a wavelet tree, a code shape, a text and a counter, and differ only in which nodes they visit.

Without that discipline a comparison of three published results would be a comparison of three papers’ experimental setups, which is the thing this collection exists not to do.

The two currencies, on one frame

Since one saving is bits and two are operations, the composition question has a second form: does the size change cost anything at query time, and does the operation change cost anything in bits?

Neither, and both are checked.

The enumeration adds no bits. It is a different traversal of a structure already built — the same vectors, the same directories, the same code table — so an index supporting rank supports it.

The compound walk adds no bits either, for the same reason.

The sparse marks add no operations to a search and a small number to a locate: a rank on an Elias–Fano array is a select and a bucket walk of 1.25 accesses rather than a directory read, and a locate performs one per LF step of its walk.

So the composition is: two free operation savings that are one, plus a bits saving that costs a little on one operation nobody counted.

The size ladder, and the last rung spends what the others savedA bidirectional index over 16,384 characters at one sampled position in 32, with each change applied on top of the last. Dropping the reverse half's locating apparatus takes it to 90.4%. Representing the remaining marks as an Elias-Fano array takes it to 84.2% — and that second step is worth 15,570 bits against the first step's 24,080, which is most of a saving that was attributed entirely to the first. The last rung is not a saving at all: it spends the whole of what was saved on sampling the surviving half four times as densely, and lands at 96.4% of where it started with a locate several times faster. That is the trade the strand exists for, and it is one plate rather than two.both halves locate250,584100.0%the reverse half counts only226,50490.4%and its marks are Elias-Fano210,93484.2%the saving spent on sampling241,47596.4%16,384 characters · one in 3284.2% smaller, or the same size and faster
Fig. 5 The size ladder, cumulative: what each change takes off a bidirectional index over sixteen thousand characters at one sampled position in thirty-two.

Why the question is worth asking at all

There is a version of this essay that would not need writing, and it is worth saying why this is not it.

If three improvements had been made to three different parts of a structure — one to the wavelet tree, one to the sampling, one to the search — nobody would ask whether they compose. Independent parts compose by construction.

These three are not independent. All three are about the same index; two of them are about the same operation; and the two that are about the same operation were developed one strand apart, by different arguments, and reported as separate results.

That is exactly the circumstance in which a reader multiplies. Two papers, two factors, one structure — and the arithmetic is available and wrong.

What makes it wrong is not visible from either result. The count that was already there describes a saving on an extension by a named symbol and the branches that find nothing describes a saving on a search that branches, and neither says that the second’s operation contains the first’s. Discovering that required building an index with both and measuring it.

The general form is uncomfortable and worth stating: two savings developed separately are reported separately, and whether they compose is a third measurement nobody’s incentives produce. It is not in either strand’s interest to make it and it is the number a reader needs.

Three ways to extend an interval, on the same searchA search for 8-character patterns within 1 error over 8,192 characters of a 27-symbol alphabet, 6 patterns, counted in bit-vector ranks. The published shape asks about every character of the alphabet at every node: 2,638,440. The compound walk makes each of those questions cheaper by returning the rank and the smaller-count from one descent, which is 14x. Enumerating the interval's symbols removes the questions instead, which is 109x — and the 60.5% of extensions that found nothing are what it removed. All three visit the same live intervals in the same order and report the same occurrences; that is the check, and it is what makes this a plate about cost.a loop over the alphabet2,638,440the compound walk, inside the loop188,46014xone descent per node24,100109x6 patterns · 1 error · sigma 2760.5% of the extensions were dead
Fig. 6 The same three machines on a twenty-six letter alphabet, where every factor is larger and the shortfall is still exactly the walk’s own.

The ladder

Applying the size changes one at a time on a bidirectional index over sixteen thousand characters at one in thirty-two:

Both halves locate: 250,584 bits. The reverse half counts only: 226,504 — 90.4%. And its marks are Elias–Fano: 210,934 — 84.2%. The saving spent on sampling the surviving half four times as densely: 241,475 — 96.4%, with a locate several times faster.

The second step is worth 15,570 bits against the first step’s 24,080, which is nearly two thirds of what the deletion was — on a structure where the deletion was the headline. The floor was the marks is where that reordering is worked out.

The fourth rung is not a saving. It is the whole of the first three spent on something else, which is the ladder, and the rung that spends.

Bits and steps on one frame

The strand’s last measurement puts the two currencies on one plate against the parameter that moves both, which is the convention this collection adopted after reporting a trade as a win.

Bits paid and steps spent, against the one dial that moves bothThe whole structure's size as a share of the plain bidirectional index, and the LF steps one occurrence costs, on the same frame and against the same parameter. The size falls from 78.0% to 84.7% as the sampling thins, and the walk to a sampled position rises from 1.5 steps to 67.5. Neither line is the answer on its own: a reader choosing a sampling rate is choosing a point on this pair, and a plate showing only the first reports a structure that gets better forever. The two operation savings this strand measures move neither line, which is what "orthogonal" means here and is why they compose with this and not with each other.020406080255075100125one sampled position in every …of the plain index, per centsize: 84.7%67.5 steps16,384 characters · 8-character patternssize falls, the walk lengthens
Fig. 7 The structure’s size as a share of the plain index, and the LF steps one occurrence costs, on one frame against the sampling rate.

The size falls from 99.5% to 86.3% as the sampling thins, and the walk to a sampled position rises from 2.5 steps to 63.6. Neither line is the answer on its own: a reader choosing a sampling rate is choosing a point on the pair, and a plate showing only the first reports a structure that gets better forever.

What the two operation savings do to that pair is nothing. They move neither line, which is what orthogonal means here and is why they compose with the sampling and not with each other.

Bits and steps on one frame is that plate’s own essay, and the convention it follows was set by the half-index strand: both currencies on one frame, when a change trades.

What the fourth change would be

The strand has three savings and there is a fourth available that nothing here builds, and it is worth naming so the list does not read as complete.

The sampled positions are (n/s)⌈log₂ n⌉ bits of arbitrary numbers, and they are the largest part of the locating apparatus once the marks are sparse. Nothing here compresses them, and there is a published approach — storing differences along the phi function rather than absolute values — that this collection built for a different structure entirely.

A function with r pieces is that structure, and its sampling is at run boundaries rather than at regular intervals: 2r values and no marks whatsoever. On a repetitive collection that is a different and larger change than anything in this strand.

So the honest state is three savings measured, one of which is two, and a fourth that would replace the thing all three are about.

The other composition question

There is a second pair worth checking and this strand does check it: whether the size saving composes with itself across the two halves of a bidirectional index.

Dropping the reverse half’s locating apparatus removes marks and sampled positions from one half. Representing the surviving half’s marks sparsely shrinks the other. Those are different arrays in different halves, so they compose trivially — and the measured ladder confirms it: 250,584 to 226,504 to 210,934, with the second step’s 15,570 bits being exactly the forward half’s mark saving.

What makes that worth measuring rather than assuming is the ordering effect it reveals. Reported as “dropping the apparatus is worth 9.6% and then the marks are worth 6.9%”, the two look like a large improvement and a smaller one.

Reported the other way — marks first, then the drop — the numbers are different: representing both halves’ marks sparsely first, then dropping one half’s, gives a first step worth more and a second worth less, because the thing being dropped is now smaller.

Same total, different attribution. That is a general property of cumulative savings on overlapping parts and it is why a ladder’s rungs should be read as a total rather than as individual credits.

The size ladder, and the last rung spends what the others savedA bidirectional index over 16,384 characters at one sampled position in 64, with each change applied on top of the last. Dropping the reverse half's locating apparatus takes it to 88.9%. Representing the remaining marks as an Elias-Fano array takes it to 82.0% — and that second step is worth 17,250 bits against the first step's 27,920, which is most of a saving that was attributed entirely to the first. The last rung is not a saving at all: it spends the whole of what was saved on sampling the surviving half four times as densely, and lands at 88.4% of where it started with a locate several times faster. That is the trade the strand exists for, and it is one plate rather than two.both halves locate250,584100.0%the reverse half counts only222,66488.9%and its marks are Elias-Fano205,41482.0%the saving spent on sampling221,45488.4%16,384 characters · one in 6482.0% smaller, or the same size and faster
Fig. 8 The same ladder at a sparser sampling, where the mark representation is worth more and the deletion less.

What a reader should carry

Three numbers and a rule.

On a branching search: 78×, on a twenty-symbol alphabet at one error, from the enumeration. The compound walk contributes nothing on top of it.

On an exact search: 11×, from the compound walk. The enumeration contributes a loss on top of it.

On the structure: 16%, from dropping a counting half’s locating apparatus and representing the survivor’s marks properly, at one in thirty-two — and both of those numbers depend on the sampling rate by a factor of several.

And the rule: two savings acting on the same quantity do not multiply, and two acting on different quantities do. Deciding which case is at hand means asking what each removes rather than how large each is, and the question is answerable before any measurement.

Applied here: the marks remove bits and the other two remove operations, so those compose. The walk removes σ ranks from computing a smaller-count and the enumeration removes the extensions those ranks were for, so those do not — the second removes the work the first was making cheaper.

Asking “what does this remove?” of each of three changes takes a minute and would have got the answer without building anything. What building it added was the exactness: the shortfall is 11.0 against a walk factor of 11.0, which turns a plausible argument into a measurement.

That is the division this collection generally settles on. The reasoning says which way the answer goes and the measurement says whether the reasoning was complete, and here it was — which is worth knowing, because the two previous times this collection expected a composition it was wrong about the mechanism as well as the magnitude.

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.

Backtracking searchBidirectional indexCompositionCompound walkIndex sizeInterval symbolsSample marks