The other axis

One separator, or one for each

A shared separator costs one alphabet symbol and is free. Fifteen distinct ones take the alphabet from twenty-two to thirty-five, which crosses a power of two, so every character of every document costs a sixth bit — 1.2 times the packed collection, to tell the boundaries apart.

The occurrences a join invents establishes that a concatenation without separators holds strings no document holds, and that one separator makes them unmatchable. This essay is about which separator, and it is a question with a surprisingly sharp answer.

The packed collection under three joins, at 15 documents15 documents of 512 characters. Running them together gives an alphabet of 21; one shared separator gives 22; one separator each gives 35. The alphabet sits inside a logarithm, so for most of the range the three cost the same — and then the largest crosses a power of two and every character of every document costs a whole extra bit, 1.20x the packed size. The cost of naming the documents is charged to the text.run together38,400 bitsσ 21 · 5 bitsone separator38,470 bitsσ 22 · 5 bitsa separator each46,164 bitsσ 35 · 6 bits15 documents of 512 charactersthe separators are the only differenceEnglish-like · 15 documents1.20x for the distinct marks
Fig. 1 Fifteen documents of five hundred and twelve characters, joined three ways. The packed collection is the same size for two of them and 1.2 times for the third.

Why anybody wants distinct separators

A shared separator solves the artefact problem completely. Anything more is for the construction, not for the search, and there are two reasons somebody would want it.

Comparability of suffixes. Some suffix-array constructions need every suffix to be strictly comparable — no suffix a prefix of another — which a single sentinel at the end of the whole string achieves and a repeated internal separator does not. With one shared separator, two documents ending identically produce suffixes that compare equal until the separator, and then compare equal again through it into the next document’s text, so the order between them is decided by content that belongs to neither.

Identifying the document from a row. Some document-retrieval structures read the document identity out of the transform itself, which needs the separator to carry the identity. Distinct separators do that for free; a shared one needs a separate document array, which is the third essay on this ladder.

Neither reason is about correctness of the search. Both are about what the surrounding machinery can be built out of, and both are real.

What each costs

The text is nineteen distinct symbols, so:

join alphabet bits a character packed, 15 documents
run together 21 5 38,400
one separator 22 5 38,470
a separator each 35 6 46,164

Two of the three are the same, and the third is 1.2 times the packed collection. Nothing about the documents changed between the rows — the same characters in the same order, joined three ways.

The reason is entirely the power of two. Twenty-two symbols need five bits and so do thirty-two; thirty-five need six. A collection of fifteen documents over a nineteen-symbol alphabet lands just past the boundary, and every character of every document pays for it.

The step is the finding, not the slope

A cost that is invisible over most of its range and then jumps by twenty per cent is a particular kind of cost, and it is worth naming what makes it awkward.

At two documents the distinct join costs the same as the shared one — alphabet twenty-two either way, five bits. At four documents, twenty-four symbols, still five bits. At eight, twenty-eight, still five. At fifteen, thirty-five, six bits and a 1.2-fold jump.

So a developer measuring at eight documents and extrapolating sees no cost at all and concludes the distinct separators are free. The cost arrives without warning at whatever document count pushes σ+d\sigma + d past the next power of two, and where that is depends on the text’s alphabet as much as on the number of documents.

This collection’s own ledger records the same arithmetic biting once before, in the opposite direction: a function computing bits for a counter was used to compute bits for an index, the two differ exactly at the powers of two, and every alphabet size and array length on this site sits at one. The powers of two are where this field’s constants live, and a measurement taken between two of them measures nothing.

The packed collection under three joins, at 15 documents15 documents of 512 characters. Running them together gives an alphabet of 4; one shared separator gives 5; one separator each gives 18. The alphabet sits inside a logarithm, so for most of the range the three cost the same — and then the largest crosses a power of two and every character of every document costs a whole extra bit, 1.67x the packed size. The cost of naming the documents is charged to the text.run together15,360 bitsσ 4 · 2 bitsone separator23,082 bitsσ 5 · 3 bitsa separator each38,470 bitsσ 18 · 5 bits15 documents of 512 charactersthe separators are the only differencefour symbols, uniform · 15 documents1.67x for the distinct marks
Fig. 2 The same three joins on four-symbol text, where the base alphabet is small and fifteen separators nearly quadruple it — three bits a character against two.

