# Sparse V3: how Pinecone's sparse index learned to skip

> Pinecone's sparse index V3 groups posting data by term instead of document range, cutting disk I/O up to 1,428x and latency up to 119x, with no loss in recall.

Rustam Nassyrov, Noah Rizika, Lea Wang-Tomic · 2026-07-09

**TLDR: **Pinecone's V2 sparse index organized posting data in document-major blocks, which forced every query to load the entire index regardless of which terms it contained. V3 reorganizes the index around terms: each term owns its own sequence of posting blocks, and a query only loads the blocks for terms it actually references. From disk, this reduces I/O by 151× for SPLADE queries and 1,428× for BM25 queries, with no loss in recall, and measurably better recall for BM25.

---

## **Sparse indexes at Pinecone**

In March 2025, [Pinecone launched sparse-only indexes](https://www.pinecone.io/learn/sparse-retrieval/), bringing keyword and lexical search into the same serverless platform as dense vector retrieval, supporting both BM25 and learned sparse models including SPLADE and Pinecone's own pinecone-sparse-english-v0.

Sparse retrieval is built on inverted indexes, a data structure at the heart of search engines for decades. Each term gets mapped to a posting list: a record of every document containing it and a relevance score for that document. Traditional scoring functions like BM25 weight terms by frequency within a document and rarity across the corpus. Learned sparse models like SPLADE go further, using a transformer to assign context-aware weights and expand a document's representation to include related terms. A document about machine learning might score non-zero on "training" and "inference" even without those words appearing verbatim. Pinecone's implementation runs this on the MaxScore algorithm, which makes retrieval fast at scale by skipping documents that can't beat the current top-k threshold rather than scoring every candidate.

Supporting sparse vectors alongside dense, operating a separate Elasticsearch or Solr cluster is no longer needed, and the search pipeline gets further consolidated..Sparse and dense indexes are queryable together in a single index (sparse for keyword matching, dense for semantic breadth) with Pinecone managing both.

## **The scale problem**

The architecture that powered V1 and V2 organized posting data in what's called a **document-major** layout. Documents are divided into groups by ordinal position (1–1000, 1001–2000, and so on). Each group gets a block on disk containing the interleaved posting data of all terms in any of the group’s documents.

```json
{
  "_key": "bf0c0918a0a4",
  "_type": "htmlEmbed",
  "code": {
    "_type": "code",
    "code": "<!DOCTYPE html>\n<html lang=\"en\">\n<head>\n  <meta charset=\"UTF-8\">\n  <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\">\n  <title>V2 Full-Index Scan</title>\n  <link href=\"https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;500&display=swap\" rel=\"stylesheet\">\n  <style>\n    *, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }\n\n    /* ── Design tokens ── */\n    :root {\n      --bg:          #ffffff;\n      --dot:         #cac8c4;\n      --dot-opacity: 0.11;\n      --text:        #3d3a36;\n      --muted:       rgba(61,58,54,0.82);\n      --muted2:      rgba(61,58,54,0.7);\n      --muted3:      rgba(61,58,54,0.55);\n      --border:      #e2e0dc;\n      --node-fill:   #ffffff;\n      --blue:        #0019FF;\n      --blue-faint:  rgba(0,25,255,0.025);\n      --blue-border: rgba(0,25,255,0.22);\n      --conn-gray:   #d0cdc8;\n      --amber:       #f59e0b;\n    }\n\n    html.dark {\n      --bg:          #0f0e0b;\n      --dot:         rgba(255,255,255,1);\n      --dot-opacity: 0.03;\n      --text:        #dedad5;\n      --muted:       rgba(222,218,213,0.82);\n      --muted2:      rgba(222,218,213,0.68);\n      --muted3:      rgba(222,218,213,0.5);\n      --border:      rgba(255,255,255,0.09);\n      --node-fill:   #1e1c18;\n      --blue:        #5573ff;\n      --blue-faint:  rgba(85,115,255,0.06);\n      --blue-border: rgba(85,115,255,0.28);\n      --conn-gray:   rgba(255,255,255,0.2);\n      --amber:       #fbbf24;\n    }\n\n    html { height: 620px; }\n\n    body {\n      background: var(--bg);\n      height: 620px;\n      overflow: hidden;\n      transition: background 0.25s;\n    }\n\n    body::before {\n      content: '';\n      position: fixed; inset: 0;\n      background-image: radial-gradient(circle, var(--dot) 1px, transparent 1px);\n      background-size: 5.5px 5.5px;\n      opacity: var(--dot-opacity);\n      pointer-events: none;\n    }\n\n    .scan-page {\n      position: relative;\n      z-index: 1;\n      height: 100%;\n      display: flex;\n      align-items: center;\n      justify-content: center;\n    }\n\n    .outer {\n      position: relative;\n      z-index: 1;\n      width: 100%;\n      height: 100%;\n      display: flex;\n      align-items: center;\n      justify-content: center;\n      padding: 16px;\n      box-sizing: border-box;\n    }\n\n    svg {\n      height: 100%;\n      width: auto;\n      max-width: 100%;\n      display: block;\n    }\n\n    svg text {\n      paint-order: stroke fill;\n      stroke: var(--bg);\n      stroke-width: 2px;\n      stroke-linejoin: round;\n    }\n\n    .t-primary  { fill: var(--text) !important; }\n    .t-muted    { fill: var(--muted) !important; }\n    .t-muted2   { fill: var(--muted2) !important; }\n    .t-muted3   { fill: var(--muted3) !important; }\n    .t-amber    { fill: var(--amber) !important; }\n\n    html.dark .nd-box     { fill: var(--node-fill) !important; stroke: var(--border) !important; }\n    html.dark .t-primary  { fill: var(--text) !important; }\n    html.dark .t-muted    { fill: var(--muted) !important; }\n    html.dark .t-muted2   { fill: var(--muted2) !important; }\n    html.dark .t-muted3   { fill: var(--muted3) !important; }\n    html.dark .t-amber    { fill: var(--amber) !important; }\n    html.dark .ln-gray    { stroke: var(--conn-gray) !important; }\n    html.dark .bar-fill   { fill: rgba(255,255,255,0.08) !important; }\n    html.dark .scan-overlay { fill: var(--amber) !important; stroke: var(--amber) !important; }\n  </style>\n</head>\n<body>\n<div class=\"scan-page\">\n<div class=\"outer\" role=\"img\" aria-label=\"V2 doc-major index scan: all 8 index blocks are scanned sequentially to find matching documents, reading 3,396 MB of data\">\n<svg viewBox=\"0 0 520 560\"\n     xmlns=\"http://www.w3.org/2000/svg\"\n     font-family=\"'JetBrains Mono', monospace\"\n     aria-hidden=\"true\">\n\n  <defs>\n    <marker id=\"ah-gray-l\" markerWidth=\"9\" markerHeight=\"9\" refX=\"8\" refY=\"4.5\" orient=\"auto\">\n      <path d=\"M2,2 L8,4.5 L2,7\" fill=\"none\" stroke=\"#d0cdc8\" stroke-width=\"1.4\"\n        stroke-linecap=\"round\" stroke-linejoin=\"round\"/>\n    </marker>\n    <marker id=\"ah-gray-d\" markerWidth=\"9\" markerHeight=\"9\" refX=\"8\" refY=\"4.5\" orient=\"auto\">\n      <path d=\"M2,2 L8,4.5 L2,7\" fill=\"none\" stroke=\"rgba(255,255,255,0.2)\" stroke-width=\"1.4\"\n        stroke-linecap=\"round\" stroke-linejoin=\"round\"/>\n    </marker>\n  </defs>\n\n  <!-- ══ QUERY BOX ══════════════════════════════════════════════════════════ -->\n  <g id=\"nd-query\" opacity=\"0\">\n    <rect class=\"nd-box\" x=\"140\" y=\"16\" width=\"240\" height=\"44\" fill=\"white\" stroke=\"#e2e0dc\" stroke-width=\"1\" rx=\"0\"/>\n    <text class=\"t-muted3\" x=\"260\" y=\"31\" text-anchor=\"middle\" font-size=\"12\" letter-spacing=\"0.06em\" fill=\"rgba(61,58,54,0.55)\">QUERY</text>\n    <text class=\"t-primary\" x=\"260\" y=\"49\" text-anchor=\"middle\" font-size=\"13\" font-weight=\"500\" fill=\"#3d3a36\">apple · orange</text>\n  </g>\n\n  <!-- ══ ARROW query→index ═════════════════════════════════════════════════ -->\n  <line id=\"ln-arrow\" class=\"ln-gray\" x1=\"260\" y1=\"60\" x2=\"260\" y2=\"86\"\n    stroke=\"#d0cdc8\" stroke-width=\"1\" marker-end=\"url(#ah-gray-l)\" opacity=\"0\"/>\n\n  <!-- ══ INDEX LABEL ════════════════════════════════════════════════════════ -->\n  <text id=\"nd-index-label\" class=\"t-muted2\" x=\"260\" y=\"84\" text-anchor=\"middle\"\n    font-size=\"12\" letter-spacing=\"0.03em\" fill=\"rgba(61,58,54,0.7)\" opacity=\"0\">doc-major index — all terms interleaved per block</text>\n\n  <!-- ══ 8 STACKED BLOCKS ══════════════════════════════════════════════════ -->\n  <g id=\"nd-blocks\" opacity=\"0\">\n\n    <!-- Block 0: y=92 -->\n    <rect class=\"nd-box\" x=\"20\" y=\"92\" width=\"480\" height=\"52\" fill=\"white\" stroke=\"#e2e0dc\" stroke-width=\"1\" rx=\"0\"/>\n    <line stroke=\"#e2e0dc\" stroke-width=\"1\" x1=\"120\" y1=\"92\" x2=\"120\" y2=\"144\"/>\n    <text class=\"t-muted2\" x=\"70\" y=\"121\" text-anchor=\"middle\" font-size=\"12\" fill=\"rgba(61,58,54,0.7)\">docs 1K–2K</text>\n    <rect class=\"bar-fill\" x=\"128\" y=\"104\" width=\"22\" height=\"5\" fill=\"#e2e0dc\" fill-opacity=\"0.3\" rx=\"0\"/>\n    <rect class=\"bar-fill\" x=\"155\" y=\"104\" width=\"35\" height=\"5\" fill=\"#e2e0dc\" fill-opacity=\"0.3\" rx=\"0\"/>\n    <rect class=\"bar-fill\" x=\"195\" y=\"104\" width=\"18\" height=\"5\" fill=\"#e2e0dc\" fill-opacity=\"0.3\" rx=\"0\"/>\n    <rect class=\"bar-fill\" x=\"218\" y=\"104\" width=\"28\" height=\"5\" fill=\"#e2e0dc\" fill-opacity=\"0.3\" rx=\"0\"/>\n    <rect class=\"bar-fill\" x=\"251\" y=\"104\" width=\"40\" height=\"5\" fill=\"#e2e0dc\" fill-opacity=\"0.3\" rx=\"0\"/>\n    <rect class=\"bar-fill\" x=\"296\" y=\"104\" width=\"15\" height=\"5\" fill=\"#e2e0dc\" fill-opacity=\"0.3\" rx=\"0\"/>\n    <rect class=\"bar-fill\" x=\"316\" y=\"104\" width=\"32\" height=\"5\" fill=\"#e2e0dc\" fill-opacity=\"0.3\" rx=\"0\"/>\n    <rect class=\"bar-fill\" x=\"353\" y=\"104\" width=\"20\" height=\"5\" fill=\"#e2e0dc\" fill-opacity=\"0.3\" rx=\"0\"/>\n    <rect class=\"bar-fill\" x=\"378\" y=\"104\" width=\"38\" height=\"5\" fill=\"#e2e0dc\" fill-opacity=\"0.3\" rx=\"0\"/>\n    <rect class=\"bar-fill\" x=\"128\" y=\"114\" width=\"38\" height=\"5\" fill=\"#e2e0dc\" fill-opacity=\"0.28\" rx=\"0\"/>\n    <rect class=\"bar-fill\" x=\"171\" y=\"114\" width=\"15\" height=\"5\" fill=\"#e2e0dc\" fill-opacity=\"0.28\" rx=\"0\"/>\n    <rect class=\"bar-fill\" x=\"191\" y=\"114\" width=\"28\" height=\"5\" fill=\"#e2e0dc\" fill-opacity=\"0.28\" rx=\"0\"/>\n    <rect class=\"bar-fill\" x=\"224\" y=\"114\" width=\"40\" height=\"5\" fill=\"#e2e0dc\" fill-opacity=\"0.28\" rx=\"0\"/>\n    <rect class=\"bar-fill\" x=\"269\" y=\"114\" width=\"20\" height=\"5\" fill=\"#e2e0dc\" fill-opacity=\"0.28\" rx=\"0\"/>\n    <rect class=\"bar-fill\" x=\"294\" y=\"114\" width=\"30\" height=\"5\" fill=\"#e2e0dc\" fill-opacity=\"0.28\" rx=\"0\"/>\n    <rect class=\"bar-fill\" x=\"329\" y=\"114\" width=\"12\" height=\"5\" fill=\"#e2e0dc\" fill-opacity=\"0.28\" rx=\"0\"/>\n    <rect class=\"bar-fill\" x=\"346\" y=\"114\" width=\"36\" height=\"5\" fill=\"#e2e0dc\" fill-opacity=\"0.28\" rx=\"0\"/>\n    <rect class=\"bar-fill\" x=\"128\" y=\"124\" width=\"28\" height=\"5\" fill=\"#e2e0dc\" fill-opacity=\"0.25\" rx=\"0\"/>\n    <rect class=\"bar-fill\" x=\"161\" y=\"124\" width=\"40\" height=\"5\" fill=\"#e2e0dc\" fill-opacity=\"0.25\" rx=\"0\"/>\n    <rect class=\"bar-fill\" x=\"206\" y=\"124\" width=\"18\" height=\"5\" fill=\"#e2e0dc\" fill-opacity=\"0.25\" rx=\"0\"/>\n    <rect class=\"bar-fill\" x=\"229\" y=\"124\" width=\"32\" height=\"5\" fill=\"#e2e0dc\" fill-opacity=\"0.25\" rx=\"0\"/>\n    <rect class=\"bar-fill\" x=\"266\" y=\"124\" width=\"22\" height=\"5\" fill=\"#e2e0dc\" fill-opacity=\"0.25\" rx=\"0\"/>\n    <rect class=\"bar-fill\" x=\"293\" y=\"124\" width=\"35\" height=\"5\" fill=\"#e2e0dc\" fill-opacity=\"0.25\" rx=\"0\"/>\n    <rect class=\"bar-fill\" x=\"333\" y=\"124\" width=\"15\" height=\"5\" fill=\"#e2e0dc\" fill-opacity=\"0.25\" rx=\"0\"/>\n    <rect class=\"bar-fill\" x=\"353\" y=\"124\" width=\"38\" height=\"5\" fill=\"#e2e0dc\" fill-opacity=\"0.25\" rx=\"0\"/>\n    <rect class=\"bar-fill\" x=\"128\" y=\"136\" width=\"18\" height=\"5\" fill=\"#e2e0dc\" fill-opacity=\"0.3\" rx=\"0\"/>\n    <rect class=\"bar-fill\" x=\"151\" y=\"136\" width=\"35\" height=\"5\" fill=\"#e2e0dc\" fill-opacity=\"0.3\" rx=\"0\"/>\n    <rect class=\"bar-fill\" x=\"191\" y=\"136\" width=\"28\" height=\"5\" fill=\"#e2e0dc\" fill-opacity=\"0.3\" rx=\"0\"/>\n    <rect class=\"bar-fill\" x=\"224\" y=\"136\" width=\"40\" height=\"5\" fill=\"#e2e0dc\" fill-opacity=\"0.3\" rx=\"0\"/>\n    <rect class=\"bar-fill\" x=\"269\" y=\"136\" width=\"22\" height=\"5\" fill=\"#e2e0dc\" fill-opacity=\"0.3\" rx=\"0\"/>\n    <rect class=\"bar-fill\" x=\"296\" y=\"136\" width=\"30\" height=\"5\" fill=\"#e2e0dc\" fill-opacity=\"0.3\" rx=\"0\"/>\n    <rect class=\"bar-fill\" x=\"331\" y=\"136\" width=\"20\" height=\"5\" fill=\"#e2e0dc\" fill-opacity=\"0.3\" rx=\"0\"/>\n    <rect class=\"bar-fill\" x=\"356\" y=\"136\" width=\"38\" height=\"5\" fill=\"#e2e0dc\" fill-opacity=\"0.3\" rx=\"0\"/>\n\n    <!-- Block 1: y=144 -->\n    <rect class=\"nd-box\" x=\"20\" y=\"144\" width=\"480\" height=\"52\" fill=\"white\" stroke=\"#e2e0dc\" stroke-width=\"1\" rx=\"0\"/>\n    <line stroke=\"#e2e0dc\" stroke-width=\"1\" x1=\"120\" y1=\"144\" x2=\"120\" y2=\"196\"/>\n    <text class=\"t-muted2\" x=\"70\" y=\"173\" text-anchor=\"middle\" font-size=\"12\" fill=\"rgba(61,58,54,0.7)\">docs 2K–3K</text>\n    <rect class=\"bar-fill\" x=\"128\" y=\"156\" width=\"22\" height=\"5\" fill=\"#e2e0dc\" fill-opacity=\"0.3\" rx=\"0\"/>\n    <rect class=\"bar-fill\" x=\"155\" y=\"156\" width=\"35\" height=\"5\" fill=\"#e2e0dc\" fill-opacity=\"0.3\" rx=\"0\"/>\n    <rect class=\"bar-fill\" x=\"195\" y=\"156\" width=\"18\" height=\"5\" fill=\"#e2e0dc\" fill-opacity=\"0.3\" rx=\"0\"/>\n    <rect class=\"bar-fill\" x=\"218\" y=\"156\" width=\"28\" height=\"5\" fill=\"#e2e0dc\" fill-opacity=\"0.3\" rx=\"0\"/>\n    <rect class=\"bar-fill\" x=\"251\" y=\"156\" width=\"40\" height=\"5\" fill=\"#e2e0dc\" fill-opacity=\"0.3\" rx=\"0\"/>\n    <rect class=\"bar-fill\" x=\"296\" y=\"156\" width=\"15\" height=\"5\" fill=\"#e2e0dc\" fill-opacity=\"0.3\" rx=\"0\"/>\n    <rect class=\"bar-fill\" x=\"316\" y=\"156\" width=\"32\" height=\"5\" fill=\"#e2e0dc\" fill-opacity=\"0.3\" rx=\"0\"/>\n    <rect class=\"bar-fill\" x=\"353\" y=\"156\" width=\"20\" height=\"5\" fill=\"#e2e0dc\" fill-opacity=\"0.3\" rx=\"0\"/>\n    <rect class=\"bar-fill\" x=\"378\" y=\"156\" width=\"38\" height=\"5\" fill=\"#e2e0dc\" fill-opacity=\"0.3\" rx=\"0\"/>\n    <rect class=\"bar-fill\" x=\"128\" y=\"166\" width=\"38\" height=\"5\" fill=\"#e2e0dc\" fill-opacity=\"0.28\" rx=\"0\"/>\n    <rect class=\"bar-fill\" x=\"171\" y=\"166\" width=\"15\" height=\"5\" fill=\"#e2e0dc\" fill-opacity=\"0.28\" rx=\"0\"/>\n    <rect class=\"bar-fill\" x=\"191\" y=\"166\" width=\"28\" height=\"5\" fill=\"#e2e0dc\" fill-opacity=\"0.28\" rx=\"0\"/>\n    <rect class=\"bar-fill\" x=\"224\" y=\"166\" width=\"40\" height=\"5\" fill=\"#e2e0dc\" fill-opacity=\"0.28\" rx=\"0\"/>\n    <rect class=\"bar-fill\" x=\"269\" y=\"166\" width=\"20\" height=\"5\" fill=\"#e2e0dc\" fill-opacity=\"0.28\" rx=\"0\"/>\n    <rect class=\"bar-fill\" x=\"294\" y=\"166\" width=\"30\" height=\"5\" fill=\"#e2e0dc\" fill-opacity=\"0.28\" rx=\"0\"/>\n    <rect class=\"bar-fill\" x=\"329\" y=\"166\" width=\"12\" height=\"5\" fill=\"#e2e0dc\" fill-opacity=\"0.28\" rx=\"0\"/>\n    <rect class=\"bar-fill\" x=\"346\" y=\"166\" width=\"36\" height=\"5\" fill=\"#e2e0dc\" fill-opacity=\"0.28\" rx=\"0\"/>\n    <rect class=\"bar-fill\" x=\"128\" y=\"176\" width=\"28\" height=\"5\" fill=\"#e2e0dc\" fill-opacity=\"0.25\" rx=\"0\"/>\n    <rect class=\"bar-fill\" x=\"161\" y=\"176\" width=\"40\" height=\"5\" fill=\"#e2e0dc\" fill-opacity=\"0.25\" rx=\"0\"/>\n    <rect class=\"bar-fill\" x=\"206\" y=\"176\" width=\"18\" height=\"5\" fill=\"#e2e0dc\" fill-opacity=\"0.25\" rx=\"0\"/>\n    <rect class=\"bar-fill\" x=\"229\" y=\"176\" width=\"32\" height=\"5\" fill=\"#e2e0dc\" fill-opacity=\"0.25\" rx=\"0\"/>\n    <rect class=\"bar-fill\" x=\"266\" y=\"176\" width=\"22\" height=\"5\" fill=\"#e2e0dc\" fill-opacity=\"0.25\" rx=\"0\"/>\n    <rect class=\"bar-fill\" x=\"293\" y=\"176\" width=\"35\" height=\"5\" fill=\"#e2e0dc\" fill-opacity=\"0.25\" rx=\"0\"/>\n    <rect class=\"bar-fill\" x=\"333\" y=\"176\" width=\"15\" height=\"5\" fill=\"#e2e0dc\" fill-opacity=\"0.25\" rx=\"0\"/>\n    <rect class=\"bar-fill\" x=\"353\" y=\"176\" width=\"38\" height=\"5\" fill=\"#e2e0dc\" fill-opacity=\"0.25\" rx=\"0\"/>\n    <rect class=\"bar-fill\" x=\"128\" y=\"188\" width=\"18\" height=\"5\" fill=\"#e2e0dc\" fill-opacity=\"0.3\" rx=\"0\"/>\n    <rect class=\"bar-fill\" x=\"151\" y=\"188\" width=\"35\" height=\"5\" fill=\"#e2e0dc\" fill-opacity=\"0.3\" rx=\"0\"/>\n    <rect class=\"bar-fill\" x=\"191\" y=\"188\" width=\"28\" height=\"5\" fill=\"#e2e0dc\" fill-opacity=\"0.3\" rx=\"0\"/>\n    <rect class=\"bar-fill\" x=\"224\" y=\"188\" width=\"40\" height=\"5\" fill=\"#e2e0dc\" fill-opacity=\"0.3\" rx=\"0\"/>\n    <rect class=\"bar-fill\" x=\"269\" y=\"188\" width=\"22\" height=\"5\" fill=\"#e2e0dc\" fill-opacity=\"0.3\" rx=\"0\"/>\n    <rect class=\"bar-fill\" x=\"296\" y=\"188\" width=\"30\" height=\"5\" fill=\"#e2e0dc\" fill-opacity=\"0.3\" rx=\"0\"/>\n    <rect class=\"bar-fill\" x=\"331\" y=\"188\" width=\"20\" height=\"5\" fill=\"#e2e0dc\" fill-opacity=\"0.3\" rx=\"0\"/>\n    <rect class=\"bar-fill\" x=\"356\" y=\"188\" width=\"38\" height=\"5\" fill=\"#e2e0dc\" fill-opacity=\"0.3\" rx=\"0\"/>\n\n    <!-- Block 2: y=196 -->\n    <rect class=\"nd-box\" x=\"20\" y=\"196\" width=\"480\" height=\"52\" fill=\"white\" stroke=\"#e2e0dc\" stroke-width=\"1\" rx=\"0\"/>\n    <line stroke=\"#e2e0dc\" stroke-width=\"1\" x1=\"120\" y1=\"196\" x2=\"120\" y2=\"248\"/>\n    <text class=\"t-muted2\" x=\"70\" y=\"225\" text-anchor=\"middle\" font-size=\"12\" fill=\"rgba(61,58,54,0.7)\">docs 3K–4K</text>\n    <rect class=\"bar-fill\" x=\"128\" y=\"208\" width=\"22\" height=\"5\" fill=\"#e2e0dc\" fill-opacity=\"0.3\" rx=\"0\"/>\n    <rect class=\"bar-fill\" x=\"155\" y=\"208\" width=\"35\" height=\"5\" fill=\"#e2e0dc\" fill-opacity=\"0.3\" rx=\"0\"/>\n    <rect class=\"bar-fill\" x=\"195\" y=\"208\" width=\"18\" height=\"5\" fill=\"#e2e0dc\" fill-opacity=\"0.3\" rx=\"0\"/>\n    <rect class=\"bar-fill\" x=\"218\" y=\"208\" width=\"28\" height=\"5\" fill=\"#e2e0dc\" fill-opacity=\"0.3\" rx=\"0\"/>\n    <rect class=\"bar-fill\" x=\"251\" y=\"208\" width=\"40\" height=\"5\" fill=\"#e2e0dc\" fill-opacity=\"0.3\" rx=\"0\"/>\n    <rect class=\"bar-fill\" x=\"296\" y=\"208\" width=\"15\" height=\"5\" fill=\"#e2e0dc\" fill-opacity=\"0.3\" rx=\"0\"/>\n    <rect class=\"bar-fill\" x=\"316\" y=\"208\" width=\"32\" height=\"5\" fill=\"#e2e0dc\" fill-opacity=\"0.3\" rx=\"0\"/>\n    <rect class=\"bar-fill\" x=\"353\" y=\"208\" width=\"20\" height=\"5\" fill=\"#e2e0dc\" fill-opacity=\"0.3\" rx=\"0\"/>\n    <rect class=\"bar-fill\" x=\"378\" y=\"208\" width=\"38\" height=\"5\" fill=\"#e2e0dc\" fill-opacity=\"0.3\" rx=\"0\"/>\n    <rect class=\"bar-fill\" x=\"128\" y=\"218\" width=\"38\" height=\"5\" fill=\"#e2e0dc\" fill-opacity=\"0.28\" rx=\"0\"/>\n    <rect class=\"bar-fill\" x=\"171\" y=\"218\" width=\"15\" height=\"5\" fill=\"#e2e0dc\" fill-opacity=\"0.28\" rx=\"0\"/>\n    <rect class=\"bar-fill\" x=\"191\" y=\"218\" width=\"28\" height=\"5\" fill=\"#e2e0dc\" fill-opacity=\"0.28\" rx=\"0\"/>\n    <rect class=\"bar-fill\" x=\"224\" y=\"218\" width=\"40\" height=\"5\" fill=\"#e2e0dc\" fill-opacity=\"0.28\" rx=\"0\"/>\n    <rect class=\"bar-fill\" x=\"269\" y=\"218\" width=\"20\" height=\"5\" fill=\"#e2e0dc\" fill-opacity=\"0.28\" rx=\"0\"/>\n    <rect class=\"bar-fill\" x=\"294\" y=\"218\" width=\"30\" height=\"5\" fill=\"#e2e0dc\" fill-opacity=\"0.28\" rx=\"0\"/>\n    <rect class=\"bar-fill\" x=\"329\" y=\"218\" width=\"12\" height=\"5\" fill=\"#e2e0dc\" fill-opacity=\"0.28\" rx=\"0\"/>\n    <rect class=\"bar-fill\" x=\"346\" y=\"218\" width=\"36\" height=\"5\" fill=\"#e2e0dc\" fill-opacity=\"0.28\" rx=\"0\"/>\n    <rect class=\"bar-fill\" x=\"128\" y=\"228\" width=\"28\" height=\"5\" fill=\"#e2e0dc\" fill-opacity=\"0.25\" rx=\"0\"/>\n    <rect class=\"bar-fill\" x=\"161\" y=\"228\" width=\"40\" height=\"5\" fill=\"#e2e0dc\" fill-opacity=\"0.25\" rx=\"0\"/>\n    <rect class=\"bar-fill\" x=\"206\" y=\"228\" width=\"18\" height=\"5\" fill=\"#e2e0dc\" fill-opacity=\"0.25\" rx=\"0\"/>\n    <rect class=\"bar-fill\" x=\"229\" y=\"228\" width=\"32\" height=\"5\" fill=\"#e2e0dc\" fill-opacity=\"0.25\" rx=\"0\"/>\n    <rect class=\"bar-fill\" x=\"266\" y=\"228\" width=\"22\" height=\"5\" fill=\"#e2e0dc\" fill-opacity=\"0.25\" rx=\"0\"/>\n    <rect class=\"bar-fill\" x=\"293\" y=\"228\" width=\"35\" height=\"5\" fill=\"#e2e0dc\" fill-opacity=\"0.25\" rx=\"0\"/>\n    <rect class=\"bar-fill\" x=\"333\" y=\"228\" width=\"15\" height=\"5\" fill=\"#e2e0dc\" fill-opacity=\"0.25\" rx=\"0\"/>\n    <rect class=\"bar-fill\" x=\"353\" y=\"228\" width=\"38\" height=\"5\" fill=\"#e2e0dc\" fill-opacity=\"0.25\" rx=\"0\"/>\n    <rect class=\"bar-fill\" x=\"128\" y=\"240\" width=\"18\" height=\"5\" fill=\"#e2e0dc\" fill-opacity=\"0.3\" rx=\"0\"/>\n    <rect class=\"bar-fill\" x=\"151\" y=\"240\" width=\"35\" height=\"5\" fill=\"#e2e0dc\" fill-opacity=\"0.3\" rx=\"0\"/>\n    <rect class=\"bar-fill\" x=\"191\" y=\"240\" width=\"28\" height=\"5\" fill=\"#e2e0dc\" fill-opacity=\"0.3\" rx=\"0\"/>\n    <rect class=\"bar-fill\" x=\"224\" y=\"240\" width=\"40\" height=\"5\" fill=\"#e2e0dc\" fill-opacity=\"0.3\" rx=\"0\"/>\n    <rect class=\"bar-fill\" x=\"269\" y=\"240\" width=\"22\" height=\"5\" fill=\"#e2e0dc\" fill-opacity=\"0.3\" rx=\"0\"/>\n    <rect class=\"bar-fill\" x=\"296\" y=\"240\" width=\"30\" height=\"5\" fill=\"#e2e0dc\" fill-opacity=\"0.3\" rx=\"0\"/>\n    <rect class=\"bar-fill\" x=\"331\" y=\"240\" width=\"20\" height=\"5\" fill=\"#e2e0dc\" fill-opacity=\"0.3\" rx=\"0\"/>\n    <rect class=\"bar-fill\" x=\"356\" y=\"240\" width=\"38\" height=\"5\" fill=\"#e2e0dc\" fill-opacity=\"0.3\" rx=\"0\"/>\n\n    <!-- Block 3: y=248 -->\n    <rect class=\"nd-box\" x=\"20\" y=\"248\" width=\"480\" height=\"52\" fill=\"white\" stroke=\"#e2e0dc\" stroke-width=\"1\" rx=\"0\"/>\n    <line stroke=\"#e2e0dc\" stroke-width=\"1\" x1=\"120\" y1=\"248\" x2=\"120\" y2=\"300\"/>\n    <text class=\"t-muted2\" x=\"70\" y=\"277\" text-anchor=\"middle\" font-size=\"12\" fill=\"rgba(61,58,54,0.7)\">docs 4K–5K</text>\n    <rect class=\"bar-fill\" x=\"128\" y=\"260\" width=\"22\" height=\"5\" fill=\"#e2e0dc\" fill-opacity=\"0.3\" rx=\"0\"/>\n    <rect class=\"bar-fill\" x=\"155\" y=\"260\" width=\"35\" height=\"5\" fill=\"#e2e0dc\" fill-opacity=\"0.3\" rx=\"0\"/>\n    <rect class=\"bar-fill\" x=\"195\" y=\"260\" width=\"18\" height=\"5\" fill=\"#e2e0dc\" fill-opacity=\"0.3\" rx=\"0\"/>\n    <rect class=\"bar-fill\" x=\"218\" y=\"260\" width=\"28\" height=\"5\" fill=\"#e2e0dc\" fill-opacity=\"0.3\" rx=\"0\"/>\n    <rect class=\"bar-fill\" x=\"251\" y=\"260\" width=\"40\" height=\"5\" fill=\"#e2e0dc\" fill-opacity=\"0.3\" rx=\"0\"/>\n    <rect class=\"bar-fill\" x=\"296\" y=\"260\" width=\"15\" height=\"5\" fill=\"#e2e0dc\" fill-opacity=\"0.3\" rx=\"0\"/>\n    <rect class=\"bar-fill\" x=\"316\" y=\"260\" width=\"32\" height=\"5\" fill=\"#e2e0dc\" fill-opacity=\"0.3\" rx=\"0\"/>\n    <rect class=\"bar-fill\" x=\"353\" y=\"260\" width=\"20\" height=\"5\" fill=\"#e2e0dc\" fill-opacity=\"0.3\" rx=\"0\"/>\n    <rect class=\"bar-fill\" x=\"378\" y=\"260\" width=\"38\" height=\"5\" fill=\"#e2e0dc\" fill-opacity=\"0.3\" rx=\"0\"/>\n    <rect class=\"bar-fill\" x=\"128\" y=\"270\" width=\"38\" height=\"5\" fill=\"#e2e0dc\" fill-opacity=\"0.28\" rx=\"0\"/>\n    <rect class=\"bar-fill\" x=\"171\" y=\"270\" width=\"15\" height=\"5\" fill=\"#e2e0dc\" fill-opacity=\"0.28\" rx=\"0\"/>\n    <rect class=\"bar-fill\" x=\"191\" y=\"270\" width=\"28\" height=\"5\" fill=\"#e2e0dc\" fill-opacity=\"0.28\" rx=\"0\"/>\n    <rect class=\"bar-fill\" x=\"224\" y=\"270\" width=\"40\" height=\"5\" fill=\"#e2e0dc\" fill-opacity=\"0.28\" rx=\"0\"/>\n    <rect class=\"bar-fill\" x=\"269\" y=\"270\" width=\"20\" height=\"5\" fill=\"#e2e0dc\" fill-opacity=\"0.28\" rx=\"0\"/>\n    <rect class=\"bar-fill\" x=\"294\" y=\"270\" width=\"30\" height=\"5\" fill=\"#e2e0dc\" fill-opacity=\"0.28\" rx=\"0\"/>\n    <rect class=\"bar-fill\" x=\"329\" y=\"270\" width=\"12\" height=\"5\" fill=\"#e2e0dc\" fill-opacity=\"0.28\" rx=\"0\"/>\n    <rect class=\"bar-fill\" x=\"346\" y=\"270\" width=\"36\" height=\"5\" fill=\"#e2e0dc\" fill-opacity=\"0.28\" rx=\"0\"/>\n    <rect class=\"bar-fill\" x=\"128\" y=\"280\" width=\"28\" height=\"5\" fill=\"#e2e0dc\" fill-opacity=\"0.25\" rx=\"0\"/>\n    <rect class=\"bar-fill\" x=\"161\" y=\"280\" width=\"40\" height=\"5\" fill=\"#e2e0dc\" fill-opacity=\"0.25\" rx=\"0\"/>\n    <rect class=\"bar-fill\" x=\"206\" y=\"280\" width=\"18\" height=\"5\" fill=\"#e2e0dc\" fill-opacity=\"0.25\" rx=\"0\"/>\n    <rect class=\"bar-fill\" x=\"229\" y=\"280\" width=\"32\" height=\"5\" fill=\"#e2e0dc\" fill-opacity=\"0.25\" rx=\"0\"/>\n    <rect class=\"bar-fill\" x=\"266\" y=\"280\" width=\"22\" height=\"5\" fill=\"#e2e0dc\" fill-opacity=\"0.25\" rx=\"0\"/>\n    <rect class=\"bar-fill\" x=\"293\" y=\"280\" width=\"35\" height=\"5\" fill=\"#e2e0dc\" fill-opacity=\"0.25\" rx=\"0\"/>\n    <rect class=\"bar-fill\" x=\"333\" y=\"280\" width=\"15\" height=\"5\" fill=\"#e2e0dc\" fill-opacity=\"0.25\" rx=\"0\"/>\n    <rect class=\"bar-fill\" x=\"353\" y=\"280\" width=\"38\" height=\"5\" fill=\"#e2e0dc\" fill-opacity=\"0.25\" rx=\"0\"/>\n    <rect class=\"bar-fill\" x=\"128\" y=\"292\" width=\"18\" height=\"5\" fill=\"#e2e0dc\" fill-opacity=\"0.3\" rx=\"0\"/>\n    <rect class=\"bar-fill\" x=\"151\" y=\"292\" width=\"35\" height=\"5\" fill=\"#e2e0dc\" fill-opacity=\"0.3\" rx=\"0\"/>\n    <rect class=\"bar-fill\" x=\"191\" y=\"292\" width=\"28\" height=\"5\" fill=\"#e2e0dc\" fill-opacity=\"0.3\" rx=\"0\"/>\n    <rect class=\"bar-fill\" x=\"224\" y=\"292\" width=\"40\" height=\"5\" fill=\"#e2e0dc\" fill-opacity=\"0.3\" rx=\"0\"/>\n    <rect class=\"bar-fill\" x=\"269\" y=\"292\" width=\"22\" height=\"5\" fill=\"#e2e0dc\" fill-opacity=\"0.3\" rx=\"0\"/>\n    <rect class=\"bar-fill\" x=\"296\" y=\"292\" width=\"30\" height=\"5\" fill=\"#e2e0dc\" fill-opacity=\"0.3\" rx=\"0\"/>\n    <rect class=\"bar-fill\" x=\"331\" y=\"292\" width=\"20\" height=\"5\" fill=\"#e2e0dc\" fill-opacity=\"0.3\" rx=\"0\"/>\n    <rect class=\"bar-fill\" x=\"356\" y=\"292\" width=\"38\" height=\"5\" fill=\"#e2e0dc\" fill-opacity=\"0.3\" rx=\"0\"/>\n\n    <!-- Block 4: y=300 -->\n    <rect class=\"nd-box\" x=\"20\" y=\"300\" width=\"480\" height=\"52\" fill=\"white\" stroke=\"#e2e0dc\" stroke-width=\"1\" rx=\"0\"/>\n    <line stroke=\"#e2e0dc\" stroke-width=\"1\" x1=\"120\" y1=\"300\" x2=\"120\" y2=\"352\"/>\n    <text class=\"t-muted2\" x=\"70\" y=\"329\" text-anchor=\"middle\" font-size=\"12\" fill=\"rgba(61,58,54,0.7)\">docs 5K–6K</text>\n    <rect class=\"bar-fill\" x=\"128\" y=\"312\" width=\"22\" height=\"5\" fill=\"#e2e0dc\" fill-opacity=\"0.3\" rx=\"0\"/>\n    <rect class=\"bar-fill\" x=\"155\" y=\"312\" width=\"35\" height=\"5\" fill=\"#e2e0dc\" fill-opacity=\"0.3\" rx=\"0\"/>\n    <rect class=\"bar-fill\" x=\"195\" y=\"312\" width=\"18\" height=\"5\" fill=\"#e2e0dc\" fill-opacity=\"0.3\" rx=\"0\"/>\n    <rect class=\"bar-fill\" x=\"218\" y=\"312\" width=\"28\" height=\"5\" fill=\"#e2e0dc\" fill-opacity=\"0.3\" rx=\"0\"/>\n    <rect class=\"bar-fill\" x=\"251\" y=\"312\" width=\"40\" height=\"5\" fill=\"#e2e0dc\" fill-opacity=\"0.3\" rx=\"0\"/>\n    <rect class=\"bar-fill\" x=\"296\" y=\"312\" width=\"15\" height=\"5\" fill=\"#e2e0dc\" fill-opacity=\"0.3\" rx=\"0\"/>\n    <rect class=\"bar-fill\" x=\"316\" y=\"312\" width=\"32\" height=\"5\" fill=\"#e2e0dc\" fill-opacity=\"0.3\" rx=\"0\"/>\n    <rect class=\"bar-fill\" x=\"353\" y=\"312\" width=\"20\" height=\"5\" fill=\"#e2e0dc\" fill-opacity=\"0.3\" rx=\"0\"/>\n    <rect class=\"bar-fill\" x=\"378\" y=\"312\" width=\"38\" height=\"5\" fill=\"#e2e0dc\" fill-opacity=\"0.3\" rx=\"0\"/>\n    <rect class=\"bar-fill\" x=\"128\" y=\"322\" width=\"38\" height=\"5\" fill=\"#e2e0dc\" fill-opacity=\"0.28\" rx=\"0\"/>\n    <rect class=\"bar-fill\" x=\"171\" y=\"322\" width=\"15\" height=\"5\" fill=\"#e2e0dc\" fill-opacity=\"0.28\" rx=\"0\"/>\n    <rect class=\"bar-fill\" x=\"191\" y=\"322\" width=\"28\" height=\"5\" fill=\"#e2e0dc\" fill-opacity=\"0.28\" rx=\"0\"/>\n    <rect class=\"bar-fill\" x=\"224\" y=\"322\" width=\"40\" height=\"5\" fill=\"#e2e0dc\" fill-opacity=\"0.28\" rx=\"0\"/>\n    <rect class=\"bar-fill\" x=\"269\" y=\"322\" width=\"20\" height=\"5\" fill=\"#e2e0dc\" fill-opacity=\"0.28\" rx=\"0\"/>\n    <rect class=\"bar-fill\" x=\"294\" y=\"322\" width=\"30\" height=\"5\" fill=\"#e2e0dc\" fill-opacity=\"0.28\" rx=\"0\"/>\n    <rect class=\"bar-fill\" x=\"329\" y=\"322\" width=\"12\" height=\"5\" fill=\"#e2e0dc\" fill-opacity=\"0.28\" rx=\"0\"/>\n    <rect class=\"bar-fill\" x=\"346\" y=\"322\" width=\"36\" height=\"5\" fill=\"#e2e0dc\" fill-opacity=\"0.28\" rx=\"0\"/>\n    <rect class=\"bar-fill\" x=\"128\" y=\"332\" width=\"28\" height=\"5\" fill=\"#e2e0dc\" fill-opacity=\"0.25\" rx=\"0\"/>\n    <rect class=\"bar-fill\" x=\"161\" y=\"332\" width=\"40\" height=\"5\" fill=\"#e2e0dc\" fill-opacity=\"0.25\" rx=\"0\"/>\n    <rect class=\"bar-fill\" x=\"206\" y=\"332\" width=\"18\" height=\"5\" fill=\"#e2e0dc\" fill-opacity=\"0.25\" rx=\"0\"/>\n    <rect class=\"bar-fill\" x=\"229\" y=\"332\" width=\"32\" height=\"5\" fill=\"#e2e0dc\" fill-opacity=\"0.25\" rx=\"0\"/>\n    <rect class=\"bar-fill\" x=\"266\" y=\"332\" width=\"22\" height=\"5\" fill=\"#e2e0dc\" fill-opacity=\"0.25\" rx=\"0\"/>\n    <rect class=\"bar-fill\" x=\"293\" y=\"332\" width=\"35\" height=\"5\" fill=\"#e2e0dc\" fill-opacity=\"0.25\" rx=\"0\"/>\n    <rect class=\"bar-fill\" x=\"333\" y=\"332\" width=\"15\" height=\"5\" fill=\"#e2e0dc\" fill-opacity=\"0.25\" rx=\"0\"/>\n    <rect class=\"bar-fill\" x=\"353\" y=\"332\" width=\"38\" height=\"5\" fill=\"#e2e0dc\" fill-opacity=\"0.25\" rx=\"0\"/>\n    <rect class=\"bar-fill\" x=\"128\" y=\"344\" width=\"18\" height=\"5\" fill=\"#e2e0dc\" fill-opacity=\"0.3\" rx=\"0\"/>\n    <rect class=\"bar-fill\" x=\"151\" y=\"344\" width=\"35\" height=\"5\" fill=\"#e2e0dc\" fill-opacity=\"0.3\" rx=\"0\"/>\n    <rect class=\"bar-fill\" x=\"191\" y=\"344\" width=\"28\" height=\"5\" fill=\"#e2e0dc\" fill-opacity=\"0.3\" rx=\"0\"/>\n    <rect class=\"bar-fill\" x=\"224\" y=\"344\" width=\"40\" height=\"5\" fill=\"#e2e0dc\" fill-opacity=\"0.3\" rx=\"0\"/>\n    <rect class=\"bar-fill\" x=\"269\" y=\"344\" width=\"22\" height=\"5\" fill=\"#e2e0dc\" fill-opacity=\"0.3\" rx=\"0\"/>\n    <rect class=\"bar-fill\" x=\"296\" y=\"344\" width=\"30\" height=\"5\" fill=\"#e2e0dc\" fill-opacity=\"0.3\" rx=\"0\"/>\n    <rect class=\"bar-fill\" x=\"331\" y=\"344\" width=\"20\" height=\"5\" fill=\"#e2e0dc\" fill-opacity=\"0.3\" rx=\"0\"/>\n    <rect class=\"bar-fill\" x=\"356\" y=\"344\" width=\"38\" height=\"5\" fill=\"#e2e0dc\" fill-opacity=\"0.3\" rx=\"0\"/>\n\n    <!-- Block 5: y=352 -->\n    <rect class=\"nd-box\" x=\"20\" y=\"352\" width=\"480\" height=\"52\" fill=\"white\" stroke=\"#e2e0dc\" stroke-width=\"1\" rx=\"0\"/>\n    <line stroke=\"#e2e0dc\" stroke-width=\"1\" x1=\"120\" y1=\"352\" x2=\"120\" y2=\"404\"/>\n    <text class=\"t-muted2\" x=\"70\" y=\"381\" text-anchor=\"middle\" font-size=\"12\" fill=\"rgba(61,58,54,0.7)\">docs 6K–7K</text>\n    <rect class=\"bar-fill\" x=\"128\" y=\"364\" width=\"22\" height=\"5\" fill=\"#e2e0dc\" fill-opacity=\"0.3\" rx=\"0\"/>\n    <rect class=\"bar-fill\" x=\"155\" y=\"364\" width=\"35\" height=\"5\" fill=\"#e2e0dc\" fill-opacity=\"0.3\" rx=\"0\"/>\n    <rect class=\"bar-fill\" x=\"195\" y=\"364\" width=\"18\" height=\"5\" fill=\"#e2e0dc\" fill-opacity=\"0.3\" rx=\"0\"/>\n    <rect class=\"bar-fill\" x=\"218\" y=\"364\" width=\"28\" height=\"5\" fill=\"#e2e0dc\" fill-opacity=\"0.3\" rx=\"0\"/>\n    <rect class=\"bar-fill\" x=\"251\" y=\"364\" width=\"40\" height=\"5\" fill=\"#e2e0dc\" fill-opacity=\"0.3\" rx=\"0\"/>\n    <rect class=\"bar-fill\" x=\"296\" y=\"364\" width=\"15\" height=\"5\" fill=\"#e2e0dc\" fill-opacity=\"0.3\" rx=\"0\"/>\n    <rect class=\"bar-fill\" x=\"316\" y=\"364\" width=\"32\" height=\"5\" fill=\"#e2e0dc\" fill-opacity=\"0.3\" rx=\"0\"/>\n    <rect class=\"bar-fill\" x=\"353\" y=\"364\" width=\"20\" height=\"5\" fill=\"#e2e0dc\" fill-opacity=\"0.3\" rx=\"0\"/>\n    <rect class=\"bar-fill\" x=\"378\" y=\"364\" width=\"38\" height=\"5\" fill=\"#e2e0dc\" fill-opacity=\"0.3\" rx=\"0\"/>\n    <rect class=\"bar-fill\" x=\"128\" y=\"374\" width=\"38\" height=\"5\" fill=\"#e2e0dc\" fill-opacity=\"0.28\" rx=\"0\"/>\n    <rect class=\"bar-fill\" x=\"171\" y=\"374\" width=\"15\" height=\"5\" fill=\"#e2e0dc\" fill-opacity=\"0.28\" rx=\"0\"/>\n    <rect class=\"bar-fill\" x=\"191\" y=\"374\" width=\"28\" height=\"5\" fill=\"#e2e0dc\" fill-opacity=\"0.28\" rx=\"0\"/>\n    <rect class=\"bar-fill\" x=\"224\" y=\"374\" width=\"40\" height=\"5\" fill=\"#e2e0dc\" fill-opacity=\"0.28\" rx=\"0\"/>\n    <rect class=\"bar-fill\" x=\"269\" y=\"374\" width=\"20\" height=\"5\" fill=\"#e2e0dc\" fill-opacity=\"0.28\" rx=\"0\"/>\n    <rect class=\"bar-fill\" x=\"294\" y=\"374\" width=\"30\" height=\"5\" fill=\"#e2e0dc\" fill-opacity=\"0.28\" rx=\"0\"/>\n    <rect class=\"bar-fill\" x=\"329\" y=\"374\" width=\"12\" height=\"5\" fill=\"#e2e0dc\" fill-opacity=\"0.28\" rx=\"0\"/>\n    <rect class=\"bar-fill\" x=\"346\" y=\"374\" width=\"36\" height=\"5\" fill=\"#e2e0dc\" fill-opacity=\"0.28\" rx=\"0\"/>\n    <rect class=\"bar-fill\" x=\"128\" y=\"384\" width=\"28\" height=\"5\" fill=\"#e2e0dc\" fill-opacity=\"0.25\" rx=\"0\"/>\n    <rect class=\"bar-fill\" x=\"161\" y=\"384\" width=\"40\" height=\"5\" fill=\"#e2e0dc\" fill-opacity=\"0.25\" rx=\"0\"/>\n    <rect class=\"bar-fill\" x=\"206\" y=\"384\" width=\"18\" height=\"5\" fill=\"#e2e0dc\" fill-opacity=\"0.25\" rx=\"0\"/>\n    <rect class=\"bar-fill\" x=\"229\" y=\"384\" width=\"32\" height=\"5\" fill=\"#e2e0dc\" fill-opacity=\"0.25\" rx=\"0\"/>\n    <rect class=\"bar-fill\" x=\"266\" y=\"384\" width=\"22\" height=\"5\" fill=\"#e2e0dc\" fill-opacity=\"0.25\" rx=\"0\"/>\n    <rect class=\"bar-fill\" x=\"293\" y=\"384\" width=\"35\" height=\"5\" fill=\"#e2e0dc\" fill-opacity=\"0.25\" rx=\"0\"/>\n    <rect class=\"bar-fill\" x=\"333\" y=\"384\" width=\"15\" height=\"5\" fill=\"#e2e0dc\" fill-opacity=\"0.25\" rx=\"0\"/>\n    <rect class=\"bar-fill\" x=\"353\" y=\"384\" width=\"38\" height=\"5\" fill=\"#e2e0dc\" fill-opacity=\"0.25\" rx=\"0\"/>\n    <rect class=\"bar-fill\" x=\"128\" y=\"396\" width=\"18\" height=\"5\" fill=\"#e2e0dc\" fill-opacity=\"0.3\" rx=\"0\"/>\n    <rect class=\"bar-fill\" x=\"151\" y=\"396\" width=\"35\" height=\"5\" fill=\"#e2e0dc\" fill-opacity=\"0.3\" rx=\"0\"/>\n    <rect class=\"bar-fill\" x=\"191\" y=\"396\" width=\"28\" height=\"5\" fill=\"#e2e0dc\" fill-opacity=\"0.3\" rx=\"0\"/>\n    <rect class=\"bar-fill\" x=\"224\" y=\"396\" width=\"40\" height=\"5\" fill=\"#e2e0dc\" fill-opacity=\"0.3\" rx=\"0\"/>\n    <rect class=\"bar-fill\" x=\"269\" y=\"396\" width=\"22\" height=\"5\" fill=\"#e2e0dc\" fill-opacity=\"0.3\" rx=\"0\"/>\n    <rect class=\"bar-fill\" x=\"296\" y=\"396\" width=\"30\" height=\"5\" fill=\"#e2e0dc\" fill-opacity=\"0.3\" rx=\"0\"/>\n    <rect class=\"bar-fill\" x=\"331\" y=\"396\" width=\"20\" height=\"5\" fill=\"#e2e0dc\" fill-opacity=\"0.3\" rx=\"0\"/>\n    <rect class=\"bar-fill\" x=\"356\" y=\"396\" width=\"38\" height=\"5\" fill=\"#e2e0dc\" fill-opacity=\"0.3\" rx=\"0\"/>\n\n    <!-- Block 6: y=404 -->\n    <rect class=\"nd-box\" x=\"20\" y=\"404\" width=\"480\" height=\"52\" fill=\"white\" stroke=\"#e2e0dc\" stroke-width=\"1\" rx=\"0\"/>\n    <line stroke=\"#e2e0dc\" stroke-width=\"1\" x1=\"120\" y1=\"404\" x2=\"120\" y2=\"456\"/>\n    <text class=\"t-muted2\" x=\"70\" y=\"433\" text-anchor=\"middle\" font-size=\"12\" fill=\"rgba(61,58,54,0.7)\">docs 7K–8K</text>\n    <rect class=\"bar-fill\" x=\"128\" y=\"416\" width=\"22\" height=\"5\" fill=\"#e2e0dc\" fill-opacity=\"0.3\" rx=\"0\"/>\n    <rect class=\"bar-fill\" x=\"155\" y=\"416\" width=\"35\" height=\"5\" fill=\"#e2e0dc\" fill-opacity=\"0.3\" rx=\"0\"/>\n    <rect class=\"bar-fill\" x=\"195\" y=\"416\" width=\"18\" height=\"5\" fill=\"#e2e0dc\" fill-opacity=\"0.3\" rx=\"0\"/>\n    <rect class=\"bar-fill\" x=\"218\" y=\"416\" width=\"28\" height=\"5\" fill=\"#e2e0dc\" fill-opacity=\"0.3\" rx=\"0\"/>\n    <rect class=\"bar-fill\" x=\"251\" y=\"416\" width=\"40\" height=\"5\" fill=\"#e2e0dc\" fill-opacity=\"0.3\" rx=\"0\"/>\n    <rect class=\"bar-fill\" x=\"296\" y=\"416\" width=\"15\" height=\"5\" fill=\"#e2e0dc\" fill-opacity=\"0.3\" rx=\"0\"/>\n    <rect class=\"bar-fill\" x=\"316\" y=\"416\" width=\"32\" height=\"5\" fill=\"#e2e0dc\" fill-opacity=\"0.3\" rx=\"0\"/>\n    <rect class=\"bar-fill\" x=\"353\" y=\"416\" width=\"20\" height=\"5\" fill=\"#e2e0dc\" fill-opacity=\"0.3\" rx=\"0\"/>\n    <rect class=\"bar-fill\" x=\"378\" y=\"416\" width=\"38\" height=\"5\" fill=\"#e2e0dc\" fill-opacity=\"0.3\" rx=\"0\"/>\n    <rect class=\"bar-fill\" x=\"128\" y=\"426\" width=\"38\" height=\"5\" fill=\"#e2e0dc\" fill-opacity=\"0.28\" rx=\"0\"/>\n    <rect class=\"bar-fill\" x=\"171\" y=\"426\" width=\"15\" height=\"5\" fill=\"#e2e0dc\" fill-opacity=\"0.28\" rx=\"0\"/>\n    <rect class=\"bar-fill\" x=\"191\" y=\"426\" width=\"28\" height=\"5\" fill=\"#e2e0dc\" fill-opacity=\"0.28\" rx=\"0\"/>\n    <rect class=\"bar-fill\" x=\"224\" y=\"426\" width=\"40\" height=\"5\" fill=\"#e2e0dc\" fill-opacity=\"0.28\" rx=\"0\"/>\n    <rect class=\"bar-fill\" x=\"269\" y=\"426\" width=\"20\" height=\"5\" fill=\"#e2e0dc\" fill-opacity=\"0.28\" rx=\"0\"/>\n    <rect class=\"bar-fill\" x=\"294\" y=\"426\" width=\"30\" height=\"5\" fill=\"#e2e0dc\" fill-opacity=\"0.28\" rx=\"0\"/>\n    <rect class=\"bar-fill\" x=\"329\" y=\"426\" width=\"12\" height=\"5\" fill=\"#e2e0dc\" fill-opacity=\"0.28\" rx=\"0\"/>\n    <rect class=\"bar-fill\" x=\"346\" y=\"426\" width=\"36\" height=\"5\" fill=\"#e2e0dc\" fill-opacity=\"0.28\" rx=\"0\"/>\n    <rect class=\"bar-fill\" x=\"128\" y=\"436\" width=\"28\" height=\"5\" fill=\"#e2e0dc\" fill-opacity=\"0.25\" rx=\"0\"/>\n    <rect class=\"bar-fill\" x=\"161\" y=\"436\" width=\"40\" height=\"5\" fill=\"#e2e0dc\" fill-opacity=\"0.25\" rx=\"0\"/>\n    <rect class=\"bar-fill\" x=\"206\" y=\"436\" width=\"18\" height=\"5\" fill=\"#e2e0dc\" fill-opacity=\"0.25\" rx=\"0\"/>\n    <rect class=\"bar-fill\" x=\"229\" y=\"436\" width=\"32\" height=\"5\" fill=\"#e2e0dc\" fill-opacity=\"0.25\" rx=\"0\"/>\n    <rect class=\"bar-fill\" x=\"266\" y=\"436\" width=\"22\" height=\"5\" fill=\"#e2e0dc\" fill-opacity=\"0.25\" rx=\"0\"/>\n    <rect class=\"bar-fill\" x=\"293\" y=\"436\" width=\"35\" height=\"5\" fill=\"#e2e0dc\" fill-opacity=\"0.25\" rx=\"0\"/>\n    <rect class=\"bar-fill\" x=\"333\" y=\"436\" width=\"15\" height=\"5\" fill=\"#e2e0dc\" fill-opacity=\"0.25\" rx=\"0\"/>\n    <rect class=\"bar-fill\" x=\"353\" y=\"436\" width=\"38\" height=\"5\" fill=\"#e2e0dc\" fill-opacity=\"0.25\" rx=\"0\"/>\n    <rect class=\"bar-fill\" x=\"128\" y=\"448\" width=\"18\" height=\"5\" fill=\"#e2e0dc\" fill-opacity=\"0.3\" rx=\"0\"/>\n    <rect class=\"bar-fill\" x=\"151\" y=\"448\" width=\"35\" height=\"5\" fill=\"#e2e0dc\" fill-opacity=\"0.3\" rx=\"0\"/>\n    <rect class=\"bar-fill\" x=\"191\" y=\"448\" width=\"28\" height=\"5\" fill=\"#e2e0dc\" fill-opacity=\"0.3\" rx=\"0\"/>\n    <rect class=\"bar-fill\" x=\"224\" y=\"448\" width=\"40\" height=\"5\" fill=\"#e2e0dc\" fill-opacity=\"0.3\" rx=\"0\"/>\n    <rect class=\"bar-fill\" x=\"269\" y=\"448\" width=\"22\" height=\"5\" fill=\"#e2e0dc\" fill-opacity=\"0.3\" rx=\"0\"/>\n    <rect class=\"bar-fill\" x=\"296\" y=\"448\" width=\"30\" height=\"5\" fill=\"#e2e0dc\" fill-opacity=\"0.3\" rx=\"0\"/>\n    <rect class=\"bar-fill\" x=\"331\" y=\"448\" width=\"20\" height=\"5\" fill=\"#e2e0dc\" fill-opacity=\"0.3\" rx=\"0\"/>\n    <rect class=\"bar-fill\" x=\"356\" y=\"448\" width=\"38\" height=\"5\" fill=\"#e2e0dc\" fill-opacity=\"0.3\" rx=\"0\"/>\n\n    <!-- Block 7: y=456 -->\n    <rect class=\"nd-box\" x=\"20\" y=\"456\" width=\"480\" height=\"52\" fill=\"white\" stroke=\"#e2e0dc\" stroke-width=\"1\" rx=\"0\"/>\n    <line stroke=\"#e2e0dc\" stroke-width=\"1\" x1=\"120\" y1=\"456\" x2=\"120\" y2=\"508\"/>\n    <text class=\"t-muted2\" x=\"70\" y=\"485\" text-anchor=\"middle\" font-size=\"12\" fill=\"rgba(61,58,54,0.7)\">docs 8K–9K</text>\n    <rect class=\"bar-fill\" x=\"128\" y=\"468\" width=\"22\" height=\"5\" fill=\"#e2e0dc\" fill-opacity=\"0.3\" rx=\"0\"/>\n    <rect class=\"bar-fill\" x=\"155\" y=\"468\" width=\"35\" height=\"5\" fill=\"#e2e0dc\" fill-opacity=\"0.3\" rx=\"0\"/>\n    <rect class=\"bar-fill\" x=\"195\" y=\"468\" width=\"18\" height=\"5\" fill=\"#e2e0dc\" fill-opacity=\"0.3\" rx=\"0\"/>\n    <rect class=\"bar-fill\" x=\"218\" y=\"468\" width=\"28\" height=\"5\" fill=\"#e2e0dc\" fill-opacity=\"0.3\" rx=\"0\"/>\n    <rect class=\"bar-fill\" x=\"251\" y=\"468\" width=\"40\" height=\"5\" fill=\"#e2e0dc\" fill-opacity=\"0.3\" rx=\"0\"/>\n    <rect class=\"bar-fill\" x=\"296\" y=\"468\" width=\"15\" height=\"5\" fill=\"#e2e0dc\" fill-opacity=\"0.3\" rx=\"0\"/>\n    <rect class=\"bar-fill\" x=\"316\" y=\"468\" width=\"32\" height=\"5\" fill=\"#e2e0dc\" fill-opacity=\"0.3\" rx=\"0\"/>\n    <rect class=\"bar-fill\" x=\"353\" y=\"468\" width=\"20\" height=\"5\" fill=\"#e2e0dc\" fill-opacity=\"0.3\" rx=\"0\"/>\n    <rect class=\"bar-fill\" x=\"378\" y=\"468\" width=\"38\" height=\"5\" fill=\"#e2e0dc\" fill-opacity=\"0.3\" rx=\"0\"/>\n    <rect class=\"bar-fill\" x=\"128\" y=\"478\" width=\"38\" height=\"5\" fill=\"#e2e0dc\" fill-opacity=\"0.28\" rx=\"0\"/>\n    <rect class=\"bar-fill\" x=\"171\" y=\"478\" width=\"15\" height=\"5\" fill=\"#e2e0dc\" fill-opacity=\"0.28\" rx=\"0\"/>\n    <rect class=\"bar-fill\" x=\"191\" y=\"478\" width=\"28\" height=\"5\" fill=\"#e2e0dc\" fill-opacity=\"0.28\" rx=\"0\"/>\n    <rect class=\"bar-fill\" x=\"224\" y=\"478\" width=\"40\" height=\"5\" fill=\"#e2e0dc\" fill-opacity=\"0.28\" rx=\"0\"/>\n    <rect class=\"bar-fill\" x=\"269\" y=\"478\" width=\"20\" height=\"5\" fill=\"#e2e0dc\" fill-opacity=\"0.28\" rx=\"0\"/>\n    <rect class=\"bar-fill\" x=\"294\" y=\"478\" width=\"30\" height=\"5\" fill=\"#e2e0dc\" fill-opacity=\"0.28\" rx=\"0\"/>\n    <rect class=\"bar-fill\" x=\"329\" y=\"478\" width=\"12\" height=\"5\" fill=\"#e2e0dc\" fill-opacity=\"0.28\" rx=\"0\"/>\n    <rect class=\"bar-fill\" x=\"346\" y=\"478\" width=\"36\" height=\"5\" fill=\"#e2e0dc\" fill-opacity=\"0.28\" rx=\"0\"/>\n    <rect class=\"bar-fill\" x=\"128\" y=\"488\" width=\"28\" height=\"5\" fill=\"#e2e0dc\" fill-opacity=\"0.25\" rx=\"0\"/>\n    <rect class=\"bar-fill\" x=\"161\" y=\"488\" width=\"40\" height=\"5\" fill=\"#e2e0dc\" fill-opacity=\"0.25\" rx=\"0\"/>\n    <rect class=\"bar-fill\" x=\"206\" y=\"488\" width=\"18\" height=\"5\" fill=\"#e2e0dc\" fill-opacity=\"0.25\" rx=\"0\"/>\n    <rect class=\"bar-fill\" x=\"229\" y=\"488\" width=\"32\" height=\"5\" fill=\"#e2e0dc\" fill-opacity=\"0.25\" rx=\"0\"/>\n    <rect class=\"bar-fill\" x=\"266\" y=\"488\" width=\"22\" height=\"5\" fill=\"#e2e0dc\" fill-opacity=\"0.25\" rx=\"0\"/>\n    <rect class=\"bar-fill\" x=\"293\" y=\"488\" width=\"35\" height=\"5\" fill=\"#e2e0dc\" fill-opacity=\"0.25\" rx=\"0\"/>\n    <rect class=\"bar-fill\" x=\"333\" y=\"488\" width=\"15\" height=\"5\" fill=\"#e2e0dc\" fill-opacity=\"0.25\" rx=\"0\"/>\n    <rect class=\"bar-fill\" x=\"353\" y=\"488\" width=\"38\" height=\"5\" fill=\"#e2e0dc\" fill-opacity=\"0.25\" rx=\"0\"/>\n    <rect class=\"bar-fill\" x=\"128\" y=\"500\" width=\"18\" height=\"5\" fill=\"#e2e0dc\" fill-opacity=\"0.3\" rx=\"0\"/>\n    <rect class=\"bar-fill\" x=\"151\" y=\"500\" width=\"35\" height=\"5\" fill=\"#e2e0dc\" fill-opacity=\"0.3\" rx=\"0\"/>\n    <rect class=\"bar-fill\" x=\"191\" y=\"500\" width=\"28\" height=\"5\" fill=\"#e2e0dc\" fill-opacity=\"0.3\" rx=\"0\"/>\n    <rect class=\"bar-fill\" x=\"224\" y=\"500\" width=\"40\" height=\"5\" fill=\"#e2e0dc\" fill-opacity=\"0.3\" rx=\"0\"/>\n    <rect class=\"bar-fill\" x=\"269\" y=\"500\" width=\"22\" height=\"5\" fill=\"#e2e0dc\" fill-opacity=\"0.3\" rx=\"0\"/>\n    <rect class=\"bar-fill\" x=\"296\" y=\"500\" width=\"30\" height=\"5\" fill=\"#e2e0dc\" fill-opacity=\"0.3\" rx=\"0\"/>\n    <rect class=\"bar-fill\" x=\"331\" y=\"500\" width=\"20\" height=\"5\" fill=\"#e2e0dc\" fill-opacity=\"0.3\" rx=\"0\"/>\n    <rect class=\"bar-fill\" x=\"356\" y=\"500\" width=\"38\" height=\"5\" fill=\"#e2e0dc\" fill-opacity=\"0.3\" rx=\"0\"/>\n\n  </g><!-- /nd-blocks -->\n\n  <!-- ══ SCAN OVERLAY RECTS ════════════════════════════════════════════════ -->\n  <rect id=\"scan-0\" class=\"scan-overlay\" x=\"20\" y=\"92\"  width=\"480\" height=\"52\" rx=\"0\" fill=\"#f59e0b\" fill-opacity=\"0\" stroke=\"#f59e0b\" stroke-opacity=\"0\" stroke-width=\"1.5\"/>\n  <rect id=\"scan-1\" class=\"scan-overlay\" x=\"20\" y=\"144\" width=\"480\" height=\"52\" rx=\"0\" fill=\"#f59e0b\" fill-opacity=\"0\" stroke=\"#f59e0b\" stroke-opacity=\"0\" stroke-width=\"1.5\"/>\n  <rect id=\"scan-2\" class=\"scan-overlay\" x=\"20\" y=\"196\" width=\"480\" height=\"52\" rx=\"0\" fill=\"#f59e0b\" fill-opacity=\"0\" stroke=\"#f59e0b\" stroke-opacity=\"0\" stroke-width=\"1.5\"/>\n  <rect id=\"scan-3\" class=\"scan-overlay\" x=\"20\" y=\"248\" width=\"480\" height=\"52\" rx=\"0\" fill=\"#f59e0b\" fill-opacity=\"0\" stroke=\"#f59e0b\" stroke-opacity=\"0\" stroke-width=\"1.5\"/>\n  <rect id=\"scan-4\" class=\"scan-overlay\" x=\"20\" y=\"300\" width=\"480\" height=\"52\" rx=\"0\" fill=\"#f59e0b\" fill-opacity=\"0\" stroke=\"#f59e0b\" stroke-opacity=\"0\" stroke-width=\"1.5\"/>\n  <rect id=\"scan-5\" class=\"scan-overlay\" x=\"20\" y=\"352\" width=\"480\" height=\"52\" rx=\"0\" fill=\"#f59e0b\" fill-opacity=\"0\" stroke=\"#f59e0b\" stroke-opacity=\"0\" stroke-width=\"1.5\"/>\n  <rect id=\"scan-6\" class=\"scan-overlay\" x=\"20\" y=\"404\" width=\"480\" height=\"52\" rx=\"0\" fill=\"#f59e0b\" fill-opacity=\"0\" stroke=\"#f59e0b\" stroke-opacity=\"0\" stroke-width=\"1.5\"/>\n  <rect id=\"scan-7\" class=\"scan-overlay\" x=\"20\" y=\"456\" width=\"480\" height=\"52\" rx=\"0\" fill=\"#f59e0b\" fill-opacity=\"0\" stroke=\"#f59e0b\" stroke-opacity=\"0\" stroke-width=\"1.5\"/>\n\n  <!-- ══ I/O COUNTER ════════════════════════════════════════════════════════ -->\n  <g id=\"nd-counter\" opacity=\"0\">\n    <text class=\"t-muted3\" x=\"350\" y=\"520\" font-size=\"11\" letter-spacing=\"0.06em\" fill=\"rgba(61,58,54,0.55)\">I/O READ</text>\n    <text id=\"io-counter-text\" class=\"t-amber\" x=\"350\" y=\"537\" font-size=\"14\" font-weight=\"500\" fill=\"#f59e0b\">0 MB</text>\n  </g>\n\n</svg>\n</div>\n</div>\n\n<script>\n(function () {\n  'use strict';\n\n  const R = window.matchMedia('(prefers-reduced-motion: reduce)').matches;\n  const $ = id => document.getElementById(id);\n  const wait = ms => R ? Promise.resolve() : new Promise(r => setTimeout(r, ms));\n\n  // ── Theme ──────────────────────────────────────────────────────────────────\n  function applyTheme(t) {\n    document.documentElement.classList.toggle('dark', t === 'dark');\n  }\n\n  (function initTheme() {\n    const param = new URLSearchParams(location.search).get('theme');\n    const osDark = window.matchMedia('(prefers-color-scheme: dark)').matches;\n    applyTheme(param || (osDark ? 'dark' : 'light'));\n    window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', e => {\n      if (!new URLSearchParams(location.search).get('theme')) applyTheme(e.matches ? 'dark' : 'light');\n    });\n    window.addEventListener('message', e => {\n      if (e.data && (e.data.theme === 'dark' || e.data.theme === 'light')) applyTheme(e.data.theme);\n    });\n  })();\n\n  // ── Helpers ────────────────────────────────────────────────────────────────\n  function fadeIn(id, ms) {\n    ms = ms || 280;\n    const el = $(id); if (!el) return Promise.resolve();\n    if (R) { el.setAttribute('opacity', 1); return Promise.resolve(); }\n    return new Promise(res => {\n      const t0 = performance.now();\n      (function tick(now) {\n        const p = Math.min((now - t0) / ms, 1);\n        el.setAttribute('opacity', 1 - Math.pow(1 - p, 2.2));\n        p < 1 ? setTimeout(function() { tick(performance.now()); }, 16) : res();\n      })(t0);\n    });\n  }\n\n  function scanBlock(idx) {\n    const el = $('scan-' + idx);\n    if (!el) return Promise.resolve();\n    return new Promise(res => {\n      const dur = 460;\n      const t0 = performance.now();\n      (function tick(now) {\n        const p = Math.min((now - t0) / dur, 1);\n        var fillOp, strokeOp;\n        if (p < 0.35) {\n          var t = p / 0.35;\n          fillOp = t * 0.22;\n          strokeOp = t * 0.55;\n        } else {\n          var t = (p - 0.35) / 0.65;\n          fillOp = 0.22 - (0.22 - 0.08) * t;\n          strokeOp = 0.55 - (0.55 - 0.25) * t;\n        }\n        el.setAttribute('fill-opacity', fillOp);\n        el.setAttribute('stroke-opacity', strokeOp);\n        p < 1 ? setTimeout(function() { tick(performance.now()); }, 16) : res();\n      })(t0);\n    });\n  }\n\n  function tickCounter(duration) {\n    const el = $('io-counter-text');\n    if (!el) return Promise.resolve();\n    const target = 3396;\n    return new Promise(res => {\n      const t0 = performance.now();\n      (function tick(now) {\n        const p = Math.min((now - t0) / duration, 1);\n        const eased = 1 - Math.pow(1 - p, 2.5);\n        const val = Math.round(eased * target);\n        el.textContent = val.toLocaleString() + ' MB';\n        p < 1 ? setTimeout(function() { tick(performance.now()); }, 16) : res();\n      })(t0);\n    });\n  }\n\n  function resetAll() {\n    ['nd-query','ln-arrow','nd-index-label','nd-blocks','nd-counter'].forEach(function(id) {\n      var el = $(id); if (el) el.setAttribute('opacity', 0);\n    });\n    for (var i = 0; i < 8; i++) {\n      var el = $('scan-' + i);\n      if (el) {\n        el.setAttribute('fill-opacity', 0);\n        el.setAttribute('stroke-opacity', 0);\n      }\n    }\n    var counter = $('io-counter-text');\n    if (counter) counter.textContent = '0 MB';\n  }\n\n  async function animate() {\n    resetAll();\n\n    await fadeIn('nd-query', 320);\n    await wait(400);\n    await fadeIn('ln-arrow', 200);\n    await wait(250);\n    await fadeIn('nd-index-label', 200);\n    await wait(200);\n    await fadeIn('nd-blocks', 260);\n    await wait(700);\n    await fadeIn('nd-counter', 180);\n    await wait(300);\n\n    // Sequential scan + counter tick concurrently\n    const scanDelay = 220;\n    const counterDur = 2000;\n\n    const counterP = tickCounter(counterDur);\n    const blockPs = [];\n    for (let i = 0; i < 8; i++) {\n      blockPs.push(wait(i * scanDelay).then(() => scanBlock(i)));\n    }\n    await Promise.all([counterP].concat(blockPs));\n\n    await wait(400);\n    await wait(3000);\n    animate();\n  }\n\n  if (R) {\n    ['nd-query','ln-arrow','nd-index-label','nd-blocks','nd-counter'].forEach(function(id) {\n      var el = $(id); if (el) el.setAttribute('opacity', 1);\n    });\n    for (var i = 0; i < 8; i++) {\n      var el = $('scan-' + i);\n      if (el) {\n        el.setAttribute('fill-opacity', 0.08);\n        el.setAttribute('stroke-opacity', 0.25);\n      }\n    }\n    var counter = $('io-counter-text');\n    if (counter) counter.textContent = '3,396 MB';\n  } else {\n    resetAll();\n    new IntersectionObserver(function(entries) {\n      if (entries[0].isIntersecting) animate();\n    }, { threshold: 0.2 }).observe(document.querySelector('.scan-page'));\n  }\n})();\n</script>\n</body>\n</html>"
  },
  "title": "sparse-v3-scan"
}
```

At query time, the search algorithm has to read each of these blocks, because any block might contain postings for any of the query's terms. A query for "apple" and "orange" reads the block covering documents 1–1000, then 1001–2000, then every other block through the entire index. There's no way to skip ahead without reading.

When an index fits in memory, this is survivable. SIMD scoring is fast, and scanning several gigabytes at memory bandwidth completes in hundreds of milliseconds. But as sparse indexes gained adoption, larger workloads appeared. A billion-vector SPLADE index can run to hundreds of gigabytes. At that size, keeping the full index in memory is expensive. And in a serverless environment where many users share the same underlying hardware, it often isn't feasible: memory is allocated dynamically across workloads, so one large index needing most of the available memory delays other active indexes.

When an index can't stay in memory, every query becomes a series of disk reads—nd reads from disk are orders of magnitude slower than memory. The V2 disk benchmarks make this concrete: every SPLADE query took 3,407 milliseconds regardless of what it was searching for, at p50, p90, _and_ p99. The completely flat distribution exposed disk throughput as the limiting factor.

For billion-scale indexes, the only path to sub-second latency _used to be_ dedicated hardware sized to keep the full index in memory.

## **What V3 changes**

We needed a way to reduce disk reads. Specifically, instead of loading the whole index into memory, we needed to devise a method to identify and load only the relevant postings. That way, a query touching 2 terms out of a 50,000-term vocabulary still had to read all 50,000 terms' worth of posting data (because they were packed together indistinguishably).

V3 overhauls the layout so queries can skip directly to what they need.

- **Term-major layout**: Each term gets its own blocks on disk; a query only loads data for the terms it contains.
- **In-memory term directory**: A compact map keeps every term's disk location in memory, so finding a term costs no I/O.
- **Metadata-first block skipping**: A small summary block per term lets the algorithm decide what to skip before loading any posting data.
- **Compressed posting blocks**: Doc IDs and scores are compressed per-term, reducing block sizes up to x2.5.

### **Organizing by term instead of by document range**

V3 flips the grouping strategy: it groups posting data by term instead of document range. All of "apple"'s postings live in their own contiguous sequence of blocks on disk; all of "orange"'s postings live in another sequence elsewhere. A query then reads only the blocks for the terms it actually contains. A SPLADE query typically includes dozens of non-zero terms; a BM25 query, just a few. Against a vocabulary of tens of thousands, both are a small fraction of the index to load.

### **A compact directory that lives in memory**

Reorganizing around terms creates a new problem: with posting data scattered across potentially tens of thousands of separate locations on disk, how does the search algorithm find a given term's blocks without scanning the index?

V3 solves this with a **term directory**, a compact map stored in the index metadata that records, for each term, where its posting data begins on disk. The metadata loads into memory when the index is opened and stays there. Every term lookup is an in-memory operation; finding a term's disk location costs no I/O.

The directory uses [Elias-Fano encoding](https://jermp.github.io/assets/pdf/notes/elias_fano_notes.pdf), which compresses sorted integer sequences while preserving O(1) lookup. For 100,000 terms, the result fits in about 300 KB (small enough to stay in the CPU’s L2 cache).

| Structure | Size (100K Terms) | Lookup |
| Sorted array of pairs | ~800 KB  | O(log n)  |
| Hash map | ~2.4 MB  | O(1)  |
| Elias-Fano | ~300 KB |  O(1)¹ |

¹ O(1) positional access by term ID; the term dictionary resolves strings to dense integer IDs upstream.

### **Deciding whether to load a block before loading it**

```json
{
  "_key": "e6254d38db13",
  "_type": "htmlEmbed",
  "code": {
    "_type": "code",
    "code": "<!DOCTYPE html>\n<html lang=\"en\">\n<head>\n  <meta charset=\"UTF-8\">\n  <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\">\n  <title>Sparse V3 — Term-Major Skip</title>\n  <link href=\"https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;500&display=swap\" rel=\"stylesheet\">\n  <style>\n    *, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }\n\n    /* ── Design tokens ── */\n    :root {\n      --bg:          #ffffff;\n      --dot:         #cac8c4;\n      --dot-opacity: 0.11;\n      --text:        #3d3a36;\n      --muted:       rgba(61,58,54,0.82);\n      --muted2:      rgba(61,58,54,0.7);\n      --muted3:      rgba(61,58,54,0.55);\n      --border:      #e2e0dc;\n      --node-fill:   #ffffff;\n      --blue:        #0019FF;\n      --blue-faint:  rgba(0,25,255,0.025);\n      --blue-border: rgba(0,25,255,0.22);\n      --stats-fill:  rgba(0,25,255,0.03);\n      --lbl-bg:      rgba(255,255,255,0.92);\n      --lbl-text:    rgba(61,58,54,0.85);\n      --div-line:    #e8e6e3;\n      --div-text:    rgba(61,58,54,0.7);\n      --conn-gray:   #d0cdc8;\n    }\n\n    html.dark {\n      --bg:          #0f0e0b;\n      --dot:         rgba(255,255,255,1);\n      --dot-opacity: 0.03;\n      --text:        #dedad5;\n      --muted:       rgba(222,218,213,0.82);\n      --muted2:      rgba(222,218,213,0.68);\n      --muted3:      rgba(222,218,213,0.5);\n      --border:      rgba(255,255,255,0.09);\n      --node-fill:   #1e1c18;\n      --blue:        #5573ff;\n      --blue-faint:  rgba(85,115,255,0.06);\n      --blue-border: rgba(85,115,255,0.28);\n      --stats-fill:  rgba(85,115,255,0.07);\n      --lbl-bg:      rgba(18,16,12,0.94);\n      --lbl-text:    rgba(222,218,213,0.85);\n      --div-line:    rgba(255,255,255,0.08);\n      --div-text:    rgba(222,218,213,0.65);\n      --conn-gray:   rgba(255,255,255,0.2);\n    }\n\n    html { height: 620px; }\n\n    body {\n      background: var(--bg);\n      height: 620px;\n      overflow: hidden;\n      transition: background 0.25s;\n    }\n\n    body::before {\n      content: '';\n      position: fixed; inset: 0;\n      background-image: radial-gradient(circle, var(--dot) 1px, transparent 1px);\n      background-size: 5.5px 5.5px;\n      opacity: var(--dot-opacity);\n      pointer-events: none;\n    }\n\n    .skip-page {\n      position: relative;\n      z-index: 1;\n      height: 100%;\n      display: flex;\n      align-items: center;\n      justify-content: center;\n    }\n\n    .outer {\n      position: relative;\n      z-index: 1;\n      width: 100%;\n      height: 100%;\n      display: flex;\n      align-items: center;\n      justify-content: center;\n      padding: 16px;\n      box-sizing: border-box;\n    }\n\n    svg {\n      height: 100%;\n      width: auto;\n      max-width: 100%;\n      display: block;\n    }\n\n    /* Paint-order trick — knock dots behind glyphs */\n    svg text {\n      paint-order: stroke fill;\n      stroke: var(--bg);\n      stroke-width: 2px;\n      stroke-linejoin: round;\n    }\n\n    .t-primary  { fill: var(--text) !important; }\n    .t-muted    { fill: var(--muted) !important; }\n    .t-muted2   { fill: var(--muted2) !important; }\n    .t-muted3   { fill: var(--muted3) !important; }\n    .t-blue     { fill: var(--blue) !important; }\n\n    html.dark .t-primary  { fill: var(--text) !important; }\n    html.dark .t-muted    { fill: var(--muted) !important; }\n    html.dark .t-muted2   { fill: var(--muted2) !important; }\n    html.dark .t-muted3   { fill: var(--muted3) !important; }\n    html.dark .t-blue     { fill: var(--blue) !important; }\n\n    html.dark .nd-dir   { fill: var(--stats-fill) !important; stroke: var(--blue-border) !important; }\n    html.dark .nd-query { fill: var(--node-fill) !important; stroke: var(--border) !important; }\n    html.dark .ln-gray  { stroke: var(--conn-gray) !important; }\n    html.dark .blk-fill { fill: var(--node-fill) !important; stroke: var(--border) !important; }\n    html.dark .blk-div  { stroke: var(--div-line) !important; }\n    html.dark .post-inactive { fill: rgba(200,198,194,0.15) !important; }\n    html.dark .post-active   { fill: var(--blue) !important; }\n    html.dark .hl-active { fill: var(--blue-faint) !important; stroke: var(--blue-border) !important; }\n  </style>\n</head>\n<body>\n<div class=\"skip-page\" id=\"page\">\n<div class=\"outer\" role=\"img\" aria-label=\"V3 term-major index: query reads only the two matching term blocks, skipping 49,998 others\">\n<svg viewBox=\"0 0 520 646\"\n     xmlns=\"http://www.w3.org/2000/svg\"\n     font-family=\"'JetBrains Mono', monospace\"\n     aria-hidden=\"true\">\n\n  <defs>\n    <marker id=\"ah-gray-l\" markerWidth=\"9\" markerHeight=\"9\" refX=\"8\" refY=\"4.5\" orient=\"auto\">\n      <path d=\"M2,2 L8,4.5 L2,7\" fill=\"none\" stroke=\"#d0cdc8\" stroke-width=\"1.4\"\n        stroke-linecap=\"round\" stroke-linejoin=\"round\"/>\n    </marker>\n  </defs>\n\n  <!-- ══ QUERY BOX ══════════════════════════════════════════════════════ -->\n  <g id=\"g-query\" opacity=\"0\">\n    <rect class=\"nd-query\" x=\"20\" y=\"10\" width=\"480\" height=\"44\"\n      fill=\"white\" stroke=\"#e2e0dc\" stroke-width=\"1\" rx=\"0\"/>\n    <text class=\"t-muted3\" x=\"260\" y=\"26\" text-anchor=\"middle\" font-size=\"12\">QUERY</text>\n    <text class=\"t-primary\" x=\"260\" y=\"44\" text-anchor=\"middle\" font-size=\"14\" font-weight=\"500\">apple · orange</text>\n  </g>\n\n  <!-- ══ ARROW query → directory ════════════════════════════════════════ -->\n  <line id=\"ln-arrow\" class=\"ln-gray\" x1=\"260\" y1=\"54\" x2=\"260\" y2=\"64\"\n    stroke=\"#d0cdc8\" stroke-width=\"1\" marker-end=\"url(#ah-gray-l)\" opacity=\"0\"/>\n\n  <!-- ══ TERM DIRECTORY BOX ═════════════════════════════════════════════ -->\n  <g id=\"g-dir\" opacity=\"0\">\n    <rect class=\"nd-dir\" x=\"20\" y=\"70\" width=\"480\" height=\"90\"\n      fill=\"rgba(0,25,255,0.03)\" stroke=\"rgba(0,25,255,0.22)\" stroke-width=\"1\" rx=\"0\"/>\n    <text class=\"t-blue\" x=\"30\" y=\"86\" font-size=\"12\" font-weight=\"500\">term directory — in memory</text>\n    <!-- apple row highlight -->\n    <rect id=\"dir-hl-apple\" x=\"21\" y=\"91\" width=\"478\" height=\"13\"\n      fill=\"rgba(0,25,255,0.07)\" opacity=\"0\" rx=\"0\"/>\n    <text id=\"dir-apple\" class=\"t-muted2\" x=\"33\" y=\"100\" font-size=\"11\" opacity=\"0\">apple    → 0x1F3A</text>\n    <!-- learning row (plain) -->\n    <text id=\"dir-learning\" class=\"t-muted2\" x=\"33\" y=\"114\" font-size=\"11\" opacity=\"0\">learning → 0x8B11</text>\n    <!-- machine row (plain) -->\n    <text id=\"dir-machine\" class=\"t-muted2\" x=\"33\" y=\"127\" font-size=\"11\" opacity=\"0\">machine  → 0xC304</text>\n    <!-- orange row highlight -->\n    <rect id=\"dir-hl-orange\" x=\"21\" y=\"131\" width=\"478\" height=\"13\"\n      fill=\"rgba(0,25,255,0.07)\" opacity=\"0\" rx=\"0\"/>\n    <text id=\"dir-orange\" class=\"t-muted2\" x=\"33\" y=\"141\" font-size=\"11\" opacity=\"0\">orange   → 0x4F2A</text>\n    <!-- more terms -->\n    <text class=\"t-muted3\" x=\"33\" y=\"153\" font-size=\"10.5\">… 49,996 more terms</text>\n  </g>\n  <!-- ══ INDEX LABEL ════════════════════════════════════════════════════ -->\n  <text id=\"idx-label\" class=\"t-muted2\" x=\"260\" y=\"184\" text-anchor=\"middle\"\n    font-size=\"12\" letter-spacing=\"0.03em\" opacity=\"0\">term-major index — one block sequence per term</text>\n\n  <!-- ══ ACTIVE HIGHLIGHTS (behind blocks) ═════════════════════════════ -->\n  <rect id=\"hl-apple\" class=\"hl-active\" x=\"20\" y=\"196\" width=\"480\" height=\"52\"\n    fill=\"rgba(0,25,255,0.06)\" stroke=\"rgba(0,25,255,0.4)\" stroke-width=\"1.5\" rx=\"0\" opacity=\"0\"/>\n  <rect id=\"hl-orange\" class=\"hl-active\" x=\"20\" y=\"456\" width=\"480\" height=\"52\"\n    fill=\"rgba(0,25,255,0.06)\" stroke=\"rgba(0,25,255,0.4)\" stroke-width=\"1.5\" rx=\"0\" opacity=\"0\"/>\n\n  <!-- ══ BLOCKS ════════════════════════════════════════════════════════ -->\n  <g id=\"g-blocks\" opacity=\"0\">\n\n    <!-- Block 0: apple  y=196  ACTIVE -->\n    <g id=\"blk-0\">\n      <rect class=\"blk-fill\" x=\"20\" y=\"196\" width=\"480\" height=\"52\"\n        fill=\"white\" stroke=\"#e2e0dc\" stroke-width=\"1\" rx=\"0\"/>\n      <line class=\"blk-div\" x1=\"120\" y1=\"196\" x2=\"120\" y2=\"248\" stroke=\"#e2e0dc\" stroke-width=\"1\"/>\n      <text class=\"t-blue\" x=\"70\" y=\"227\" text-anchor=\"middle\" font-size=\"14\" font-weight=\"500\">apple</text>\n      <rect id=\"post-0a\" class=\"post-active\" x=\"130\" y=\"218\" width=\"60\" height=\"8\" fill=\"rgba(0,25,255,0.3)\" rx=\"0\"/>\n      <rect id=\"post-0b\" class=\"post-active\" x=\"198\" y=\"218\" width=\"60\" height=\"8\" fill=\"rgba(0,25,255,0.3)\" rx=\"0\"/>\n      <rect id=\"post-0c\" class=\"post-active\" x=\"266\" y=\"218\" width=\"60\" height=\"8\" fill=\"rgba(0,25,255,0.3)\" rx=\"0\"/>\n    </g>\n\n    <!-- Block 1: cat  y=228 -->\n    <g id=\"blk-1\">\n      <rect class=\"blk-fill\" x=\"20\" y=\"248\" width=\"480\" height=\"52\"\n        fill=\"white\" stroke=\"#e2e0dc\" stroke-width=\"1\" rx=\"0\"/>\n      <line class=\"blk-div\" x1=\"120\" y1=\"248\" x2=\"120\" y2=\"300\" stroke=\"#e2e0dc\" stroke-width=\"1\"/>\n      <text class=\"t-muted2\" x=\"70\" y=\"279\" text-anchor=\"middle\" font-size=\"14\" font-weight=\"500\">cat</text>\n      <rect class=\"post-inactive\" x=\"130\" y=\"270\" width=\"60\" height=\"8\" fill=\"rgba(61,58,54,0.15)\" rx=\"0\"/>\n      <rect class=\"post-inactive\" x=\"198\" y=\"270\" width=\"60\" height=\"8\" fill=\"rgba(61,58,54,0.15)\" rx=\"0\"/>\n      <rect class=\"post-inactive\" x=\"266\" y=\"270\" width=\"60\" height=\"8\" fill=\"rgba(61,58,54,0.15)\" rx=\"0\"/>\n    </g>\n\n    <!-- Block 2: dog  y=280 -->\n    <g id=\"blk-2\">\n      <rect class=\"blk-fill\" x=\"20\" y=\"300\" width=\"480\" height=\"52\"\n        fill=\"white\" stroke=\"#e2e0dc\" stroke-width=\"1\" rx=\"0\"/>\n      <line class=\"blk-div\" x1=\"120\" y1=\"300\" x2=\"120\" y2=\"352\" stroke=\"#e2e0dc\" stroke-width=\"1\"/>\n      <text class=\"t-muted2\" x=\"70\" y=\"331\" text-anchor=\"middle\" font-size=\"14\" font-weight=\"500\">dog</text>\n      <rect class=\"post-inactive\" x=\"130\" y=\"322\" width=\"60\" height=\"8\" fill=\"rgba(61,58,54,0.15)\" rx=\"0\"/>\n      <rect class=\"post-inactive\" x=\"198\" y=\"322\" width=\"60\" height=\"8\" fill=\"rgba(61,58,54,0.15)\" rx=\"0\"/>\n      <rect class=\"post-inactive\" x=\"266\" y=\"322\" width=\"60\" height=\"8\" fill=\"rgba(61,58,54,0.15)\" rx=\"0\"/>\n    </g>\n\n    <!-- Block 3: learning  y=352 -->\n    <g id=\"blk-3\">\n      <rect class=\"blk-fill\" x=\"20\" y=\"352\" width=\"480\" height=\"52\"\n        fill=\"white\" stroke=\"#e2e0dc\" stroke-width=\"1\" rx=\"0\"/>\n      <line class=\"blk-div\" x1=\"120\" y1=\"352\" x2=\"120\" y2=\"404\" stroke=\"#e2e0dc\" stroke-width=\"1\"/>\n      <text class=\"t-muted2\" x=\"70\" y=\"383\" text-anchor=\"middle\" font-size=\"14\" font-weight=\"500\">learning</text>\n      <rect class=\"post-inactive\" x=\"130\" y=\"374\" width=\"60\" height=\"8\" fill=\"rgba(61,58,54,0.15)\" rx=\"0\"/>\n      <rect class=\"post-inactive\" x=\"198\" y=\"374\" width=\"60\" height=\"8\" fill=\"rgba(61,58,54,0.15)\" rx=\"0\"/>\n      <rect class=\"post-inactive\" x=\"266\" y=\"374\" width=\"60\" height=\"8\" fill=\"rgba(61,58,54,0.15)\" rx=\"0\"/>\n    </g>\n\n    <!-- Block 4: machine  y=404 -->\n    <g id=\"blk-4\">\n      <rect class=\"blk-fill\" x=\"20\" y=\"404\" width=\"480\" height=\"52\"\n        fill=\"white\" stroke=\"#e2e0dc\" stroke-width=\"1\" rx=\"0\"/>\n      <line class=\"blk-div\" x1=\"120\" y1=\"404\" x2=\"120\" y2=\"456\" stroke=\"#e2e0dc\" stroke-width=\"1\"/>\n      <text class=\"t-muted2\" x=\"70\" y=\"435\" text-anchor=\"middle\" font-size=\"14\" font-weight=\"500\">machine</text>\n      <rect class=\"post-inactive\" x=\"130\" y=\"426\" width=\"60\" height=\"8\" fill=\"rgba(61,58,54,0.15)\" rx=\"0\"/>\n      <rect class=\"post-inactive\" x=\"198\" y=\"426\" width=\"60\" height=\"8\" fill=\"rgba(61,58,54,0.15)\" rx=\"0\"/>\n      <rect class=\"post-inactive\" x=\"266\" y=\"426\" width=\"60\" height=\"8\" fill=\"rgba(61,58,54,0.15)\" rx=\"0\"/>\n    </g>\n\n    <!-- Block 5: orange  y=456  ACTIVE -->\n    <g id=\"blk-5\">\n      <rect class=\"blk-fill\" x=\"20\" y=\"456\" width=\"480\" height=\"52\"\n        fill=\"white\" stroke=\"#e2e0dc\" stroke-width=\"1\" rx=\"0\"/>\n      <line class=\"blk-div\" x1=\"120\" y1=\"456\" x2=\"120\" y2=\"508\" stroke=\"#e2e0dc\" stroke-width=\"1\"/>\n      <text class=\"t-blue\" x=\"70\" y=\"487\" text-anchor=\"middle\" font-size=\"14\" font-weight=\"500\">orange</text>\n      <rect id=\"post-5a\" class=\"post-active\" x=\"130\" y=\"478\" width=\"60\" height=\"8\" fill=\"rgba(0,25,255,0.3)\" rx=\"0\"/>\n      <rect id=\"post-5b\" class=\"post-active\" x=\"198\" y=\"478\" width=\"60\" height=\"8\" fill=\"rgba(0,25,255,0.3)\" rx=\"0\"/>\n      <rect id=\"post-5c\" class=\"post-active\" x=\"266\" y=\"478\" width=\"60\" height=\"8\" fill=\"rgba(0,25,255,0.3)\" rx=\"0\"/>\n    </g>\n\n    <!-- Block 6: wolf  y=488 -->\n    <g id=\"blk-6\">\n      <rect class=\"blk-fill\" x=\"20\" y=\"508\" width=\"480\" height=\"52\"\n        fill=\"white\" stroke=\"#e2e0dc\" stroke-width=\"1\" rx=\"0\"/>\n      <line class=\"blk-div\" x1=\"120\" y1=\"508\" x2=\"120\" y2=\"560\" stroke=\"#e2e0dc\" stroke-width=\"1\"/>\n      <text class=\"t-muted2\" x=\"70\" y=\"539\" text-anchor=\"middle\" font-size=\"14\" font-weight=\"500\">wolf</text>\n      <rect class=\"post-inactive\" x=\"130\" y=\"530\" width=\"60\" height=\"8\" fill=\"rgba(61,58,54,0.15)\" rx=\"0\"/>\n      <rect class=\"post-inactive\" x=\"198\" y=\"530\" width=\"60\" height=\"8\" fill=\"rgba(61,58,54,0.15)\" rx=\"0\"/>\n      <rect class=\"post-inactive\" x=\"266\" y=\"530\" width=\"60\" height=\"8\" fill=\"rgba(61,58,54,0.15)\" rx=\"0\"/>\n    </g>\n\n    <!-- Block 7: zebra  y=540 -->\n    <g id=\"blk-7\">\n      <rect class=\"blk-fill\" x=\"20\" y=\"560\" width=\"480\" height=\"52\"\n        fill=\"white\" stroke=\"#e2e0dc\" stroke-width=\"1\" rx=\"0\"/>\n      <line class=\"blk-div\" x1=\"120\" y1=\"560\" x2=\"120\" y2=\"612\" stroke=\"#e2e0dc\" stroke-width=\"1\"/>\n      <text class=\"t-muted2\" x=\"70\" y=\"591\" text-anchor=\"middle\" font-size=\"14\" font-weight=\"500\">zebra</text>\n      <rect class=\"post-inactive\" x=\"130\" y=\"582\" width=\"60\" height=\"8\" fill=\"rgba(61,58,54,0.15)\" rx=\"0\"/>\n      <rect class=\"post-inactive\" x=\"198\" y=\"582\" width=\"60\" height=\"8\" fill=\"rgba(61,58,54,0.15)\" rx=\"0\"/>\n      <rect class=\"post-inactive\" x=\"266\" y=\"582\" width=\"60\" height=\"8\" fill=\"rgba(61,58,54,0.15)\" rx=\"0\"/>\n    </g>\n\n  </g><!-- /g-blocks -->\n\n  <!-- ══ SKIP INDICATORS ════════════════════════════════════════════════ -->\n  <!-- skip-0: cat -->\n  <g id=\"skip-0\" opacity=\"0\">\n    <text class=\"t-muted3\" x=\"490\" y=\"278\" text-anchor=\"end\" font-size=\"11\">× skip</text>\n  </g>\n  <!-- skip-1: dog -->\n  <g id=\"skip-1\" opacity=\"0\">\n    <text class=\"t-muted3\" x=\"490\" y=\"330\" text-anchor=\"end\" font-size=\"11\">× skip</text>\n  </g>\n  <!-- skip-2: learning -->\n  <g id=\"skip-2\" opacity=\"0\">\n    <text class=\"t-muted3\" x=\"490\" y=\"382\" text-anchor=\"end\" font-size=\"11\">× skip</text>\n  </g>\n  <!-- skip-3: machine -->\n  <g id=\"skip-3\" opacity=\"0\">\n    <text class=\"t-muted3\" x=\"490\" y=\"434\" text-anchor=\"end\" font-size=\"11\">× skip</text>\n  </g>\n  <!-- skip-4: wolf -->\n  <g id=\"skip-4\" opacity=\"0\">\n    <text class=\"t-muted3\" x=\"490\" y=\"538\" text-anchor=\"end\" font-size=\"11\">× skip</text>\n  </g>\n  <!-- skip-5: zebra -->\n  <g id=\"skip-5\" opacity=\"0\">\n    <text class=\"t-muted3\" x=\"490\" y=\"590\" text-anchor=\"end\" font-size=\"11\">× skip</text>\n  </g>\n\n  <!-- ══ I/O COUNTER ════════════════════════════════════════════════════ -->\n  <g id=\"g-io\" opacity=\"0\">\n    <text class=\"t-muted3\" x=\"490\" y=\"618\" text-anchor=\"end\" font-size=\"11\">I/O READ</text>\n    <text id=\"io-text\" class=\"t-blue\" x=\"490\" y=\"634\" text-anchor=\"end\" font-size=\"14\" font-weight=\"500\">0.5 MB</text>\n  </g>\n\n  <!-- ══ ANNOTATION ════════════════════════════════════════════════════ -->\n  <text id=\"g-annotation\" class=\"t-muted2\" x=\"200\" y=\"634\" text-anchor=\"middle\"\n    font-size=\"12\" opacity=\"0\">49,998 terms skipped</text>\n\n</svg>\n</div>\n</div>\n\n<script>\n(function () {\n  /* ── Dark mode ── */\n  function applyTheme(dark) {\n    document.documentElement.classList.toggle('dark', dark);\n  }\n  var params = new URLSearchParams(location.search);\n  if (params.get('theme') === 'dark') applyTheme(true);\n  window.addEventListener('message', function (e) {\n    if (e.data && e.data.theme) applyTheme(e.data.theme === 'dark');\n  });\n\n  /* ── Helpers ── */\n  function fade(el, to, dur) {\n    return new Promise(function (res) {\n      var from = parseFloat(el.getAttribute('opacity') || '0');\n      var t0 = performance.now();\n      (function tick(now) {\n        var p = Math.min((now - t0) / dur, 1);\n        var eased = p < 1 ? 1 - Math.pow(1 - p, 2.2) : 1;\n        el.setAttribute('opacity', from + (to - from) * eased);\n        if (p < 1) {\n          setTimeout(function () { tick(performance.now()); }, 16);\n        } else {\n          el.setAttribute('opacity', to);\n          res();\n        }\n      })(t0);\n    });\n  }\n\n  function wait(ms) {\n    return new Promise(function (res) { setTimeout(res, ms); });\n  }\n\n  /* ── Elements ── */\n  var gQuery      = document.getElementById('g-query');\n  var gDir        = document.getElementById('g-dir');\n  var dirApple    = document.getElementById('dir-apple');\n  var dirOrange   = document.getElementById('dir-orange');\n  var dirLearning = document.getElementById('dir-learning');\n  var dirMachine  = document.getElementById('dir-machine');\n  var dirHlA      = document.getElementById('dir-hl-apple');\n  var dirHlO      = document.getElementById('dir-hl-orange');\n  var lnArrow     = document.getElementById('ln-arrow');\n  var idxLabel    = document.getElementById('idx-label');\n  var gBlocks     = document.getElementById('g-blocks');\n  var hlApple     = document.getElementById('hl-apple');\n  var hlOrange    = document.getElementById('hl-orange');\n  var gIo         = document.getElementById('g-io');\n  var gAnnotation = document.getElementById('g-annotation');\n\n  var skipEls = [0, 1, 2, 3, 4, 5].map(function (i) {\n    return document.getElementById('skip-' + i);\n  });\n\n  /* inactive block groups: cat,dog,learning,machine,wolf,zebra = 1,2,3,4,6,7 */\n  var inactiveBlks = [1, 2, 3, 4, 6, 7].map(function (i) {\n    return document.getElementById('blk-' + i);\n  });\n\n  var activePostIds = ['post-0a', 'post-0b', 'post-0c', 'post-5a', 'post-5b', 'post-5c'];\n  var activePostRects = activePostIds.map(function (id) {\n    return document.getElementById(id);\n  });\n\n  /* ── Reset ── */\n  function reset() {\n    [gQuery, gDir, lnArrow, idxLabel, gBlocks, hlApple, hlOrange, gIo, gAnnotation]\n      .forEach(function (el) { el.setAttribute('opacity', '0'); });\n    [dirApple, dirOrange, dirLearning, dirMachine, dirHlA, dirHlO]\n      .forEach(function (el) { el.setAttribute('opacity', '0'); });\n    skipEls.forEach(function (el) { el.setAttribute('opacity', '0'); });\n    inactiveBlks.forEach(function (el) { el.setAttribute('opacity', '1'); });\n    activePostRects.forEach(function (el) { el.setAttribute('opacity', '0.3'); });\n  }\n\n  /* ── Reduced motion: show final state immediately ── */\n  var prefersReduced = window.matchMedia('(prefers-reduced-motion: reduce)').matches;\n  if (prefersReduced) {\n    [gQuery, gDir, lnArrow, idxLabel, gBlocks, hlApple, hlOrange, gIo, gAnnotation]\n      .forEach(function (el) { el.setAttribute('opacity', '1'); });\n    [dirApple, dirOrange, dirLearning, dirMachine, dirHlA, dirHlO]\n      .forEach(function (el) { el.setAttribute('opacity', '1'); });\n    skipEls.forEach(function (el) { el.setAttribute('opacity', '1'); });\n    inactiveBlks.forEach(function (el) { el.setAttribute('opacity', '0.28'); });\n    activePostRects.forEach(function (el) { el.setAttribute('opacity', '0.6'); });\n    return;\n  }\n\n  /* ── Animation loop ── */\n  var running = false;\n\n  async function animate() {\n    if (running) return;\n    running = true;\n    reset();\n\n    // 1. Fade in query box\n    await fade(gQuery, 1, 320);\n    // 2. Wait\n    await wait(300);\n    // 3. Fade in arrow1 (query → directory) + directory box together\n    await Promise.all([fade(lnArrow, 1, 200), fade(gDir, 1, 280)]);\n    // 4. Wait\n    await wait(200);\n    // 5. Fade in apple and orange rows\n    await Promise.all([fade(dirApple, 1, 200), fade(dirOrange, 1, 200)]);\n    // 6. Wait\n    await wait(300);\n    // 7. Fade in learning and machine (plain rows) + highlight apple and orange\n    await Promise.all([\n      fade(dirHlA, 1, 250),\n      fade(dirHlO, 1, 250),\n      fade(dirLearning, 1, 250),\n      fade(dirMachine, 1, 250)\n    ]);\n    // 8. Wait\n    await wait(400);\n    // 9. Arrow2 + index label\n    await fade(idxLabel, 1, 200);\n    // 10. Wait\n    await wait(200);\n    // 11. All blocks fade in simultaneously\n    await fade(gBlocks, 1, 260);\n    // 12. Wait\n    await wait(400);\n    // 13 + 14. Inactive blocks fade to 0.28, skip indicators fade in simultaneously\n    var dimPromises = inactiveBlks.map(function (el) { return fade(el, 0.28, 200); });\n    skipEls.forEach(function (el) { fade(el, 1, 150); });\n    await Promise.all(dimPromises);\n    // 15. Active blocks pulse (setTimeout-based to avoid WAAPI throttling)\n    await new Promise(function (res) {\n      var dur = 500, t0 = performance.now();\n      (function tick(now) {\n        var p = Math.min((now - t0) / dur, 1);\n        // Triangle easing: ramp up then down\n        var t = p < 0.5 ? p * 2 : (1 - p) * 2;\n        [hlApple, hlOrange].forEach(function (el) {\n          el.setAttribute('opacity', 0.06 + t * 0.09);\n        });\n        activePostRects.forEach(function (el) {\n          el.setAttribute('opacity', 0.45 + t * 0.25);\n        });\n        if (p < 1) {\n          setTimeout(function () { tick(performance.now()); }, 16);\n        } else {\n          [hlApple, hlOrange].forEach(function (el) { el.setAttribute('opacity', '0.06'); });\n          activePostRects.forEach(function (el) { el.setAttribute('opacity', '0.45'); });\n          res();\n        }\n      })(t0);\n    });\n    // 16. Wait\n    await wait(300);\n    // 17. I/O counter\n    await fade(gIo, 1, 200);\n    // 18. Wait\n    await wait(200);\n    // 19. Annotation\n    await fade(gAnnotation, 1, 200);\n    // 20. Wait then loop\n    await wait(3500);\n    running = false;\n    animate();\n  }\n\n  /* ── IntersectionObserver ── */\n  var page = document.getElementById('page');\n  var observer = new IntersectionObserver(function (entries) {\n    entries.forEach(function (entry) {\n      if (entry.isIntersecting && !running) animate();\n    });\n  }, { threshold: 0.2 });\n  observer.observe(page);\n\n  // Start if already visible on load\n  if (page.getBoundingClientRect().top < window.innerHeight) animate();\n})();\n</script>\n</body>\n</html>"
  },
  "title": "sprase-v3-skip"
}
```

Even after locating a term's posting data, not every block for that term may be worth loading. A term that appears in many documents will have many posting blocks. MaxScore pruning can skip the weaker ones, but applying that logic requires knowing each block's maximum possible score before deciding to load it.

V3 stores this information in a **metadata block** at the start of each term's data. Before any posting blocks are read, the search algorithm loads this metadata block (a small, fast I/O) and gets back a summary for every posting block the term contains: the range of document IDs in the block, the highest score any posting in the block achieves, and the number of postings. With that summary in hand, three classes of skip decisions happen without reading any posting data: metadata filtering, MaxScore pruning, and position skipping.

### **Compressing what's left**

The blocks that aren't skipped need to be as small as possible to reduce I/O and better fit in the memory cache during scoring. V3 applies two compression techniques that only work best with its term-major layout.

**Document ID compression.** Rather than storing each document ID as a full 32-bit integer, V3 stores only the _offset_ from the block's minimum document ID. The minimum ID is already recorded in the metadata block, so the decoder knows the reference point before reading any posting data. The maximum offset within a block determines how many bits each offset needs. For example, a block spanning documents 50,000 to 50,128 only needs 7 bits per offset rather than 32. SIMD operations pack and unpack each posting block’s values in a single pass with no branching. The result is 2–2.5× compression for tightly clustered blocks, and meaningful compression even for spread-out ones.

| Block Spread | Bits per ID  | Total |
| Tight (range < 256) | 8 | 256 B |
| Medium (range < 64K) | 16 | 384 B |
| Wide(range < 1M)   | 20 | 448 B |

A full posting block fits in a handful of L1 CPU cache lines.

**Per-term score quantization.** Scores are stored as single bytes mapped to a score range. Whereas V2 used one range across all terms in a document block to represent score, V3 uses each term's own range. This finer mapping explains V3’s increased recall, especially for BM25 models.

## **Finding the right heuristics**

The performance-critical decisions—window sizing, merge strategy, and scoring granularity—depend on hardware cache geometry and posting list density in ways that resist analytical derivation.How these parameters got tuned turned out to be the most unconventional part of the build, and in some ways more interesting than the structural changes it was optimizing.

The approach: property-based tests and recall benchmarks as the fitness function, with Claude driving hundreds of iterations testing algorithmic combinations, and a human reviewing direction and killing runs that were clearly going wrong. When a property test failed or recall dropped, the loop backtracked. When throughput improved with recall holding, the change was retained. The heuristics that shipped (window sizes tuned to L1 cache, adaptive behavior across posting densities) came from this iterative, empirical process. It worked because the test harness was rigorous enough to trust, as a false pass would have been worse than a slow run.

## **Results**

![](https://cdn.sanity.io/images/vr8gru94/production/f72c88bd3270bd4abcd7a1a0a3c603ad6583b8b8-1432x725.png)


Benchmarks run on MS MARCO, a standard retrieval corpus of 8.8 million documents using SPLADE embeddings (averaging ~45 non-zero query terms) and BM25 (averaging ~3 query terms). BM25 gains are larger because sparse query vectors reference fewer terms, leaving more of the index for V3 to skip.

### **Scanning under I/O pressure (from disk)**

This is the case V3 was built for: an index too large to keep resident, where every query pays in disk reads. Under V2, that meant reading the whole index on every query, no matter how few terms it touched.

| Metric | V2 | V3 | Change |
| Recall@100 | 1.0000 | 1.0000 | -- |
| Bytes loaded / query (p50) | 3,396 MB | 22.5 MB | 151x less |
| Latency p50 | 3,407 ms | 128 ms  | 27x faster |
| Latency p90 | 3,407 ms  | 252 ms | 14x faster |

V2's latency is flat across percentiles. Every query did the same work, so every query took the same time— a little over three seconds regardless of what was asked. V3 executes those same queries in a couple hundred milliseconds, scaling by the number of terms involved rather than the flat size of the index.

| Metric | V2 | V3 | Change |
| Recall@100 | 0.9840 | 0.9840 | -- |
| Bytes loaded / query (p50) | 714 MB | 0.5 MB | 1,428× less |
| Latency p50 | 715 ms | 6 ms | 119× faster |
| Latency p90 | 719 ms | 13 ms | 55× faster |

For a typical short query, **V3 reads 1,428× less data and returns in milliseconds**, because it pays only for the terms it contains. The recall a user gets back is identical.

The payoff shows up directly in production. One customer running billion-scale sparse queries migrated to V3 and went from multiple seconds per query to 40 milliseconds on the same hardware. For their users, that’s the difference between a search that stalls and one that feels instant. It’s also what makes billion-scale sparse retrieval affordable on shared serverless infrastructure, rather than something that demands dedicated hardware sized to the worst case.

---

## **Conclusion**

The document-major layout had one flaw that scale exposed: queries couldn't skip all irrelevant data. Every query read the entire index, so a two-term lookup costed as much as scanning everything. This worked well when indices could fit in memory.. Once they didn't, every query fell back to disk and the design didn't hold.

V3 reorganizes posting data around terms rather than document ranges:

- each term owns its blocks
- in-memory directory finds them with no I/O
- per-block metadata lets the algorithm skip what it doesn't need before reading it from disk
- per-term compression shrinks what remains

A full-index scan becomes a read that touches only the terms a query contains.

This algorithm runs in all of Pinecone's sparse indexes today (thanks to the compaction process, changing algorithms was a completely online procedure, without disrupting any ongoing operations). Queries pay for what they use, recall holds, and billion-scale sparse retrieval runs on shared serverless infrastructure alongside dense retrieval in a single pipeline.



