You're viewing the readable version of this site. The interactive extras (search, diagrams, read-aloud) need JavaScript and a current browser. Enable JavaScript; if it is already enabled, update your browser.

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.

search, information-retrieval, binary-format, lazy-decoding, web-performance, validation, algorithms, learn

“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.”

RFC 9112, HTTP/1.1

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:

Tfirst=Tfetch+Tdecompress+Tvalidate+Tconstruct+Tquery. T_{first}=T_{fetch}+T_{decompress}+T_{validate}+T_{construct}+T_{query}.

For later queries,

Twarm=Tquery+Tdeferred+Tbookkeeping. T_{warm}=T_{query}+T_{deferred}+T_{bookkeeping}.

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.

Prediction — identify the boundary the user actually waits on.

Eager and lazy are placements in time

Let

  • CC be eager construction cost after validation;
  • MM be lazy descriptor and bookkeeping setup;
  • DD be the average extra decode and bookkeeping cost per query under laziness;
  • qq be queries in one index lifetime.

Ignoring shared costs, eager pays CC and lazy pays M+qDM+qD. Laziness wins when

M+qD<C. M+qD<C.

If D>0D>0, the crossover is

q<CMD. q<\frac{C-M}{D}.

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 DD 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:

  1. preserve bytes already received;
  2. request only the unfilled suffix;
  3. accept every positive short read;
  4. stop on the exact required length;
  5. distinguish clean end before a record from truncation inside one;
  6. bound zero progress, cancellation, and deadline;
  7. 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:

0offsetoffset+lengthactualLength 0\le offset\le offset+length\le actualLength

with checked addition, plus required alignment, non-overlap where the grammar requires it, maximum counts, and checked products such as

payloadBytes=count×width/8. payloadBytes=\left\lceil count\times width/8\right\rceil.

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.

Reveal — decide what laziness may postpone.

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:

  1. a manifest says digest hh is present;
  2. the content addressed by hh 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

Transfer — solve the eager–lazy crossover.
  1. Extend the crossover equation when a decoded lane is cached and each query independently reuses an already decoded lane with probability rr.
  2. Design an exact-read state machine that distinguishes EOF before a header, EOF inside a header, cancellation, timeout, and repeated zero progress.
  3. For a lane with count cc and width ww, write the checked arithmetic needed before computing cw/8\lceil cw/8\rceil.
  4. Create an artifact whose header extents fit but whose final varint is truncated. Explain how an allocation-free walk catches it.
  5. Name three consumer operations that could accidentally force a lazy lane and the counter that would expose each one.
  6. Draw a memory ledger for a worker plus WebAssembly decoder while old and new index generations overlap.

References

  1. Roy T. Fielding, Mark Nottingham, and Julian Reschke. “RFC 9112: HTTP/1.1.” 2022.
  2. WHATWG. “Streams Standard.” Living Standard, sections on chunks, byte streams, cancellation, and BYOB readers.
  3. W3C Web Performance Working Group. “Resource Timing.” Candidate Recommendation Draft, definitions of transfer, encoded-body, and decoded-body sizes.
  4. WebAssembly Community Group. “WebAssembly JavaScript Interface.” Living specification, memory buffers and growth.