The index that replaces the text

The shape arrived early and the bytes late

A document array coded by its documents' lengths was predicted to survive a growing collection: fitted once at a quarter of a project's files, it would lose under a tenth of a bit a symbol by the end, because the spread of the sizes is set early. The spread was set early — 0.88 at 56 files, 0.90 at 222. The fitted code loses 0.50 to 2.08 bits all the same, because the files added after the fit hold 43% of the final bytes and every one of their symbols is named through an escape. Rebuilding only the escape's subtree brings it to 0.18.

The collections the saving was quoted for measured a document index’s document array on real collections. The array holds one entry per character, naming the document the character came from, and coded by document frequency it costs the entropy of the collection’s length distribution, weighted by characters. On one project’s 222 source files that came to a tenth smaller than the plain array of ⌈log⁡2d⌉\lceil \log_2 d \rceil bits a symbol. The files’ sizes were close to log-normal, with the logarithms spreading by a standard deviation of 0.90.

Every collection there was measured whole, and the essay’s closing section noted that real collections are not. The same directory had grown from twenty files to 222, documents added and documents growing. A document array’s code is fitted to the lengths it is built from. Rebuilt at every change it stays at the entropy. Kept, it must give a code word to every document it never saw, through an escape, and its old code words grow stale as documents change size. The section proposed replaying the directory’s history. It predicted that a code fitted once, when the directory held a quarter of its files, would lose under a tenth of a bit a symbol at the end against a code refitted at every version, because the log-normal shape of the sizes is set early and stays. It said the prediction could fail if the large modules arrived late.

The shape was set early, as predicted, and the prediction fails anyway. The large modules did not arrive late. What arrived late was most of the characters.

The replay, and two escapes

The history has 47 versions, every one at which a source file was added, removed or resized, from the first with 20 files to the last with 222. The last version is exactly the collection the earlier essay measured. At each version the cost of the document array is computed from the file sizes alone, by the identity the array is the length distribution checked on built indexes: a code that gives document ii a code word of ℓi\ell_i bits costs ∑iLiℓi/∑iLi\sum_i L_i \ell_i / \sum_i L_i bits a symbol.

Four codes are kept. Refitted at every version is a Huffman code built afresh from each version’s sizes, the best a code of this kind can do there. Fitted once is a Huffman code built at a chosen version over the files present then, plus one more symbol, an escape. A file added later is named by the escape’s code word followed by a balanced code over all files added since the fit, a subtree that needs no weights and grows by one leaf a file. A file present at the fit keeps its code word whatever its size becomes. The escape’s own weight has to be chosen at the fit, before anyone knows how much will be added. One version weights it as a single typical document, the collection’s mean size; the other weights it as the whole collection, which gives it a one-bit code word and budgets for the collection doubling. Subtree rebuilt is the second of these with the escape’s subtree rebuilt at each version as a Huffman code over the added files’ own sizes. That can be done without touching any entry the fit coded.

A tenth of a bit, missed by five to twenty times

Replayed from 20 files to 222: a code refitted at every version ends at 7.16 bits a symbol against the plain 8; a code fitted once at 56 files ends at 9.24 with an escape weighted as one document and 7.67 with one weighted as the whole collection, and 7.34 when the escape's subtree is rebuilt from the added files' lengths — losses of 2.08, 0.50 and 0.18 bits, against a predicted tenthBits a symbol of the document array at each of 47 versions of one project's source files, against the number of files (logarithmic axis), for the plain array, a Huffman code refitted at every version, and a code fitted once at the version holding 56 files with each of two escapes and with its escape's subtree rebuilt. Plain, ⌈log₂ d⌉ bits: 20 files 5.000, 56 files 6.000, 94 files 7.000, 120 files 7.000, 135 files 8.000, 222 files 8.000. Fitted at 56 files, a one-document escape: 56 files 5.487, 94 files 7.356, 120 files 7.983, 135 files 8.023, 222 files 9.239. Fitted at 56 files, a whole-collection escape: 56 files 6.465, 94 files 6.597, 120 files 6.648, 135 files 7.100, 222 files 7.668. The same, its escape subtree rebuilt: 56 files 6.465, 94 files 6.353, 120 files 6.611, 135 files 6.845, 222 files 7.343. Refitted at every version: 20 files 4.171, 56 files 5.465, 94 files 6.221, 120 files 6.573, 135 files 6.557, 222 files 7.164.468files in the directory (logarithmic)bits a symbol of the document array2050100222the fitplain, ⌈log₂ d⌉ bitsfitted at 56 files, aone-document escapefitted at 56 files, awhole-collection escapethe same, its escape subtreerebuiltrefitted at every version47 versions, one project's source filesdashed vertical: the version of the fit
Fig. 1 Bits a symbol across the 47 versions. Refitted at every version: 4.17 at 20 files, 5.47 at 56, 7.16 at 222, against the plain array’s 5, 6 and 8. Fitted once at 56 files: 9.24 at 222 with a one-document escape, 7.67 with a whole-collection escape, 7.34 with the escape’s subtree rebuilt.