On a small alphabet it is much worse

Four-symbol text is the case this field cares most about, and it is where distinct separators hurt.

An alphabet of four needs two bits. One shared separator makes it five, which needs three — so the shared separator is not free here; it is already a fifty per cent premium, because four is a power of two and every alphabet of five or more needs a third bit.

Fifteen distinct separators make it eighteen, which needs five. That is 1.67 times the shared-separator collection and 2.5 times the raw packed text, to tell fifteen boundaries apart in a text of eight thousand characters.

The general form: distinct separators multiply the packed size by log2(σ+d)/log2σ\lceil\log_2(\sigma+d)\rceil / \lceil\log_2\sigma\rceil, which is close to one when σd\sigma \gg d and can be several when it is not. A genome collection — four symbols, thousands of sequences — is the worst case in this field and is exactly the corpus the field is built for.

That is the sharpest thing on this ladder, and it is arithmetic rather than a measurement: the corpora that most want document boundaries are the ones where naming the boundaries costs most.

A separator is not free even when it is free

The four-symbol case above is worth a section of its own, because it is the one where the shared separator — the design this ladder recommends everywhere else — is expensive.

Four symbols is a power of two, so a DNA-like text packs at exactly two bits a character with nothing wasted. Adding a single character to the alphabet takes it to five, and five symbols need three bits: a fifty per cent premium on the whole collection, to hold one character that appears d1d-1 times.

There is no way around it inside a fixed-width packing, and that is the point. The three bits are what a fixed-width code charges for an alphabet of five, and the actual information content of a symbol drawn from a distribution that is four symbols at high frequency and one at d/nd/n is barely more than two bits. The premium is the packing’s, not the text’s.

Which is exactly what a compressed index does not pay. The index that is smaller than the text measures a wavelet tree with a Huffman shape, where a rare symbol gets a long code and a common one a short one, so the separators cost their own log2(d/n)-\log_2(d/n) bits each and nothing per character of document.

So the step function in the table above is a property of the packed representation, and it is the right number for a reader storing the text and the wrong one for a reader storing a compressed self-index. The two differ by exactly the gap between a fixed-width code and an entropy, which is the bits a coder emits all over again.

What it does to the structures above the text

The alphabet is not only a packing cost. It appears in three more places.

The wavelet tree’s depth is log2σ\lceil\log_2\sigma\rceil for a balanced tree, so a rank query costs one more level. With a Huffman-shaped tree — which is what this collection’s indexes use — the separators are rare and get long codes, so the mean depth barely moves and the worst case does.

The first-column table is σ\sigma entries of log2n\lceil\log_2 n\rceil bits, which goes from 22 to 35 entries: a few hundred bits, negligible.

And the run count rises by roughly dd, because each separator is a character occurring where nothing else does. Measured on fifteen documents: 1,854 runs with a shared separator, 1,863 with distinct ones — under half a per cent, and proportional to the document count rather than the collection size.

So the packing is the whole of the cost, and the packing is a step function of the alphabet.

What the separators cost the two measures of repetitionThe run count and the phrase count of the same documents under three joins. At 15 documents the run count is 512 run together and 530 with a separator each; the phrase count is 275 and 297. Each separator is a character that appears where nothing else does, so it ends a run and it ends a phrase — a cost proportional to the number of documents and not to their length, which is why it is small here and would not be on a corpus of a million short documents. Both axes are logarithmic.1,000runs · phrases24815characters in the collection · documents belowr, separator eachr, run togetherzEnglish-like · 256 a document+18 runs · +22 phrases
Fig. 3 The measures of repetition under each join. The separators cost a few runs and a few phrases, proportionally to how many documents there are.

