The index that replaces the text

The half that is never asked where

A bidirectional index is two indexes and one interval. One of them is asked for ranks several hundred times a search and for a position never — and the parts that answer "where" are two of the five it is made of.

A bidirectional index is two FM-indexes — one over the text, one over the text reversed — and one interval carried by both.

An interval that grows at both ends is the construction, and the structure that was supposed to halve is the finding that it does not halve anything: two indexes cost exactly 2.00 times one, and what they buy is the order the pattern may be consumed in.

This page is about a smaller question that turns out to have a useful answer. What is the reverse half actually asked to do?

The two halves, and the two parts one of them does not needA bidirectional index over 8,192 characters, sampling every 32 rows, drawn as the five parts each half is made of. The reverse half exists to be counted in: an extension asks it for ranks and never for a position, because its rows are positions in a text written backwards and the occurrences a search reports come out of the forward interval. So the marks and the sampled positions — the pale rows — can go, and what is left still answers every question anything asks it. That is 11,791 bits of 70,610, or 16.7% of the whole structure.forward · wavelet tree18,377forward · rank directories5,037forward · C table100forward · sample marks8,193forward · sampled positions3,598reverse · wavelet tree18,377reverse · rank directories5,037reverse · C table100reverse · sample marks8,193droppedreverse · sampled positions3,598dropped8,192 characters · sampling every 3216.7% of both halves
Fig. 1 Both halves, drawn as the five parts each is made of. The pale rows are the two that answer “where”, and one half never answers it.

Ranks, and nothing else.

What an extension needs

The interval is four numbers: the rows of the pattern in the forward index, and the rows of the reversed pattern in the reverse index. Both intervals have the same width at every moment — they are the same occurrences counted twice — and keeping them that way is the whole mechanism.

Extending left by a character c is an ordinary backward-search step in the forward index: two ranks give the new forward rows. But the reverse interval has to move too, and the amount it moves by is the total width of the intervals for every symbol sorting before c — because in the reverse index those extensions sit before this one.

So a left extension asks the forward index for two ranks of c and the forward index for a rank of every smaller symbol. A right extension asks the same of the reverse index. Neither asks for a position.

That is not an oversight in the implementation. It is what a bidirectional search is: the search’s job is to narrow an interval, and the occurrences are read out at the end, once.

Why the reverse rows could not be read anyway

The reverse half’s rows are positions in a text written backwards, so a row of the reverse index says where a reversed suffix begins in a reversed text — which is a number about a string nobody has.

It could be converted: position p in the reversed text is position n − p − 1 in the original. But the conversion is only useful for the reverse pattern’s occurrences, and those are the same occurrences the forward interval already holds, in the forward index, at the right coordinates.

So there is no query anywhere in a bidirectional search that wants a position out of the reverse half. Not “not needed in this implementation” — not available in a useful form at all.

The same search, on both structuresA pattern of 15 characters within 2 substitutions, searched under the same scheme on a symmetric bidirectional index and on one whose reverse half cannot locate. The two rows are the positions each found: they are the same 6 positions, because the reverse half was never asked. The nodes visited are identical too — 498 — since the search's shape depends on the intervals and not on what is stored beside them. The structure below is 16.7% smaller than the one above.6812,0443,4074,7696,1327,495both halves locate — 70,612 bitsreverse half counts only — 58,821 bits498 interval extensions on each8,192 characters · pigeonhole, 2 errors16.7% smaller, same answer
Fig. 2 The same search on both structures: the positions found are identical, because they all came out of the forward interval. The nodes explored are identical too.

The five parts, and what each answers

An FM-index here is five things, and the split is not the one a size formula suggests.

The wavelet tree over the transform — 18,377 bits of the 35,305 at eight thousand characters. This is what answers rank, and rank is what backward search is made of. Rank is the only thing it does is the essay about why a whole index reduces to it.

The rank directories — 5,037 bits. The blocks and superblocks that make a rank constant-time rather than linear.

The C table — 100 bits. One cumulative count per symbol.

Those three answer how many. Between them they support count queries, interval extensions, and the whole of a search that only wants to know whether and how often.