Fitted at 56 files, a quarter of the final 222, the fitted code costs 9.24 bits a symbol at the end with an escape weighted as one document and 7.67 with an escape weighted as the whole collection, against 7.16 for a code refitted there: losses of 2.08 and 0.50 bits, where the prediction was under 0.1. The first is worse than the plain array’s eight bits by more than a bit. The second is worse than the refitted code by half a bit, which is more than everything the frequency-shaped array saves on the plain one at the end, 0.84 bits.

The plate shows how the losses build. Just after the fit, every design but the refitted one starts above it: the escape has been given probability mass that nothing yet uses. A one-document escape costs little then, since one document among 56 takes about six bits of code space, but every file added later pays those six bits plus eight bits of balanced tail, fourteen bits a symbol for a file whose refitted code word would be seven or eight. As files arrive the cost climbs past the plain array’s, and is above it from 94 files on. A whole-collection escape takes one bit from every file present at the fit, so the old files start a bit worse off, and the added files pay one bit of escape and the balanced tail, eight or nine bits. It ends half a bit worse.

The shape was right

The shape is set early and the bytes arrive late: the spread of the log sizes is 0.88 at 56 files and 0.90 at 222, but the files present at 56 hold 57% of the final bytes, and a code fitted then has to name the other 43% through its escapeFor each version of one project's source files, against the number of files (logarithmic): the standard deviation of the natural logarithms of the file sizes, the share of the final version's bytes held by files already present, and the share of the final files already present. Spread of log sizes, σ: 20 files 0.63, 56 files 0.88, 94 files 0.87, 120 files 0.82, 135 files 0.99, 222 files 0.90. Share of the final bytes in files present: 20 files 0.19, 56 files 0.57, 94 files 0.71, 120 files 0.79, 135 files 0.84, 222 files 1.00. Share of the final files present: 20 files 0.09, 56 files 0.25, 94 files 0.42, 120 files 0.54, 135 files 0.61, 222 files 1.00.00.2500.5000.7501files in the directory (logarithmic)σ, or a share2050100222spread of log sizes, σshare of the final bytes infiles presentshare of the final files present47 versionsdashed: the quarter-way fit
Fig. 2 For each version: the spread of the logarithms of the file sizes, 0.63 at 20 files, 0.88 at 56, 0.87 at 94, 0.82 at 120, 0.99 at 135 and 0.90 at 222; the share of the final bytes held by files already present, 0.19, 0.57, 0.71, 0.79, 0.84, 1.00; and the share of the final files present, 0.09, 0.25, 0.42, 0.54, 0.61, 1.00.

The spread of the logarithms of the sizes is 0.88 at 56 files and 0.90 at 222: the shape the prediction relied on was set by the time the directory held a quarter of its files. The files present at 56 also kept their relative sizes. They grew, by 1.77 times in total over the rest of the history, but the logarithms of their sizes at 56 files and at 222 correlate at 0.81, and none of them was removed. A corpus that was not generated measured a real collection beside the model of it, because a generated one has whatever shape its generator chose. This real one’s shape is about as stable as a shape can be.