What a document-retrieval structure actually needs

The two reasons for distinct separators deserve testing rather than accepting, because one of them turns out not to be a reason.

Comparability of suffixes is real but narrow. It matters for constructions that sort suffixes by comparison and need a total order without ties; a construction using ranks and doubling, which is what this collection’s suffixArray does, breaks ties by position and needs no sentinel at all. So the requirement is a property of a particular construction algorithm rather than of suffix arrays.

Identifying the document from the transform is the one that does not survive. A structure that reads the document out of the separator character is reading it out of the nearest preceding separator, which is not something a transform supplies directly — that separator has to be found, which is a walk or a rank on something. Having the identity in the alphabet does not remove the work; it moves it.

What actually answers “which document is this position in” cheaply is a predecessor structure over the boundaries, which is the bit vector below and which is needed whether or not the separators are distinct.

So the honest case for distinct separators is one construction’s precondition, and it costs a bit a character on every document to satisfy. That is a poor trade and it is worth being explicit about, because “documents are separated by distinct sentinels” is stated as a convention in a lot of this field’s writing, without the price.

The alternative: a shared separator and a boundary vector

There is a third design and it is the one this collection’s index uses.

Keep one shared separator, and store the document boundaries as a bit vector over the text with a one at each document’s start. The document of a position is then a rank on that vector — one operation — and the vector is nn bits plus a rank directory, which for eight thousand characters is about nine thousand bits.

Compare that against distinct separators on four-symbol text, where the alphabet cost is two extra bits per character over eight thousand characters: sixteen thousand bits. The bit vector wins, and it wins by more as the documents get longer, because its cost is nn bits regardless of dd while the alphabet cost is nlog2(σ+d)n\lceil\log_2(\sigma+d)\rceil.

It also composes better. A bit vector with a rank directory is a structure this field already has, is compressible when the documents are long (a sparse vector of dd ones in nn positions costs dlog(n/d)d\log(n/d) bits rather than nn), and answers the “which document” question directly rather than requiring the answer to be decoded from a transform.

This collection has the compressed version already — what is still proportional to n measures a sparse bit vector at dlog(n/d)+o(n)d\log(n/d) + o(n) rather than nn — so on a collection of fifteen documents in eight thousand characters the boundary vector costs about two hundred bits rather than nine thousand.

The fourth design: no separator at all

Three joins are compared and the recommended one still puts a character in the text. Once the boundary vector exists, it does not have to.

The separator’s only remaining job is to make a window spanning two documents unmatchable. But a spanning window is exactly a window with a document start strictly inside it, and the boundary vector answers that in two ranks: an occurrence at [p,p+m)[p, p+m) crosses a boundary if and only if rank1(p+m1)>rank1(p)\mathrm{rank}_1(p+m-1) > \mathrm{rank}_1(p). Filter the occurrences instead of poisoning the text.

The saving is the whole alphabet premium. On four-symbol text a shared separator costs a third bit a character — fifty per cent of the packed collection, 24,576 bits against 16,384 at eight thousand characters — and removing it takes the packing back to exactly two bits. A third of the collection, for two ranks per reported occurrence, on a structure that already holds the vector those ranks run on.

The precondition is one this page has already established. Distinct sentinels are needed by constructions that sort suffixes by comparison; a shared one is needed by nothing in particular; and this collection’s suffix array sorts by ranks and breaks ties by position, so it needs no sentinel — not one per document and not one at the end. The requirement that made a separator look mandatory is the same requirement the page traces to one construction and discards.

What it costs beyond the two ranks is a change in where correctness lives. With a separator, a spanning window cannot match and the index is safe by construction; without one, the index reports occurrences the collection does not contain and a filter removes them. That is a real weakening — a caller reaching past the filter gets artefacts, and the filter has to be applied everywhere an occurrence is produced, including inside a propagation queue.

