<?xml version="1.0" encoding="UTF-8"?>
<rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0">
  <channel>
    <title><![CDATA[Thomas Veasey - Elasticsearch Labs]]></title>
    <description><![CDATA[Articles and tutorials from the Search team at Elastic]]></description>
    <copyright><![CDATA[© 2026. Elasticsearch B.V. All Rights Reserved]]></copyright>
    <image>
      <title><![CDATA[Thomas Veasey - Elasticsearch Labs]]></title>
      <url>https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt1121c0bf0e8a6e65/6a88da6340a1841030ef456f/search-labs-thumbnail.png</url>
      <link>https://www.elastic.co/search-labs/author/thomas-veasey</link>
    </image>
    <link>https://www.elastic.co/search-labs/author/thomas-veasey</link>
    <atom:link href="https://www.elastic.co/search-labs/rss/author/thomas-veasey.xml" rel="self" type="application/rss+xml"/>
    <language><![CDATA[en]]></language>
    <lastBuildDate>Wed, 23 Sep 2026 07:16:30 GMT</lastBuildDate>
  <item>
    <title><![CDATA[Trust, but benchmark: How we let an AI agent optimize Elasticsearch]]></title>
    <description><![CDATA[We share how we built a harness that automatically identifies and implements optimizations in the Elasticsearch codebase.]]></description>
    <content:encoded><![CDATA[<p>Elasticsearch executes a diverse set of workloads, including sustained heavy index building and real-time search and analytics. Delivering excellent performance across the board requires going broad in coverage while simultaneously diving deep enough into the codebase to understand optimization opportunities for each workload. Traditionally, human attention has been the bottleneck in this process; there simply aren't enough engineering hours to scrutinize every hot code path looking for inefficiency across a large and evolving surface area.</p><p>However, with the rapid progression of coding agents, performance optimization has become a task we can tackle semiautomatically. Unlike many software engineering challenges, optimizing code offers a cheap and objective verifier. If you ask an AI model to make code faster, there’s a hard number at the end telling you exactly what happened, backed by profiling tools that explain why. This makes performance a perfect candidate for automation, provided you can actually trust the numbers.</p><p>If you simply point a coding agent at a benchmark, you typically get low signal-to-noise: wins that fall inside the variance of the environment, or variations caused by thermal throttling rather than better code. To capture optimizations that actually benefit Elasticsearch users, we had to bridge the gap between "checkable in principle" and "checked in practice." We built a highly trustworthy measurement loop: a <a href="https://en.wikipedia.org/wiki/Agent_harness">harness</a> that assumes the agent will be wrong a good fraction of the time but reliably catches and proves it when it’s right and then helps guide it where to look next.</p><p>Once the machinery is in place, the results speak for themselves. By letting this harness loose on the codebase, we've already begun uncovering meaningful wins across the stack. In part 2 of this post, we’ll dive into some examples it has found so far, including string conversion inefficiencies in Elasticsearch Query Language (ES|QL), an improvement to our NEON vector dot product implementation, and an upgrade opportunity for the gzip library we were using. In this part, we’ll take a look at the design choices we made and how they relate to the broader topic of effective harness development.</p><h2>The AI code optimization pipeline architecture</h2><p>The first step in any software engineering problem is to identify the correct high-level components. We made an architectural choice that turned out to be very helpful for this problem: separate understanding where opportunities exist from the loop making code changes. The agent starts with a real workload but only uses it to mine information about where to seek performance improvements. At this stage, it’s instructed to go broad and consider a range of performance-related signals. Once it has found and classified the hot spots, the agent reads the context of the code around them to understand the optimization opportunities. We use a separate task to condense the ranked list of hot spots into artifacts that a loop can iterate against in minutes: a microbenchmark that we prove exercises the hot path in its real operating regime. Finally, we use a proposer-verifier loop to actually make changes to the codebase to improve performance on the benchmark. This hands off to validation to assess the impact on real workloads at the end. Our CLI (<code>atune</code>) supplies the tools this process needs, and the rest is largely automated by a set of task-specific instructions.</p><p>For context, our high-level architecture looks like the following. Pink boxes are the humans, and teal boxes are the agent. There are three task types, one skeleton loop, and one referee.</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt0e561ef2d3b61421/6aa3e83fd1556f0fcc7269f8/unnamed.png" alt="AI code optimization pipeline: exploration to benchmark, human approval, exploitation, validation and PR review" /><p><em>An exploration task profiles a real workload and produces ranked opportunities; a human promotes one into an exploitation task, which iterates against an approved microbenchmark and commits each accepted experiment; a validation run on the real workload guards the result before a human reviews and opens the PR. Where no benchmark covers the hot path, a benchmark task authors one and a human approves it into a registry. A performance atlas informs every task and accumulates what each one learns.</em></p><h2>Why performance optimization suits autonomous agents</h2><p>Three properties make a task ideally suited for autonomous work, and it's worth being explicit about them because they provide a checklist you can use to evaluate automation candidates. You want:</p><ol><li><p>An objective verdict so that the agent can be held to something other than its own opinion.</p></li><li><p>A dense guiding signal so that it knows where to look next instead of guessing.</p></li><li><p>A bounded blast radius so that being wrong is affordable.</p></li></ol><p>Performance gives you all three. Benchmarks provide the verdict and profilers provide the gradient, while a rejected patch costs you wall-clock time rather than correctness. The change gets reverted, and the reason gets recorded. Life goes on. Regarding the first two, we've come to think the gradient matters more than the verdict. ″This got faster″ is binary, whereas a profile hints at what to try next. An agent can generate its next hypothesis conditioned on a rich guiding signal, the richer the better, rather than grinding through a list.</p><h2>Signals are what the agent gets to see</h2><p>A useful mental model is that the CLI is the agent's sensory apparatus. That changes how you design each command. Rather than exposing a capability, you design it to return a clear and concise answer to a question about the task at hand, and, when relevant, an explanation the model can reason over, instead of raw data it has to parse and interpret. These are the signals that the agent acts on, and we ended up with the following for our harness:</p><p><strong>Signal</strong></p><p><strong>Question it answers</strong></p><p>Facet-decomposed macro profile</p><p>Where in the code does real workload time go, per query type?</p><p>Allocation and lock sampling, in the same capture</p><p>Is the cost cycles, garbage, or contention?</p><p>Cost-composition classification</p><p>Is this in scope compute, other product code, GC, JIT tax, or parked threads?</p><p>Input-shape instrumentation</p><p>What does the workload actually feed this code?</p><p>Statistical verdict</p><p>Did this change help, at this measured noise floor?</p><p>Allocation-rate comparison</p><p>Did the new code end up allocating more?</p><p>Interpreted disassembly</p><p>Why did that result happen?</p><p>End-to-end A/B guard, with differential profile attribution</p><p>Did anything appear to break, and was it us?</p><p>Environment check</p><p>Is this machine even fit to measure right now?</p><p>Upstream duplicate search</p><p>Has somebody already reported or fixed this?</p><p>Four of these signals are worth dwelling on, because in each case the tool encodes a judgment that the agent would otherwise have had to keep making by hand.</p><p>Facet-decomposition is the clearest example. Blended CPU shares hide breadth: if you profile a mixed query workload, the grouping hash map insert and the percentiles sketch update can both show up as single-digit percentages of the total run time and look comparable. They aren't comparable at all, because the hash map insert is paid for by nearly every aggregation query, while the sketch update is only paid for when somebody asks for percentiles. So the profiler runs each named query facet as its own race, and every opportunity the agent records carries a breadth field (universal, broad, or narrow) and gets ranked by headroom × tractability × breadth. A universal 3% beats a narrow 10%. Putting the ranking function in the tool prevents it from having to be rediscovered on every run.</p><p>The cost-composition classification works as a router. Rather than handing the agent a flat top-N list of frame names, it buckets every sampled stack into ″in scope compute,″ ″other product compute,″ ″GC,″ ″JIT and safepoint overhead,″ ″off CPU waiting,″ and ″parked threads.″ Each of those buckets implies a different kind of investigation. If GC is above about 15%, the real target is allocation rate, and the CPU top frames will actively mislead you, because they show where objects were collected rather than where they were created. If JIT and safepoint overhead is above about 25%, you're looking at a ceiling rather than an opportunity, since no in-scope code change will move it. If threads are parked and core utilization is low, this is a concurrency problem and CPU flame graphs are the wrong instrument entirely. We wrote that mapping into the playbook as a table, so the model (even a cheap one) reads a profile the way that an experienced engineer would, rather than reaching straight for the top frame. The classification is a prior for forming a hypothesis, though, not a substitute for evidence, so the agent still has to cite specific frames when it proposes an experiment.</p><p>Interpreted disassembly is a tool we hadn't originally provided, but it most definitely earns its place. It helps answer <em>why</em>, the question that unblocks the next hypothesis. Flame graphs tell you where the time goes; they rarely tell you why a change made things worse. So <code>atune asm</code> runs the benchmark briefly with the JIT told to print the assembly for one hot method, captures both sides of the working-tree diff, reduces each to the final C2 compilation, normalizes the addresses, and diffs them. The diff alone would likely still be 4,000 lines of aarch64, so on top of it sits an interpretation layer: a per-mnemonic delta, a net instruction count, the compilation tier that was actually captured, and a vectorization signal that counts vector register references on each side and raises a warning when they're eliminated, halved, or narrowed from <a href="https://en.wikipedia.org/wiki/Advanced_Vector_Extensions">Advanced Vector Extensions</a> (AVX) to <a href="https://en.wikipedia.org/wiki/Streaming_SIMD_Extensions">Streaming SIMD Extensions</a> (SSE) width. The playbook then maps mnemonic patterns to causes:</p><p><strong>Pattern in the diff</strong></p><p><strong>Likely cause</strong></p><p><code>b.eq</code><code>/</code><code>b.ne</code> up, <code>csel</code> down</p><p>New unpredictable branches</p><p>Clusters of <code>str</code><code>/</code><code>ldr</code> against the stack pointer</p><p>The compiler ran out of registers</p><p>NEON loads replaced by scalar compares</p><p>The vector path degraded</p><p>In one experiment, the agent fused two <a href="https://en.wikipedia.org/wiki/Single_instruction,_multiple_data">SIMD</a> mask extractions into one, and the benchmark regressed by 26%. The vectorization warning explained it in about 10 seconds. Without that tool, the agent has a dead end and no working model of the machine; with it, it has a corrected model and several new ideas.</p><p>The fourth signal is less a single tool than a habit; the instruments check themselves. Core utilization is derived two independent ways, from sample density and from process sampling, so the two can be compared. The classification is rejected if the unclassified bucket exceeds a budget, on the grounds that a breakdown which can't account for its own samples shouldn't be reasoned over. The disassembly capture warns when the compilation it caught isn't the steady-state one. The upstream duplicate search is restricted to read-only commands, and that restriction is enforced by a test that greps the source, so no future edit can quietly reintroduce the ability to file anything. Each of these exists because a tool that can be confidently wrong is worse than a tool that is merely absent.</p><p>One small piece of design is worth highlighting as a specific instance of good return practice. <code>atune compare</code> returns 0 for improved, 1 for no change, 2 for regressed, and 3 for error, and the loop branches on this code. That means no parsing and no ambiguity about what the verdict was. Plus, no tokens are spent interpreting prose.</p><p>If you take one thing away from our CLI design, it’s a broader design principle. In a general setting, the interesting thing isn't the individual signals we found useful to understand performance; it's that the CLI is capturing and packaging the judgment of an experienced performance engineer into tools that return answers rather than raw data. Structurally imposing good judgment about the problem an agent is tasked with improves outcomes. The right CLI is as much part of that story as the instructions. Furthermore, tokens are saved by tools that return decisions and digests rather than data. That means a comparison verdict instead of raw JMH output; a triage summary sitting on top of a 4,000-line disassembly diff.</p><h2>From 20-second probes to hours of validation</h2><p>Building a verifier that you can afford to consult is a separate problem from building one that you can trust. This covers the affordability part. Or, if you like aphorisms, real workloads are where truth lives and where iteration goes to die. A macro profile takes 45 to 60 minutes, and an end-to-end validation run takes hours. But a microbenchmark takes minutes. That's why the exploration-then-exploitation split works; you go broad on the real workload once and then hand off to a microbenchmark that you can iterate against in minutes.</p><p>The catch is that the handoff is only sound if the microbenchmark exercises the hot path in a realistic operating regime; that is, with the right cardinality and right data distribution. If you get that wrong, your fast loop spins fast but in the wrong direction. Until we finalized the handoff procedure, we saw cases where the agent accepted changes on a benchmark whose key distributions happened to flatter it, and only the end-to-end run caught the problem.</p><h3>Handing off from exploration to exploitation</h3><p>The handoff between the two phases is structured rather than informal. An exploration task's primary deliverable is a set of opportunity records, and each one carries the scope paths that an exploitation task would be allowed to edit, a headroom estimate, a classification (constant factor, structural,</p><p>allocation, or concurrency), and the benchmark it would gate on (or an explicit "no benchmark coverage" flag, if none exists). Each also carries a narrow test pattern so that the correctness gate stays cheap. The test pattern field exists because of a specific incident; an exploration task omitted it, and the resulting exploitation task ran a very heavy test suite on every experiment. The fix was to change the upstream artifact rather than add an instruction downstream. This is a pattern we use repeatedly; make and record decisions as early as possible rather than re-derive them each time.</p><h3>Validating a new benchmark before it can gate anything</h3><p>Where benchmark coverage is genuinely missing, a dedicated benchmark task authors one, and that new benchmark has to pass validity checks before anything can rely on it. Two of them are mechanical: what fraction of the benchmark’s hot self-time comes from frames that actually appear in the production profile and whether the parameters fall inside the input shapes we measured. The third is a checklist that the agent has to attest item by item. It exists to avoid the JIT getting a simpler world than production.</p><ol><li><p>Inputs have to be reshuffled rather than fixed or sorted, so the branch predictor doesn’t get too good.</p></li><li><p>Results have to be consumed, or dead-code elimination deletes the thing that you meant to measure.</p></li><li><p>Inputs must not be compile-time constants, or they get folded away.</p></li><li><p>Call sites have to see roughly the product mix of types, because a monomorphic call site inlines, whereas a megamorphic one doesn’t.</p></li></ol><p>A human then approves it into a hash-pinned registry. Until that happens, it's inert, because task setup refuses any task citing an unapproved benchmark. We think of that approval as the strongest gate in the system, and it's deliberately placed. An approved benchmark can decide accept or reject in every future task, so it's the one place where we ask for a human signature on an artifact rather than on a decision.</p><h3>The validation ladder</h3><p>Underneath all of this sits a hierarchy of feedback mechanisms with the property that each rung is cheaper and weaker than the one below it, and the cheap tiers are for rejection only.</p><p><strong>Tier</strong></p><p><strong>Cost</strong></p><p><strong>Role</strong></p><p>probe</p><p>~20–60 s</p><p>Directionally right? Can never accept</p><p>codegen capture</p><p>~4 min</p><p>Why did that happen?</p><p>screen</p><p>~5–15 min</p><p>Cheap statistical filter</p><p>confirm</p><p>~20–60 min</p><p>The accept decision</p><p>end-to-end</p><p>hours</p><p>Regression guard, advisory</p><p>The asymmetry between accept and reject is doing real work here. A probe is a single paired fork, whereas the accept predicate requires a full confirm run with a matching calibration record. Agents are very good at telling believable stories; indeed, they're trained on many tasks judged by both LLMs and by humans, so being convincing is actively rewarded. We don't want an agent to be able to promote a cheap signal into a decision by being persuasive about it.</p><p>For this task benchmark, wall-clock is a real cost consideration. A confirm run might take an hour. So one has to weigh carefully all the costs involved when choosing the setup. A model that lands one hypothesis in four typically beats a cheaper one landing one in 10 by a margin on end-to-end metrics. The usual instinct to down-spec the model on a long-running loop is exactly backward here. When your loop has an uncertain outcome and significant costs beyond the tokens it consumes, you may well find yourself in the same situation.</p><h2>How do you know a performance improvement is real?</h2><p><em>Is it faster?</em> is a statistical question. So we made the accept predicate code rather than judgment and put it somewhere the agent can't bypass. There are four ideas in the accept decision, and in each case, the alternative we rejected is as informative as the choice we made.</p><h3>Forks are the statistical unit</h3><p>Each <a href="https://github.com/openjdk/jmh">JMH</a> fork collapses to its mean, and verdicts come from an exact <a href="https://en.wikipedia.org/wiki/Mann%E2%80%93Whitney_U_test">two-sided Mann-Whitney U test</a> at α = 0.05 plus a seeded bootstrap confidence interval over three to five fork means per side. The reason is that iterations within a fork share JIT and heap state and are therefore autocorrelated, so treating them as independent samples manufactures significance out of nothing. We rejected comparing single-run scores, which is pure noise, and iteration-level <a href="https://en.wikipedia.org/wiki/Student%27s_t-test">t-tests</a>, which can be confidently wrong. Seeding the bootstrap means that a rerun reproduces the verdict exactly because the agent needs to be able to tell the difference between a result that changed and a result that was never stable.</p><h3>Pair candidate and baseline in time</h3><p>The screen (three forks, optionally over a subset of parameters) exists only to kill bad hypotheses in maybe 10 minutes instead of an hour. The accept decision itself comes from a confirm run that measures candidate and baseline back to back using a stash-flip. In an unstable environment, thermal and background drift only cancels if both sides ran under the same conditions, so an hours-old baseline is really a different experiment.</p><h3>The noise floor is measured, not assumed</h3><p>The minimum effect size that a task will accept has to clear the A/A-calibrated coefficient of variation for that specific benchmark on that specific machine, and both the confirm run and the comparison refuse to proceed without a matching calibration record. A 1–2% improvement on a laptop is indistinguishable from noise, and because the floor varies by benchmark and by JDK, any global constant you pick will be too loose somewhere and too tight somewhere else.</p><h3>The accept rule is composite and deliberately conservative</h3><p>A parameter combination counts as improved only if p &lt; α, the effect clears the calibrated floor, and the confidence interval excludes zero. Overall acceptance then requires that nothing regressed (not the primary benchmarks and not the guards) and that at least one primary combination improved. The headline figure is the <a href="https://en.wikipedia.org/wiki/Geometric_mean">geometric mean</a> of the per-combination speedup ratios, which is always positive and composes across experiments, so a task's cumulative improvement is a meaningful number rather than a sum of incomparable percentages.</p><h3>What must not get slower</h3><p>Guards deserve a note of their own, because they answer a different question to the primary benchmarks; not <em>Did this get faster?</em> but <em>What must not get slower while it does?</em> The task definition lists them separately for that reason, and they're typically the operations and the input regimes that the change isn't aimed at. If you're optimizing insert throughput on a hash table, iteration is a guard and so is a collision-heavy key distribution. A change that improves the common case by weakening the hash function will look good on uniformly distributed keys while catastrophically degrading more adversarial inputs. We know that because it happened; the collision distribution was missing from the matrix that accepted one of our early experiments, and the task now carries a comment telling future readers never to drop it again for a hash-quality-sensitive scope. While guards cost wall-clock on every confirm, they also surface edge-case regressions, and omitting them can be much more costly in the long run.</p><p>Runtime isn't the only thing worth guarding, either. The confirm run also captures normalized allocation rate on both sides and flags any change that buys speed with more than about 15% extra garbage. That one is advisory rather than blocking, because sometimes the trade is the right one. However, it's the kind of regression a purely time-based accept rule would happily wave through but might raise a red flag to an experienced performance engineer with better understanding of the calling context.</p><h3>The end-to-end gate is one-sided and default open</h3><p>The end-to-end gate is a different statistical problem: small n, high noise, and a very strong prior that we should accept based on our microbenchmark results. Our first design treated accept and reject on an equal footing, and it produced multiple clearly spurious rejections, so the redesign is one-sided and default open. An operation is flagged only when the median regression exceeds the threshold and every candidate repetition is slower than every base repetition. That full-separation criterion is the nonparametric one-sided test at this sample size, and it's robust to the single outlier repetition that would occasionally fool us otherwise. A flag then also has to be corroborated against a differential CPU flame graph, where only a rise in the task's own in-scope CPU share counts as real. Near misses get reported for transparency but don't trigger triage, and nothing is ever auto-rejected; a flag is a request for human attention rather than a verdict. We have a final backstop which is the large suite of performance tests that we already run against Elasticsearch on a daily basis.</p><p>The one-sided gate lesson generalizes beyond benchmarking. A noisy gate should be one-sided and default open where there is strong prior reason to accept. A symmetric threshold on a noisy signal doesn't just cost you real wins, it teaches the loop to distrust its own instruments, and that’s a much more expensive failure.</p><h2>Exploration and exploitation need different permissions</h2><p>Exploration and exploitation might look like two phases of one activity, but they have different inputs (a macro workload versus a pinned scope) and different outputs (ranked opportunities versus commits). They also have different failure modes, which means they want different permissions. We made the split a first-class property of a task, which lets us enforce it; an exploration task literally cannot commit. The baselining, comparison, checkpointing, and validation CLI all refuse exploration tasks, benchmarking allows probes only, and every probe diff is always reverted. A broad, speculative survey is safe because nothing it does can edit the code.</p><p>A nice ancillary benefit is prompt focus. Each type reads one playbook, in full, with the others explicitly not loaded. If you try to write a single document covering both "find where the headroom is" and "land a validated win inside this scope", you get something that does neither well, because the instructions for good exploration (follow the profile, widen the net, a broad survey is preferred) are close to the opposite of the instructions for good exploitation (one hypothesis at a time, minimal diff, never widen scope).</p><h2>AI agent memory: Journals, knowledge bases, and postmortems</h2><p>Sessions are ephemeral, but what you can learn from them isn't, so the harness accumulates three durable assets, plus one disposable view derived from them. These are a journal of the code changes we’ve tried, a knowledge base of how the code performs, postmortems of when the harness failed and, because sessions can be stopped and resumed, a session summary. What makes them work together is a clean ownership rule about which kind of fact goes where.</p><p>The journal records what we tried and measured. It's append-only, one file per task, and one record per experiment, and it's written before the code is edited. Rejections carry a forward-looking note in the form "do not retry X because Y", which is probably the highest value line, because it's what stops the next session re-deriving a dead end. Records also carry the environment and the driving model, which means that hypothesis hit rates are comparable across models.</p><p>The knowledge base records how the code works and how it performs. It's an indexed collection of per-area summaries, each stamped with the commit it was written against, and every playbook ends with an upkeep step that appends whatever durable facts the run turned up. Because the performance characteristics of the JDK also change from time to time, for example, a new <a href="https://download.java.net/java/early_access/loom/docs/api/jdk.incubator.vector/jdk/incubator/vector/Vector.html">Vector API</a> might implement vector masking more efficiently on AArch64, findings from profile data are also tagged with the JVM version they apply to.</p><p>The postmortems record mistakes that the agent has made in the past, and they're indexed by symptom rather than by date. The question a session actually has when a number looks wrong is <em>Have I seen this shape of wrongness before?</em>, and a chronological list doesn't answer it. So the rows read like "validation fails on operations structurally unrelated to your diff", or "screen reports no matching combinations".</p><p>Keeping the journal and the knowledge base distinct sounds pedantic but isn't because without the rule, both of them turn into a diary that has a tendency to bloat the context window or miss critical information in context.</p><p>The disposable state is a session handoff, and our advice is to never write it by hand and to avoid asking a model for a session summary, if possible. Our harness regenerates a one-page digest mechanically from the journal, so a resumed session doesn’t spend time and tokens reconstructing where the task had got to. Because it’s derived rather than authored, it can’t drift from the record in the way hand-maintained content does. That’s also why it isn’t part of the audit trail; because it’s cheap to regenerate it from the journal.</p><p>Mechanical session handoffs point to two lessons, and they turn out to be the same one. Every time a person appears in the loop, it’s a source of friction and an opportunity for error. And every time you reach for a model, ask whether code can do the same job. This seems like an odd thing to advocate in a project whose primary premise is delegating to a model, but the habit is easy to fall into once you have one to hand. Judgment is expensive, wherever it happens to sit, so the person and model both have to earn their place on merit.</p><p>Two further things are critical for durable agent memory. The first is that knowledge rots, so you have to lint it. Elasticsearch's main branch moves daily, which means the knowledge base's file citations go stale, so one linter checks them. Another checks that every command, flag, and path cited in the agent-facing docs actually exists, that every postmortem is linked from the index, and that every task type has a playbook, and it runs as part of the test suite. Treating prose written for an agent as a testable artifact is the reason it stays true. The second is that loading discipline is half of memory. The instruction is to load the index and then the one or two summaries matching the task's scope; never to bulk-load the rest. Memory you can't afford to read isn't memory.</p><h2>Coding agent guardrails: Containment, scope, and stop conditions</h2><p>Elasticsearch is millions of lines of code, and an unscoped "make it faster" run against a codebase that size is unreviewable and unfalsifiable. It’s also expensive. The converse is that a verifier only protects what it can see, so we have to apply the same boundary to what it can change. The outcome is a task that fixes its goal, allowed paths, benchmarks, thresholds, and stop conditions before any code is edited, and none of those are things the agent may change during a run.</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt2bae81c11c37e29b/6aa3e867d1556f62a67269fc/unnamed.png" alt="Coding agent guardrails: human owns scope and thresholds, agent owns judgement, atune CLI enforces mechanically" /><p><em>The human owns the safety envelope, defining scope, thresholds, and stop conditions before any code is edited, and owning every handoff that crosses a trust boundary. The agent session owns judgment: reading profiles, forming one hypothesis at a time, editing inside scope. The atune CLI owns mechanical enforcement: scope checks, correctness gates, statistics, calibration, stop conditions, environment checks, and the signals. Persisted state is the record: a worktree pinned at a base commit, an append-only journal, and generated reports.</em></p><p>Containment happens in two layers. The first layer is coarse and task-agnostic; it’s a single static permissions file lets the session edit the per-task worktree (which includes a local branch of Elasticsearch, all journal entries, and CLI artifacts) and the knowledge base, and it denies the pristine Elasticsearch clone, the task definitions, the harness config, and the audit trail. The second is precise and per-task: a git-level scope check that runs over tracked and untracked files before any build, so an out-of-scope edit is rejected before it can be benchmarked or committed.</p><p>Two containment layers have a nice corollary. Widening the first layer to the whole worktree costs nothing, because nothing out of scope survives the second one. Coarse containment plus precise scope beats trying to make a single mechanism do both jobs, which is what we tried first and which left us with a permissions file that needed editing for every new task.</p><p>Stop conditions are mechanical. There's a maximum number of experiments and of consecutive rejections, along with a cumulative improvement target, and proposing a new experiment is refused once one of them fires. A human can override; the agent can't. That asymmetry is what makes the gates independent of which model happens to be driving.</p><p>Tests are add-only. New test files ship with the checkpoint, and modifying or deleting an existing test is blocked by the scope check. This closes the single most tempting shortcut in the entire problem space by construction rather than by instruction, which seems like the right way to handle any shortcut you'd otherwise have to keep asking an agent not to take.</p><p>The harness lives outside the code it optimizes. The Elasticsearch clone is a separate, gitignored directory, and each task gets a worktree pinned at a base commit. The payoffs compound; the subject repo stays pristine and upstream mergeable with nothing related to the harness leaking into a PR, and the audit trail is versioned independently of a codebase that moves daily. Also, tasks are isolated from each other and from any developer checkout. Targeting a newer Elasticsearch means creating a new task rather than repointing an old one, because that task's numbers are tied to its base. It also means that the harness is retargetable in principle, since the Elasticsearch-specific parts are configuration, knowledge base, and benchmark registry rather than architecture.</p><h3>The decisions the agent never makes</h3><p>The rule we settled on is that the agent runs the loops and a human owns every step that crosses a trust boundary. That is creating work, blessing a measurement instrument, publishing a branch, or acting outside the repo. None of those are in the agent's allowlist, and each has a reason worth stating:</p><ul><li><p>Humans have to sign off on the task because the thing being constrained can't set its own limits.</p></li><li><p>Calibrating the noise floor needs to be done once per benchmark, per machine, and by default is measured rather than assumed. However, it's rather expensive and we allow a human to override if they know the environment well.</p></li><li><p>Deciding what to optimize next by promoting an opportunity is a judgment and a new scope.</p></li><li><p>Since benchmarks go on to gate other tasks, we consider reviewing this artifact part of the correctness safety net.</p></li><li><p>We leave outward-facing actions, such as pushing a branch or filing an issue, to a human until we're confident in the process.</p></li><li><p>We allow actions to be forced, but the override has to sit outside the thing being overridden.</p></li></ul><p>How the human actions get surfaced in the workflow matters. The generated report and the session handoff both print the human actions currently due, at the moment they become due, rather than leaving them to be inferred from the playbook.</p><p>We're deliberately not taking a position on how permanent the manual processes are. The right amount of supervision for a new technology is an empirical question, and we'd rather measure it than argue about it. We’ve started with a relatively high degree of supervision because that's the cheap direction in which to be wrong (a gate you never needed is easier to remove than a regression you shipped) and because the harness makes the question answerable. Every gate is a named, logged transition, so over time we can see which of them ever changed an outcome and which only ever cost friction. In summary, measure first, and then refine.</p><h2>Building the harness is the same kind of loop</h2><p>A lot of the harness design didn't fall out of an initial design document. The signal set, the ranking function, the shape of a task, and the exact wording of a playbook rule each came from watching a run go wrong. If there's one piece of advice here that generalizes, it's to use the thing before it's ready and to instrument your own disappointment.</p><p>The clearest example is a rule we now call <em>distrust surprising results</em>. A validation run reported that every operation had regressed, the worst of them by 14.8%. It was wrong twice over. A target operation pattern had overmatched a completely different code path, and a stale output directory from an earlier run was being read alongside the new one. Offered a coherent story, the agent took it and reverted a change that was actually good.</p><p>What went into the playbook after that incident is not "be careful." It's a three-step check to run before acting on a surprising verdict:</p><ol><li><p>Trace the code path, and confirm that the thing which moved can even reach your diff.</p></li><li><p>Read the raw per-repetition data rather than the summary, and recompute one headline number by hand.</p></li><li><p>Compare the report's shape against a known good run, because a structurally different report implicates the pipeline rather than the code.</p></li></ol><p>Alongside that, there’s another important rule, which is if the agent concludes that the harness is buggy, it must <em>not</em> fix it mid-run, because a mid-run harness change makes every result in that run incomparable. It should journal the evidence and stop.</p><p>Improving the harness is itself a loop worth describing. Asking the model to review its own transcripts and the harness documents, and to propose the rule itself, usually works well. It's good at spotting where its own instructions were ambiguous, in a way that's hard to reproduce by rereading the instructions yourself. What makes that output useful is having somewhere for it to land: a terse rule in the playbook, the narrative in a dated postmortem, a symptom keyed index row, and a linter that keeps the citations honest.</p><p>Restraint turns out to be part of the same discipline. The design document carries an explicit list of extension points that we've deliberately not built, because the need for them is still speculative. That's the same "don't guess, wait for evidence" rule we impose on the optimization loop, applied to ourselves.</p><h2>How this applies beyond performance optimization</h2><p>A few of these themes aren't specific to performance work or to Elasticsearch.</p><p>Verifiable work is the current frontier. The same insight drives <a href="https://arxiv.org/pdf/2411.15124">reinforcement learning with verifiable rewards</a>, and it’s what <a href="https://arxiv.org/pdf/2506.13131">AlphaEvolve</a> is built around. The tasks agents are consistently good at are the ones that come with a cheap oracle (tests, compilers, benchmarks), and the interesting move isn't finding more such domains but manufacturing oracles for domains that lack them. Performance is an instructive case precisely because the oracle is, in some senses, obvious, and yet building it still took most of the engineering.</p><p>The referee pattern generalizes, too. Separating a fallible optimizer from mechanical enforcement is the same shape as sandboxed execution and policy engines: judgment in the model, invariants in code. The practical consequence is that the system's safety doesn't depend on which model drives it. A weaker model wastes benchmark time, but it can't corrupt the code or accept a bogus win.</p><p><a href="https://en.wikipedia.org/wiki/Goodhart%27s_law">Goodhart</a> is a standing adversary for any optimization task that uses an agent, and it has a <a href="https://arxiv.org/pdf/2209.13085">formal treatment worth reading</a>. An agent optimizes <em>exactly</em> what you tell it to, so the two-tier benchmark structure, add-only tests, benchmark approval registry, adversarial guard distributions, and a marker file that makes timing an instrumented build mechanically impossible are all one design theme wearing different clothes.</p><p>Tools are context engineering. The <a href="https://www.anthropic.com/engineering/effective-context-engineering-for-ai-agents">consensus is drifting away from "expose everything"</a> and toward a few well-shaped <a href="https://www.anthropic.com/engineering/writing-tools-for-agents">tools that return digests</a>: the principles of progressive disclosure, self-documenting interfaces, and verdicts rather than payloads.</p><p>Memory is becoming architecture. A rules file, per-task playbooks, a durable knowledge base, and an append-only journal form a hierarchy with different lifetimes, owners, and loading rules, and the hard part is eviction and staleness rather than storage; hence, the linters.</p><p>Finally, human-in-the-loop is a dial rather than a switch, so where it should sit is something to measure per domain rather than assert.</p><h2>What's in part 2 of this post</h2><p>The harness design is a set of hypotheses about what autonomous performance work needs, and the harness was built so that we could test them. Part 2 is that test: the first four PRs we raised using it, their gains, how many hypotheses it took to get each one, which gates actually caught something, and where the harness got in its own way. That includes the changes which didn't survive end-to-end validation, since as usual, the rejections are as informative.</p>]]></content:encoded>
    <link>https://www.elastic.co/search-labs/blog/ai-code-optimization-elasticsearch-agent-harness</link>
    <guid isPermaLink="true">https://www.elastic.co/search-labs/blog/ai-code-optimization-elasticsearch-agent-harness</guid>
    <category><![CDATA[Inside Elastic]]></category>
    <category><![CDATA[Agentic AI]]></category>
    <dc:creator><![CDATA[Thomas Veasey,Chris Hegarty]]></dc:creator>
    <enclosure url="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/bltf9efb1bf91e4491b/6aa3e56cd909f868f36d69a6/unnamed.png" length="0" type="image/png"/>
    <pubDate>Fri, 11 Sep 2026 00:00:00 GMT</pubDate>
  </item>
  <item>
    <title><![CDATA[17% faster search, zero config: auto-calibrating vector quantization in Elasticsearch]]></title>
    <description><![CDATA[Automatic calibration at merge time picks vector quantization parameters for each segment by predicting recall from a small sample. Here's how we built it into Elasticsearch's merge path.]]></description>
    <content:encoded><![CDATA[<p>Elasticsearch's <a href="https://www.elastic.co/search-labs/blog/diskbbq-elasticsearch-introduction">DiskBBQ</a> format (IVF clustering plus binary quantization, built for on-disk ANN search at scale) offers several knobs to shape the recall/cost tradeoff of an index. Automatic calibration seeks to optimize those knobs to achieve optimal performance.</p><p>In our <a href="https://www.elastic.co/search-labs/blog/vector-quantization-auto-calibration-elasticsearch">previous blog</a>, we laid out the statistical model behind that calibration: a manifold model for how nearest-neighbor distances scale with index size, a Gaussian error model for quantization noise, and a closed-form way to combine the two into an expected recall@k for a given rerank depth. If you haven't read it, the one thing you need going in is this: given a candidate quantization encoding and a rerank depth, we can predict recall@k without building an index and benchmarking it, by fitting two small models to a sample of the corpus.</p><p>In this post we’ll go through how to score candidate configurations cheaply and how to leverage that to make merge-time decisions that are themselves cheap, correct, and consistent across real, constantly-merging indexes. This led to some pretty impressive improvements: we see almost 17% average improvement in QPS across a broad range of datasets all while increasing recall (in one case by a factor of 3). What’s more you get this immediately by adding one line to your index options, <code>"auto_calibrate": true</code>, and our plan is to make this our default once it has had the chance to bake a bit.</p><h2>Why manual vector quantization tuning is unreliable</h2><p><code>bbq_disk</code> exposes several knobs: quantization bits for documents (1, 2, 4 or 7), a separate bit width for queries, an oversampling factor for reranking, and whether to <a href="https://www.elastic.co/search-labs/blog/elasticsearch-bbq-preconditioning-vectors">precondition</a> vectors before quantizing. None of these act independently, and their effect on recall depends on the data: a 4-bit/1-bit encoding might be plenty for one embedding model and clearly insufficient for another. A single index is also built out of many segments, merged over time, each with an eventually different vector distribution. Hand-tuning one configuration for an entire index is, at best, a compromise, which is the motivation for <a href="https://github.com/elastic/elasticsearch/pull/152894">automatic calibration</a>: let each segment have its own configuration, re-evaluated every time it is involved in a merge operation.</p><h2>How Elasticsearch runs auto calibration at merge time</h2><p>When a number of segments are merged and automatic calibration is enabled, Elasticsearch samples documents and queries from the vectors being merged and:</p><ol><li><p>fits the manifold model over a sequence of nested samples of the merged corpus;</p></li><li><p>fits the error model, predicting the quantization error's standard deviation for each candidate <code>(query bits, document bits, precondition)</code> combination;</p></li><li><p>sweeps candidate configurations in ascending cost order; the candidate encodings are <code>(1,1)</code>, <code>(4,1)</code>, <code>(4,2)</code>, <code>(4,4)</code> and <code>(7,7)</code> (query bits, document bits), each tried across oversampling factors of <code>1.25</code>, <code>1.5</code>, <code>1.75</code>, <code>2.0</code>, <code>2.5</code> and <code>3.0</code>;</p></li><li><p>estimates recall@10 for each candidate using the model described in our first post, and stops at the first (cheapest) configuration predicted to hit the target of 90% recall@10.</p></li></ol><p>The winning configuration (encoding, oversample factor, precondition flag) is stored directly in the segment's metadata, so it travels with the segment and is picked up automatically at query time unless a request explicitly overrides it.</p><p>Small segments skip this altogether: below 10,000 merged vectors, there isn't enough data to fit a reliable model, so Elasticsearch just uses the current DiskBBQ defaults (4-bit query / 1-bit document encoding, no preconditioning, 3x oversampling).</p><h2>How the vector quantization cost model works</h2><p>Following the principles described in our first post, we started by picking candidates with three nested loops that are essentially how you might imagine hand jamming a lookup table. Start with the quantization scheme as the outer loop, ordered cheapest to most expensive by document bits (<code>(1,1) → (4,1) → (4,2) → (4,4) → (7,7)</code>). Then we set the rerank depth within the middle loop, ordered shallow to deep (<code>1.25× → 3.0×</code>). Finally we set preconditioning within the inner loop (<code>off → on</code>).</p><p>That ordering has a cost model baked into it, it's just implicit rather than written down: exhaust every rerank depth at the current bit tier before ever trying more bits. Document bits were effectively the only resource priced as expensive; oversampling was treated as nearly free by comparison, since the sweep would always max out rerank depth on a cheap encoding before considering a pricier one.</p><p>The current implementation replaces that with an explicit, continuous cost function:</p>cost = document_bits + 1.3 × rerank_depth<p>Query bits still don't factor into cost at all, only document bits (which drive index size) and rerank depth (which drives how many candidates get rescored per query). Preconditioning also stays outside the formula: Elasticsearch runs the whole cost-ordered sweep once with preconditioning off, and only if nothing meets the recall target does it re-run the sweep with preconditioning on, treating it as a fallback lever rather than something priced bit-for-bit against the other two.</p><p>With this cost model, rerank depth costs noticeably more per unit than a document bit, so the sweep will often prefer stepping up a bit tier over pushing oversampling deeper.</p><p>The main reason for this is that once you're running in a serverless deployment, compute and storage are billed and scaled independently, on very different clocks. An extra document bit is mostly a one-time, indexing-time cost; it makes the segment marginally bigger on object storage, which is cheap and doesn't need to be pre-provisioned against a spike in query traffic. It does carry a smaller recurring cost too, since quantized vectors sitting in page cache or loaded for scoring take proportionally more RAM per document as bit width grows, but that scales linearly and predictably with corpus size, and doesn't spike with query load. Rerank depth is the opposite: it's a recurring, per-query cost. </p><p>Every extra unit of oversample factor means fetching and rescoring that many more full-precision candidate vectors from disk, on <em>every</em> search request, for as long as the index is queried. That's compute and DRAM pressure on the search-serving tier, which has to autoscale in close to real time to match query concurrency. It sits on the hot path of the latency-and-cost budget in a way storage capacity, and the RAM footprint of the bits themselves, does not. Weighting rerank depth higher than document bits in the cost formula is what makes the sweep reflect that asymmetry.</p><h2>Efficiently estimating vector quantization error</h2><p>The cost model above works with the premise that the recall estimate behind it is trustworthy. The manifold and error models need to be accurate for the recall assessment to be trustworthy. While the manifold model of the k-th to N-th nearest neighbors distance is cheap to compute, the standard deviation of the quantization noise for a given candidate encoding is a bit more expensive in principle.</p><p>DiskBBQ uses fixed count clusters to accelerate nearest neighbor queries. Our quantization procedure takes advantage of this by only quantizing the vector residuals from the cluster centroids. This means as the data scales, the magnitude of vectors we quantize relative to the various components of the similarity calculation shrinks. As such, quantization accuracy increases. We need to account for this when converting our sample estimates to the segment as a whole.</p><p>Clustering the corpus at several sample sizes and fitting how the error scales with cluster size requires re-clustering a real sample of the corpus at several different sizes and fitting a regression model to see how the error shrinks as the effective cluster size grows. We also add a conservative +3σ margin on top of the fitted estimate to guard against noise in the fit itself. This is accurate and appropriately cautious; however, while benchmarking on common dense retrieval datasets, we found that performing several hierarchical k-means passes per candidate was expensive.</p><p>To speed things up, we tried approximating residuals with a synthetic isotropic-Gaussian formula. Instead of clustering increasing-size samples, this approach generated synthetic residuals from the manifold model's local density estimate. It was fast and fit for background merges, with the full repeated clustering approach reserved for force-merges only. However, it turned out to inflate error when embeddings (residuals) are anisotropic (some directions carry a lot more variance than others). As a result, the estimated error could grow significantly on strongly anisotropic data (e.g., Fashion-MNIST-style image embeddings).</p><p>So instead we looked for a still fast but more accurate way of calculating residuals. We opted for using a single clustering pass over a smaller sample (2,048 vectors). The clustering runs once per merge and is then warm-started for every candidate encoding evaluated afterward, instead of re-clustering from scratch each time. To get the error's dependence on corpus size, which the baseline learns by re-clustering at multiple sizes, this approach instead reuses the manifold model's <code>invDim</code> as a <a href="https://web.stanford.edu/class/archive/stats/stats200/stats200.1172/Lecture17.pdf">plug-in</a> for that dependence, extrapolating from the single real measurement rather than fitting the size relationship separately. </p><p>We also trimmed the query sample used during calibration from 1,024 to 256 vectors, on the reasoning that a smaller sample is enough once the error is being measured from real data rather than synthesized (and validated by benchmarks). The net effect was comparable wall-clock cost to the synthetic residual formula it replaced, but grounded in real per-cluster residuals, accurate enough that force-merge and background merge could be unified onto one path.</p><p>As an example, we take five different benchmark datasets and calculate the quantization error <a href="https://en.wikipedia.org/wiki/Standard_deviation">standard deviation</a> (SD) by directly measuring the gap between exact and quantized dot products on a sample of real (or, for the synthetic residual formula, fabricated) residuals, then extrapolating that measurement to the full corpus size. They differ only in how much sampling and regression goes into that extrapolation: the multi-sample scaling fit sweeps fifteen sample sizes and fits how error scales with cluster size, the single-pass real residual measurement takes one larger real residual sample and reuses the manifold's intrinsic dimension to estimate the size dependency, and the synthetic residual formula skips real residuals altogether and samples from a synthetic Gaussian from the manifold's expected rank distance. We treat the multi-sample scaling fit as ground truth in this comparison because it's the most sample rich of the three, not because it's a zero variance measurement of the "true" corpus-wide error (it has its own sampling noise too). The table below summarises the methods and findings.</p><p>Method</p><p>How it works</p><p>Speed</p><p>Accuracy</p><p>When used</p><p>Multi-sample scaling fit</p><p>Clusters at 15 sample sizes, fits regression</p><p>Slow</p><p>	Gold standard</p><p>Ground truth baseline</p><p>Single-pass real residual</p><p>One clustering pass + manifold invDim plugin</p><p>Fast</p><p>Near gold standard</p><p>	Background + force merge</p><p>Synthetic residual formula</p><p>Gaussian from manifold density estimate</p><p>Fast</p><p>	Inflated on anisotropic data</p><p>Deprecated</p><p>In order to exchange methods, we only need to be confident that they agree. This question can be answered independently of the correctness of the actual estimates, which we verified in our <a href="https://www.elastic.co/search-labs/blog/vector-quantization-auto-calibration-elasticsearch">previous post</a> for the multi-sample scaling fit. The figures below report the predicted quantization SD and the predicted recall@10, which is influenced by how we estimate the error. We report the analytical recall the manifold model predicts as a function of the quantization parameters, given the estimated error distribution perturbing the true distance ordering. This way, we isolate the quantization error's effect on ranking from any separate recall loss the IVF index itself might introduce, which is a distinct error.</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt2ab129decffc7de5/6a6a33f15af6b78d878d6898/8e38157b97d3ab6ff0b8e711e7586c408e2368a8-2048x766.jpg" alt="Bar charts comparing vector quantization error estimation methods across five datasets for predicted recall and error std" /><p>The single-pass real residual measurement's calculated error SD is closer to the multi-sample scaling fit (our gold standard), with respect to the synthetic Gaussian residuals. Consequently, the predicted recall is closer when using the single-pass + manifold plugin method. Indeed, we found the models to be essentially interchangeable regarding the indexing decisions they lead to. Critically, we lower the calibration overhead by an order of magnitude.</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt92f5f35bd60f01ec/6a6a33f20a222b3ff8877f36/2198ab91820a1f90fc70005dc27d7ae95c7ddb91-1744x1170.jpg" alt="Bar chart comparing wall-clock calibration time across three vector quantization error estimation methods and five datasets" /><h2>Auto calibration overhead on indexing performance</h2><p>We compared the cost of auto calibration on indexing, when compared with ES defaults, over 18 public benchmarks. We noticed that more than 50% of the datasets report an auto calibration overhead below 2%. Three datasets report 16-27% overhead, while two datasets sit in the 31-35% overhead.</p><p>The merge overhead is larger for smaller datasets (Fashion-MNIST, FiQA) that get indexed in a few seconds; that is expected as the size of the vector samples being used for calibration is fixed and therefore more noticeable with tiny datasets. In fact, for larger datasets like DBPedia-Entity and HotpotQA (5M doc vectors) the overhead is sometimes not noticeable and within 11% in the worst case.</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt344490ae214fc84c/6a6a33f315fc5c197b9e4941/d73ffb22c77f669a0b4205cc2825bda7611494b9-1424x1256.jpg" alt="Bar chart showing auto-calibration indexing time overhead as a percentage across 18 vector quantization benchmark datasets" /><h2>What quantization parameters does auto calibration choose?</h2><p>Looking at the encoding auto-calibration landed on for each of the real datasets:</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt085423af7a4831fd/6a6a33f4f3dc0ea46a6b78a2/527d33a1ac915bd84700447a936cd0113e84a856-2048x996.jpg" alt="Auto-calibration quantization parameter choices across 18 datasets: document bit-width and oversample depth distribution" /><p>Query bits were 4 in every dataset. While query bits aren't priced into the cost formula, we still iterate through lower query bits first (e.g., at 1 bit doc vectors, we first evaluate recall for 1 bit query vectors, then for 4 bit query vectors); so it’s possible for some datasets to even choose symmetric 1-bit quantization. The center of mass is a 2-bit document encoding with somewhere between 1.5x and 1.75x oversampling; 4-bit only shows up for two genuinely harder datasets (Fashion-MNIST's image embeddings, GIST-1M), and 1-bit only for a handful of the text-embedding models that are most robust to quantization. In fact, our own models are among those that quantize best: we selected 1 bit documents for all three corpuses we tested with <a href="https://www.elastic.co/search-labs/blog/jina-embeddings-v3-elastic-inference-service">Jina v3</a>.</p><h2>Recall and QPS improvements from automatic calibration</h2><p>Auto-calibration is a broad win across the eighteen datasets: QPS improves in 15 of 18 cases (often substantially, double digits on about ten, and over +50% on FiQA GTE, Fashion MNIST, and Glove-200), and recall improves in 15 of 18 cases too, including a dramatic +295.7% rescue on Fashion MNIST. Most datasets see gains on both metrics simultaneously, and even the more modest cases still land solidly positive, recall improvements are commonly in the high single digits to double digits, QPS gains follow a similar pattern. Where either metric does dip, the drops are small and contained: the three QPS regressions all stay under 1.5%, and the three recall regressions all stay under 2%.</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/bltbb5e4929226cdbff/6a6a33f440a4946b5dca5c9e/517b55375a9a4bfb81ed2bcf8a2a24757f5b0373-2048x1140.jpg" alt="QPS and recall percentage change from auto-calibrated vector quantization vs Elasticsearch defaults across 18 datasets" /><h2>How to enable auto-calibrated vector quantization in Elasticsearch</h2><p>The feature is not enabled by default for now, and opt-in via <code>auto_calibrate</code> on <code>bbq_disk</code> index options:</p>"index_options": {
    "type": "bbq_disk",
    "auto_calibrate": true
}<p>With this set, you no longer need to guess at bits, oversampling, or preconditioning: each segment picks the cheapest configuration that's predicted to hit 90% recall@10 for its own vector distribution, and re-evaluates that choice every time it's merged.</p><h2>What's next for automatic vector quantization in Elasticsearch</h2><p>Our <a href="https://www.elastic.co/search-labs/blog/vector-quantization-auto-calibration-elasticsearch">first post</a> showed that recall could be predicted in closed form from a small sample. Turning that into something running inside a real merge path meant a second round of engineering decisions that the model itself doesn't answer: how to order a sweep over candidates so it's cheap in the common case, how to price oversampling against document bits given how each is actually paid for at query time, and how to estimate the error term itself cheaply without quietly wrecking its accuracy.</p><p>In the end, we have a feature that allows us to tailor indexing choices to the data characteristics, with less than 11% overhead to index time for large indices. This gives us the ability to accurately control recall while optimizing quantization and oversampling choices for query performance. We got an average increase of 16.7% in QPS when we enabled this feature compared to our previous default settings for DiskBBQ. All while reliably achieving our target recall. Taking away the configuration burden from the user actually allows us to make better choices; it is a win-win.</p><p>This is the beginning of a longer journey that we’re working on to bring automatic configuration based on a combination of better understanding of the operating environment and better understanding of the data characteristics. We look forward to sharing more of this work with you in the near future.</p>]]></content:encoded>
    <link>https://www.elastic.co/search-labs/blog/vector-quantization-auto-calibration-diskbbq</link>
    <guid isPermaLink="true">https://www.elastic.co/search-labs/blog/vector-quantization-auto-calibration-diskbbq</guid>
    <category><![CDATA[Vector Database]]></category>
    <category><![CDATA[ML Research]]></category>
    <category><![CDATA[Elastic Cloud Serverless]]></category>
    <dc:creator><![CDATA[Tommaso Teofili,Thomas Veasey]]></dc:creator>
    <enclosure url="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/bltcd9291668fb96d26/6a6a33f58c87dc83b00d067e/6f40d849745ffb10d753d47d76c12b4639213c90-2382x1326.png" length="0" type="image/png"/>
    <pubDate>Tue, 28 Jul 2026 00:00:00 GMT</pubDate>
  </item>
  <item>
    <title><![CDATA[How Elasticsearch detects multiple change points in time series with 0.99 recall]]></title>
    <description><![CDATA[ES|QL's CHANGE_POINT command finds structural shifts, variance changes and spikes in any metric in ~1ms, without tuning anything per series.]]></description>
    <content:encoded><![CDATA[<p>The current generation of agentic models are remarkably good system troubleshooters. Given a hypothesis and the means to test it, they can reason about a failing service much the way a seasoned SRE does: form a theory, look for corroborating evidence, discard it when the data disagrees, and narrow in on a root cause. Their main limitation is not their ability to reason but their reach: they can only investigate what their tools let them see.</p><p>This is where Elasticsearch earns its place in the stack. It is already where a great deal of operational telemetry lives (logs, metrics, traces, events), and it exposes them through an expressive query and aggregation layer. That makes it a natural tool for an agent debugging a live system: it can slice by attribute, aggregate over time, correlate across signals, and drill from a symptom down to the documents that produced it.</p><p>We've been building an agentic layer on top of Elasticsearch that continuously monitors a system and root-causes issues as they arise. A recurring primitive in that workflow is time series event analysis. For example, given an error rate, a p99 latency, a queue depth and a throughput counter, tell me whether something happened, what it was and when. A transient spike in errors, a regime change in latency, and a step up in CPU usage are the signatures of the underlying fault, and they're typically what an agent examines first as it forms and tests hypotheses.</p><p>Elasticsearch has shipped a single-change-point aggregation for some time. It answers "did this series change?" with one verdict and the most significant change it found. That's a good fit for a dashboard, but less so for an agent, which often wants to interrogate a long window and enumerate everything of interest in it: the error spike at 02:14, the latency regime shift at 02:30, the throughput dip while the pod was being rescheduled. So we upgraded the capability to detect and report multiple events of multiple kinds in a single series. At the same time, we took the opportunity to further harden it to work reliably against whatever telemetry the agent points it at. This post describes how it works.</p><h2>Why single change point detection isn't enough for agents</h2><p>Concretely, we want a single entry point that takes a numeric time series and returns a small list of interesting events, each with a type, a location, a significance, and some key characteristics. We care about three classes of event, because they map onto three different kinds of underlying fault:</p><p>Event type</p><p>What changes</p><p>Detection channel</p><p>Example fault</p><p>Structural change</p><p>Level (step) or slope (trend) shifts to a new sustained regime</p><p>Value channel</p><p>Config push doubles baseline latency; memory leak turns a flat curve into a ramp</p><p>Distribution change</p><p>Noise level (variance) shifts while the mean holds steady</p><p>Dispersion channel</p><p>Service responds erratically at the same average latency</p><p>Point anomaly</p><p>Isolated spike or dip against a stable background</p><p>Value channel (pulse detector)</p><p>Single burst of errors; one-minute throughput drop during GC pause</p><p>The hard part is not detecting any one of these on clean, well-behaved data. The hard part is doing it on arbitrary telemetry without per-series tuning. The agent does not know in advance whether the series it is examining is near-constant, smoothly drifting, <a href="https://en.wikipedia.org/wiki/Homoscedasticity_and_heteroscedasticity">heteroscedastic</a> (quiet in places and noisy in others), sparsely populated, or has a magnitude of . It is not scalable to hand-pick parameters for every series it needs to analyze. Whatever we build has to be robust to all of that while maintaining excellent recall and precision. If it fails to detect important events it runs the risk of missing key corroborating evidence for a working hypothesis. Conversely, an analysis tool that reports an event for every minor fluctuation will pollute the context the agent reasons over.</p><p>The design goals, in priority order, are: correct on diverse data out of the box, parsimonious (report only what matters), and cheap enough to run interactively across many series.</p><h2>How PELT and BIC power change point detection</h2><p>Change-point detection is a well studied field. The classical offline formulation searches for the segmentation of a series that minimizes a penalized cost: a per-segment goodness-of-fit term plus a penalty for each added break to stop the optimizer from putting a boundary between every pair of points. Solved naively, this is combinatorial, but PELT (<a href="https://arxiv.org/pdf/1101.1438">Pruned Exact Linear Time, Killick et al.</a>) finds the optimal partition in roughly linear time by using a dynamic program to prune candidate boundaries that can never be optimal. On the labeling side, comparing nested models by an information criterion such as the Bayesian Information Criterion (BIC) gives a principled, scale-aware way to decide whether a candidate break is really important and what sort of change it constitutes.</p><p>These are good building blocks, and we use them. However, the textbook recipe assumes more than telemetry gives you. It typically assumes a single change type (a mean shift), a known and stationary noise level, and reasonably benign numerics. Real telemetry violates all three: variance changes matter as much as mean changes, the noise level is unknown, often heavy-tailed and changing, and the data spans extreme magnitudes and degenerate cases, such as perfectly constant segments, that wreck an ill-conditioned polynomial fit or a fixed-variance cost. Most of the engineering I describe below is about closing that gap.</p><h2>Splitting one time series into three detection channels</h2><p>Rather than trying to find one detector that does everything, we run three focused detectors and then merge their findings. Two of the three are the same structural detector applied to two different views of the data, or channels.</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blted8fd73801bac283/6a6a33ed15fc5c4ade9e493d/02fa7892061a4343a7720c7269c8df82ef67603f-1508x1132.png" alt="Elasticsearch time series split into value and dispersion channels detecting step changes, spikes and variance shifts" /><p>Two detectors run on the value channel: a structural detector that flags step and trend changes, and a pulse detector that identifies point spikes and dips. The dispersion channel, a windowed measure of spread, is fed to a second copy of the structural detector, where a variance change shows up as a level shift and is relabeled a distribution change. The two channels are complementary by construction: a step is a level shift the value channel flags but only has a single large first difference the dispersion channel ignores, while a variance change is invisible to the value channel yet shows up clearly in the dispersion channel. A thin orchestration layer then merges and de-duplicates the streams.</p><p>Keeping the three concerns separate makes each one tractable. A mean-shift detector and a variance-shift detector pull in opposite directions if you try to fuse them; a point-anomaly detector and a regime detector need opposite robustness settings. Separated, each can be tuned to its job.</p><h3>PELT with a scale-free cost</h3><p>For the structural channel, we fit each candidate segment with a low-order polynomial (constant or linear) and score it with the profiled-variance Gaussian cost. If a segment  of length  has residual sum of squares , the cost is</p><p>This is the negative log-likelihood of a Gaussian segment after profiling out the variance, i.e., after substituting the maximum-likelihood estimate  back into the log-likelihood. The total objective PELT minimizes is the sum of segment costs plus a per-break penalty,</p><p>Here,  is the BIC complexity term (number of parameters times  where  is the number of values in the time series) and the scale factor lets us trade sensitivity against parsimony in one place.</p><p>Using the profiled cost rather than a cost against a fixed global variance is a deliberate and important choice. The global noise level of telemetry is unreliable: on a smoothly varying series the natural estimate (the spread of first differences) can collapse toward zero, and a fixed-variance cost then treats every wiggle as enormously significant and over-segments. The profiled cost depends only on the ratio , so it is invariant to the absolute scale and immune to that failure. A small floor on  keeps the logarithm finite, so a zero-residual segment is not rewarded without bound.</p><h3>Keeping the fit stable using robust local weighting</h3><p>Before PELT runs, we robustly down-weight points so that an excursion does not create spurious breaks or drag a segment boundary onto itself. Crucially, these weights enter into the weighted residual moments, so they shape not just the segment fit but its residual variance, and so the segment costs themselves. Each point gets a Cauchy weight, , of its residual  from a rolling-median baseline against a robust scale : points near the local median keep full weight and points far from it are progressively discounted. A nice side effect of measuring excursions from the median on a window centered on each point is that clean structural breaks do not get down-weighted at all, because the majority of values land on the same side of the break as the point whose residual is being computed. So a sustained regime keeps full weight, but a lone spike does not.</p><p>There is a nice justification for scoring a weighted Gaussian cost when what we really want is robustness to a heavy tail. The Cauchy weight is exactly the <a href="https://en.wikipedia.org/wiki/Iteratively_reweighted_least_squares">iteratively reweighted least squares</a> (IRLS) weight of its loss: , so the weighted normal equations  are identical to the Cauchy M-estimator's estimating equations . A weighted-mean (or weighted-line) fit at those weights is therefore a <a href="https://en.wikipedia.org/wiki/M-estimator">Cauchy M-estimate</a>, not a Gaussian one. The cost we actually evaluate inherits the same properties. Because  is concave in , its tangent at the current residual lies above it. This gives a pointwise bound  with  the weight at the tangent point; summing, the weighted residual sum of squares  is a tangent upper bound on the total Cauchy loss, touching it in both value and gradient at the weights' anchor point. Minimizing the weighted RSS is thus one step of a <a href="https://en.wikipedia.org/wiki/MM_algorithm">majorize–minimize scheme</a> that provably decreases the true Cauchy objective, and the profiled-variance cost we feed the BIC is that majorizer standing in for the Cauchy deviance. The only approximation is that we anchor the weights once, at the rolling-median baseline, rather than iterating IRLS to its fixed point; this is exact for the inliers that sit near the baseline, and correct for gross outliers, whose vanishing weight removes them from the cost wherever the bound is loosest.</p><p>Finally, the trick that makes this work on heteroscedastic data is that the residual is judged primarily against a <em>local</em> robust scale, not a <em>global</em> one: the MAD of residuals in the same sliding window. On a series that is quiet in one stretch and noisy in another, this means a spike in the quiet stretch that is multiple local sigmas is correctly suppressed. The local MAD can collapse on quiet stretches, so we use a backstop that is a fraction of a global composite of robust scales and a floor related to the quantization error for discrete series and numerical precision otherwise.</p><h3>From candidates to labeled events using BIC verification</h3><p>PELT gives a globally optimal penalized segmentation, so we take its boundaries as candidates and verify each one. For a candidate at index  we look at the window of length  spanning to its nearest neighboring candidates and compare a no-change null against step and trend alternatives by BIC,</p><p>where  counts the fitted parameters (the same parameter count as in the PELT penalty above). We map the BIC gain of an alternative over the null to a significance via , to turn a threshold into a decision boundary). We treat this  as a significance score for ranking and thresholding, not as a calibrated tail probability.</p><p>At this stage we allow higher-degree models to avoid splitting smoothly varying trends. These are problematic in PELT itself because it considers short segments, which they overfit. We keep the most parsimonious alternative that clears the significance threshold and survives a persistence check. The persistence check re-scores with the weights immediately around the candidate muted, and if the evidence collapses, the "change" was driven by a few extreme points – an excursion, not a regime change – and we reject it. The polynomial order is applied symmetrically to the null and to each side of the split, so the alternative is always the same model class merely split at the candidate, and therefore strictly more flexible. The whole process can be thought of as Bayesian model selection with a preference for the null.</p><p>When no candidate survives, we still say something useful: we report the series as "stationary" (best no-change model is a constant) or "non-stationary" (best model has a slope), with the trend direction. For an agent, "this series is cleanly trending up over the window" is itself a finding.</p><h3>Detecting distribution changes with a dispersion channel</h3><p>A variance change is indirectly visible to the mean channel – worse, the robust weighting there actively mutes the excursions that signal it. So we detect it on a separate dispersion channel and reuse the exact same structural detector, because on this channel a variance change is just an ordinary level change.</p><p>The channel is built from one sample per non-overlapping window. Within a window, we take the <a href="https://en.wikipedia.org/wiki/Interquartile_range">inter-quartile range</a> of the first differences, rescaled to a standard-deviation equivalent (), and pass it through :</p><p>Then . Three choices matter here. First-differencing cancels level and slope, so a mean step contributes a single large difference rather than inflating the whole window, and a steady ramp produces a flat channel. Non-overlapping windows keep the samples independent; overlapping windows <a href="https://en.wikipedia.org/wiki/Autocorrelation">autocorrelates</a> the channel and makes the segmenter over-detect. And the IQR is used rather than the median (which is too robust and will miss a window that is 40% noisy then flatlines) or the raw standard deviation (which is not robust enough since one spike's two large differences inflate the window). Because the dispersion channel is a fraction of the original length, the verifier there is restricted to a lower-order null so a genuine low-high-low variance bump is not absorbed.</p><p>The functional form  is worth dwelling on, because each part earns its place. The log makes the channel respond to ratios of noise level rather than absolute differences. Variance changes in telemetry are typically multiplicative: a regime is "twice as noisy". On a raw-scale channel, a doubling shows up as an enormous absolute jump at a high baseline and a negligible one at a low baseline, so an additive step-cost detector would find variance changes trivially in loud series and miss them in quiet ones. Under a log, a factor- change in scale is the same offset  wherever it occurs, which is exactly the additive-step behavior the structural detector is built for. The " is a soft floor. A bare  diverges to  as the scale goes to zero, which is precisely what happens on a near constant stretch, and would manufacture a huge spurious step at the first noisy window after it. Conversely,  is finite and smooth at zero, behaves linearly () while the noise is small, and recovers the multiplicative  behavior once the noise is appreciable. This gives graceful degradation instead of a singularity, and with no tuned epsilon to pick. Note that squaring the scale (using a variance instead) only doubles the dynamic range; it makes no difference to the detector either way, since  differs only by a constant the threshold absorbs.</p><h3>Detecting point anomalies as excursions from a local baseline</h3><p>Spikes and dips are detected as point excursions from the local rolling-median baseline. Working from the local residual rather than raw values means level structure is removed and smooth curvature is tracked; even for time series that change significantly the detector is sensitive to significant local deviations.</p><p>The pipeline is a generous proposer followed by a strict gate:</p><ol><li><p>Propose every point whose residual exceeds a threshold number of robust sigmas. The scale is the larger of the global first-difference noise (which stays meaningful on smooth data where most residuals are exactly zero) and a composite of robust scales of the residuals (which inflates once a frequent large-residual population appears).</p></li><li><p>Merge adjacent same-sign candidates into excursions, dropping any that span a full minimum segment: that is a regime, and is owned by the structural channels.</p></li><li><p>Rank and cap the excursions by peak <a href="https://en.wikipedia.org/wiki/Standard_score">z-score</a>, keeping the top , so a pathological series cannot drown the output.</p></li><li><p>Gate using one shared null: build a <a href="https://en.wikipedia.org/wiki/Kernel_density_estimation">Gaussian KDE</a> from the series with all of the retained excursions removed, and keep an excursion only if its peak's Bonferroni-corrected upper-/lower-tail probability under that null clears the threshold.</p></li></ol><p>Removing all the tested excursions from the single null at once is a key trick. The leave-one-out alternative – score each excursion against a null containing the others – lets the largest spike and dip mask everything else. Removing them together means several genuinely distinct excursions are each judged against the remainder and all survive, while a recurring population is still rejected.</p><p>The proposer and the gate ask deliberately different questions, and that distinction drives two further choices. The proposer works on residuals from the rolling median since it wants recall, and a residual is what tells you a point stands out from its local neighborhood. The gate is value-based: it asks "is this magnitude one we see at other times in the series?", so a spike to a level that recurs elsewhere — such as periodic batch jobs — is suppressed even though it is a large local residual. Those are the right semantics for an agent, but they expose a heteroscedasticity problem, because telemetry noise is almost always a function of magnitude. Periodic spikes can sit orders of magnitude above the background, and a single KDE bandwidth fitted to the whole value range is then far too narrow up in the high tail. So ordinary large values come back as significant, a steady source of false positives.</p><p>The fix is a <a href="https://en.wikipedia.org/wiki/Variance-stabilizing_transformation">variance-stabilizing transform</a>. We run the value gate in  space, where  is a robust measure of the spread of the background.  is linear for  and logarithmic for , which turns a multiplicative (magnitude-dependent) spread into a roughly constant one, so a single bandwidth is valid across orders of magnitude. It is also odd and finite at zero, so exact zeros and sign changes (dips below a small baseline) need no special handling, unlike a bare log. Crucially, it is monotone and so does not change what is tested (for any monotone function , ) so it only fixes the estimate of that tail probability.</p><p>One subtlety closes the loop. The KDE null and the kernel bandwidth are taken from different scales, on purpose. The null is the stabilized background values: so it models any mode in the data, which is what makes a recurring large magnitude unsurprising. However, the bandwidth is taken from the stabilized residuals, not the stabilized values, because a genuine level change makes the value distribution bimodal, and a bandwidth computed from that bimodal spread would balloon, masking a real spike sitting on top of a shifted regime. The residual removes the step, so the bandwidth always reflects within-regime noise and the gate stays sensitive to a deviation that is extreme relative to its own neighborhood if it is also outside the envelope for the series as a whole.</p><h3>Merging structural, distribution, and point anomaly events</h3><p>Finally, an orchestration layer merges the structural, distribution, and point-anomaly event streams. Structural and distribution events that mark the same regime boundary are de-duplicated to the more significant one (a boundary that shifts both level and spread is one event, not two). Pulses are a separate stream added after de-duplication, because a spike that lands on a structural boundary is a real, separate finding and must not be suppressed. Everything is then mapped back from the internal value-array index space to source-bucket indices.</p><h2>Handling extreme magnitudes and edge cases</h2><p>What makes this usable as an unattended tool is a collection of defensive choices for the cases that break naive implementations:</p><ul><li><p>Variance computed as  loses all precision at large magnitudes: a constant series at  can manufacture phantom change points purely from floating-point error. We center every PELT input by a constant offset first; the polynomial RSS is invariant to that shift in exact arithmetic, but the working magnitudes drop from  to .</p></li><li><p>Using raw indices as the regressor results in poor condition polynomial fits: the largest moment is , which is about  for a cubic over a 2000-point window. This trips the SVD singularity guard and silently degrades the fit. Mapping  affinely onto  leaves the fit identical (RSS is invariant under reparametrization) but every moment becomes .</p></li><li><p>Scale-free cost, as described, means the segmentation cost doesn't depend on tuning a noise estimate.</p></li><li><p>Down-weighting wants a primarily <em>local</em> scale: suppress whatever is anomalous in its own neighbourhood. It uses the maximum of MAD and a small global floor on the differences from the rolling median. Conversely, spike/dip detection wants a <em>global</em> scale: we care about global outliers. It uses a composite of robust scales of all differences. Using the wrong one in either place produces characteristic failures – irrelevant spikes in a quiet segment, or locally large excursions creating spurious breaks – and maintaining separate channels allows us to pick appropriately.</p></li><li><p>Using one p-value threshold, <a href="https://en.wikipedia.org/wiki/Bonferroni_correction">Bonferroni-corrected</a> by the number of candidates, applied consistently across all three detectors, means that "how surprised should I be" is consistent everywhere.</p></li></ul><p>The recurring theme is that the difference between a detector that works in a notebook and one that works on a firehose of production time series is mainly in handling the edge cases gracefully.</p><h2>Performance: ~1ms per series on a single core</h2><p>Detection cost is dominated by PELT. Its segment cost is a profiled-variance linear fit, which we evaluate in constant time from prefix-summed weighted moments rather than maintaining a regression per candidate boundary, so a single segment cost is a handful of array reads and a 2×2 solve. Cost grows a little faster than linearly with series length since PELT's pruned candidate set does not stay constant on noisy data. Therefore, to bound the worst case on very long series, we downsample ahead of detection: above a cap (2000 samples), the series is collapsed into macro-buckets, keeping two samples per bucket: the median and the largest local deviation. This is inspired by the <a href="https://www.vldb.org/pvldb/vol7/p797-jugel.pdf">M4 downsampling scheme</a>, but because we need only the median and the largest excursion for structural-change and outlier detection, respectively, we can then afford to double the bucket resolution. The downsampled series carries its original bucket indices, so every reported event still maps back to a real source bucket; below the cap it is a no-op. The whole analysis is a single pass over the (possibly downsampled) series with no per-series configuration. This lets the agent call it freely across many signals.</p><p>In absolute terms, this means the analysis is comfortably interactive. On a single core, post-warmup, a typical series of 140–350 buckets is analysed in about 1 ms (≈220,000 buckets/s), and a series long enough to hit the downsample cap (say 5,000 buckets, collapsed to 2,000) takes about 40 ms, which is the effective worst case per call. For an agent issuing a handful of these calls per investigation, and parallelising across the many series in a `BY` query, the latency is negligible.</p><h2>Evaluation on synthetic and production telemetry</h2><h3>Synthetic benchmark</h3><p>We evaluate first on a synthetic generator with known ground truth, because it lets us measure the things that matter precisely. The generator produces random time series with diverse behaviors, which we group into three families. The <strong>positive</strong> family injects known events of each type: clean and noisy step changes (up and down, SNR around 10, at several positions), trend onsets and ramps (including a flat–ramp–flat sequence with two boundaries), variance changes with a constant mean (single steps and a low–high–low bump), and isolated spikes and dips. The <strong>null</strong> family ideally produces no event: stationary noise, perfectly constant series, smooth quadratic drift and clean ramps, and periodic signals. (A variance change with a constant mean is not null: it is a distribution change, an abrupt step on the dispersion channel, and so it belongs in the positive family above. The related null requirement, that such a change does not surface on the value channel as a step, is checked separately.) The <strong>adversarial</strong> family stresses the robustness machinery: a perfectly flat series at a magnitude of  (for which a naive variance arithmetic manufactures phantom breaks here through catastrophic cancellation) and, conversely, a genuine spike or step in a noisy baseline as high as  (which must still be found and located, the high baseline notwithstanding); a spike on top of a step; a within-regime spike after a 100 level jump; a recurring train of equal peaks (a population, not individual spikes); wide sustained excursions (a structural change, not a spike or dip); and fuzzed random level-shift series. On these series we track recall per event type, precision and the false-positive rate on the null family, localization error, parsimony (events per series and adherence to the count limit), and invariance under constant offsets, rescaling and extreme magnitudes.</p><p>The table below summarizes what the suite tests.</p><p>Event family</p><p>Representative scenarios</p><p>Required outcome</p><p>Localization tolerance</p><p>Step</p><p>clean / noisy, up / down, single and multiple, several positions</p><p>detected</p><p>≤ 4–8 buckets</p><p>Trend</p><p>slope change and flat–ramp–flat, clean and in noise</p><p>detected</p><p>≤ 8–12 buckets</p><p>Distribution</p><p>variance step (mean constant), low–high–low bump</p><p>detected</p><p>≤ 1 dispersion window</p><p>Spike / dip</p><p>isolated, multiple distinct, on heavy-tailed and high-magnitude series, within-regime after a step</p><p>detected, capped at max(5, 2% of n)</p><p>≤ 2 buckets</p><p>Null series</p><p>stationary noise, constant, smooth drift / ramp, periodic</p><p>no event reported</p><p>n/a</p><p>Invariance / robustness</p><p>constant offset, rescaling, 10^5–10^9 magnitudes, spike-on-step, recurring population, wide excursion</p><p>result unchanged / no spurious event</p><p>n/a</p><p>Running this over 400 series per scenario, just over half a million buckets in total, and scoring with the same two categories we use for the real-data evaluation below (any regime change versus point spikes and dips) gives:</p><p>Event type</p><p>Recall</p><p>Precision</p><p>Median localization error</p><p>Structural change</p><p>0.994</p><p>0.664</p><p>0</p><p>Spike / dip</p><p>0.847</p><p>0.746</p><p>0</p><p>Two things stand out. When an event is detected, it is placed essentially exactly: the median localization error is zero buckets for both categories; and the point-wise accuracy, 0.998, is directly comparable to the 0.995 we report on real telemetry below: the overwhelming majority of buckets are correctly left unmarked.</p><p>The precision figures are lower than on real data, and understandably so. A sixth of this population is a <em>hostile</em> null family (periodic signals, smooth drift, clean ramps) chosen precisely because they tempt a detector into a spurious break, and every false alarm on them counts against precision. The resulting false-positive rate is 0.5% per bucket, with 15% of null series carrying at least one spurious event. We treat this as the conservative end of the range: on the real-telemetry mix below, where the null series are less adversarial, precision rises to 0.85 (structural) and 0.90 (spikes/dips). Finally, offset and scale invariance holds on all 400 series: the same events, to within a few buckets, whether the series is shifted by a constant or rescaled by up to three orders of magnitude.</p><p>That raw precision also misses how the result is consumed. The agent reads events most-significant-first, so a false positive only does harm if it outranks a genuine one, and by and large it does not. The median p-value of a true positive is about , against about  for a false positive: the genuine events are typically overwhelmingly more significant. Concretely, if we keep only the top  events per series by significance (with  the true count) precision rises to 0.94, and a randomly chosen true positive is more significant than a randomly chosen false positive 93% of the time. It is not a perfectly clean separation: a strong periodicity or a sharp curve genuinely can contain a significant-looking break, which is why that figure is 0.94 rather than 1. However, the ranking is reliable enough that an agent reading from the top, or applying a stricter significance cut-off, sees the real events first and the false alarms as a lower-significance tail. This is also why exposing the detector's significance to the agent (see <a href="https://www.elastic.co/search-labs/blog/change-point-detection-time-series-esql#whats-next-for-es|ql-time-series-analysis">What's next</a>) matters more than squeezing the raw precision higher.</p><h3>Real cloud telemetry</h3><p>To evaluate on real data, we scraped around 300 metrics from our production cloud environment. These cover HTTP status-code counts, failed memory allocations, memory usage, network usage, page faults, CPU usage, and throttling metrics, measured both per instance and aggregated across the fleet as a whole. Their values range over more than 12 orders of magnitude, and they display a variety of behaviors including ramps, periodicity, step changes, distribution changes, and trend changes. The figure below shows a sample of series together with the detections we make on them.</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blte0bc6fbc138cca1c/6a6a33ee065b162280702003/2c30fd3e2f97e32b5b47bf66a923f73b08ec015d-2510x1284.png" alt="Grid of 12 production cloud telemetry series with detected structural changes, distribution changes and anomalies marked by Elasticsearch's change point detector" /><p>To get a sense of the accuracy on this set data, we labeled a subset of 150 most interesting time series, marking the visually clearest features in each. This labeling is not necessarily optimized for our target use case, where false negatives are typically more problematic than false positives: an agent consumes these results as part of a broader investigation and can pull additional information to corroborate them. Even so, we find excellent agreement with human judgment on these series. Since each series comprises between 140 and 350 points, the point-wise accuracy, at 0.995, is extremely high: the great majority of points are correctly identified as neither a change, a spike, nor a dip. But the more telling metrics are recall and precision on the human-labeled points, shown in the table below. The human labels did not attempt to categorize each change, so we break the results down only into "structural changes" and "spikes / dips" — the same two categories, and the same point-wise accuracy and recall/precision metrics, as the synthetic benchmark above.</p><p>Event type</p><p>Recall</p><p>Precision</p><p>Structural change</p><p>0.94</p><p>0.89</p><p>Spike / dip</p><p>0.97</p><p>0.92</p><p>It is worth covering exactly why we get disagreements. These largely fall into three categories: spikes and dips in context, isolated breaks, and small-magnitude breaks in stable series. We deliberately do not try to detect spikes and dips that are unusual only in their immediate context; that is, not globally unusual but visually significant relative to an inferred periodicity in the data, for example. Trying to account for these without fully modeling the seasonality in the data hurt precision more than it helped recall, and we have a separate persistent anomaly-detection process that builds more complete models of baseline behavior over time. Isolated breaks are an artifact we decided to live with: PELT's cost function tends to isolate a change point with a few values intermediate between the two regimes, because absorbing it into either neighboring span inflates that span's cost. Humans are good at judging such situations visually and assign a single change point. Finally, small-magnitude changes are simply not visually obvious. We detect them deliberately and regard this as a strength of a quantitative approach, since they are often the early precursors of an incident whose later, larger effects drown them out.</p><h2>How the agent uses change point results in ES|QL</h2><p>To the agent, all of this is one tool call: it points it at a collection of time series and gets back a typed, located, ranked list of events. That list is small by construction, which means it drops cleanly into the model's context without crowding out everything else it is reasoning about. Because the result contains multiple events, a single call over a window can hand the agent the whole local story – "error spike at 02:14, latency regime change at 02:30, throughput dip at 02:31" – and let it correlate across signals to a root cause.</p><p>Operationally, we expose this through both the ES|QL <code>CHANGE_POINT</code> <a href="https://www.elastic.co/docs/reference/query-languages/esql/commands/change-point">command</a> and the <code>change_point</code> <a href="https://www.elastic.co/docs/reference/aggregations/search-aggregations-change-point-aggregation">aggregation</a>. Currently, we have not extended the <code>change_point</code> aggregation to return multiple change points since it breaks backwards compatibility of the output schema. It just returns the most significant event. We don't have the same restriction for ES|QL since it returns change points annotated onto the table rows to which they apply. We do plan to revisit the output schema for both ES|QL and the aggregation in a later version. We'd like to migrate to optionally returning significance in log-space, which doesn't underflow, and including a short verbal description of each change, which we expect to help agents when seeing just the change points themselves.</p><p>ES|QL is Elasticsearch's piped query language, and <code>CHANGE_POINT</code> runs the detector as one stage in a pipeline. Its <code>BY</code> clause enables it to analyze many series at once (one per group) so the agent can, in a single query, segment every service's latency or every host's error rate side by side rather than issuing a call per series. The actual leverage, compared to the <code>change_point</code> aggregation, is composability: the events come back as ordinary rows in the pipeline, so the agent then has the entire ES|QL language to manipulate them downstream. It can filter to a window, join change points against deploy markers, count events per service, rank by significance, feed the survivors into a further aggregation, and so on.</p><p>For example, suppose the agent wants to find out whether any servers have recently seen a sudden CPU spike or a prolonged step change in CPU usage over the last 12 hours, and whether that might point to a load-balancing issue. It could use the following query:</p><p>Here it's using a <code>STATS ... BY host.pod</code> to see how the detected events cluster across other dimensions of the data, such as the Kubernetes pod, and so judge whether they share a common cause.</p><h2>What's next for ES|QL time series analysis</h2><p>As far as detecting events of interest in time series, the foundation is in place: a single, robust, parsimonious tool that turns a raw telemetry series into the short list of events that actually matter, which is exactly the kind of reach an agentic SRE needs. Going forward, we plan to explore the best mechanism for feeding the detector's uncertainty to the agent, so that a borderline event can be flagged as "worth a second look" rather than silently included or dropped. Also, this is the first of several analytical tools we plan to build into the ES|QL query language to enable agents to triage and RCA issues more effectively; so stay tuned for further updates.</p>]]></content:encoded>
    <link>https://www.elastic.co/search-labs/blog/change-point-detection-time-series-esql</link>
    <guid isPermaLink="true">https://www.elastic.co/search-labs/blog/change-point-detection-time-series-esql</guid>
    <category><![CDATA[ES|QL]]></category>
    <category><![CDATA[ML Research]]></category>
    <category><![CDATA[Agentic AI]]></category>
    <dc:creator><![CDATA[Thomas Veasey]]></dc:creator>
    <enclosure url="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt097721c03648a84e/6a6a33ef0a222b4c70877f32/8f6b95800c65fe389d3e8d8281e8e8dc351f734d-992x342.png" length="0" type="image/png"/>
    <pubDate>Fri, 24 Jul 2026 00:00:00 GMT</pubDate>
  </item>
  <item>
    <title><![CDATA[How Elasticsearch auto-tunes vector quantization to hit your recall target]]></title>
    <description><![CDATA[Learn the geometric model that lets Elasticsearch predict recall with R² &gt; 0.98 accuracy and auto-select vector quantization parameters from a small data sample.]]></description>
    <content:encoded><![CDATA[<h2>What makes a good vector store?</h2><p>A vector store that achieves good performance without tuning is more valuable than one that requires expert tuning. In fact, our contention is a data store that can be coaxed to exceptional performance by an expert who spends a week hand-tuning it is less useful than one that beats it consistently out of the box. In other words, easily achieving good performance is a first class property, not a nice to have. We can see this clearly in our telemetry. The great majority of users will never tune the internals of vector search at all, and why should they: it is just an enabler for what they're trying to build.</p><p>This is the imperative behind features like auto-calibration. The system as a whole should look at your data and your quality target and choose good parameters for you. Indeed we think this is a win-win, since it has far more nuanced information available to it to make these choices than we expose.</p><p>To make "good performance" precise, it helps to name the three attributes that characterize any vector search system, because they trade off against one another and you can't talk about one without fixing the others:</p><ol><li><p>Performance: throughput (QPS), latency, and so on.</p></li><li><p>Hardware cost: a fair comparison always holds cost fixed. It's trivial to buy your way to more QPS or better recall by throwing hardware at the problem; the interesting question is what you achieve <em>per dollar</em>.</p></li><li><p>Search quality: recall, nDCG, and related measures of whether you're returning the right results.</p></li></ol><p>The three form a frontier. Push one and, at fixed budget, you pay in another. Any honest comparison of approaches pins two down and measures the third.  What we describe in this post is the mechanism we're introducing to pick quantization parameters for a fixed recall budget. It is a step on a longer journey towards a vector store that configures itself well across the board.</p><h3>Why recall is the right quality metric for vector search</h3><p>Search quality is tricky, because the "right" results depend on relevance labels you usually don't have at index time. So we lean on recall as a safe proxy. The argument is simple: recall measures how well the approximate index reproduces the results of exact search over the <em>same embeddings</em>. If recall is high, you have not degraded search quality relative to what the underlying model can do; you can be confident you’ve faithfully preserved the baseline. You might still wish for a better embedding model, we've got you <a href="https://www.elastic.co/search-labs/blog/jina-embeddings-v5-text">covered</a>, but that's a separate concern from the index not damaging what the model already gives you.</p><p>This is why controlling recall matters so much, and why you should be wary of any system that doesn't reliably control it. If a vendor can't control recall, they can silently degrade your search experience, achieving impressive QPS numbers while quietly returning worse results, and you'd have no way to know without a labeled evaluation set. The method in this post is about maximizing performance while keeping a firm, predictable grip on quality<strong>.</strong></p><h3>Why vector quantization parameters must be chosen at index time</h3><p>What makes the problem genuinely hard is that vectors are quantized <em>as they are indexed</em>, so the parameters that govern quality (how many bits, how deep to rerank, whether to <a href="https://www.elastic.co/search-labs/blog/robust-optimized-scalar-quantization">precondition</a>) have to be evaluated before we've seen the data laid out in its final form. We can't index everything, measure recall, and iterate; by then the quantization is baked in.</p><p>So we need to estimate what we'll need from a small sample, cheaply and in advance. Fortunately the Elasticsearch gives us natural moments to do this: segment merges are exactly such an opportunity. When segments are combined we have to rewrite the data anyway and can assess the data and (re)choose parameters. And as we'll see, models fit to small random samples give excellent estimates of the quantities we actually need to control. They’re typically good enough to set parameters once, with a small margin, and trust them as the index grows.</p><h2>How vector quantization affects nearest-neighbor recall</h2><p>With that motivation in place, let's start to dig into the details.</p><p>Vector quantization is a critical component for making approximate nearest-neighbor (ANN) search affordable at scale; it's an area we've <a href="https://www.elastic.co/search-labs/blog/scalar-quantization-optimization">innovated</a> in the past. Instead of storing and comparing full-precision embeddings, we store a lossy, compressed representation and search over that. The catch is the one above: lossy representations move distances around, so the "nearest" neighbors under quantized distances are not always the true nearest neighbors and recall suffers.</p><p>The standard fix is to over-retrieve and rerank. We use the cheap quantized distances to pull back the top  candidates, then recompute exact distances for those  and keep the best . As long as the true top- are present somewhere in the retrieved top-, reranking recovers them exactly.</p><p>Reranking isn’t free, we have to fetch high precision vectors from disk. However, we can precisely characterize the performance of reranking based on hardware characteristics alone. This reframes the whole problem. The question is no longer "how much does quantization distort distances?" in the abstract, but something which relates back to the attributes we care about:</p>Given a quantization scheme with some error magnitude, and a rerank budget of  candidates, what recall@ should we expect. As an immediate consequence, what is the <em>cheapest</em> set of parameters that hits our recall target?<p>This post derives a model that answers exactly that. The core of it is a single, surprisingly clean idea: if we can characterize the <em>distribution of distances to the </em><em>-th nearest neighbor</em>, and we have a model of the <em>quantization error distribution</em>, then we can compute expected recall after reranking in closed form (up to a one-dimensional integral). Everything else – bit counts, rerank depth, whether to precondition – becomes a search over a model we can fit cheaply from a small sample, instead of an expensive empirical sweep over full indices built with those parameters.</p><p>We build it up to this in three stages: the geometry of nearest-neighbor distances, the scaling law that falls out of it, and then the recall model that ties quantization error to recall given a reranking budget. Be warned, the following gets a little bit involved, but to give you intuition about what is happening see the video below.</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt78cc494d516dd35f/6a6119ed258cd202d9c16ec8/4c7a1499f27a3f9ed406e98565bdf8f9c6c7b823-900x506.gif" alt="Animation showing how vector quantization error displaces nearest-neighbor distances and how reranking to depth n recovers recall by re-scoring candidates with exact distances" /><h2>Quantization error vs. the nearest-neighbor distance gap</h2><p>Fix a query  and rank the database vectors by their true distance to it: , so  is the distance to the -th nearest neighbor. Reranking the top  succeeds for the true -th neighbor whenever it is not pushed past rank  by quantization noise.</p><p>Two competing quantities govern this:</p><ul><li><p>The quantization error that is essentially <em>fixed</em> for a given scheme and dataset: it depends on the embedding dimension, the vector distribution, and the number of bits, but not on how big the index is.</p></li><li><p>The criticality gap , which is the distance between the -th and the -th nearest neighbor. This is the margin we have to absorb error. Crucially, it <em>shrinks as the index grows</em>: pack more vectors into the same region and neighbors crowd together.</p></li></ul><p>There’s a detail here we’ll gloss over for the sake of presentation: for IVF style indices, we’re quantizing the residual from a cluster’s centroid. This does in fact couple the quantization error to the index size, but we can handle it much the same way we handle the distance to the -th nearest neighbor.</p><p>For reranking to recover the recall lost to quantization, we need the error to only rarely exceed the gap. If we can write down the distribution of  and the distribution of the error, we can make that statement quantitative. The first job is to estimate the distribution of nearest-neighbor distances.</p><h2>Deriving the nearest-neighbor distance distribution</h2><p>Real embeddings don't fill their ambient space; they concentrate on a lower-dimensional <a href="https://en.wikipedia.org/wiki/Manifold">manifold</a>. Near a query, though, we can make a mild local assumption: in a small neighborhood  around the query, the data density is roughly uniform. Here  is the intrinsic dimension of the manifold; it is unknown and generally far smaller than the embedding dimension. How to estimate it is the subject of Section 4.</p><p>Let  be the  vectors falling in , modeled as <a href="https://en.wikipedia.org/wiki/Independent_and_identically_distributed_random_variables">i.i.d.</a> uniform on , and define the distance from  to its nearest neighbor:</p><p>To get the distribution of  we use the standard order-statistics trick: rather than ask where the minimum is, ask for the probability it exceeds some radius . The event  is exactly the event that every point lands outside the -ball centered on the query .</p><p>A single point lands inside  with probability equal to the ratio of the ball's volume to the region's volume </p><p>where  is the <a href="https://en.wikipedia.org/wiki/Volume_of_an_n-ball">volume</a> of the unit -ball. (We assume  is large enough that the relevant  is small, so the ball doesn't spill outside  and boundary effects are negligible.) Because the points positions are assumed to be independent, the <a href="https://en.wikipedia.org/wiki/Survival_function">survival function</a> is</p><p>What we're really interested in is how R behaves on average. To compute this, we use the identity that the expectation of a non-negative random variable is the integral of its survival function, . Evaluating this with (2) gives the headline result:</p><p>(The exact integral carries an extra  factor; it's an  constant that we can fold into a fitted coefficient later, so we drop it here.)</p><h3>Glacial scaling: why neighbor distances barely change as your index grows</h3><p>It is interesting to consider what this formula tells us about how distances change with dataset size: . The exponent is , and in high intrinsic dimensions that is a <em>very</em> small number. This is a property the method leans on, so it's worth plugging in some numbers:</p><ul><li><p>If  then doubling  multiplies  by , so distances drop by ~30%.</p></li><li><p>If  then doubling  multiplies  by , so distances drop by a little over 1%.</p></li></ul><p>In high dimensions, neighbor distances barely move even if you add a lot of data; call it glacial scaling<strong>.</strong> It's the reason we can choose quantization parameters <em>once</em> from a tiny sample, with a small safety margin, and trust them to remain valid even after the index grows substantially before the next re-quantization.</p><h2>Expected distance to the k-th neighbor and the criticality gap</h2><p>We actually care about the whole sequence of order statistics , , not just the minimum. There's a simple way to get them.</p><p>Map each radius to the <em>cumulative volume</em> it encloses by defining</p><p>By (1), each  is exactly the probability of landing within radius , so the  are uniform on . The order statistics of uniforms are <a href="https://en.wikipedia.org/wiki/Order_statistic#Order_statistics_sampled_from_a_uniform_distribution">textbook</a>: the -th smallest of  uniforms follows a Beta distribution,</p><p>Inverting the volume map, , gives the scaling of the -th neighbor distance:</p><p>That's all we need for the expected gap:</p><p>The last form is the intuitive one: the gap between the -th and -th neighbors is the distance to the -th neighbor, scaled by . Widening the rerank depth  relative to  opens the gap; higher intrinsic dimension  closes it (the exponent  pushes  toward 1).</p><h3>Why the expected gap is sufficient to predict recall</h3><p>Working with an expectation is only legitimate if the gap doesn't fluctuate wildly around it. It doesn't because concentration of measure saves us. Applying the <a href="https://en.wikipedia.org/wiki/Delta_method">delta method</a> to  and using  from the Beta distribution, a little algebra gives</p><p>So the <a href="https://en.wikipedia.org/wiki/Coefficient_of_variation">coefficient of variation</a> is about . For any reasonable intrinsic dimension this is negligible, which justifies modeling only the expected distances. (If you're worried about the delta method approximation, you can check the results numerically: the delta-method variance and the resulting  coefficient of variation match the exact expressions to several significant figures.)</p><h3>Extending the model to cosine similarity and inner product search</h3><p>The derivation is for the Euclidean metric, but the other common metrics reduce to it:</p><ul><li><p>For cosine similarity, the equidistant surface is the intersection of a sphere around the query with the unit sphere. This is called a <a href="https://en.wikipedia.org/wiki/Spherical_cap">hyperspherical cap</a>, whose volume scales as  for small . Therefore, the analysis carries over unchanged up to constants, with the dimension reduced by one.</p></li><li><p>For MIPS (maximum inner product), some extra care is needed, because nearest neighbors aren't confined to a compact region. A distant vector can still win on inner product if its norm is large enough, so the gap is really governed by the tail of the norm distribution. However, there is a clean fix, which is to use the <a href="https://proceedings.mlr.press/v40/Neyshabur15.pdf">Neyshabur–Srebro transformation</a>. This lifts vectors onto a unit hypersphere in  dimensions. After this operation, it's just the cosine case.</p></li></ul><h2>Fitting intrinsic dimension and scale from a small sample</h2><p>Equation (3) has a known functional form but two unknown parameters: the intrinsic dimension  and the scale . Both are easy to fit, and it's more convenient to fit them from raw neighbor distances than from gaps directly.</p><p>Sample several subsets of database vectors  of sizes  and a set of query vectors . For each query  and each subset, measure , the distance to the -th nearest neighbor of  within . Taking logs of the scaling law  linearises it:</p><p>Specifically, this is linear in  and , so ordinary least squares recovers  and . Varying the subset size  is what makes it possible to estimate : it's precisely the rate at which distances shrink with data volume. With the fitted parameters, the whole-index expected gap is</p><p>Figure 1 shows how well this fits in practice (and it’s remarkably good): predicted versus actual average distance to the -th neighbor, across a range of datasets and metrics, have  between 0.996 and 0.999.</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/bltdf3e51da370c7099/6a6119eef2e1c4515ffd2a24/325063c65304dfbc414d211065073f6d3334864d-1622x1256.png" alt="Estimated vs actual nearest-neighbor distances across six datasets and metrics showing vector quantization distance model fit with R² between 0.996 and 0.999" /><h2>Modeling vector quantization error as Gaussian</h2><p>With the nearest-neighbor distance model established, the second component is the quantization error distribution. For every metric we use, the quantized distance estimate differs from the true distance by an error that is a sum of many independent per-dimension contributions. By the <a href="https://en.wikipedia.org/wiki/Central_limit_theorem">Central Limit Theorem</a> that sum tends to Gaussian, so we model the error as normal with a variance we estimate empirically:</p><p>where  is the quantized distance estimate using -bit vectors and  is the total number of (query, neighbor) pairs in our sample set. In other words: sample, quantize, measure the squared distance errors, average.</p><p>Figure 2 shows the empirical basis for the Gaussian assumption: measured quantization error densities against best-fit Gaussians across a variety of datasets. The fit is good, which is what lets the rest of the model stay analytic.</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt596d6c8d2f751972/6a6119ef61ff792ec9cd608a/d48a34c93bac53a94a1b164b60362f631285c472-1614x1270.png" alt="Vector quantization error density histograms across six datasets at 1-bit precision with Gaussian fits overlaid, confirming the Central Limit Theorem prediction used in the recall model" /><p>We could stop here and take a <a href="https://en.wikipedia.org/wiki/Minimax">minimax</a> view: threshold the probability that the -th and -th neighbors swap, using the expected gap (4) against the error scale . But that controls a worst-case event, and what we actually want to control is average recall. The outcome would be overly conservative quantization parameters and we'd pay some performance. The next section estimates expected recall properly.</p><h2>Predicting expected recall after reranking</h2><p>Combining the distance model and the error model gives a closed-form estimate of expected recall after reranking. Model the <em>noisy</em> distance of the -th true neighbor as a Gaussian centered on its true distance:</p><p>The -th neighbor survives reranking, i.e., lands in the retrieved top , if fewer than  other vectors have a smaller noisy distance. Condition on  and count the competitors closer than :</p><p>Then the probability of recalling neighbor  integrates over where its own noisy distance lands:</p><p>The terms of  are independent Bernoullis but not identically distributed, since every neighbor  sits at a different true distance , so each has its own probability of intruding on the top- set:</p><p>with  the standard normal CDF. This makes  a <a href="https://en.wikipedia.org/wiki/Poisson_binomial_distribution">Poisson-binomial</a> variable. Since we sum many of them (because ), the Lyapunov CLT applies and we approximate</p><p>with the standard Poisson-binomial moments</p><p>The survival probability then has a clean closed form:</p><p>This is where the two halves of the post so far finally meet. We don't need to know the individual  because the manifold scaling law from Section 3 supplies them: . So the moments become explicit sums over ranks, which we truncate at a safe cutoff (say , since distant neighbors contribute negligibly):</p><p>Finally, average recall@ given rerank depth  sums the per-neighbor recall over the top :</p><p>Here  is the standard normal density. Each integral is smooth and one-dimensional, so Gauss–Legendre quadrature evaluates it in microseconds. The entire recall prediction for a set of candidate parameters costs a handful of quadrature evaluations, not index build and benchmark run.</p><p>Figure 3 validates the end-to-end model: predicted average recall against measured recall across many parameter settings and multiple datasets has .</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt16ee340b47ddffa8/6a6119ef1f595ca8d3fe72eb/2b6663f167a1f9f08558610c71ca538eef257cb5-1464x1442.png" alt="Predicted vs actual average vector quantization recall across four datasets with R² = 0.982, validating the end-to-end recall prediction model" /><h2>How the recall model selects vector quantization parameters</h2><p>With a fast recall predictor available, parameter selection becomes a cheap ordered search. Given a target recall and a rerank budget  (typically expressed as a multiple of ), we can find the <em>minimum</em> document and query bit counts, and other knobs, that clear the target. There are a few things to note that are practically important:</p><ol><li><p>Glacial scaling gives us some safety because  moves so slowly with  for even moderate intrinsic dimension. A small margin in the calculation means the chosen parameters stay valid if a lot of vectors are added before parameters are restimated.</p></li><li><p>Small  is the worst case if  is a fixed multiple of . The gap \mathbb{E}[R_{(k)}]( is smallest for small  so if a parameter choice satisfies the recall target at  then it will for larger  will too.</p></li><li><p>We can treat quantization as a black box because the error model only needs the empirical error variance. This means we can test <em>any</em> configuration, including preconditioning, the same way and we can simply order candidate parameter tuples by increasing index and query cost, and stop at the first choice that hits the target recall. For tuples of (query bits, doc bits, rerank depth, precondition) a sensible search sequence increases query precision first, then document precision , , , , , , , ,  and  each combined (via an outer product ) with rerank depths like  and precondition , exiting as soon as the target is met.</p></li></ol><h3>Results: auto-selected quantization parameters and recall across datasets</h3><p>In this section, we discuss the results of the initial experiments on the end-to-end behavior. We’ve made some further refinements as part of the work to fully integrate with Elasticsearch that we discuss in our other post.</p><p>The table below shows auto-selected parameters targeting recall 0.97, measured with brute-force search, so the number reflects loss due to quantization <em>alone</em> (64 query clusters, targeting document clusters of size 384, which matches the settings of <a href="https://www.elastic.co/search-labs/blog/diskbbq-elasticsearch-introduction">DiskBBQ</a>).</p><p>Dataset</p><p>Query bits</p><p>Doc bits</p><p>Precondition</p><p>Depth</p><p>Recall</p><p>FiQA E5 small</p><p>4</p><p>2</p><p>false</p><p>30</p><p>0.97</p><p>FiQA arctic</p><p>2</p><p>2</p><p>false</p><p>30</p><p>0.95</p><p>FiQA GTE</p><p>2</p><p>1</p><p>true</p><p>30</p><p>0.98</p><p>MNIST</p><p>3</p><p>1</p><p>true</p><p>30</p><p>0.99</p><p>Fashion MNIST</p><p>3</p><p>1</p><p>true</p><p>30</p><p>0.99</p><p>Quora E5 small</p><p>2</p><p>2</p><p>false</p><p>30</p><p>0.99</p><p>Quora arctic</p><p>2</p><p>1</p><p>false</p><p>30</p><p>0.97</p><p>Quora GTE</p><p>1</p><p>1</p><p>false</p><p>30</p><p>0.98</p><p>Dbpedia E5 small</p><p>4</p><p>2</p><p>false</p><p>30</p><p>0.99</p><p>Dbpedia arctic</p><p>2</p><p>1</p><p>false</p><p>30</p><p>0.94</p><p>Dbpedia GTE</p><p>2</p><p>1</p><p>false</p><p>30</p><p>0.96</p><p>Wiki Cohere</p><p>2</p><p>2</p><p>false</p><p>30</p><p>0.99</p><p>Hotpot E5 small</p><p>4</p><p>2</p><p>false</p><p>30</p><p>0.97</p><p>Hotpot GTE</p><p>2</p><p>1</p><p>false</p><p>30</p><p>0.96</p><p>Glove 100</p><p>4</p><p>2</p><p>false</p><p>30</p><p>0.87</p><p>Glove 200</p><p>4</p><p>2</p><p>false</p><p>30</p><p>0.89</p><p>SIFT128</p><p>4</p><p>4</p><p>false</p><p>20</p><p>0.99</p><p>There are a few things worth highlighting:</p><ul><li><p>The recall is very sensitive to rerank depth. This is why we nearly always end up choosing the maximum depth available: a step up in rerank depth from 20 to 30 is typically what pushes us to hit the recall target for fewer bits and we prefer fewer bits. In the real system, we tuned this behavior based on a more representative reranking cost.</p></li><li><p>Glove underperforms partly we approximate the query distribution with random samples from the corpus, but Glove is also less well characterized by the model than the other datasets. A plausible explanation is that the approximately uniform local density assumption from Section 2 is less reliable for Glove embeddings, which would show up as higher recall variance between queries. However, Glove embeddings are not representative of the actual vectors we need to store.</p></li><li><p>The FiQA GTE preconditioning choice is a knife-edge case: preconditioning produced only a tiny expected recall improvement, but the prediction sat right at the recall cutoff and allows us to drop the query from 3 to 2 bits. If we'd rather only keep preconditioning where its benefit is clear-cut, we can enforce a minimum uplift threshold. This sort of fine-tuning of the decision logic leaves all the heavy lifting to estimate recall unaffected.</p></li></ul><h2>Key takeaways: auto-tuning vector quantization from first principles</h2><p>We presented a method to pick optimal quantization parameters to achieve a target recall. It rests on two models that compose cleanly:</p><ol><li><p>A geometric model of neighbor distances that follows from a local uniform density assumption. We use this to derive the nearest-neighbor distance, the  glacial scaling law of the expected distance, and the expected distance profile . We show that fitting  and  by a simple log-linear regression to average distances in small random samples from the corpus gives an extremely accurate predictive model.</p></li><li><p>A Gaussian quantization error model that is justified by the CLT. Its only parameter  is an empirical variance we estimate by comparing quantized and raw vector similarities for a sample of the corpus.</p></li></ol><p>Finally, we show that it is possible to feed the estimated distance model into a Poisson-binomial count of neighbors that intrude on the top- set. Applying the Lyapunov CLT the expected recall@ after reranking to depth  falls out as a one-dimensional integral we evaluate by quadrature.</p><p>The outcome is an accurate () predictive model of recall as a function of the quantization parameters. Choosing quantization parameters then becomes an ordered search with a predictive model telling us if we’ve hit the recall constraint. And nicely one that also comes with a built-in argument (glacial scaling) for why the chosen parameters remain safe even when estimated from a relatively small fraction of the data.</p><p>We’ve built this entire mechanism into Elasticsearch using segment merges as an opportunity to reassess our quantization choices. Aside from the peace of mind this brings (that you’ll achieve good recall whatever vectors you throw at it), it also allows us to chose near optimal parameters from a performance perspective. This closes the loop on our original objective: near optimal performance out of the box, at least as far as quantization goes. We’re pretty excited about the advantages that model based tuning can bring to vector search and look forward to sharing other work we have in this direction in the near future.</p>]]></content:encoded>
    <link>https://www.elastic.co/search-labs/blog/vector-quantization-auto-calibration-elasticsearch</link>
    <guid isPermaLink="true">https://www.elastic.co/search-labs/blog/vector-quantization-auto-calibration-elasticsearch</guid>
    <category><![CDATA[Vector Database]]></category>
    <category><![CDATA[ML Research]]></category>
    <dc:creator><![CDATA[Thomas Veasey,Tommaso Teofili]]></dc:creator>
    <enclosure url="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt950edbc25d291821/6a6119f01b1d495dc56f181b/31783975126874424fc20c3c96bd95fe28d5f201-1280x720.png" length="0" type="image/png"/>
    <pubDate>Tue, 21 Jul 2026 00:00:00 GMT</pubDate>
  </item>
  <item>
    <title><![CDATA[Cutting Elasticsearch DiskBBQ query quantization time by 5x]]></title>
    <description><![CDATA[See how asymmetric quantization cuts DiskBBQ query quantization overhead from about 20% to 4% with little recall impact.]]></description>
    <content:encoded><![CDATA[<p>Asymmetric quantization cuts the time Elasticsearch DiskBBQ spends quantizing queries by 5x. We discovered that too much time was spent quantizing queries. DiskBBQ started off quantizing queries with the same centroids as the indexed documents. However, we can make this cheaper by quantizing the queries with coarser-grained centroids. This improves query latency with very little observed recall impact in our tests.</p><h2>How DiskBBQ uses two centroid tiers for asymmetric quantization</h2><p>DiskBBQ now uses two centroid tiers (fine-grained document centroids and coarser query centroids) so queries are quantized once per parent centroid instead of once per document centroid.</p><p>The old mental model is "one centroid does everything for a posting list." The new model splits responsibilities:</p><ul><li><p>Document centroids (fine-grained): Still used for posting-list structure and document centering.</p></li><li><p>Query centroids (coarser): A parent centroid reused across multiple document centroids.</p></li></ul><p>So instead of quantizing the query independently for every document centroid we visit, we quantize per parent centroid and reuse that work across all of its children. Since we were already using two-tier clustering logic as the index size grew, it was a natural fit. We can reuse the work we already do during querying.</p><p>These images are a simple representation of our goal: Quantizing per centroid gives us overhead per centroid. Let’s get rid of it!</p><p>The goal is to significantly reduce the number of times we actually need to quantize a given query.</p><h2>The math behind asymmetric BBQ in Elasticsearch</h2><p>To center the data prior to computing quantized query and document vectors,  and , we rewrite the dot product  as  and expand. We can perform exactly the same operation but using different centroids for the query vector  and document vector . Specifically,</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/bltd01f666c5dfa4f24/6a4695b015103586c4202edb/f395654cf3cc965254dfc8d7e66b57743d29cf41-1566x206.png" alt="" /><p>As for standard Better Binary Quantization (BBQ), we quantize  and  in order to estimate the per (document, query) pair component of the dot product. The quantities   and  are scalars so just two extra additions per dot product we compute. For , we compute naturally when finding the nearest centroid. For , this can be stored with the quantized document vectors, which are just 4 bytes overhead. Below, we’ll discuss how to manage the other term on the fly.</p><h3>Asymmetric BBQ in DiskBBQ</h3><p>We cluster the document centroids (using k-means, for example) into  clusters, for  and  the query and document centroid count, respectively. This means there’s a many-to-one mapping from document centroids to query centroids. We’ll denote the document centroids by their index  and define this mapping to the query centroids as →.</p><p>Since there’s a unique query centroid for each document centroid, we only need to cache one value for  per quantized document vector, that is, for each document vector  in posting list , we need to cache  with the quantized document vector.</p><p>When we come to compute the dot products between a query and the document vectors in a cluster, we look up the quantized query vector corresponding to  and we compute  once and use it to process the whole posting list. The quantization process is significantly more expensive than computing the dot product, so this is a big net win.</p><p>The  term is estimated using the usual <a href="https://www.elastic.co/search-labs/blog/scalar-quantization-optimization">BBQ machinery</a>, that is, these vectors will be quantized and the dot product value estimated from the quantized vectors. Then we can use (1) to compute the final dot product estimate. Notice that this means we only need to quantize the query at most  times. Furthermore, we typically visit many centroids from the same parent centroid in a search because they’re close to one another.</p><h3>Euclidean distance corrections for asymmetric quantization</h3><p>For Euclidean, we can write  and treat the  term exactly as above. In fact, there’s a slightly nicer form. Substituting, we have that:</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/bltf7cc1ab13a3dc512/6a4695b274bff735d8a05b44/8faf7cd146c3221f3a3929e07286ceb82ac95a04-1598x122.png" alt="" /><p>We can rewrite this as follows:</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/bltce5ca0055196ceff/6a4695b62d406b3c77ba2bac/91d700d38d0e7842f2efef5d7778a6a34142c384-1172x362.png" alt="" /><p>The corrective terms are the norm of query vector  minus the document centroid , the norm of the document vector  minus the query centroid , and the norm of the difference of query and document centroids. As before,  can be stored as a single float with each document.</p><h2>What changed in DiskBBQ indexing and scoring</h2><p>At indexing/merge time, centroids can be clustered into parent groups when centroid count is large enough. Posting metadata moved from "centroid ordinal + centroid score" to a shape that explicitly carries query-centroid ordinal and document-centroid score. That decoupling is what lets scoring read documents and query centering from different places. For Euclidean, let’s break it down further by our mathematics above:</p><p> &lt;- This is the distance from a “query vector ” to “document centroid ”. We already gather this when we find the nearest centroids during querying. No new work.</p><p> &lt;- This is the distance from “document vector ” to “query centroid ”. However, recalling our <a href="https://www.elastic.co/search-labs/blog/scalar-quantization-optimization">original quantization work</a>, this can simply replace a previously stored float value. No new storage is required.</p><p> &lt;- This is just the distance between query centroid  and document centroid . This is just a single extra floating point value per postings list.</p><p>The practical change for dot product spaces is even simpler; the only correction value change is  being stored instead of .</p><p>These changes don’t introduce new computation costs and marginally reduce storage costs because we no longer quantize queries with document centroids. Those raw centroids don’t need to be present with the posting lists.</p><p>One cost we did add is a small cache of quantized query values. This is to account for clustering edge cases. For example, it's possible that query  is very close to query centroid  but not quite as close as . That said, the actual nearest three document centroids could have a relative order: . So, to prevent the query from being quantized twice, we keep a limited cache of the most recent quantized values for a given query.</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt1aeb75c07434e77f/6a4695b9c71ec47ccbb9846c/d0df2be8b601fb46005667bfa81fc89b2fdaee48-1538x1092.png" alt="Diagram showing a blue circle labeled “q” connected by colored arrows to two dashed oval regions. The green oval contains orange circles labeled dc_0–dc_2 and a green diamond labeled qc_0; and the purple oval contains pink circles labeled dc_3–dc_5 and a purple diamond labeled qc_1. Arrows illustrate relationships between q and the cluster components." /><p>Here’s a visualization of the situation described above. In the typical iteration scenario, we don’t want to risk unnecessarily quantizing the query against the same query centroid multiple times.</p><h2>DiskBBQ asymmetric quantization: performance results</h2><p>The flame graphs below show a before and after comparison. Before, about 20% of the time was spent quantizing queries when we visited each cluster. After our adjustment, it dropped to about 4%.</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt8e2b9d73c7ab998a/6a4695bc91c425d3b5732cb1/a17c3618a29b83d4196088d8422a7ede6eba5c3e-1999x655.png" alt="Flame graph showing computational costs using symmetric quantization, with stacked colored blocks labeled for Elasticsearch and JDK vectorization functions. Each block’s width represents relative processing time, and the tooltip highlights quantization activity within Elasticsearch query code." /><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt7bfd2b99e2fec276/6a4695bf5f1d903f44e3ba97/bb47abf8a7c7da5f5e8990beaaf3319741abfc52-1999x661.png" alt="Flame graph showing reduced computational time spent on quantization after introducing asymmetric quantization, with stacked colored blocks labeled for Elasticsearch and JDK vectorization functions. Each block’s width represents relative processing time, and a tooltip highlights quantization activity within Elasticsearch query code." /><p>Of course, the bulk of the cost is still just scoring the vectors in each cluster. But every little bit helps.</p><p>Here’s a better view of the full end-to-end performance and recall. The data set was 1 million <a href="https://github.com/iai-group/DBpedia-Entity/">DBpedia</a> docs encoded with the <a href="https://huggingface.co/thenlper/gte-base">GTE-Base</a> model. Here, “sec” indicates the number of clusters per secondary (parent) cluster. Note that symmetric quantization is still impacted by the secondary cluster size as it also impacts the two-tier clustering indexing we do already.</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/bltefa647d89e1eb323/6a4695c1a3096d631b9ce7bf/92ec17f1d4ca18a98d3c20b430af72cdf8d8be8a-1260x900.png" alt="Line chart titled “Latency vs Recall Pareto (sec = 16),” comparing asymmetric and symmetric quantization. The blue asymmetric line shows higher recall at each latency value than the red symmetric line, indicating improved latency with minimal recall impact. Axes are labeled “Latency (ms)” and “Recall.”" /><p>However, the impact on our current index structure is still dominated by centroid scoring and scoring vectors in the cluster. Asymmetric quantization removes a frustratingly expensive part of our scoring overhead, but the impact isn’t dramatic given our current structure.</p><h2>What's next for DiskBBQ quantization</h2><p>This simple piece of mathematics decouples our query quantization from our document quantization, giving us better storage efficiency and faster queries. This is in Elasticsearch Serverless now and will be in Elastic Stack version 9.4.0.</p><p>This now means that query quantization time isn’t a direct concern for future decisions. We can make larger index changes without worrying about the consistent overhead of quantization directly with document centroids.</p><p>This was a nerdy one. I hope you survived all the math (and that I copied it all down correctly). It’s always fun to be able to tackle complex problems with simple mathematics, and the results are actually positive in real use cases and data.</p>]]></content:encoded>
    <link>https://www.elastic.co/search-labs/blog/diskbbq-asymmetric-query-quantization</link>
    <guid isPermaLink="true">https://www.elastic.co/search-labs/blog/diskbbq-asymmetric-query-quantization</guid>
    <category><![CDATA[Vector Database]]></category>
    <dc:creator><![CDATA[Benjamin Trent,Thomas Veasey]]></dc:creator>
    <enclosure url="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt9fe4355576b40f9a/6a4695a774bff7d11ba05b40/265ce999fd38f21943d91e29c0bc49ab01f0196d-1999x1546.png" length="0" type="image/png"/>
    <pubDate>Wed, 27 May 2026 00:00:00 GMT</pubDate>
  </item>
  <item>
    <title><![CDATA[Fast approximate Elasticsearch ES|QL - part II]]></title>
    <description><![CDATA[Explaining the approach we use to obtain fast approximate Elasticsearch ES|QL queries and the testing we did of error estimation.]]></description>
    <content:encoded><![CDATA[<p>As we discussed in our <a href="https://www.elastic.co/search-labs/blog/fast-approximate-esql-part-1">previous blog</a>, we’re introducing fast approximate <a href="https://www.elastic.co/docs/explore-analyze/query-filter/languages/esql">ES|QL</a> <code>STATS</code> queries, which will be available in version 9.4 of Elasticsearch and the Elastic Stack. This feature allows users to estimate an expensive analytics query, often orders of magnitude faster than running the full query, by relaxing the constraint that it returns the exact value. We believe this has many uses; for example, we’re planning to integrate it into Kibana to obtain fast chart previews where possible.</p><p>In order for you to be able to trust our estimates, we provide error estimates. Furthermore, since there are edge cases in error estimation, we certify when the estimated value and error are trustworthy. In this blog post, we’ll dive into the theory for approximating and estimating the error in such queries, as well as discuss the testing we’ve done.</p><h3>Background</h3><p>In order to estimate ES|QL <code>STATS</code> queries efficiently, we make use of a property that’s shared by many statistics: Their estimates computed from a large number of independent samples from a dataset approach their true value. In the case of an index with some field  we can think of the true value of a statistic as its value computed for a random variable with uniform discrete distribution on . In the following we denote this quantity ; it can be things like <code>AVG</code>, <code>MEDIAN</code>, and so on. If we make  independent draws from , denoted , such that each value is selected with probability , we have  independent copies of this random variable. The property we rely on means that a sample statistic value  computed from  approaches  as  becomes large. For example, if  is the mean of some metric values then  as  becomes large. Indeed, for many statistics the limiting error distribution is known to be normal. Furthermore, it only depends on the distribution of , the size of the sample  and the type of the aggregation . This means supported <code>STATS</code> queries can be approximated with fixed accuracy independent of the index size .</p><p>It is easy to pick values at random from a Lucene index: create a filter that takes exponentially distributed jumps through the dataset, where the expected jump size is controlled by the desired sample probability. The AND of this filter and any other Lucene query can be performed extremely efficiently, since AND’ing filter queries is one of the things for which it is well optimized. In our other post, we discussed some real-world query examples to give a sense of the speedup we obtain for different levels of accuracy.</p><p>So far, we've only discussed obtaining an estimate of a query. While such a point estimator can be useful, without knowing anything about its error those uses are limited. We found that ES|QL has existing capabilities that make it relatively easy to incorporate cheap, flexible, and accurate error estimation at the same time. We'll discuss this next.</p><h3>Error estimates</h3><p>We view providing an accurate understanding of the uncertainty in our estimates as crucial for users to be able to trust the approximation. While having the option to quickly estimate an ES|QL query alone can be useful in certain situations, we wanted to provide a richer feature that allows clients to make intelligent choices. For example, if an approximate query is being used to preview a chart and the error is only a couple of pixels, there’s little point in running another expensive query to redraw it.</p><p>The way we've chosen to represent error is by a confidence interval: the -central confidence interval, to be precise. This can be expressed in terms of the <a href="https://en.wikipedia.org/wiki/Cumulative_distribution_function">cumulative density</a>, , of the statistic being estimated. Specifically, it's the interval which contains the true value of the statistic with probability  whose endpoints are  and . Confidence interval calculations are surprisingly subtle. There are also important constraints for our use case that make standard approaches undesirable. Next, we’ll take a look in more detail at the motivation and the design for the approach we’ve adopted.</p><p>A key requirement of the whole project is to dramatically accelerate expensive analytics queries. It’s therefore vital that the overhead of estimating uncertainty isn’t too large compared to estimating the query result itself. We also want the feature to be as general as possible, but “isolated” within the language. In other words, ES|QL is a flexible language, and we want estimation to work with as much of it as possible. At the same time, we don’t want to introduce a cross-cutting feature that incurs development costs on every new feature we ship.</p><p>With these considerations in mind, we chose to estimate confidence intervals by partitioning the sample set and computing the query output on each subsample. This is reminiscent of bootstrap; however, since we ensure that each partition receives a disjoint random subset of the sample data, we know that they comprise true estimates of the statistic distribution. To achieve the best possible estimate of the statistic itself, we still compute its value on the full sample. For example, to estimate the mean and its distribution the process can be expressed as follows:</p><p>This introduces a complication to account for the discrepancy between the count of values used to estimate a query statistic and used to sample its distribution. This is a downside; however, there are some significant advantages.</p><p>Most of the work in analytic queries resides in computing the aggregate statistics: post-processing after a <code>STATS</code> reduction acts on a far smaller table, and the cost is often relatively small. In this scheme, every row in the input data to the <code>STATS</code> command is processed exactly twice compared to just estimating the statistic. Therefore, roughly speaking we pay a fixed overhead that's the same order of magnitude as the cost of estimating the query in order to estimate its uncertainty. Since we often achieve multiple orders of magnitude speedup on the exact query, this is acceptable.</p><p>Because this process uses a plain old table, with extra columns for the distribution samples, we can pass the whole table through any ES|QL pipeline and compute confidence intervals on the final results. For example, if we include <code>EVAL square_avg = avg * avg</code> in the pipeline above, we'd have exactly the same <code>square_avg</code>, <code>square_avg_0</code>, …, <code>square_avg_B-1</code> extra values. At the end of the pipeline, we have samples from the distribution of the original statistics and all quantities that are computed using them. Therefore, we can apply our standard confidence interval machinery to reduce the table and convert samples into confidence intervals for derived quantities as well. This whole process is essentially transparent to the rest of the ES|QL language, and as we showed above, can be achieved by query rewriting.</p><h3>The confidence interval calculation</h3><p>We have independent samples of the statistic distribution . However, they're computed with fewer values than our estimate . We also have a relatively small number of distribution samples, to avoid the count discrepancy being too large, and so we don’t inflate the table too much. We therefore prefer a parametric approach for estimating confidence intervals.</p><p>The errors in the statistics for which we support estimation tend to normal distributions in the limit they're computed from many values. So a natural choice, the standard interval, is to estimate the mean and standard deviation from the samples and report the corresponding normal confidence intervals . Here,  denotes the standard normal distribution function. For heavy-tailed data and statistical functions that are sensitive to outliers, such as <code>STD_DEV</code>, convergence to normality can be slow, resulting in poorly calibrated intervals.</p><p>Briefly, in order to assess the quality of the intervals, one can examine their calibration. Specifically, one computes a quantity called the <a href="https://en.wikipedia.org/wiki/Coverage_probability">coverage</a>. For a central confidence interval, it should contain the true statistic value roughly  times for  trials. In fact, since we seek the central confidence interval, we can make the stronger statement that the true value should be above, or below, the confidence interval endpoints in roughly  out  trials. The empirical coverage is this fraction computed for a large number of trials. It allows us to compare alternative approaches by simulation. We return to this when we report our test results.</p><p>In order to obtain better confidence intervals, we tried a couple of different approaches: the <a href="https://en.wikipedia.org/wiki/Cornish%E2%80%93Fisher_expansion">Cornish-Fisher</a> correction of quantiles and an adaptation of <a href="https://en.wikipedia.org/wiki/Bootstrapping_(statistics)#Deriving_confidence_intervals_from_the_bootstrap_distribution">bias-corrected accelerated</a> (BCa) confidence intervals. Simulation showed BCa provided more robust calibration across a range of confidences, so this is the approach we selected. The basic idea, which was introduced by Efron, is to assume that there exists a monotonic transformation of the underlying statistic  which, when applied to a distribution sample normalizes its distribution:</p><p>Here, ,  and  is the standard normal random variable. This is clearly a relaxation of the assumption that the statistic itself is normally distributed, which is used to derive the standard interval. In fact, this family includes many distributions, since  is only constrained to be monotonic. (You can think of  as a first-order Taylor expansion of the case that the variance is an arbitrary function of the true parameter value. This further relaxes the assumption that the normalizing transformation also stabilizes the variance.) The nice thing about this ansatz is that  never needs to be explicitly computed, and there exist standard approaches for estimating the parameters  and  from the distribution samples.</p><p>To handle  one simply arranges for the estimate to land at the median of transformed distribution. If we assume the cumulative distribution function in theta space is  then , where  is the estimated statistic value, and as before  is the standard normal distribution function. Typically,  is approximated by the empirical distribution function, computed indirectly by bootstrap. However, somewhat surprisingly, extensive simulation showed that we obtained better calibrated intervals using a normal approximation to our sample values, i.e.  with  and  their empirical mean and standard deviation, respectively.</p><p>To complete the procedure, one can rearrange (1) to derive  quantiles for  as follows:</p><p>where  is the standard normal z-score for quantile . Typically, one uses the inverse empirical cumulative density estimate of  to convert quantiles back to a confidence interval. However, because we have a mismatch between the count of values used to compute distribution samples and the query estimate, we need to do some sort of scaling. Exploring options by simulation, we again found it best to use a normal approximation, , where  is the number of distribution samples we use. This is just applying the usual scaling of variance by .</p><p>Efron showed that in the case  is distributed as , i.e. that it depends only on the true value , then the acceleration  can be estimated without any knowledge of . In particular, . By assumption, our statistics tend to normal distributions with mean . Since skew is translation and scale invariant, this gives that , i.e. one sixth of the skew of our distribution samples. One thing this glosses over is the dependence of skew, and therefore acceleration, on sample size. We know it tends to zero as the count increases. In fact, skew also asymptotes to zero as  and so we also adjust acceleration to be  to account for the count mismatch between the samples  and estimate .</p><p>Although we significantly improve the calibration of confidence intervals by using a better methodology, we still see issues in the case that the underlying distribution has very heavy tails for some of the supported <code>STATS</code> functions. Therefore, we introduce some additional guard rails we discuss next.</p><h3>Guard rails</h3><p>To avoid the user having to understand too much about edge cases, we provide additional safeguards that surface when we've been unable to confirm  that the distribution samples behave as we expect. This typically happens when the statistic isn’t computed from a sufficient number of values given the metric distribution. It's exacerbated by very skewed metric data and certain aggregation functions, such as the <code>STD_DEV</code>, which are sensitive to outliers.</p><p>We have some global constraints on the minimum count of values used to estimate a statistic for which we'll certify it. For example, if any bucket is empty, then we can’t rely on the distribution samples. This is because ES|QL allows mixing approximate statistics, which treat empty buckets differently. For example, consider the following query:</p><p>There is no self-contained way of correctly assigning a value to <code>mix</code> for empty buckets, since summing requires that we treat them as zero, in which case we bias our estimate of <code>avg</code>. Alternatively, ignoring empty buckets introduces bias in the <code>sum</code>. There is also a global minimum count of values for which we’ve verified our certification method is sufficiently reliable; this is 10.</p><p>We explored a variety of additional tests to certify the results. These were based on both tests of the underlying data distribution, specifically <a href="https://en.wikipedia.org/wiki/Heavy-tailed_distribution#Hill.27s_tail-index_estimator">Hill’s estimator</a>, as well as the statistic’s distribution properties. If the true distribution of the statistic is sufficiently normal, then our estimate and confidence interval calculation behaves as we expect: The interval is well calibrated and the interval width is representative of the actual error. Therefore, in the end, we chose to use a test based on the p-value for distribution samples’ <a href="https://en.wikipedia.org/wiki/Skewness">skewness</a> and <a href="https://en.wikipedia.org/wiki/Kurtosis">kurtosis</a> versus a normal distribution null hypothesis. To certify a result, we require that the two tail p-values are greater than 0.05 for both tests. As we show below, we found this test was well aligned to our actual needs: to distinguish results for which the estimate and its confidence interval are more and less reliable.</p><p>There's a simple trick we can use to boost the accuracy of the accuracy of the test: Create multiple independent distribution samples and use a vote. Given a test to certify results with a failure rate , the distribution of the count of  failures for  tests is  for the case the null hypothesis, that the estimate is trustworthy, is true. For example, for the majority vote assuming  and  then the significance of the test is , i.e. we fail to certify fewer than 1% of trustworthy results. Note that we can compute multiple trials relatively easily using different seeds for the <code>RANDOM</code> bucket identifier.</p><p>This additional check allows us to certify that we trust our estimates and their errors. We surface this information in the approximate query results. When we can’t certify results, they won’t necessarily be inaccurate, but they should be treated with more caution.</p><h3>Testing</h3><p>The two main aims of the testing we discuss here were to understand the calibration of the confidence intervals and to see how well they characterize the statistics' estimation errors. The count function is particularly well behaved, its error distribution is binomial, so the majority of our testing focused on metric aggregations. We study smooth distributions but make sure we cover a range of tail behaviors. The presence of outliers is the key factor that reduces the accuracy of estimated statistics. For example, if an outlier isn’t sampled at all, it can significantly affect the value of some statistics.</p><p>We explored a range of light-tailed distributions, such as uniform and normal, and skewed and heavy-tailed distributions, such as exponential, log-normal, Cauchy, and Pareto. For each family of distribution, we used multiple parameterizations, focusing primarily on varying the scale parameter. In total, we had 24 distinct data distributions. Figure 1 shows some example sample distributions from this set. Note that we’ve truncated the charts to remove extreme outliers, which are present for both the Cauchy and log-normal distributions.</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt6e7b7df23a7e1faa/6a170ca0c1e8a505d7f88312/fb088c17f9755c0d1b3173fb917f0af2c0f83847-1712x950.png" alt="" /><p>For each data distribution, we evaluated 14 different sample sizes, ranging from 1000 to 500000. Then, for each sample set, we evaluated <code>AVG</code>, <code>COUNT</code>, <code>MEDIAN_ABSOLUTE_DEVIATION</code>, <code>MEDIAN</code>, <code>PERCENTILE([25, 75, 90, 95, 99])</code>, <code>SUM</code> and <code>STD_DEV</code> at two levels of confidence, 50% and 90%. In total, we have around 7500 distinct experiments. For each experiment, we assessed the interval calibration using 100 runs and counting the number of times the true statistic lands in the confidence interval. This gives us a binomially distributed estimate for the true confidence interval coverage. The variation we expect in the estimated coverage changes slightly with the level of confidence; for example, at 50% we expect to see values mainly between 0.44 and 0.56, and for 90% we expect to see values mainly between 0.86 and 0.94 using 100 trials.</p><p>Figure 2 shows <a href="https://en.wikipedia.org/wiki/Box_plot">box plots</a> for the empirical coverage for the two confidence levels computed from all experiments. In all cases, the confidence intervals are reasonably well calibrated. Extreme percentiles are biased for small sample sizes, which leads to increased outlier counts for small sample sizes. As a rule of thumb, you’d want roughly  samples to ensure that you have enough samples in the appropriate tail.</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blte33fb7bc54844a83/6a170ca2839dfa7a19dcff38/02a5375025e811ba18c4e823e1d984261bbf6f42-631x763.png" alt="" /><p>Next, we examine the degree to which the confidence intervals capture the typical size of the estimate error. To do this, we examine the distribution of the ratio of the estimated statistics' error and half the confidence interval width for all certified results. The higher the confidence, the wider the interval, so different confidence levels shift the mean of this distribution. Figure 3 shows this distribution computed for the 90% confidence interval. As expected, the distribution is roughly normal, albeit with a tail of some larger errors. We see in all cases the confidence interval width gives the order of magnitude of the estimated statistics' actual errors.</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt3d4bc25885b66945/6a170ca360084b472d3c45b4/2d4ab88a07910edac7e8406ae4942694751f0090-1000x600.png" alt="" /><p>We’ve shown that certified results are nearly always reliable; however, we’d also like some insight into the proportion of results which we fail to certify that are actually reliable, to confirm that the test aligns with our objective. We use <em>reliable</em> here in the fairly strong sense that the confidence interval is well calibrated. Specifically, for the 50% and 90% confidence intervals, we count the proportion of uncertified results for which the confidence interval empirical calibration has an acceptable margin of error, given the number of trials used to estimate it. Using this procedure, the false positive rate across all experiments is around 1%. This agrees well with the failure rate we expect by chance, given our test parameters, and confirms the assumption underlying the test.</p><p>Finally, to better understand the difference between certified and uncertified results, Figure 4 shows the error distribution of the ratio of the estimated statistics' errors and half the 90% confidence interval for the reliable and unreliable results separately. Note that we truncated the range for uncertified intervals.</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt0c4f729eb4c1127c/6a170ca5961e69c63cc4cf66/c0cd6d42f5d061aac15767539209a7c443ed1acd-1000x600.png" alt="" /><h3>Wrapping up</h3><p>In this post, we present the background behind our approach for quickly estimating ES|QL queries and providing an indication of their errors. To do this, we developed an effective confidence interval mechanism that allows us to provide error estimates. Our approach also allows us to estimate confidence intervals for quantities derived from sampled statistics via other pipeline operations. Quantifying the error comes with a relatively small overhead compared to just estimating the query. Finally, we developed a statistical test to certify results we return. Values that aren’t certified can still be accurate, but we’re less confident in them.</p><p>As well as testing the feature on a range of real-world use cases, which we discuss in <a href="https://www.elastic.co/search-labs/blog/fast-approximate-esql-part-1">our companion post</a>, we tested the error estimation by extensive simulation across a range of data characteristics, sample sizes, aggregation functions, and confidence levels. This showed confidence intervals are well calibrated, and the interval itself provides a good approximation of the actual error we observe in the estimates. Finally, we showed that we were able to certify intervals with a low false negative rate.</p><p>We’re planning to integrate this feature into other stack capabilities in the future, so stay tuned.

</p>]]></content:encoded>
    <link>https://www.elastic.co/search-labs/blog/fast-approximate-esql-part-2</link>
    <guid isPermaLink="true">https://www.elastic.co/search-labs/blog/fast-approximate-esql-part-2</guid>
    <category><![CDATA[ES|QL]]></category>
    <dc:creator><![CDATA[Thomas Veasey,Jan Kuipers]]></dc:creator>
    <enclosure url="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt0c4f729eb4c1127c/6a170ca5961e69c63cc4cf66/c0cd6d42f5d061aac15767539209a7c443ed1acd-1000x600.png" length="0" type="image/png"/>
    <pubDate>Fri, 17 Apr 2026 00:00:00 GMT</pubDate>
  </item>
  <item>
    <title><![CDATA[Fast approximate Elasticsearch ES|QL - part I]]></title>
    <description><![CDATA[Introducing the work we've done on a fast approximate querying mode for Elasticsearch ES|QL. In many cases, it allows us to achieve orders of magnitude latency reductions while providing accurate estimates.]]></description>
    <content:encoded><![CDATA[<p>Analytics workloads typically involve summarizing large volumes of data into a much smaller number of statistics. The Elasticsearch Query Language (ES|QL) implements this capability using the <a href="https://www.elastic.co/docs/reference/query-languages/esql/commands/stats-by">STATS command</a>. This allows you to select various aggregation functions and apply them to the previous query results, as well as grouping the results by one or more ES|QL expressions. This is a flexible operation that, coupled with ES|QL querying capabilities, allows one to perform <a href="https://en.wikipedia.org/wiki/MapReduce">MapReduce</a> on data stored in collections of Elasticsearch indices.</p><p>One of the key requirements for a pleasant user experience is that these operations are performed quickly. Large language model–based (LLM) agents also introduce new <a href="https://arxiv.org/pdf/2509.00997">higher bandwidth and speculative query patterns</a> that can potentially benefit from different optimization strategies.</p><p>In this two-part blog series, we discuss an optimization approach we’re introducing to ES|QL in version 9.4 of Elasticsearch and the Elastic Stack, which exploits a relaxation of the problem. Rather than trying to get exact values for aggregates, we allow ourselves to return approximate values, together with some characterization of their error. A key benefit of approximation is that it breaks the dependency between performance and dataset size: The accuracy with which one can approximate a query doesn’t depend on the original dataset size but, principally, its data characteristics and the query itself. As we’ll see later, this allows us to achieve some dramatic performance improvements.</p><p>In our<a href="https://www.elastic.co/search-labs/blog/fast-approximate-esql-part-2"> next blog post</a>, we will discuss the theory behind our approach and the validation we’ve done of its statistical properties. Here, we introduce the syntax and give a sense of how it’s achieved using standard ES|QL and query rewriting. You can explore its performance on a subset of the popular <a href="https://github.com/ClickHouse/ClickBench">ClickBench</a> benchmark. Finally, we discuss some limitations and gotchas that are worth understanding when you use query approximation.</p><h3>Syntax and behavior</h3><p>So how do you actually use it?</p><p>That’s it. You simply introduce the new line <code>SET approximation=true;</code> and write your <code>STATS</code> query pipeline as usual. Below, we discuss some advanced configuration options and some limitations around the <code>agg(...)</code> and <code>commands</code>. However, essentially, we choose defaults so that this will typically provide useful approximations while achieving significant speedups.</p><p>With this change, you’ll see some differences in the query results. Let’s look at a concrete example to illustrate this. Suppose the raw query is as follows:</p><p>The results might look something like this:</p>item_category        | count
---------------------+------
Household Essentials | 5165
Kitchen              | 2132
Storage              | 1121
Home Decor           | 877
Furniture            | 357<p>Approximating this query introduces some extra columns for each quantity that’s estimated:</p>item_category | count | _approximation_confidence_interval(count) | _approximation_certified(count)
--------------+-------+-------------------------------------------+--------------------------------
Essentials    | 5150  | [5100, 5250]                              | true
Kitchen       | 2150  | [2100, 2200]                              | true
Storage       | 1120  | [1100, 1150]                              | true
Home Decor    | 880   | [860, 900]                                | true
Furniture     | 330   | [310, 350]                                | true<p>The count column now contains an estimate, and you’ll see it’s somewhat different from the exact values above. The <code>_approximation_confidence_interval(count)</code> column defaults to the central 90% confidence interval for the <code>count</code> estimate and the <code>_approximation_certified(count)</code> column indicates if we’re highly confident that the results and their confidence interval are trustworthy. In outline, the <em>confidence interval</em> is an interval we expect has a high probability (0.9) of containing the true value for the quantity being estimated. The <em>certified column</em> indicates the distribution of the approximation is behaving as we expect. When the result isn’t certified, it’s often still accurate, but our test of the properties of its distribution hasn’t been able to confirm this. These quantities are discussed in more detail in our second post.</p><h3>Implementation</h3><p>An approximate query is rewritten before query execution using random sampling and extrapolation. Let’s take a look at the query of the previous section. The part of the rewritten query responsible for obtaining the best estimate looks like:</p><p>The query samples a fraction of the data, and therefore the final count has to be extrapolated by scaling up with the inverse of the sample probability. Extrapolation clearly depends on the underlying aggregation function, and we handle this appropriately for all functions we support.</p><p>To obtain the sample probability, we're setting a fixed <code>number_of_rows</code> to be processed by the <code>STATS</code> command. In this case, the probability is calculated as follows:</p><p>This query is executed before the final approximate query is executed.</p><p>As well as this best estimate, confidence intervals and a statistical test used to certify that the value distribution is behaving as we expect also need to be computed. The intervals are computed using a variant of the <a href="https://blogs.sas.com/content/iml/2017/07/12/bootstrap-bca-interval.html">bias-corrected and accelerated bootstrap confidence interval</a> (BCa) method. Therefore, the data needs to be partitioned into B buckets, which are used in turn to compute the intervals. Omitting some implementation details, this approximate query looks like:</p><p>To certify the estimate and confidence interval, there should be enough data, and the distribution of the bucket values should tend to normality.</p><p>Some queries can be efficiently computed using only summary statistics maintained in the index. To handle these correctly, where sampling is both slower and inaccurate, we updated the physical query planner, since detecting this case requires information that’s only available where the data resides. When the planner detects this is possible, it simply executes the query as normal. Such queries are typically fast anyway, and there’s no real side effect, so you don’t need to worry about this when using approximation; however, you’ll see that confidence intervals for such queries always have zero length, indicating the results are exact.</p><h3>Results</h3><p>To explore the performance improvements, we use <a href="https://github.com/ClickHouse/ClickBench">ClickBench</a>. This is a benchmark for analytics workloads for database management systems (DBMS). It comprises approximately 100 million rows, with a focus on clickstream and traffic analysis, web analytics, machine-generated data, structured logs, and events data. The benchmark also defines 43 queries that are typical of ad-hoc analytics and real-time dashboards.</p><p>Some of the queries aren’t suitable for approximation. For example, we don’t support approximating the unique count of a categorical value or computing the minimum and maximum of a metric value. We also don’t care about queries targeting search alone, for which Elasticsearch has excellent performance in any case. We therefore exclude these types of query from our evaluation. Finally, we also want to test a few additional aggregation functions, such as percentiles, which are not well represented in the original query set, so add some variants of the original metric queries to this end.</p><p>Queries in the benchmark are written using standard SQL and so need porting to use ES|QL syntax. This translation is fairly straightforward. Here’s an example:</p>SELECT SUM(AdvEngineID), COUNT(*), AVG(ResolutionWidth) FROM hits<p>becomes:</p><p>when rewritten in ES|QL.</p><p>For running all benchmarks, we use an Elastic Cloud Hosted instance with 870GB disk, 29GB Ram, and 4 vCPUs, in effect, an Amazon Elastic Compute Cloud (EC2) i3.xlarge instance. In the following results, we simply compare ES|QL with and without query approximation. Extensive results on a range of different hardware setups and datastores can be found <a href="https://benchmark.clickhouse.com/">here</a>. Even with significantly constrained test hardware (matching the vCPUs of the smallest setup), our approximation approach achieves competitive results against much larger systems.</p><p>We run each query and its approximation five times in a random order, clearing the <a href="https://www.elastic.co/docs/api/doc/elasticsearch/operation/operation-indices-clear-cache">query cache</a> between each run. We report the average run time over all five runs. While clearing the cache should be sufficient to avoid most of the advantage of running second, we wanted to avoid any possible accidental prewarming effects, which is why we alternate.</p><p>The results break down into four categories:</p><ol><li><p>Queries which are rewritten to use index summary statistics (three queries).</p></li><li><p>Queries that perform well (13 queries).</p></li><li><p>Queries with high cardinality partitioning (seven queries).</p></li><li><p>Queries with restrictive filters (12 queries).</p></li></ol><p>Roughly speaking, for these four categories, approximate querying is: equivalent (1); faster and accurate (2); faster but unreliable (3); and slightly slower (4), compared to exact querying, respectively.</p><p>For category 1, the planner automatically detects that we’re able to perform the query using summary statistics, and we end up executing the queries in the same way. To do this, we need information that’s only available on the data nodes, so we perform the rewrite only after we've estimated the sample probability. Because we're able to do this very efficiently, the overhead is small (around 10–15%). In both cases, the results are exact.</p><p>Queries in category 2 run on average 23 faster if estimating the values and computing confidence intervals and 72 faster if just estimating the values, which you can select as follows: <code>SET approximation={"confidence_level":null}</code>. These headline figures hide quite some variation in the impact of approximation on performance. The table below shows some queries sampled from the range of speedups we see:</p><p>Query</p><p>Baseline / ms</p><p>Approximate with CI / ms</p><p>Approximate without CI / ms</p><p>3</p><p>1725</p><p>145</p><p>15</p><p>10</p><p>4340</p><p>1721</p><p>56</p><p>13</p><p>32912</p><p>6106</p><p>3821</p><p>21</p><p>46739</p><p>3284</p><p>2139</p><p>22</p><p>252505</p><p>6478</p><p>5019</p><p>Here are the corresponding queries:</p><p>We'll return to the accuracy of the approximation in the next blog post, but to give a sense of this, we plot below the exact and approximate values for a sample run for query 13:</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt5f6acbc3a2c9e58f/6a170ee34a531bbd2c36aa17/9ab83c13f42f88253a242d78339356f4a7c48700-2094x1358.png" alt="approximation-of-clickbench-query-13" /><p>For category 3, we get an average speedup of . However, the results of queries in this category can miss some partitions and often have large estimation errors. Approximation can still be valuable for such queries, particularly in the context of agentic workflows, but requires larger sample sizes than out default if accuracy is important. As we discuss in the next section, we provide an API to explicitly control the sample size. If the source dataset is sufficiently large, this can be increased and approximation will still yield significant performance improvements. The table below shows a couple of query examples for this category:</p><p>Query</p><p>Baseline / ms</p><p>Approximate with CI / ms</p><p>Approximate without CI / ms</p><p>15</p><p>8256</p><p>1187</p><p>124</p><p>17</p><p>70641</p><p>2109</p><p>982</p><p>Here are the corresponding queries:</p><p>Finally, category 4 queries use selective filters and end up being executed exactly, but they run slightly slower because of the work done in the query rewrite stage. Typically, all these queries run fast anyway, so the absolute slowdown is small. On average, they run approximately 14% or 370ms slower than the “without” sampling for our test setup.</p><h3>Limitations and best practices</h3><p>It’s worth explicitly mentioning some limitations. In particular, the following queries are not currently supported:</p><ol><li><p>Queries using the <code>TS</code> source command.</p></li><li><p>Queries using the <code>FORK</code> or <code>JOIN</code> processing command.</p></li><li><p>Pipelines which use two or more <code>STATS</code> commands.</p></li><li><p>The <code>ABSENT</code>, <code>PRESENT</code>, <code>DISTINCT_COUNT</code>, <code>MIN</code>, <code>MAX</code>, <code>TOP</code>, <code>ST_CENTROID_AGG</code> and <code>ST_EXTENT_AGG</code> aggregation functions.</p></li></ol><p>We plan to lift some of these restrictions in future releases, such as approximating queries using <code>TS</code>, <code>FORK</code> and <code>JOIN</code>; however, some are intrinsic. For example, while there’s prior art for estimating the <a href="https://en.wikipedia.org/wiki/Generalized_extreme_value_distribution">minimum and maximum</a> of a metric dataset or the count of unique values of a categorical dataset (see, for example, <a href="https://arxiv.org/pdf/2202.02800">this</a> paper), they require making certain distributional assumptions, either explicitly or implicitly. In summary, we view trying to automatically provide estimates of these statistics as being too open to accidental misuse.</p><p>For the expert user, we provide another route: ES|QL supports using the <code>SAMPLE</code> command directly. This allows one to obtain “point estimates” of any query, albeit with no attempt to correct for the impact of sampling or quantify error. For example:</p><p>computes the unique count of the value field on a sample of roughly 1/100th of the dataset. The sample probability can be adjusted to get a sense of how this is asymptoting, or more sophisticated estimation procedures can use <code>STATS COUNT() BY value</code> to estimate the frequency profile of the data.</p><p>There are a couple of cases that are more problematic for sampling. If a very restrictive filter is applied in the query, then sampling is of little value, since few rows match anyway. In such cases, we discover that we’d have to sample too large a proportion of the rows to estimate the query in the rewrite phase. In this case, we revert to running the query without sampling and its result is exact. However, the search procedure to determine the fraction of rows to sample comes with some overhead. One therefore pays a penalty, albeit less than the original query cost, for no benefit. If you know in advance that the query is expected to match relatively few rows, it's best to run it without approximation.</p><p>The second case only applies when computing <code>STATS</code> partitioned by some expression. If the cardinality of this expression is very high, then even if many rows are searched, individual statistics may be computed from a small number of rows. Some cases are more problematic than others. Sorting by ascending count, that is, finding the rarest partitions, can be impossible to estimate in a single query if heavy hitters would require us to sample most of the dataset to find them. For this particular case, heavy hitting partitions can be estimated first and sometimes efficiently excluded by updating the query. In general, infrequent partitions may be lost in the sampling process, and their statistics' estimation errors can be high. It’s worth noting that we won’t attempt to estimate any statistic for which we have fewer than 10 samples, and we simply drop them from the result set. In the case of very high cardinality <code>BY</code> clause, for example, a field whose value is unique for every row, this means the query can return no results. If you find approximate query results are too inaccurate, you have the option to increase the sample size, which by default is 1,000,000 for <code>STATS</code>, which uses grouping and 100,000 otherwise. Currently, this needs to be done manually, and we provide the following API for this:</p><p>Occasionally, functions significantly alter the distribution characteristics of the quantities they act on. A contrived example is the following:</p><p>If the variation in the estimate <code>sl</code> is much larger than  we expect the distribution of <code>csl</code> to be mainly flat in the interval  with peaks near both endpoints. In this particular case, it’s not clear that the central confidence interval is a particularly useful concept, since the modes of the distribution lie outside almost all central confidence intervals. In any case, just observing the samples of <code>csl</code>, our standard confidence interval machinery won’t reliably characterize this distribution and it will underestimate the variability of <code>csl</code>. However, our statistical test should detect this problem, and the result won’t be certified.</p><p>Finally, we note that Elasticsearch implements some query optimization strategies that ideally <a href="https://github.com/elastic/elasticsearch/issues/138151">need to account for the fact that sampling is taking place</a>. These rewrite the query at the Lucene level and the preprocessing involved in this rewrite can be relatively expensive. Accelerating an expensive string matching operation by first building a suitable data structure makes sense if the query needs to process every row, but if it processes only a small fraction of them, the trade-off is different. This is something we plan to enhance in future.</p><h3>Conclusions</h3><p>In this blog post, we introduced a new form of query optimization we’re bringing to ES|QL that enables dramatically faster querying by relaxing the constraint that the results are exact. We found on ClickBench that we were able to accurately estimate query values and their confidence intervals up to 100 times faster and values alone up to 250 times faster than we can compute them exactly. Furthermore, we expect this advantage to grow as the dataset size increases, because the approximation accuracy is independent of the dataset size. This feature works with many features of the ES|QL language and is enabled by simply prepending <code>SET approximation=true;</code> to the query to estimate.</p><p>As well as providing a point estimate, we also estimate confidence intervals and indicate whether we think that the underlying assumptions used to compute these are satisfied. This allows us to certify the results if the results are reliable. We explain the theory behind this feature and discuss the testing of its accuracy in our <a href="https://www.elastic.co/search-labs/blog/fast-approximate-esql-part-2">next post</a>.</p>]]></content:encoded>
    <link>https://www.elastic.co/search-labs/blog/fast-approximate-esql-part-1</link>
    <guid isPermaLink="true">https://www.elastic.co/search-labs/blog/fast-approximate-esql-part-1</guid>
    <category><![CDATA[ES|QL]]></category>
    <dc:creator><![CDATA[Jan Kuipers,Thomas Veasey]]></dc:creator>
    <enclosure url="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/bltba968f10a7cac60c/6a170ee50c48571b5301ab34/17afc59be8a46957a341faec1f44c9cb0a221894-1918x1176.png" length="0" type="image/png"/>
    <pubDate>Thu, 16 Apr 2026 00:00:00 GMT</pubDate>
  </item>
  <item>
    <title><![CDATA[Evaluating search relevance part 1 - The BEIR benchmark]]></title>
    <description><![CDATA[Learn to evaluate your search system in the context of better understanding the BEIR benchmark, with tips &amp; techniques to improve your search evaluation processes.]]></description>
    <content:encoded><![CDATA[<p>This is the first in a series of blog posts discussing how to think about evaluating your own search systems in the context of better understanding the BEIR benchmark. We will introduce specific tips and techniques to improve your search evaluation processes in the context of better understanding BEIR. We will also introduce common gotchas which make evaluation less reliable. Finally, we note that LLMs provide a powerful new tool in the search engineers' arsenal and we will show by example how one can use them to help evaluate search.</p><h2>Understanding the BEIR benchmark in search relevance evaluation</h2><p>To improve any system you need to be able to measure how well it is doing. In the context of search <a href="https://arxiv.org/abs/2104.08663">BEIR</a> (or equivalently the Retrieval section of the <a href="https://huggingface.co/spaces/mteb/leaderboard">MTEB</a> leaderboard) is considered the “holy grail” for the information retrieval community and there is no surprise in that. It’s a very well-structured benchmark with varied datasets across different tasks. More specifically, the following areas are covered:</p><ul><li><p>Argument retrieval (ArguAna, Touche2020)</p></li><li><p>Open-domain QA (HotpotQA, Natural Questions, FiQA)</p></li><li><p>Passage retrieval (MSMARCO)</p></li><li><p>Duplicate question retrieval (Quora, CQADupstack)</p></li><li><p>Fact-checking (FEVER, Climate-FEVER, Scifact)</p></li><li><p>Biomedical information retrieval (TREC-COVID, NFCorpus, BioASQ)</p></li><li><p>Entity retrieval (DBPedia)</p></li><li><p>Citation prediction (SCIDOCS)</p></li></ul><p>It provides a single statistic, nDCG@10, related to how well a system matches the most relevant documents for each task example in the top results it returns. For a search system that a human interacts with relevance of top results is critical. However, there are many nuances to evaluating search that a single summary statistic misses.</p><h2>Structure of a BEIR dataset</h2><p>Each benchmark has three artefacts:</p><ul><li><p>the corpus or documents to retrieve</p></li><li><p>the queries</p></li><li><p>the relevance judgements for the queries (aka <code>qrels</code>).</p></li></ul><p>Relevance judgments are provided as a score which is zero or greater. Non-zero scores indicate that the document is somewhat related to the query.</p><p>Dataset</p><p>Corpus size</p><p>#Queries in the test set</p><p>#qrels positively labeled</p><p>#qrels equal to zero</p><p>#duplicates in the corpus</p><p>Arguana</p><p>8,674</p><p>1,406</p><p>1,406</p><p>0</p><p>96</p><p>Climate-FEVER</p><p>5,416,593</p><p>1,535</p><p>4,681</p><p>0</p><p>0</p><p>DBPedia</p><p>4,635,922</p><p>400</p><p>15,286</p><p>28,229</p><p>0</p><p>FEVER</p><p>5,416,568</p><p>6,666</p><p>7,937</p><p>0</p><p>0</p><p>FiQA-2018</p><p>57,638</p><p>648</p><p>1,706</p><p>0</p><p>0</p><p>HotpotQA</p><p>5,233,329</p><p>7,405</p><p>14,810</p><p>0</p><p>0</p><p>Natural Questions</p><p>2,681,468</p><p>3,452</p><p>4,021</p><p>0</p><p>16,781</p><p>NFCorpus</p><p>3,633</p><p>323</p><p>12,334</p><p>0</p><p>80</p><p>Quora</p><p>522,931</p><p>10,000</p><p>15,675</p><p>0</p><p>1,092</p><p>SCIDOCS</p><p>25,657</p><p>1,000</p><p>4,928</p><p>25,000</p><p>2</p><p>Scifact</p><p>5,183</p><p>300</p><p>339</p><p>0</p><p>0</p><p>Touche2020</p><p>382,545</p><p>49</p><p>932</p><p>1,982</p><p>5,357</p><p>TREC-COVID</p><p>171,332</p><p>50</p><p>24,763</p><p>41,663</p><p>0</p><p>MSMARCO</p><p>8,841,823</p><p>6,980</p><p>7,437</p><p>0</p><p>324</p><p>CQADupstack (sum)</p><p>457,199</p><p>13,145</p><p>23,703</p><p>0</p><p>0</p><p><strong>Table 1</strong>: Dataset statistics. The numbers were calculated on the test portion of the datasets (<code>dev</code> for <code>MSMARCO</code>).</p><p><strong>Table 1</strong> presents some statistics for the datasets that comprise the <code>BEIR</code> benchmark such as the number of documents in the corpus, the number of queries in the test dataset and the number of positive/negative (query, doc) pairs in the <code>qrels</code> file. From a quick a look in the data we can immediately infer the following:</p><ul><li><p>Most of the datasets do not contain any negative relationships in the <code>qrels</code> file, i.e. zero scores, which would explicitly denote documents as irrelevant to the given query.</p></li><li><p>The average number of document relationships per query (<code>#qrels</code> / <code>#queries</code>) varies from 1.0 in the case of <code>ArguAna</code> to 493.5 (<code>TREC-COVID</code>) but with a value <code>&lt;</code>5 for the majority of the cases.</p></li><li><p>Some datasets suffer from duplicate documents in the corpus which in some cases may lead to incorrect evaluation i.e. when a document is considered relevant to a query but its duplicate is not. For example, in <code>ArguAna</code> we have identified 96 cases of duplicate doc pairs with only one doc per pair being marked as relevant to a query. By “expanding” the initial qrels list to also include the duplicates we have observed a relative increase of ~1% in the <code>nDCG@10</code> score on average.</p></li></ul>{
  "_id": "test-economy-epiasghbf-pro02b",
  "title": "economic policy international africa society gender house believes feminisation",
  "text": "Again employment needs to be contextualised with …",
  "metadata": {}
}
{
  "_id": "test-society-epiasghbf-pro02b",
  "title": "economic policy international africa society gender house believes feminisation",
  "text": "Again employment needs to be contextualised with …",
  "metadata": {}
}
<p><strong>Example of duplicate pairs in ArguAna. In the qrels file only the first appears to be relevant (as counter-argument) to query (“test-economy-epiasghbf-pro02a”)</strong></p><p>When comparing models on the MTEB leaderboard it is tempting to focus on average retrieval quality. This is a good proxy to the overall quality of the model, but it doesn't necessarily tell you how it will perform for you. Since results are reported per data set, it is worth understanding how closely the different data sets relate to your search task and rescore models using only the most relevant ones. If you want to dig deeper, you can additionally check for topic overlap with the various data set corpuses. Stratifying quality measures by topic gives a much finer-grained assessment of their specific strengths and weaknesses.</p><p>One important note here is that when a document is not marked in the <code>qrels</code> file then by default it is considered irrelevant to the query. We dive a little further into this area and collect some evidence to shed more light on the following question: “How often is an evaluator presented with (query, document) pairs for which there is no ground truth information?". The reason that this is important is that when only shallow markup is available (and thus not every relevant document is labeled as such) one Information Retrieval system can be judged worse than another just because it “chooses” to surface different relevant (but unmarked) documents. This is a common gotcha in creating high quality evaluation sets, particularly for large datasets. To be feasible manual labelling usually focuses on top results returned by the current system, so potentially misses relevant documents in its blind spots. Therefore, it is usually preferable to focus more resources on fuller mark up of fewer queries than broad shallow markup.</p><h2>Leveraging the BEIR benchmark for search relevance evaluation</h2><p>To initiate our analysis we implement the following scenario (see the <a href="https://github.com/elastic/elasticsearch-labs/blob/main/supporting-blog-content/evaluating-search-relevance-part-1/retrieve-and-rerank.ipynb">notebook</a>):</p><ol><li><p>First, we load the corpus of each dataset into an Elasticsearch index.</p></li><li><p>For each query in the test set we retrieve the top-100 documents with BM25.</p></li><li><p>We rerank, the retrieved documents using a variety of SOTA reranking models.</p></li><li><p>Finally, we report the “judge rate” for the top-10 documents coming from steps 2 (after retrieval) and 3 (after reranking). In other words, we calculate the average percentage of the top-10 documents that have a score in the <code>qrels</code> file.</p></li></ol><p>The list of reranking of models we used is the following:</p><ul><li><p><a href="https://docs.cohere.com/reference/rerank">Cohere's</a> <code>rerank-english-v2.0</code> and <code>rerank-english-v3.0</code></p></li><li><p><a href="https://huggingface.co/BAAI/bge-reranker-base">BGE-base</a></p></li><li><p><a href="https://huggingface.co/mixedbread-ai/mxbai-rerank-xsmall-v1">mxbai-rerank-xsmall-v1</a></p></li><li><p><a href="https://huggingface.co/cross-encoder/ms-marco-MiniLM-L-6-v2">MiniLM-L-6-v2</a></p></li></ul><p></p><p>Retrieval</p><p>Reranking</p><p></p><p></p><p></p><p></p><p>Dataset</p><p>BM25 (%)</p><p>Cohere Rerank v2 (%)</p><p>Cohere Rerank v3 (%)</p><p>BGE-base (%)</p><p>mxbai-rerank-xsmall-v1 (%)</p><p>MiniLM-L-6-v2 (%)</p><p>Arguana</p><p>7.54</p><p>4.87</p><p>7.87</p><p>4.52</p><p>4.53</p><p>6.84</p><p>Climate-FEVER</p><p>5.75</p><p>6.24</p><p>8.15</p><p>9.36</p><p>7.79</p><p>7.58</p><p>DBPedia</p><p>61.18</p><p>60.78</p><p>64.15</p><p>63.9</p><p>63.5</p><p>67.62</p><p>FEVER</p><p>8.89</p><p>9.97</p><p>10.08</p><p>10.19</p><p>9.88</p><p>9.88</p><p>FiQa-2018</p><p>7.02</p><p>11.02</p><p>10.77</p><p>8.43</p><p>9.1</p><p>9.44</p><p>HotpotQA</p><p>12.59</p><p>14.5</p><p>14.76</p><p>15.1</p><p>14.02</p><p>14.42</p><p>Natural Questions</p><p>5.94</p><p>8.84</p><p>8.71</p><p>8.37</p><p>8.14</p><p>8.34</p><p>NFCorpus</p><p>31.67</p><p>32.9</p><p>33.91</p><p>30.63</p><p>32.77</p><p>32.45</p><p>Quora</p><p>12.2</p><p>10.46</p><p>13.04</p><p>11.26</p><p>12.58</p><p>12.78</p><p>SCIDOCS</p><p>8.62</p><p>9.41</p><p>9.71</p><p>8.04</p><p>8.79</p><p>8.52</p><p>Scifact</p><p>9.07</p><p>9.57</p><p>9.77</p><p>9.3</p><p>9.1</p><p>9.17</p><p>Touche2020</p><p>38.78</p><p>30.41</p><p>32.24</p><p>33.06</p><p>37.96</p><p>33.67</p><p>TREC-COVID</p><p>92.4</p><p>98.4</p><p>98.2</p><p>93.8</p><p>99.6</p><p>97.4</p><p>MSMARCO</p><p>3.97</p><p>6.00</p><p>6.03</p><p>6.07</p><p>5.47</p><p>6.11</p><p>CQADupstack (avg.)</p><p>5.47</p><p>6.32</p><p>6.87</p><p>5.89</p><p>6.22</p><p>6.16</p><p><strong>Table 2</strong>: Judge rate per (dataset, reranker) pairs calculated on the top-10 retrieved/reranked documents</p><p>From <strong>Table 2</strong>, with the exception of <code>TREC-COVID</code> (&gt;90% coverage), <code>DBPedia</code> (~65%), <code>Touche2020</code> and <code>nfcorpus</code> (~35%), we see that the majority of the datasets have a labeling rate between 5% and a little more than 10% after retrieval or reranking. This doesn’t mean that all these unmarked documents are relevant but there might be a subset of them -especially those placed in the top positions- that could be positive.</p><p>With the arrival of general purpose instruction tuned language models, we have a new powerful tool which can potentially automate judging relevance. These methods are typically far too computationally expensive to be used online for search, but here we are concerned with offline evaluation. In the following we use them to explore the evidence that some of the BEIR datasets suffer from shallow markup.</p><p>In order to further investigate this hypothesis we decided to focus on MSMARCO and select a subset of 100 queries along with the top-5 reranked (with Cohere v2) documents which are currently not marked as relevant. We followed two different paths of evaluation: First, we used a carefully tuned prompt (more on this in a later post) to prime the recently released <a href="https://huggingface.co/microsoft/Phi-3-mini-4k-instruct">Phi-3-mini-4k</a> model to predict the relevance (or not) of a document to the query. In parallel, these cases were also manually labeled in order to also assess the agreement rate between the LLM output and human judgment. Overall, we can draw the following two conclusions:</p><ul><li><p>The agreement rate between the LLM responses and human judgments was close to 80% which seems good enough as a starting point in that direction.</p></li><li><p>In 57.6% of the cases (based on human judgment) the returned documents were found to be actually relevant to the query. To state this in a different way: For 100 queries we have 107 documents judged to be relevant, but at least 0.576 x 5 x 100 = 288 extra documents which are actually relevant!</p></li></ul><p>Here, some examples drawn from the <code>MSMARCO</code>/<code>dev</code> dataset which contain the query, the annotated positive document (from <code>qrels</code>) and a false negative document due to incomplete markup:</p><p>Example 1:</p>{
  "query":
    {
        "_id": 155234,
        "text": "do bigger tires affect gas mileage"
    },
  "positive_doc":
    {
        "_id": 502713,
        "text": "Tire Width versus Gas Mileage. Tire width is one of the only tire size factors that can influence gas mileage in a positive way. For example, a narrow tire will have less wind resistance, rolling resistance, and weight; thus increasing gas mileage.",
    },
    "negative_doc":
    {
        "_id": 7073658,
        "text": "Tire Size and Width Influences Gas Mileage. There are two things to consider when thinking about tires and their effect on gas mileage; one is wind resistance, and the other is rolling resistance. When a car is driving at higher speeds, it experiences higher wind resistance; this means lower fuel economy."
    }
}
<p>Example 2:</p>{
  "query":
    {
        "_id": 300674,
        "text": "how many years did william bradford serve as governor of plymouth colony?"
    },
  "positive_doc":
    {
        "_id": 7067032,
        "text": "http://en.wikipedia.org/wiki/William_Bradford_(Plymouth_Colony_governor) William Bradford (c.1590 \u00e2\u0080\u0093 1657) was an English Separatist leader in Leiden, Holland and in Plymouth Colony was a signatory to the Mayflower Compact. He served as Plymouth Colony Governor five times covering about thirty years between 1621 and 1657."
    },
    "negative_doc":
    {
        "_id": 2495763,
        "text": "William Bradford was the governor of Plymouth Colony for 30 years. The colony was founded by people called Puritans. They were some of the first people from England to settle in what is now the United States. Bradford helped make Plymouth the first lasting colony in New England."
    }
}
<p>Manually evaluating specific queries like this is a generally useful technique for understanding search quality that complements quantitive measures like nDCG@10. If you have a representative set of queries you always run when you make changes to search, it gives you important qualitative information about how performance changes, which is invisible in the statistics. For example, it gives you much more insight into the false results your search returns: it can help you spot obvious howlers in retrieved results, classes of related mistakes, such as misinterpreting domain-specific terminology, and so on.</p><p>Our result is in agreement with relevant research around <code>MSMARCO</code> evaluation. For example, <a href="https://arxiv.org/pdf/2109.00062">Arabzadeh et al.</a> follow a similar procedure where they employ crowdsourced workers to make preference judgments: among other things, they show that in many cases the documents returned by the reranking modules are preferred compared to the documents in the MSMARCO <code>qrels</code> file. Another piece of evidence comes from the authors of the <a href="https://arxiv.org/pdf/2010.08191">RocketQA</a> reranker who report that more than 70% of the reranked documents were found relevant after manual inspection.</p><p> Update - September 9th: After a careful re-evaluation of the dataset we identified 15 more cases of relevant documents, increasing their total number from 273 to 288</p><h2>Main takeaways &amp; next steps</h2><ul><li><p>The pursuit for better ground truth is never-ending as it is very crucial for benchmarking and model comparison. LLMs can assist in some evaluation areas if used with caution and tuned with proper instructions</p></li><li><p>More generally, given that benchmarks will never be perfect, it might be preferable to switch from a pure score comparison to more robust techniques capturing statistically significant differences. The work of <a href="https://arxiv.org/pdf/2109.00062">Arabzadeh et al.</a> provides a nice of example of this where based on their findings they build 95% confidence intervals indicating significant (or not) differences between the various runs. In the accompanying <a href="https://github.com/elastic/elasticsearch-labs/blob/main/supporting-blog-content/evaluating-search-relevance-part-1/retrieve-and-rerank.ipynb">notebook</a> we provide an implementation of confidence intervals using <a href="https://en.wikipedia.org/wiki/Bootstrapping_(statistics)">bootstrapping</a>.</p></li><li><p>From the end-user perspective it’s useful to think about task alignment when reading benchmark results. For example, for an AI engineer who builds a RAG pipeline and knows that the most typical use case involves assembling multiple pieces of information from different sources, then it would be more meaningful to assess the performance of their retrieval model on multi-hop QA datasets like HotpotQA instead of the global average across the whole BEIR benchmark</p></li></ul><p>In the <a href="https://www.elastic.co/search-labs/blog/evaluating-search-relevance-part-2">next blog post</a> we will dive deeper into the use of Phi-3 as LLM judge and the journey of tuning it to predict relevance.</p>]]></content:encoded>
    <link>https://www.elastic.co/search-labs/blog/evaluating-search-relevance-part-1</link>
    <guid isPermaLink="true">https://www.elastic.co/search-labs/blog/evaluating-search-relevance-part-1</guid>
    <category><![CDATA[ML Research]]></category>
    <category><![CDATA[Python]]></category>
    <dc:creator><![CDATA[Thanos Papaoikonomou,Thomas Veasey]]></dc:creator>
    <enclosure url="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt9d9912c8d4187096/6a1704f5b0367d30e672bc17/54a6e5197f5721b36fc65f27387d29803ed35589-721x420.jpg" length="0" type="image/jpeg"/>
    <pubDate>Tue, 16 Jul 2024 00:00:00 GMT</pubDate>
  </item>
  <item>
    <title><![CDATA[Aggregate data faster with new the random_sampler aggregation]]></title>
    <description><![CDATA[Aggregate billions of documents in milliseconds instead of minutes with Elastic. Learn more about how the new random_sampler aggregation gives you statistically robust results at a lower cost.]]></description>
    <content:encoded><![CDATA[<p>With 8.2, the Elastic Stack gives users the <a href="https://www.elastic.co/guide/en/elasticsearch/reference/master/search-aggregations-random-sampler-aggregation.html"><code>random_sampler</code></a> aggregation. It adds the capability to randomly sample documents in a statistically robust manner. Randomly sampling documents in aggregations allows you to balance speed and accuracy at query time. You can aggregate billions of documents with high accuracy at a fraction of the latency. This allows you to achieve faster results with fewer resources and comparable accuracy — all with a simple aggregation.</p><p>Let's run through some basic details, best practices, and how it works, so you can try it out in the Elasticsearch Service today.</p><h2>Delivering speed and accuracy</h2><p>Random sampling in Elasticsearch has never been easier or faster. If your query has many aggregations, you can quickly obtain results by using the <a href="https://www.elastic.co/guide/en/elasticsearch/reference/master/search-aggregations-random-sampler-aggregation.html"><code>random_sampler</code></a> aggregation.</p>POST _search?size=0&amp;track_total_hits=false
{
  "aggs": {
    "sampled": {
      "random_sampler": {
        "probability": 0.001,
        "seed": 42
      },
      "aggs": {
        ...
      }
    }
  }
}
<p>All the above aggregations nested under <code>random_sampler</code> will return sampled results. Each agg is roughly seeing only 0.1% of the documents (or 1 in every 1000th document). Where computational cost correlates with the number of documents, the aggregation speed increases. You may have also noticed the “<code>seed</code>” parameter. You can provide a <code>seed</code>to get consistent results on the same shards. Without a seed, a new random subset of documents is considered and you may get slightly different aggregated results.</p><p>How much faster is the <code>random_sampler</code>? The speed improves according to the provided probability as fewer documents are aggregated. The improvements relative to probability will eventually flatten out. Each aggregation has its own computational overhead regardless of the number of documents. An example of this overhead cost is comparing multi-bucket to single metric aggregations. Multi-bucket aggregations have a higher overhead due to their bucket handling logic. While speed is improved for multi-bucket aggregations, the rate of that speed increase will flatten out sooner than single metric.</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt1427bb522f9a2e33/6a17113be8fbce617c39fd55/e6a33afc9b30709dd5606bfb23726005f40bb803-800x600.png" alt="expected speedup" /><p>Figure 1. The speedup expected for aggregations of different constant overhead.</p><p>Here are some results on expected speed and error rate over an APM data set of 64 million documents.</p><p>The calculations are from: 300 query and aggregation combinations, 5 seeds, and 9 sampling probabilities. In total, 13,500 separate experiments generated the following graphs for median speedup and median relative error as a function of the downsample factor which is 1 / sample probability.</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/bltd264a08140ef2bed/6a17113dacf0883d26be9c4d/77697af30ebd43c64216df0a9e99951191dd612d-800x600.png" alt="median speedup" /><p>Figure 2. Median speedup as a function of the downsample factor (or 1 / probability provided for the sampler).</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt1f3876b646dd7b14/6a17113e1949f77a4ae7ab20/047bb5dd28bedaee14998168d71ac152e3ea4392-800x600.png" alt="median error" /><p>Figure 3. Median relative error as a function of the downsample factor (or 1 / probability provided for the sampler).</p><p>With a probability of 0.001, for half of the scenarios tested, there was an 80x speed improvement or better with a 4% relative error or less. These tests involved a little over 64 million documents but spread across many shards. More compact shards and larger data can expect better results.</p><p>But, you may ask, do the visualizations look the same?</p><p>Below are two visualizations showing document counts for every 5 minutes over 100+ million documents. The total set loads in seconds and is sampled in milliseconds. This is with almost no discernible visual difference.</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt0e84bd402ad8ec58/6a171140b339d56a6976a0c2/925ab6b529edb5065ec7d0f1886f7bc3eaa7da62-800x158.png" alt="sampled vs unsampled count" /><p>Figure 4. Sampled vs unsampled document count visualizations.</p><p>Here is another example. This time the average transaction by hour is calculated and visualized. While visually these are not exactly the same, the overall trends are still evident. For a quick overview of the data to catch trends, sampling works marvelously.</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt583f18fbd2c02001/6a171141acf0885acabe9c51/1998189c574de515e1bff2dca886bd923a2af584-800x250.png" alt="sampled vs unsampled average" /><p>Figure 5. Sampled vs. unsampled average transaction time by hour visualization.</p><h2>Best practices for using sampling aggregation</h2><p>Sampling shines when you have a large data set. In these cases you might ask, should I sample before the data is indexed in Elasticsearch? Sampling at query time and before ingestion are complimentary. Each has its distinct advantages.</p><p>When sampling at ingest time, it can save disk and indexing costs. However, if your data has multiple facets, you have to stratify sampling over facets when sampling before ingestion, unless you know exactly how it will be queried. This suffers from the <a href="https://en.wikipedia.org/wiki/Curse_of_dimensionality">curse of dimensionality</a> and you could end up with underrepresented sets of facets. Furthermore, you have to cater for the worst case when sampling before ingestion. For example, if you want to compute percentiles for two queries, one which matches 50% of the documents and one which matches 1% of documents in an index, you can get away with 7X more downsampling for the first query and achieve the same accuracy.</p><p>Here is a summary of what to expect from sampling with the <code>random_sampler</code> at query time.</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt908ecf7a4a90ba82/6a1711436234e00ddcdb1ada/3a1cc19429ee33b95e6a0ab6eed2413665a5e0bd-640x480.png" alt="relative error" /><p>Figure 6. Relative error for different aggregations.</p><p>Sampling accuracy varies across aggregations (see Figure 5 for some examples). Here is a list of some aggregations in order of descending accuracy: <a href="https://www.elastic.co/guide/en/elasticsearch/reference/current/search-aggregations-metrics-percentile-aggregation.html">percentiles</a>, <a href="https://www.elastic.co/guide/en/elasticsearch/reference/current/search-aggregations-metrics-valuecount-aggregation.html">counts</a>, <a href="https://www.elastic.co/guide/en/elasticsearch/reference/current/search-aggregations-metrics-avg-aggregation.html">means</a>, <a href="https://www.elastic.co/guide/en/elasticsearch/reference/current/search-aggregations-metrics-sum-aggregation.html">sums</a>, <a href="https://www.elastic.co/guide/en/elasticsearch/reference/current/search-aggregations-metrics-extendedstats-aggregation.html">variance</a>, <a href="https://www.elastic.co/guide/en/elasticsearch/reference/current/search-aggregations-metrics-min-aggregation.html">minimum</a>, and <a href="https://www.elastic.co/guide/en/elasticsearch/reference/current/search-aggregations-metrics-max-aggregation.html">maximum</a>. Metric aggregation accuracy will also be affected by the underlying data variation: the lower the variation in the values, the fewer samples you need to get accurate aggregate values. The minimum and maximum will not be reliable with outliers, since there is always a reasonable chance that the sampled set misses the one very large (or small) value in the data set. If you are using terms aggregations (or some partitioning such as date histogram), aggregate values for terms (or buckets) with few values will be less accurate or missed altogether.</p><p>Aggregations also have fixed overheads (see Figure 1 for an example). This means as the sample size decreases, the performance improvement will eventually level out. Aggregations which have many buckets have higher overheads and so the speedup you will gain from sampling is smaller. For example, a <a href="https://www.elastic.co/guide/en/elasticsearch/reference/current/search-aggregations-bucket-terms-aggregation.html">terms aggregation</a> for a high cardinality field will show less performance benefit.</p><p>If in doubt, some simple experiments will often suffice to determine good settings for your data set. For example, suppose you want to speed up a dashboard; try reducing the sample probability while the visualizations look similar enough. Chances are your data characteristics will be stable and so this setting will remain reliable.</p><h2>Uncovering how sampling works</h2><p>Sampling considers the entire document set within a shard. Once it creates the sampled document set, sampling applies any provided user filter. The documents that match the filter and are within the sampled set are then aggregated (see Figure 7).</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/bltfed879f2c11f87c2/6a171144ab7f08018ddb9f8f/9145ccea9747229b2d08a8badf6120c20bd0e271-800x227.png" alt="request data flow" /><p>Figure 7. Typical request and data flow for the random_sampler aggregation.</p><p>The key to the sampling is generating this random subset of the shard efficiently and without statistical biases. Taking <a href="https://en.wikipedia.org/wiki/Geometric_distribution">geometrically distributed random</a> steps through the document set is equivalent to uniform random sampling, meaning each document in the set has an equally likely chance of being selected into the sample set. The advantage of this approach is that the sampling cost scales with p (where p is the probability configured in the aggregation). This means no matter how small p is, the relative latency of performing the sampling adds will remain fixed.</p><h2>Ensuring performance reliability and accuracy</h2><p>To achieve the highest performance, accuracy, and robustness, we evaluated a range of realistic scenarios.</p><p>In the case of <code>random_sampler,</code> the evaluation process is complicated by two factors:</p><ol><li><p>It cuts right across the aggregation framework and so it needs to be evaluated with many different combinations of query and aggregation,</p></li><li><p>The results are random numbers, so rather than running just once, you need to run multiple times and test the statistical properties of the result set.</p></li></ol><p>We began with a proof of concept that showed that the overall strategy worked and the performance characteristics were remarkable. However, there are multiple factors which can affect implementation performance and accuracy. For example, we found the off-the-shelf sampling code for the geometric distribution was not fast enough. We decided to roll our own using some tricks to extract more random samples per random bit along with a very fast quantized version of the log function. You also need to be careful that you are generating statistically independent samples for different shards. In summary, as is often the case, the devil is in the details.</p><p>Undaunted, we wrote a test harness using the <a href="https://elasticsearch-py.readthedocs.io/en/stable/">Elastic Python client</a> to programmatically generate aggregations and queries, and perform statistical tests of quality.</p><p>We wanted the approximations we produce to be unbiased. This means if you run a sampled aggregation repeatedly and averaged the results it would converge towards the true value. Standard machinery allows you to test if there is statistically significant evidence of bias. We used a <a href="https://en.wikipedia.org/wiki/Student%27s_t-test">t-test</a> for the difference between the statistic and true value for each aggregation. In over 300 different experiments, the minimum p-value was around 0.0003 which — given we ran 300 experiments — has about a 9% odds of occurring by chance. This is a little low, but not enough to worry about; furthermore the median p-value was 0.38.</p><p>We also tested whether various index properties affect the statistical properties. For example, we wanted to see if we could measure a statistically significant difference between the distribution of results with and without index sorting. A <a href="https://en.wikipedia.org/wiki/Kolmogorov%E2%80%93Smirnov_test">K-S test</a> can be used to check if samples come from the same distribution. In our 300 experiments the smallest p-value was around 0.002 which occurs with odds of about 45% by chance.</p><h2>Get started today</h2><p>We're not done with this feature yet. Once you have the ability to generate fast approximate results, a key question is: how accurate are those results? We're planning to integrate a confidence interval calculation directly into the aggregation framework to answer this efficiently in a future release. Learn more about random_sampler_aggregation in this documentation. You can explore this feature and more with a <a href="https://cloud.elastic.co/registration?elektra=whats-new-elastic-8-1-0-blog">free 14-day trial of Elastic Cloud</a>.</p><p><em>The release and timing of any features or functionality described in this post remain at Elastic's sole discretion. Any features or functionality not currently available may not be delivered on time or at all.</em></p>]]></content:encoded>
    <link>https://www.elastic.co/search-labs/blog/aggregate-data-faster-with-new-the-random-sampler-aggregation</link>
    <guid isPermaLink="true">https://www.elastic.co/search-labs/blog/aggregate-data-faster-with-new-the-random-sampler-aggregation</guid>
    <category><![CDATA[AI]]></category>
    <dc:creator><![CDATA[Benjamin Trent,Thomas Veasey]]></dc:creator>
    <enclosure url="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blte47734cb679b1cc8/6a171146a929cf44d5ae0ac5/bc75e4b6f15f183c75c931db011791301523d0cb-1217x840.png" length="0" type="image/png"/>
    <pubDate>Wed, 20 Apr 2022 00:00:00 GMT</pubDate>
  </item>
  </channel>
</rss>