What the prediction did not separate is the shape from the identities. A code by document frequency is a code word per document, and the shape of the length distribution decides how long those code words are on average. It does not decide which documents get them. The files present at 56 hold 57% of the final bytes; the other 43% are in 166 files the fit never saw. Those characters have to be named somehow, and whatever the escape costs, they pay it.

The large modules arriving late would have been one way to fail, and it is not what happened. The five largest files at the end were all present at the fit, and the files added after it are smaller than the ones that were there, a median of 15.7 kilobytes at the end against 57.6. They are simply many. The prediction’s own failure condition was the right kind of thing, a statement about where the characters would be at the end, measured too narrowly.

Where the bits go

Where a quarter-way fit loses its bits at the last version: the 166 files added after it hold 43% of the final bytes, and with a one-document escape each of their symbols pays 6 bits of escape and 8 of a balanced tail, 2.36 bits a symbol over the refit, while the files the fit coded gain 0.29; a whole-collection escape costs those files 0.27 and the added ones 0.23For a code fitted at 56 files and read at the last version, the bits a symbol it loses against the refitted code, divided between the files present at the fit (57% of the final bytes) and the 166 added after it. One-document escape (escape 6 bits): files present at the fit −0.287, files added after +2.362. Whole-collection escape (escape 1 bit): files present at the fit +0.274, files added after +0.230. Whole-collection escape, subtree rebuilt (escape 1 bit): files present at the fit +0.274, files added after −0.095.files present at the fitfiles added after itone-document escape−0.29+2.36whole-collection escape+0.27+0.23whole-collection escape, subtree rebuilt+0.27−0.09fitted at 56 files, read at 222bits a symbol over the refitted code
Fig. 3 The loss of a code fitted at 56 files, read at 222, split between the files present at the fit (57% of the bytes) and the 166 added after. One-document escape: the old files −0.29 bits a symbol, the added files +2.36. Whole-collection escape: +0.27 and +0.23. Whole-collection escape with its subtree rebuilt: +0.27 and −0.10.

With a one-document escape, the 166 added files cost 2.36 bits a symbol over the refitted code and the 56 old ones gain 0.29; with a whole-collection escape the old files lose 0.27 and the added files 0.23. The split shows what each escape is a bet on. A one-document escape bets that little will be added, keeps the old files’ code words short, and charges the added files fourteen bits a symbol. A whole-collection escape bets on the collection doubling, charges every old file a bit for the privilege, and names an added file in nine.

Rebuilding the escape’s subtree changes only the second half. The added files are then named by one bit of escape and a Huffman code over their own sizes, about as cheaply as a refit would name them, and their part of the loss becomes slightly negative, −0.10. The old files’ part, the bit the escape took from each of them, stays. The loss falls to 0.18 bits a symbol, and what is left of it is the escape’s tax on the files that were there first. A prefix code has a fixed budget of room, and reserving any of it for an unknown future makes the present code words longer; the reservation is paid by the documents that never use it. A code word is at least one bit met the same budget from the other end, a symbol that cannot be given less than one bit of it however rare it is.

The escape’s weight is a forecast

The escape’s weight is the one number the fit has to guess, and the replay can say what the right guess was. Fitted at 56 files with the escape’s subtree rebuilt, the loss at the end is 1.75 bits a symbol with the escape weighted as one document, a six-bit code word. Weighted as an eighth of the collection, it gets three bits and the loss is 0.57. A quarter or a half of the collection gets it two bits and a loss of 0.29, and three quarters or more one bit and 0.18. The bytes that arrived after the fit came to three quarters of the bytes present at it, so the whole-collection escape was, by luck, the right forecast. A Huffman code gives the escape a whole number of bits, and one is the fewest. 0.18 bits a symbol is the floor of this design for a fit made at a quarter of the files, and it is reached only by a forecast that happened to be right.

The forecast has a second cost the bits a symbol do not show. A wavelet tree shaped by the Huffman code puts each document’s symbols at the depth of its code word, and the tree answers the question lists the documents in a range by descending through it. An added file’s symbols sit below the escape node, one level for the escape and as many more as its subtree needs, so every query that reaches them reads that much further down. A one-document escape puts 43% of the final characters six levels deeper than their refitted code words would.

