Learn · The Art of Not Looking
budding
The Index Is a Network Object
Transfer size is the cost you notice; validation and construction before the first answer are the costs the reader feels.
“A message that uses a valid Content-Length is incomplete if the size of the message body received is less than the value given by Content-Length.”
This is the fourteenth chapter in a book about search from first principles. The previous chapters compressed postings and preserved navigation. Here you will follow that artifact across a network and a runtime boundary. You will separate transfer, decompression, validation, construction, retention, and query costs; solve the eager–lazy crossover; design a fixed-header format that rejects truncation before decoding; retain eager invariants with an allocation-free validation walk; detect accidental forcing by counting deferred lanes; and treat cache content, build placement, versioning, copies, and view generations as part of the index contract. The next chapter returns to query semantics and derives a metric for “almost the same word.”
A small suitcase can take time to unpack
Suppose two artifacts represent the same index:
| Artifact | Transfer | Parse and construct | Retained tables |
|---|---|---|---|
| compressed text | small after content coding | general parser, strings, objects | large |
| block binary | perhaps slightly larger | checked offsets, typed views, selective decode | smaller |
The comparison cannot end at response bytes. The browser exposes distinct Resource Timing quantities for bytes transferred, encoded body bytes, and decoded body bytes because they are distinct observations. The application adds more boundaries:
For later queries,
The first number is paid once per index generation in a long-lived process and perhaps once per request in a short-lived instance. The same artifact can be a good server representation and a poor ephemeral-function representation.
Eager and lazy are placements in time
Let
- be eager construction cost after validation;
- be lazy descriptor and bookkeeping setup;
- be the average extra decode and bookkeeping cost per query under laziness;
- be queries in one index lifetime.
Ignoring shared costs, eager pays and lazy pays . Laziness wins when
If , the crossover is
The inequality is a workload statement, not a rule that laziness is good or bad. Short sessions, large indexes, and low lane reuse favor deferral. Long sessions, repeated common terms, and expensive bookkeeping favor eager construction. Cache each decoded immutable lane within its generation and falls as reuse rises, at the cost of retained memory.
Cold and warm must be reported separately:
- cold — bytes to first correct answer;
- warm — steady query cost after the working set has settled.
A long-lived server amortizes cold. A frequently discarded process repeatedly pays it. Optimizing the wrong lifetime is how a benchmark can be true and a deployment unusable.
Laziness does not remove work. It changes which lifetime and which question pays for it.
A network chunk is not a record
The Streams Standard explicitly distinguishes chunks from atomic application units: one read may return part of a record, several records, or any positive chunk the producer chooses. A consumer that needs an exact header therefore implements a total progress machine:
- preserve bytes already received;
- request only the unfilled suffix;
- accept every positive short read;
- stop on the exact required length;
- distinguish clean end before a record from truncation inside one;
- bound zero progress, cancellation, and deadline;
- publish no view through an expired or reused destination.
HTTP framing detects some incomplete messages, but an application artifact needs its own structural contract. A validly delivered response may contain a truncated, stale, or wrong-version file. Transport completeness and index validity are different propositions.
Short read — a successful read that returns fewer bytes than requested. Streams commonly permit it; the requested count is a maximum for one transfer, not a promise. Learn more.
Put the whole shape at the door
A binary artifact begins with a fixed-size header:
| Header fact | Purpose |
|---|---|
| magic and format version | reject the wrong grammar |
| total byte length | bind the complete extent |
| corpus and analyzer identity | bind semantics |
| lane count | bound descriptor work |
| lane offset and length | establish each slice |
| element count and bit width | prove packed extent arithmetic |
| flags or presence-mask extent | bound optional values |
| content digest | bind bytes across transport and cache |
Before constructing any lane, validate:
with checked addition, plus required alignment, non-overlap where the grammar requires it, maximum counts, and checked products such as
Every descriptor must fit inside the fixed header extent, and the final lane must end exactly where the format says the artifact ends. A short buffer is rejected before any partially populated index exists.
This is stronger than catching an exception midway. The public result type is either a validated whole artifact or a typed diagnostic; “index with the first seven lanes” is unrepresentable.
Keep the proof, defer the construction
An eager decoder often validates structure incidentally by walking every lane while constructing objects. A naive lazy decoder reads only the header and defers both construction and those incidental checks. Corruption then appears later, under whichever query touches the bad lane.
The repair is an allocation-free validation walk:
- step over fixed-width packed runs by checked arithmetic;
- scan presence masks without constructing values;
- walk variable-length integers only to establish termination and extent;
- confirm counts, monotonic deltas, and optional-field correspondence;
- compute or verify the digest over the canonical bytes;
- retain descriptors, not decoded semantic tables.
Admit a deferred index
ADMIT-DEFERRED-INDEX(BYTES, LIMITS)
Input: complete candidate BYTES, finite structural LIMITS
Output: validated descriptors or a typed diagnostic
header ← DECODE-FIXED-HEADER(BYTES, LIMITS)
if header is invalid
return diagnostic
for each descriptor d in header.descriptors
extent ← CHECKED-EXTENT(d, length(BYTES), LIMITS)
if extent is invalid
return diagnostic
if VALIDATION-WALK(BYTES, extent, d) fails
return diagnostic
if DIGEST(BYTES) differs from header.digest
return diagnostic
return VALIDATED-DESCRIPTORS(header, identity(BYTES))The validation walk preserves the eager proof while dropping eager allocation and construction. It costs time, so measure it separately. The relevant comparison is not “header-only lazy” versus eager; it is safe deferred validation versus safe eager construction.
Corrupt every boundary, fragment every read
The strongest format test is differential:
- a simple eager reference decoder;
- the deferred admission walk;
- the on-demand lane decoder.
For every generated valid artifact, all three agree. Then truncate at every byte boundary and corrupt each header, mask, width, count, varint terminator, and payload class. Deferred admission must be no more permissive than the eager reference. Feed the stream through an adapter that returns every admitted positive chunk size, including one byte, and inject clean EOF, truncated EOF, zero progress, cancellation, and deadline expiry.
This test catches a missing proof. A handful of malformed fixtures catches only the examples their author imagined.
One read can force the entire “lazy” index
Deferral is a property of the whole consumer graph, not the decoder module. Suppose a query compiler tests membership like this:
read postings[term]
if the value is absent, reject the term
If reading the property triggers decoding, walking the vocabulary during setup forces every lane. The new decoder becomes twice as fast, query compilation becomes slower, and total startup barely moves. A timer alone permits many stories.
Count state transitions instead:
| Moment | Deferred lanes expected |
|---|---|
| after admission | all lanes |
| after query compilation | all lanes |
| after one rare-term query | all except the touched lane |
| after repeated same-term query | unchanged |
Membership must consult descriptors or a dedicated presence structure, not read the value. Adding laziness requires auditing every consumer, because the work moves at read sites rather than at the writer that changed.
Cache truth lives in the bytes
A cache can store two facts that drift apart:
- a manifest says digest is present;
- the content addressed by has been reclaimed, truncated, or replaced.
Consulting only the manifest returns a “hit” and sends corrupt or absent bytes to the decoder. The failure is blamed on the producer even though the cache invented the disagreement.
Validity is a property of the content read. A cache hit touches the object and verifies its content digest, or uses a storage design in which lookup cannot succeed independently of the bytes. A missing artifact triggers a clean fallback; a partly present artifact must not masquerade as one.
The digest binds identity, not authorization or freshness by itself. The reader still verifies that the artifact generation, corpus identity, analyzer contract, and access label are the ones requested.
Construction is a placement decision
An expensive index can be built:
- in the reader before first use;
- once in a long-lived service process;
- ahead of time during publishing and shipped as an immutable artifact.
Moving construction to publishing removes it from reader latency rather than merely shifting it from cold to warm. This can make minimal automata, packed carriers, or large expansion tables eligible when runtime construction would disqualify them.
The move creates obligations. The artifact crosses a version boundary, so it carries the analyzer and collation contract that produced it. It also binds the source corpus generation. A perfectly stable artifact built from one corpus generation is deterministically wrong for a later one.
Set placement before comparing data structures. Otherwise a benchmark titled “algorithm cost” may actually be ranking deployment topologies.
“Zero-copy” needs a ledger
No format is zero-copy by itself. State the operation, boundaries, and retained ownership. Report at least:
| Memory row | What it counts |
|---|---|
| artifact bytes | encoded immutable backing |
| decoded tables | constructed semantic structures |
| owner scratch | reusable bounded decoder workspace |
| query temporary | per-query allocations and decoded blocks |
| retained results | identities, scores, snippets, explanations |
| generation overlap | concurrent old and new artifacts during replacement |
Also count bytes copied or transcoded at network assembly, decompression, UTF-8/UTF-16, worker messaging, and WebAssembly boundaries. Borrowing a posting lane while transcoding the query and retaining result objects is useful; it is not a zero-copy pipeline.
Every borrowed view records backing identity, offset, length, and generation. WebAssembly memory growth can detach or supersede JavaScript buffers, so a view is reacquired after possible growth. Artifact replacement similarly retires old-generation views only after their owning queries settle.
Measure distributions at the boundary
For cold and warm strata, record p50, p75, p90, and p99 of:
- fetch and connection time;
- transfer, encoded-body, and decoded-body bytes;
- content decompression;
- fixed-header admission and validation walk;
- eager construction or lazy descriptor setup;
- deferred lanes touched per query;
- first-answer and steady-query latency;
- retained and peak-overlap memory.
Network latency is multimodal, and cache hits form a different population from cold fetches. An average across them describes no reader. Use Resource Timing for delivery facts and application spans for validation and construction.
Prefetch a search index only when it is likely to be needed next and idle bandwidth is appropriate; preload asserts it is critical to the current page. Either hint changes fetch timing, not format validity or construction cost.
Lessons
- First answer adds fetch, decompression, validation, construction, and query time; transferred bytes are only one term.
- Lazy decoding wins below a computable session-length crossover and moves work from cold to warm.
- Stream reads return chunks, not application records. Exact reads preserve progress and distinguish truncation from clean EOF.
- A fixed header binds every lane extent before decoding begins.
- Defer construction, not the allocation-free proof that the artifact is whole.
- Corrupt every byte class and require deferred admission to be no more permissive than the eager oracle.
- One membership read can force an entire lazy index; count deferred lanes at consumer boundaries.
- Cache presence metadata cannot prove content validity; verify the bytes.
- Build placement changes which algorithms are eligible and creates version and freshness obligations.
- Views carry backing identity, extent, and generation; copying and transcoding remain visible in a memory ledger.
Practice
- Extend the crossover equation when a decoded lane is cached and each query independently reuses an already decoded lane with probability .
- Design an exact-read state machine that distinguishes EOF before a header, EOF inside a header, cancellation, timeout, and repeated zero progress.
- For a lane with count and width , write the checked arithmetic needed before computing .
- Create an artifact whose header extents fit but whose final varint is truncated. Explain how an allocation-free walk catches it.
- Name three consumer operations that could accidentally force a lazy lane and the counter that would expose each one.
- Draw a memory ledger for a worker plus WebAssembly decoder while old and new index generations overlap.
References
- Roy T. Fielding, Mark Nottingham, and Julian Reschke. “RFC 9112: HTTP/1.1.” 2022.
- WHATWG. “Streams Standard.” Living Standard, sections on chunks, byte streams, cancellation, and BYOB readers.
- W3C Web Performance Working Group. “Resource Timing.” Candidate Recommendation Draft, definitions of transfer, encoded-body, and decoded-body sizes.
- WebAssembly Community Group. “WebAssembly JavaScript Interface.” Living specification, memory buffers and growth.