The sample marks — 8,193 bits, one per row, saying whether this row’s position was kept.

The sampled positions — 3,598 bits, the positions themselves, one in every 32.

Those two answer where, and they exist only for that. The sampling that goes the other way is the essay about the second sampling a real index needs for extraction; this is the first one, the one that turns a row into a text position.

What dropping them leaves

An index without the last two is still an index. It counts, it extends intervals, it supports a backward search to completion, and it refuses to say where anything is.

The two halves, and the two parts one of them does not needA bidirectional index over 8,192 characters, sampling every 16 rows, drawn as the five parts each half is made of. The reverse half exists to be counted in: an extension asks it for ranks and never for a position, because its rows are positions in a text written backwards and the occurrences a search reports come out of the forward interval. So the marks and the sampled positions — the pale rows — can go, and what is left still answers every question anything asks it. That is 15,375 bits of 77,778, or 19.8% of the whole structure.forward · wavelet tree18,377forward · rank directories5,037forward · C table100forward · sample marks8,193forward · sampled positions7,182reverse · wavelet tree18,377reverse · rank directories5,037reverse · C table100reverse · sample marks8,193droppedreverse · sampled positions7,182dropped8,192 characters · sampling every 1619.8% of both halves
Fig. 3 The same split at a denser sampling. The three counting parts do not move; the two locating parts nearly double.

Built and stripped rather than built without: the object here is a full index whose samples and marks are set to null after construction, so that asking it to locate throws rather than silently returning something. That is the same discipline the chainless document listing uses — an object that still holds the arrays and merely promises not to read them proves nothing.

At one sampled position in 32, the two dropped parts are 11,791 bits of a 35,305-bit index — a third of the reverse half, and 16.7% of the whole structure.

The invariant that makes it safe

The reason this is safe is not that the reverse half is unimportant. It is that the forward interval is exact, and it is exact because both intervals are maintained in step.

If the reverse half drifted — if an extension moved its start by the wrong amount — the forward interval would move too, because a right extension computes the forward start from the reverse one. A search would then read positions out of a forward interval that was wrong, and every position it reported would be a genuine occurrence of something, just not of the pattern.

That failure has a check. The in-step assertion requires both intervals to have the same width at every step of every search, and the deliberate defect — leaving the other interval where it was — fails it on 39 of 39 substrings while changing no answer at all, because a covering search scheme is redundant enough that another search hands back what one loses.

So the guarantee this page relies on already exists: the forward interval is exact, therefore its rows are the occurrences, therefore the forward half is the only one that needs to locate. What is being dropped is the half nobody reads, and what makes that a fact rather than a hope is a check written for a different reason.

One substring, grown from each endThe same 7-character string built two ways: leftwards, one character at a time from its last, and rightwards from its first. At every step both the forward interval and the interval in the index of the reversed text are held, and the two are always the same width — they are the same occurrences counted twice, and the assertion that they are is what catches every off-by-one this structure can have. The widths fall from 1,041 to 1 either way, and the final interval is the same four numbers by either route. What the second index buys is not space: it is the freedom to add the next character at whichever end the search wants.1234567characters addedoccurrencesgrown leftwardsgrown rightwards"tgatatt"4,000 characters · four symbols1 occurrences
Fig. 4 The two intervals in step: one substring grown leftwards and rightwards, with both halves’ rows drawn at every step. Equal widths at every moment is the invariant.

How a locate works, and why it costs two parts

The two parts that answer “where” are two rather than one for a reason that is worth following, because it is what makes the saving depend on a dial.

A row of the index is a rotation of the text, and the position it starts at is what a locate has to produce. Storing all of them is a suffix array — n⌈log₂ n⌉ bits, which is the thing the whole compressed-index family exists to avoid. So an FM-index stores a sample: every 32nd position, say, indexed by row.

The trouble is that a query lands on an arbitrary row, and an arbitrary row is not sampled. The walk that fixes it is the index’s own mechanism: step backwards one character at a time — each step is a rank, which the counting parts support — until a sampled row is reached, then add the number of steps to the stored position. A search that runs backwards is the machinery, used here for a different purpose.