How late a fit has to be

How late a fit has to be to lose under a tenth of a bit: with an escape weighted as one document the loss at the last version is 2.08 bits for a fit at a quarter of the files and stays above a tenth until 91% of them are present; with an escape weighted as the whole collection 0.50 at a quarter, and never under a tenth, since that escape costs every older file a bit; with the escape's subtree rebuilt, 0.18The bits a symbol lost at the last version, against a Huffman code refitted there, by the version the fit was made at, plotted at the share of the final 222 files present then. A one-document escape: 9% 3.394, 15% 2.978, 21% 2.438, 25% 2.075, 42% 1.372, 54% 0.995, 61% 0.605, 91% 0.080. A whole-collection escape: 9% 1.091, 15% 0.766, 21% 0.506, 25% 0.504, 42% 0.366, 54% 0.549, 61% 0.456, 91% 0.821. Whole-collection, subtree rebuilt: 9% 0.384, 15% 0.238, 21% 0.154, 25% 0.179, 42% 0.337, 54% 0.468, 61% 0.360, 91% 0.797.01234share of the final files present at the fitbits a symbol lost at the last version0%25%50%75%100%the predicted tenth of a bita one-document escapea whole-collection escapewhole-collection, subtreerebuiltloss at 222 filesdashed: a tenth of a bit
Fig. 4 The loss at the last version against the share of the final files present at the fit. One-document escape: 3.39 bits at 9%, 2.08 at 25%, 0.99 at 54%, 0.08 at 91%. Whole-collection escape: 1.09, 0.50, 0.55, 0.82. Subtree rebuilt: 0.38, 0.18, 0.47, 0.80.

With a one-document escape, the loss at the end falls below a tenth of a bit only for a code fitted once the directory held 91% of its files. With a whole-collection escape it never does. The whole-collection escape is best for an early fit and worst for a late one: fitted at 202 files, it charges every file a bit to reserve room for a doubling that does not come, and loses 0.82. The one-document escape is the reverse. Neither weight is right for a fit made at an unknown point in a collection’s growth, because the right weight is the share of the final characters that will arrive after the fit, and at the fit that share is not known.

The subtree-rebuilt design is best at every early fit, 0.15 to 0.38 bits up to half the files, and it inherits the whole-collection escape’s tax on late fits. Its curve is not monotone, and neither is any other here. The history is one directory’s, with growth in spurts: from 135 files to 202 in three versions over four days near the end, nearly a third of the final count.

What keeping a fitted code costs

What keeping the array's code costs over the whole history: refitting at every version loses 0.026 bits a symbol on average and rewrites 21.04 times the final array; refitting when the files double rewrites 1.60 times it and loses 0.55 to 0.79; rebuilding the escape's subtree in between, 0.39 for 5.65 times; one fit at the start, 0.92For each way of keeping the document array's code across the 47 versions: the bits a symbol lost against a code refitted at that version, averaged over every version, and the characters whose entries were rewritten by refits, as a multiple of the final array's 6,912,669. Refitted at every version: mean loss 0.026, 47 refits, 21.04 times the final array rewritten. Refitted at doublings, one-document escape: mean loss 0.787, 4 refits, 1.60 times the final array rewritten. Refitted at doublings, whole-collection escape: mean loss 0.554, 4 refits, 1.60 times the final array rewritten. Doublings, escape subtree rebuilt: mean loss 0.389, 4 refits, 5.65 times the final array rewritten. Fitted once at 20 files: mean loss 0.917, 1 refit, 0.05 times the final array rewritten.mean bits a symbol lostentries rewritten (logarithmic)refitted at every version0.0321.04×refitted at doublings, one-document escape0.791.60×refitted at doublings, whole-collection escape0.551.60×doublings, escape subtree rebuilt0.395.65×fitted once at 20 files0.920.05×47 versions, 20 to 222 filesrewritten: as a multiple of the final array
Fig. 5 Each way of keeping the array’s code across the history: the mean bits a symbol lost against the refitted code, and the entries rewritten by refits as a multiple of the final array. Refitted at every version: 0.026 bits, 21.0 times. Refitted when the files double: 0.79 bits with a one-document escape and 0.55 with a whole-collection one, 1.60 times. Doublings with the escape’s subtree rebuilt in between: 0.39, 5.65 times. Fitted once at 20 files: 0.92, 0.05 times.