So the four designs are a ladder in both directions. Distinct separators: safe by construction, and log2(σ+d)\lceil\log_2(\sigma+d)\rceil bits a character. One shared separator: safe by construction, and one alphabet symbol — free on wide alphabets and fifty per cent on four. Shared separator plus a boundary vector: the same packing cost, and the document question answered in one rank. No separator, boundary vector, filtered occurrences: the raw packing, and correctness that depends on every caller going through the filter.

On nineteen-symbol text the fourth is worth nothing, because the third is already free. On four-symbol text it is worth a third of the collection — and four-symbol text with thousands of sequences is the corpus this field is built for, which is the occurrences a join invents’s problem meeting what is still proportional to n’s sparse vector and finding that the two together make the first one’s fix unnecessary.

The general shape: a cost inside a logarithm

Every quantity in this essay is nn times a ceiling of a logarithm of the alphabet, and that form has two properties worth separating.

It is nearly flat. Doubling the alphabet adds one bit a character. So a design that adds a few symbols is almost always free and a design that multiplies the alphabet is almost always affordable — the logarithm is doing its job.

It is a step. The ceiling means the cost arrives all at once, at a point determined by the base alphabet, and between the steps a measurement sees nothing. That is what makes it treacherous rather than merely expensive: the flatness invites extrapolation and the steps punish it.

This collection has the same shape in three other places. The index that is smaller than the text is nH0n H_0 against nlog2σn\lceil\log_2\sigma\rceil, where the gap is exactly the difference between an entropy and a ceiling. What is still proportional to n measures the directories that a logarithm hides. And a floor on the bits is the version where the logarithm is a floor rather than a cost.

The rule that comes out of all four: when a quantity is inside a ceiling of a logarithm, measure it on both sides of the nearest power of two or do not report it.

What a separator is bought for is worth drawing before the price is settled, because the thing it prevents is not a size at all.

The strings a concatenation invents8 documents of 256 characters, run together. A window spanning a join is a string the collection contains; the upper line counts them and the lower counts the ones that occur in no document at all. At 12 characters there are 77 spanning windows of which 44 are new, and every one of them is a string an index over the concatenation will report and a reader looking for documents cannot use. Putting one separator between the documents makes the count of matchable artefacts zero at every length — a pattern drawn from a document cannot contain a character no document holds.46812windowswindow lengthspanning a joinin no documentwith a separatorEnglish-like · 8 documents44 invented at m = 12
Fig. 4 Eight documents of 256 characters run together. The upper line counts the windows that span a join and the lower counts the ones that occur in no document at all: at twelve characters, 77 spanning windows of which 44 are strings the collection contains and no document does. One separator between the documents takes the lower line to zero at every length, because a pattern drawn from a document cannot contain a character no document holds.

So the choice below is not between a cheap join and an expensive one. It is between one separator and fifteen, both of which remove every artefact, and the question is what the second one is charging for.

Which one this collection’s index uses, and why

A shared separator and a boundary bit vector, and the choice was made by the measurement above rather than before it.

The consequence for everything downstream is that the document of a position is a rank on a bit vector, and the document of a row — which is what a listing needs — is that rank applied to the suffix array’s entry. Neither is free and neither touches the alphabet.

The one thing given up is the construction precondition, and this collection’s suffix array does not need it: it sorts by ranks and breaks ties by position, so two identical documents produce two suffixes that compare by index and the order is total.

That is worth recording as a design note rather than as a result. A convention adopted from the literature — distinct sentinels — was measured, found to cost a bit per character on the corpora this field cares about, and traced to a requirement one construction has and this one does not. A precondition is a property of an algorithm and it travels with the algorithm, and a convention that outlives the algorithm it was for is a cost nobody is choosing.

Where a window of 8 characters belongs to two documents6 documents of 96 characters, drawn one to a row as they sit in the concatenation. The shaded band at the end of each row is the 7 positions where a window of 8 characters starts inside one document and finishes inside the next. There are 35 such windows in this collection and every one of them is a string the index will find and no document contains. The number grows with the number of documents and with the pattern length, and not at all with how long the documents are.012345each row is one document · the shaded band is where a window spills into the nextEnglish-like · 6 documents35 spanning windows
Fig. 5 What all of this is protecting: the bands at the ends of the documents where a window spills into the next, and which one character makes unmatchable.