That walk needs to know, at each row, whether this one is sampled. A bit vector of n bits answers it in one rank, and the sampled positions live in a compact array indexed by the rank. Hence two parts: marks, one bit a row, and positions, ⌈log₂ n⌉ bits per kept sample.

The two behave completely differently under the sampling rate. The positions are n/s × ⌈log₂ n⌉ bits and shrink as s grows; the marks are n bits whatever s is. At one in 4 the positions are the larger of the two; at one in 128 the marks are seven times larger. That asymmetry is the reason the saving on this page has a floor, and it is the subject of the page after next.

What it is not

Three things, because “a third of one half is free” invites more than it says.

It is not a smaller interval or a faster search. The nodes explored are identical, the ranks performed are identical, the rows found are identical. Nothing about the search changes; a search cannot tell which structure it is running on.

It is not free of the sampling parameter. The saving is 16.7% at one sampled position in 32 and 30.5% at one in 4, because the locating apparatus is most of the index when positions are kept densely. A sixth of what, exactly is the page about that, and the short version is that any single number quoted here is a number about a dial setting.

And it is not a new structure. A careful implementation of a bidirectional index ships this, because the person writing it notices that the reverse half’s locate is never called. What is new here is the measurement: how much it is worth, at what sampling rate, and what the bits can be spent on instead.

Why the halves are the same size to begin with

One number underneath all of this deserves stating, because it is the thing that makes a third of a half worth chasing.

The two halves are the same size. Not approximately: 35,335 bits forward and 35,335 reverse in the measurement that established it, exactly 2.00 times one index. The reverse index is an FM-index over the reversed text, and reversing a text does not change its length, its alphabet, or its symbol frequencies — so the wavelet tree has the same shape, the directories the same size, and the sampling the same count.

What does change is the transform. The Burrows–Wheeler transform of a reversed text is not the reverse of the transform, and its run count is generally different — which matters for a run-length compressed index and not for the plain one measured here.

So the honest starting point for this strand is: a bidirectional index is exactly twice an index, one half of it is asked only for ranks, and a third of that half is machinery for a question it is never asked. That is 16.7% of the whole thing at a common setting, and it is the largest single piece of a bidirectional index that can be removed without changing a single answer.

Which searches this covers

Worth checking rather than assuming, because “the reverse half is never asked where” is a claim about every search a bidirectional index runs.

A search scheme is a list of searches, each consuming the pattern’s pieces in some order with an error bound per piece. The search that starts in the middle is the essay about why the orders differ and what they buy. Some searches begin in the middle and grow both ways; some go left to right; some right to left.

Every one of them maintains both intervals, because the pieces are consumed in an order that requires both directions. Every one of them ends with a forward interval whose rows are the occurrences. And every one of them reads those rows exactly once, at the end, out of the forward half.

The three searches, and where each allows its errorsA pattern of 15 characters is cut into 3 pieces, and a search is an order to visit them in plus a ceiling on the errors allowed so far after each. Every one of these three demands its FIRST piece be matched exactly — the number in its first cell is zero — which is a far stronger condition than "some piece is exact", because it applies from the first character rather than after the fact. Two of them then allow at most one error after their second piece where a scheme built straight from the pigeonhole would allow 2. The order matters because a piece can only be searched when it touches what has already been searched: the third search starts in the middle and grows outwards, which no index extended in one direction can do.piece 1piece 2piece 35 characters5 characters5 characterssearch 11 → 2 → 3≤ 01st≤ 12nd≤ 23rdsearch 22 → 1 → 3≤ 12nd≤ 01st≤ 23rdsearch 33 → 2 → 1≤ 23rd≤ 22nd≤ 01stthe ceiling on errors so far, and the order each piece is visited ink = 2 · 3 pieces3 searches
Fig. 5 The four schemes, drawn as orders and error bounds. The direction each piece is consumed in differs; where the answer is read from does not.