Refitting at every version keeps the array’s code within 0.026 bits a symbol of the best on average and rewrites the array 21 times over the history; refitting only when the number of files doubles rewrites it 1.6 times and loses 0.55 to 0.79 bits on average. A refit of a document array is not a matter of reading lengths. Every entry whose code word changes has to be rewritten, and with a wavelet tree shaped by a Huffman code that is nearly every entry. The schedule is then the classic one for a structure that has to be rebuilt as it grows. Choosing a growth factor measured a dynamic array’s trade between copying and wasted space, and doubling bounded the copying at a constant factor of the final size. Here it does the same, 1.6 times the final array, and the waste is in bits a symbol instead of empty slots.

The doubling schedule’s average loss is large because it spends most of each doubling with a code fitted to half the files. Rebuilding the escape’s subtree between doublings, which rewrites only the added files’ entries, takes the average to 0.39 at 5.65 times the final array. No schedule measured gets both the refit’s 0.026 and the doubling’s 1.6. The day a filter cannot grow found the same choice for a fingerprint table, between rebuilding when its own count says it has drifted and paying in advance for growth that may not come.

What this changes about the estimate

The earlier essay’s saving, a tenth of the plain array on this directory, is the saving of a code fitted to the collection it is used on. On a collection that grows, that saving has to be paid for in rebuilds, and the rebuilds that keep it cost twenty times the array over this history. With rebuilds only at doublings, the average code is half a bit worse than the best and, for a third to a half of the history’s versions depending on the escape, worse than the plain array it was meant to beat. On this directory’s history, a frequency-shaped document array that is not rebuilt often is not smaller than a plain one.

That is a statement about this directory’s growth, which put 43% of the final characters into files that did not exist at a quarter of the way through. A saving quoted without its collection caught a saving reported on a collection that did not have it, and the saving here has the same weakness one level up. It was measured on a collection that had stopped growing at the moment of measurement, and a document index is built on one that will not.

Where the measurement stops

One directory’s history. The 47 versions are one project’s, and its growth came in spurts, from 135 files to 202 in three versions near the end. A collection that grows by a steady trickle of documents would give an early fit fewer surprises, and one that grows by bulk imports more.

Code lengths, not built indexes. Every cost is computed from the sizes by the identity the earlier essays checked; no index was built at any version. A wavelet tree’s rank directories add the same share to every code and are not counted.

Two fixed escapes. The escape’s weight is chosen once, as one document or as the whole collection. An escape weighted by how fast the files had grown so far was not built.

Bytes for characters, as in the earlier essay: the files are measured in bytes, and a few typographic marks in comments make a file’s bytes slightly more than its characters.

Still open: an escape weighted by the growth already seen

The right weight for the escape is the share of the characters that will arrive after the fit, and the history so far is the only evidence of it. A code refitted at a doubling could weight its escape by the share of the current characters that arrived since the previous refit. In this directory’s history that share is a little under half at each doubling, which is the weight the whole-collection escape guessed.

The measurement that follows refits at doublings, weights each new escape by the growth since the last refit, and rebuilds the escape’s subtree between refits, then compares the mean loss and the entries rewritten with every schedule here. The prediction is that the mean loss falls below a quarter of a bit at under three times the final array rewritten. Most of the doubling schedule’s loss is the escape’s weight being wrong in one direction or the other, and a weight read from the history should be wrong by less. It could fail on the spurts. A doubling that arrives in a few versions, as the last one nearly did here, would be weighted by the slow growth before it. Then the escape’s estimate is always one spurt behind, and no weight read from the past can do better than the fixed guess.

Named alongside this one

Essays reaching for the same objects. Nobody chose these; they are what the concept index makes visible.

The objects this essay names

Each one links to every other essay that touches it.

AmortisationDocument arrayDocument collectionEntropyHuffman codingIndex sizePredictionWavelet tree