The measurement that would settle it on a real corpus

Everything above is on generated documents at sizes a plate can draw, and it is worth saying what a reader with a real corpus should measure rather than extrapolate.

The alphabet, exactly. Not “it is text” but the number of distinct bytes or symbols, because the whole cost is a ceiling of a logarithm of it and the ceiling is where the money is. A corpus over 200 distinct bytes is one symbol away from needing an eighth bit; a corpus over 128 is at the boundary.

The document count, against the alphabet. σ+d\sigma + d against the next power of two above σ\sigma is the whole calculation, and it takes no code.

And whether the construction in hand needs distinct sentinels at all. Most do not, and the convention travels further than the requirement.

Those three numbers decide the question before anything is built, which is unusual — most of the trades in this collection need a measurement of the data’s behaviour rather than of its shape. This one is arithmetic on two integers, and it is worth doing because the answer is either “free” or “twenty per cent of the whole collection” with nothing in between.

Two ways of listing documentsOne pattern in a collection of 8 documents, with the pattern planted more and more often inside the documents that hold it — so the answer stays at 7 documents throughout and only the occurrence count moves. Reading every row costs one visit per occurrence and runs 9 to 102. The chain answers 10 range-minimum queries however many occurrences there are, and those queries cost 149 to 213 node visits — a logarithm of the range, not the range. They do not cross inside this sweep. Both axes are logarithmic.1010010100occurrences of the patternvisitsone visit an occurrencethe chainthe answer8 documents · m = 610 queries at every point
Fig. 6 What the separators are protecting, one ladder on: the query a collection has that a text does not, and the two ways of answering it.

What is being claimed

A shared separator solves the artefact problem the occurrences a join invents measures, and costs one alphabet symbol, which is free until the alphabet crosses a power of two — free on nineteen-symbol text and a fifty per cent premium on four-symbol text, where four is itself a power of two.

Distinct separators cost log2(σ+d)\lceil\log_2(\sigma+d)\rceil bits a character rather than log2σ\lceil\log_2\sigma\rceil: 1.2 times the shared-separator collection at fifteen documents over nineteen symbols, and 1.67 times over four — where the shared separator has itself already cost fifty per cent.

The cost is a step function, invisible at eight documents and a fifth of the collection at fifteen, so a measurement taken between two powers of two measures nothing.

The corpora that most need document boundaries are the ones where distinct separators cost most, because they have small alphabets and many documents.

And the third design beats both. One shared separator plus a sparse bit vector over the boundaries is a few hundred bits, answers the document question in one rank, and does not touch the alphabet at all — which is what this collection’s index uses, and what the next two essays are built on.

Answering "which document" costs 2.42x answering "where"Every part of the structure over 16 documents totalling 8,207 characters. The suffix array and the text answer where the pattern occurs; the three below them are what the collection's own question costs — 377,522 bits against 155,933, 71% of the whole. Most of it is the range minimum, which is a segment tree here: 2n values of ⌈log₂ n⌉ bits. The published structure for it is 2n + o(n) bits with constant-time queries, which is about a hundredth of this, and it is named rather than built.suffix array114,898 bitsthe text41,035 bitsdocument array32,828 bitsprevious-occurrence chain114,898 bitsrange minimum229,796 bits16 documents · 8,207 charactersthe lower three are the collection's own questionEnglish-like · one separator71% is the listing
Fig. 7 Where the bits actually go once a collection has a question of its own. The separator is not on this plate, because at these sizes it is not the cost.

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.

AlphabetAlphabet sizeDocument arrayIndex sizeMeasurementRun-lengthSelf-indexSeparatorSpace overheadSuffix arrayTrade offWavelet tree