So the claim holds over the whole family, and the measurement below it holds for any of them. What would break it is a search that reported occurrences of the reversed pattern as a separate answer — which is not a query anybody asks, since the occurrences of a reversed pattern in a reversed text are the occurrences of the pattern in the text.

What comes next

Three pages, and they are one measurement each.

An index that cannot locate builds it and prices the parts: what a counting-only half is made of, and what the whole structure costs when one half is that.

A sixth of what, exactly sweeps the sampling rate, because the number in the title is a number about a setting — and finds that the saving does not fall to nothing at the sparse end, for a reason worth knowing.

The saving, spent is the useful one. The bits removed from the half that never locates can be given to the half that does, and at equal total size the asymmetric index locates five times faster.

The saving is a property of a dial, not of the structureWhat dropping the reverse half's locating apparatus saves, against how often a position is kept. The deferral that asked for this structure priced it at "about a sixth of the second index", and at one position in 32 it is exactly that — 16.7%. At one in 4 it is 30.5% and at one in 128 it is 14.0%. The curve does not go to zero, because half the apparatus does not depend on the rate at all: the marks are one bit a row however rarely a row is kept, and they are the floor the line flattens onto.0102030255075100125one sampled position in every …of both halves saved, per cent"about a sixth"the marks alone: 14.0%8,192 characters16.7% at one in 32
Fig. 6 The saving against the sampling rate, which is the plate the next two pages are about. At one in 32 it is the sixth the deferral predicted.

What a locate actually costs

The parts that answer “where” are worth pricing in operations as well as in bits, because the two decide different things.

A row that is not sampled is walked backwards — one LF step per character, each step a rank on the wavelet tree — until a sampled row is reached. At one sampled position in 32 that is 15 steps an occurrence on average, and the measurement agrees with the prediction of half the gap to within a step at four settings of the dial.

So a query returning a thousand occurrences performs fifteen thousand ranks after the search has finished, which is usually more than the search itself did. A sixth of what, exactly sweeps that, and the saving, spent is what happens when the reverse half’s bits are moved to the forward half’s sampling: the same total size, and three steps an occurrence instead of fifteen.

What a locate costs, at the same dialThe other side of the same setting. A row that is not sampled is walked backwards until it reaches one that is, so a sparser sample is a smaller index and a slower locate: 1.7 steps an occurrence at one in 4 and 79.0 at one in 128. The dashed line is the expected walk, half the gap — the measured points sit on it, which is what a uniform sampling over rows predicts and is worth checking rather than assuming, because the rows a search returns are not uniformly spread over the text.020406080255075100125one sampled position in every …LF steps an occurrencemeasuredhalf the gap8,192 characters · 6 occurrences79.0 steps at one in 128
Fig. 7 The other half of the dial: LF steps an occurrence against how often a position is kept, with half the gap drawn as the prediction.

Where this sits

Four pages. This one is the observation; an index that cannot locate builds the structure and prices its parts; a sixth of what, exactly sweeps the parameter that every price depends on; the saving, spent is the recommendation.

The sixth, spentThe interesting half of the result. The top row is the symmetric structure: both halves able to locate, one position kept in every 32, 15.0 LF steps an occurrence. Below it are asymmetric structures whose reverse half keeps nothing and whose forward half is sampled more densely — and the row at one in 8 fits inside 98.5% of the symmetric structure's bits while locating 5.0 times faster. The bits were not saved; they were moved to the half that uses them.both halves, one in 3215.0 steps100.0% the sizeforward sampled one in 3215.0 steps83.3% the sizeforward sampled one in 167.0 steps88.4% the sizeforward sampled one in 83.0 steps98.5% the sizeforward sampled one in 41.7 steps118.8% the size8,192 characters · 6 occurrences5.0x faster, 98.5% the size
Fig. 8 Where it ends: the same total bits, with the forward half sampled four times as densely and a locate at a fifth of the cost.
The two halves, and the two parts one of them does not needA bidirectional index over 8,192 characters, sampling every 64 rows, drawn as the five parts each half is made of. The reverse half exists to be counted in: an extension asks it for ranks and never for a position, because its rows are positions in a text written backwards and the occurrences a search reports come out of the forward interval. So the marks and the sampled positions — the pale rows — can go, and what is left still answers every question anything asks it. That is 9,999 bits of 67,026, or 14.9% of the whole structure.forward · wavelet tree18,377forward · rank directories5,037forward · C table100forward · sample marks8,193forward · sampled positions1,806reverse · wavelet tree18,377reverse · rank directories5,037reverse · C table100reverse · sample marks8,193droppedreverse · sampled positions1,806dropped8,192 characters · sampling every 6414.9% of both halves
Fig. 9 The parts at a sparser setting, where the sampled positions have shrunk and the marks have not.

The two locating parts trade places at s=log2ns = \lceil\log_2 n\rceil

The asymmetry between marks and positions is described above as the reason the saving has a floor, and the crossing point is a single expression worth writing down.

The marks are nn bits whatever the rate. The positions are (n/s)log2n(n/s)\lceil\log_2 n\rceil bits. They are equal when

s  =  log2n,s \;=\; \lceil\log_2 n\rceil,

which at eight thousand characters is thirteen. Below one sampled position in thirteen the positions are the larger part; above it the marks are.

The measurements land on that exactly. At one in 32 the ratio of marks to positions is 8,193 against 3,598, or 2.28, against a predicted 32/13=2.4632/13 = 2.46. At one in 16 it is 8,193 against 7,196, or 1.14, against 1.23. At one in 4 it is 8,193 against 28,686, or 0.29, against 0.31. Three settings, and the simple ratio s/log2ns/\lceil\log_2 n\rceil describes all of them.

That gives both ends of the saving without sweeping anything.

The sparse end. As the sampling thins, the positions vanish and the removable apparatus tends to the marks alone — 8,193 bits against an index that has shrunk to 31,707, which is 12.9% of the whole structure. The saving does not fall to nothing at the sparse end because nn bits of marks do not depend on the rate, and that is the floor the next page finds by measurement.

The dense end. As the sampling thickens, the positions dominate everything and the locating apparatus approaches the whole of the reverse half — so the saving approaches 50%. At one in 4 it is already 36,879 bits of a 120,786-bit structure, which is the 30.5% quoted above, recovered here from the parts.

So the number in this strand’s title is one point on a curve running from 12.9% to nearly 50%, and the curve’s shape is a hyperbola plus a constant with the crossover at thirteen. Quoting 16.7% without the rate is quoting a number about s=32s = 32; quoting the range is quoting the structure.

The crossover also says which part to attack. Below s=13s = 13 the positions are the target and a smaller position encoding — storing gaps rather than absolute values — is where the bits are. Above it the marks are the target, and the marks are a bit vector with one bit in ss set, which is exactly the case a sparse representation is for. Two different optimisations, and which one is worth doing is decided by a comparison between the sampling rate and the width of a position.

Which is a bit for every bit’s finding arriving as a design rule rather than an observation: the vector that says which costs nn bits regardless of how few it marks, so at any sparse rate the bookkeeping outweighs what it is bookkeeping for. A sixth of what, exactly sweeps the curve this expression describes, and the floor it reports is the marks refusing to shrink.

What this does not generalise to

One caution, because “half a structure is never asked half its questions” sounds like it should apply widely and mostly does not.

The reason it applies here is specific: the two halves of a bidirectional index are the same index over two texts, and one of the two texts is an artefact of the construction. Nobody wants positions in a reversed text, so the half over it is structurally exempt from answering “where”.

Contrast a two-level index, or a partitioned one, where both parts index real text and both are asked everything. Contrast a cache in front of a structure, where the front is asked everything and the back is asked what the front misses. Neither has a component with an ability nobody will ever call.

So the finding is not “structures have unused halves”. It is that a construction which duplicates for a mechanical reason — here, so that a pattern can be extended at either end — produces a duplicate whose usefulness is narrower than its interface, and that the narrowing is worth measuring rather than assuming.

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

The objects this essay names

Each one links to every other essay that touches it.

Backward searchBidirectional indexFM-indexIndex sizeIntervalInvariantLocateRankSamplingSearch schemeSuffix arrayWavelet tree