<?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[ML Research - 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[ML Research - 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/blog/category/ml-research</link>
    </image>
    <link>https://www.elastic.co/search-labs/blog/category/ml-research</link>
    <atom:link href="https://www.elastic.co/search-labs/rss/category/ml-research.xml" rel="self" type="application/rss+xml"/>
    <language><![CDATA[en]]></language>
    <lastBuildDate>Mon, 21 Sep 2026 19:46:55 GMT</lastBuildDate>
  <item>
    <title><![CDATA[0.35% trained, 100% competitive: the frozen-tower architecture behind jina-embeddings-v5-omni]]></title>
    <description><![CDATA[The latest jina embeddings model generates multimodal embeddings for text, images, video and audio, competing with models nearly 6x its size on vector search while training just 0.35% of the weights.]]></description>
    <content:encoded><![CDATA[<p><code>jina-embeddings-v5-omni</code> is our latest multimodal embedding model. It generates embeddings for text, image, video, and audio. Among open-weight models that support those modalities, it’s the best-performing under 2 billion parameters. The notable part is how little of it we actually trained. Every encoder tower stayed frozen, and only about 0.35% of the model's weights (projectors and a handful of delimiter tokens) were ever updated during training. We call this architecture pattern <strong>G</strong>eometry-preserving <strong>E</strong>mbeddings via <strong>L</strong>ocked <strong>A</strong>ligned <strong>TO</strong>wers (GELATO). Let's break down each letter of this acronym:</p><ul><li><p><strong>Geometry-preserving Embeddings:</strong> <code>jina-embeddings-v5-omni</code> sits atop the foundation laid by <code>jina-embeddings-v5-text</code>. That original text embedding space is completely unchanged, with its geometry left wholly intact.</p></li><li><p><strong>Locked:</strong> Synonymous with "frozen." All of the towers in this architecture have their weights locked. </p></li><li><p><strong>Aligned:</strong> Aligning the other modalities with the text model's vector space, allowing for cross-modal comparison.</p></li><li><p><strong>TOwers:</strong> Modality component that converts one type of raw input into vectors.</p></li></ul><p>The model comes in two variants: <code>small</code> and <code>nano</code>. The former has more parameters (1.57 billion) than the latter (0.95 billion), but functionally their architectures are nearly identical. For the sake of brevity, we mostly focus on <code>jina-embeddings-v5-omni-small</code> in this article.</p><h2>What are vectors, towers and frozen encoders?</h2><p><code>jina-embeddings-v5-omni</code> relies on three core machine learning (ML) concepts: vectors, towers, and frozen weights. Here's what each means; feel free to skip ahead if you're already familiar with them. </p><h3>How vectors represent data in embedding models</h3><p>How does AI understand abstract concepts? Can a machine comprehend what "ice cream" is? Does it understand that "chocolate fudge" and "rocky road" have more in common with each other than "sorbet"? The answer, surprisingly, is yes. The mechanism that makes it possible is the vector.</p><p>Take a look at this diagram.</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt0424e9623c929644/6a7c3b16b8c2e6845bbe4875/image10.png" alt="Simplex diagram mapping ice cream flavors as vector coordinates to explain how embedding models represent similarity" /><p>If you tried visualizing a way to organize all ice cream flavors, you may end up with something like this: a simplex (triangle) with three vertices, each representing a base flavor. Each flavor is closer to or farther from each vertex, depending on how much of the corresponding base makes up that particular flavor. Chocolate ice cream is all chocolate, so it hugs the top vertex. Vanilla has a similar affinity for the bottom-left vertex. But cookies and cream is roughly a 50/50 mix, so it's about equidistant from both. Neapolitan blends all three, so it sits at the center of the triangle.</p><p>This is functionally how vectors work. These flavors get funneled into an ML model (more specifically known as an <em>embedding model</em>) that will then generate coordinates for each of these flavors along this simplex. By measuring the distance between coordinates, software can parse how related or unrelated two flavors are. It's easy to see that "chocolate" and "chocolate brownie fudge" are closely related flavors because they sit close to one another, but "strawberry" and "cookies and cream" are far away from each other, so we can infer that they aren’t similar.</p><p>It won't be quite this simple though. Rather than three labeled points, real embeddings run along hundreds or even thousands of dimensions. Nor will they have nice, human-readable labels; the vertex markers are something that only the model understands. The benefit, though, is that we can graph basically <em>anything</em> like this.</p><p>To clarify some jargon: These coordinates = vectors = embeddings. For the rest of this article, we use these terms interchangeably.</p><p>Much like how embeddings go by many different names, so too do the models that create them.</p><h3>What is a tower in multimodal embedding models?</h3><p>The term <em>tower</em> comes from Contrastive Language-Image Pre-training (CLIP), a model released by OpenAI in 2021 that was one of the first to learn a shared embedding space across text and images. In CLIP, each modality is handled by a completely separate model. On an architecture schematic, these models look like towers standing side by side, each taking one type of input and producing vectors in a shared space. The name stuck, and you'll see it used broadly across multimodal ML. </p><p>In the context of <code>jina-embeddings-v5-omni</code>, the word is used a bit more loosely. Its architecture doesn't have true parallel towers in the CLIP sense. All modalities ultimately funnel into a single central text model, rather than sitting as equals beside it. </p><p>With that caveat in place: A <em>tower </em>(or <em>modality component</em>) is a pipeline that converts one type of raw input into vectors. A <em>text tower</em> vectorizes strings, and a <em>vision tower</em> generates image embeddings. An <em>audio tower</em> does the same for sound.</p><h2>Why freeze a tower instead of training it?</h2><p>If we want multimodal capabilities, could we Frankenstein multiple towers that handle each of those inputs together into one model? The issue with this approach is that vectors from different models aren’t intelligible to each other. Images will exist in one vector space and audio in another, for example. This means that we have no ability to compare across different modalities. Think of the vectors outputted from one model as existing in their own language. Let's say our audio tower outputs Spanish and our image tower outputs English. Conceptually, the vectors can be describing the same things, but downstream tools that try to make use of these embeddings are "monolingual," so we're out of luck.</p><p>The CLIP-style approach is to take several towers and train them together, letting them all reshape each other until their outputs agree. It's sort of like having Spanish and English speakers try to communicate for long enough that they eventually all start speaking Spanglish.</p><p>This approach works, but it has a side effect: Towers that you already had working get remodeled in the process. Any embeddings that they produced before are now incompatible with any that are produced by the older version of the tower. If you had a text tower and generated 100 million embeddings with it, you would now need to re-embed all of those strings.</p><p>To combat this issue, you can freeze certain towers. This locks their weights, which are the knobs and dials that influence how they behave. This way, training never changes them. What you train instead is a small projector that <em>translates</em> one tower's output into another tower's language. Many different models train some portion of projectors and towers while leaving some others frozen. What makes <code>jina-embeddings-v5-omni</code>unique is that we froze <em>every</em> encoder and trained only the projectors and delimiter tokens. By the end of this article, you’ll understand exactly how that works.</p><p>You can think of these frozen and trainable components as clusters of neurons or isolated regions of the mind, and the whole embedding model (<code>jina-embeddings-v5-omni</code>) as the entire brain. Between these learning-capable components sit fixed math operations, such as merging, squashing, selecting, and rescaling numbers on their way from one tower to the next. They have no weights, so there’s nothing in them to freeze or train. </p><p>With that groundwork laid, here’s the full architecture.</p><h2>How the jina embeddings multimodal architecture works</h2><p>The architecture routes all modalities through frozen encoders and small trainable projectors into a single shared text embedding space.</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt7e1a4560e21a7922/6a7c3b3ccffa6ee67a5e7b27/image6.png" alt="jina-embeddings-v5-omni architecture diagram showing frozen encoders, trainable projectors, and shared text embedding space" /><h2>Why the text tower is the backbone</h2><p>The first thing to notice here is the general flow of data in this diagram. Image, video, and audio inputs all (eventually) end up in the same space as text inputs. Why is the architecture set up like this?</p><p><code>jina-embeddings-v5-omni</code> builds on top of <code>jina-embeddings-v5-text</code>: It retains its text-processing backbone and extends it with pretrained vision and audio components.</p><p>This has two main advantages:</p><ol><li><p>The text model is already state of the art for its size and what it does, and it stays completely untouched. There’s no need to fix what isn't broken. We also don't need to re-embed anything we already embedded with <code>jina-embeddings-v5-text</code>.</p></li><li><p>A single shared vector space is what makes cross-modal search work at all. Because image, audio, and text all resolve to vectors in the same geometry, you can query an image with text, or audio with text, and compare them directly with <em>cosine similarity</em> (a similarity measure based on the angle between two vectors). If each modality lived in its own separate space, those comparisons would be meaningless.</p></li></ol><h2>How vision and audio encoders feed into the text model</h2><p>The first step in building upon the foundation set by <a href="https://huggingface.co/collections/jinaai/jina-embeddings-v5-text">jina-embeddings-v5-text</a> is integrating vision and audio encoders into this architecture. In this case, we use the existing <a href="https://qwen.ai/blog?id=qwen3.5">Qwen3.5</a> vision encoders and the <a href="https://qwen.ai/blog?id=qwen2.5-omni">Qwen2.5-Omni</a> audio encoder, which themselves have been adapted from <a href="https://huggingface.co/docs/transformers/model_doc/siglip2">SigLIP2</a> and <a href="https://huggingface.co/openai/whisper-large-v3">Whisper-large-v3</a>, respectively. They’re ultimately what’s responsible for generating raw vectors for all vision- and audio-based data. The emphasis is on <em>raw</em> here, since much transformation still needs to be done afterward.</p><h2>How the vision encoder processes images</h2><p>In the case of images, we’re borrowing more from Qwen than just the encoder. Additional plumbing inherited from Qwen is attached to the output of the encoder. Let's walk through what comes out of the encoder and how the inherited downstream components transform that output.</p><h3>Vision encoder (frozen)</h3><p>Let's use an image as our primary example, since the visual component of video is basically identical. Take this image of a banana split.</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blta37c8dee62d6377a/6a7c3b59ef5bef22a14f9b4f/image12.png" alt="Banana split photo used as example input for the jina-embeddings-v5-omni vision encoder" /><p>Rather than generating one single, clean vector embedding for this image, the vision encoder breaks the image up into 14-pixel by 14-pixel sections and generates a tiny <em>patch token</em> (basically a mini-vector) for each of these sections.</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt1867889b7011a10b/6a7c3b89ef5bef80504f9b53/image8.png" alt="Banana split image divided into 14x14 pixel patches showing how the vision encoder generates patch tokens" /><p>While still inside of the vision encoder, each token looks at every other token that makes up the image and pulls in information from the ones relevant to it, updating its own vector based on that context. This process allows us to preserve fine-grained details. Now, instead of one single vector for the whole image, we have multiple, smaller vectors that represent particulars of the whole dish.</p><h3>LayerNorm (frozen)</h3><p><a href="https://arxiv.org/abs/1607.06450">LayerNorm</a> rescales each patch's numbers so they sit in a consistent range before anything else touches them. It stops some patches from being wildly larger than others and drowning out the rest.</p><h3>2x2 merge </h3><p>We saw in the vision encoder section that we split up the image into small, 14-pixel by 14-pixel squares. However, processing this many patch tokens will become expensive downstream. For this reason, the 2x2 merge operation consolidates four patch tokens into one. The squares are now 28 pixels by 28 pixels. The corresponding patch tokens are similarly consolidated.</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt851c4e33cf477e3e/6a7c3ba429138b20673b1555/image4.png" alt="Banana split with 28x28 pixel patches after 2x2 merge reduces token count before the projector" /><h3>fc_vision_1 (frozen) </h3><p><code>fc_vision_1</code> is the first of two matrix multiplies; it mixes the merged patch numbers into a new set. This layer is inherited from Qwen and left as is.</p><h3>GELU </h3><p><a href="https://arxiv.org/abs/1606.08415">Gaussian Error Linear Unit (GELU)</a> is a gate applied to each number. It lets useful signals through and squashes the rest toward zero. It’s the one nonlinear step and is ultimately what lets the two <code>fc_vision</code> layers together learn shapes that a single flat multiply couldn't.</p><h3>fc_vision_2 (trainable) </h3><p>As mentioned earlier, trying to put Qwen image embeddings directly into the same vector space as Jina text embeddings would be like trying to include a Spanish sentence in an English novel. Outside of its native context, its meaning is totally lost.</p><p>That is what the trainable projector <code>fc_vision_2</code> is here to fix. It learns to translate the image embedding into something that <code>jina-embeddings-v5-text</code> can understand. For this reason, you can think of a trainable projector as a translator.</p><p>The emphasis belongs on <em>trainable</em>: This is the first component we’ve encountered in our walkthrough so far that isn’t frozen. Many components of this architecture are frozen, meaning that their weights are locked and never change during training, but <code>fc_vision_2</code> is one of the few parts that actually gets updated, because it has to <em>learn</em> how to translate Qwen's image dimensions into a form that lands meaningfully inside Jina's text space.</p><h3>× 4</h3><p>By now, you may have noticed that <code>fc_vision_2</code> has a "× 4" marked on the bottom, along with <code>fc_audio</code>, both encoders, and the ″Embedding text″ section within <code>jina-embeddings-v5-text</code>. In this case, it represents four different instances of <code>fc_vision_2</code>, each optimized and tuned to one of four slightly different tasks, outlined below.</p><p><strong>Task</strong></p><p><strong>What it facilitates</strong></p><p><strong>Example user input</strong></p><p><strong>Example end result</strong></p><p><strong>Note</strong></p><p>Retrieval</p><p>Finds a similar match for the input (that is, standard Google search)</p><p>"melting ice cream" as a string/text</p><p>Picture of a fallen ice cream cone on asphalt</p><p>
</p><p>Text-matching</p><p>Judges how similar inputs are</p><p>A text string "melting ice cream" and an image of a fallen ice cream cone on asphalt</p><p>Score judging how similar the two inputs are</p><p>The name of this task is a bit of a misnomer. It's called <em>text-matching</em>, but it works for any modality, not only text.</p><p>Clustering</p><p>Groups data into clusters</p><p>A large array of ice cream images</p><p>Lets the user discover natural groups, like "sundaes" and "popsicles"</p><p>
</p><p>Classification</p><p>Places data into predefined buckets</p><p>Two string labels: "melting" and "intact", along with a large array of ice cream images</p><p>Sorts the array of ice cream images into the two provided categories, based on their proximity in vector space to the label embeddings</p><p>
</p><p>To clarify, the "Example end result" column is the takeaway after some additional math and processing happens once the output vector is generated. The point is that  <code>jina-embeddings-v5-omni</code> only generates vectors. Those vectors take on a mildly different form to optimize for the selected task type.</p><p>These task types are also explicitly outlined in the Low-Rank Adaptation (LoRA) component of the architecture diagram, which we'll cover in a moment.</p><h2>How the audio encoder processes sound</h2><p>Before we go any deeper into the trenches of our model architecture, let's back up and see how the audio-oriented path differs and how it stays the same. If you were able to follow along during the vision section, the audio portion will be a breeze. We have no extra inherited plumbing from Qwen this time, only the audio encoder and one trainable projector.</p><h3>Audio encoder (frozen)</h3><p>Before audio can enter the encoder, it needs to be converted into a form that the encoder can work with. Raw audio is a one-dimensional wave, which isn't particularly useful to a neural network on its own. Instead, the audio is first transformed into a <em>mel spectrogram</em>: a 2D representation that maps frequency against time, weighted to emphasize the frequency ranges that the human ear is most sensitive to. Think of it as a visual fingerprint of the sound. Below is a mel spectrogram of a person saying, "I scream, you scream, we all scream for ice cream."</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt3a69dc8ef9bc87a2/6a7c3bc3b59102b0f4eebd49/image5.png" alt="Mel spectrogram of a person saying I scream you scream we all scream for ice cream, used as audio encoder input" /><p>That spectrogram is then sliced into fixed-length 40ms chunks, analogous to how the vision encoder breaks an image into 14×14 pixel-tiles. The encoder then produces one token per chunk.</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blteec40a3a17212d5e/6a7c3bd829138bb20b3b1559/image3.png" alt="Mel spectrogram sliced into 40ms chunks showing how the audio encoder tokenizes sound for multimodal embeddings" /><p>This matters for the same reason it does in the vision pipeline: Fine-grained detail would otherwise be lost. A single embedding for the full phrase "I scream, you scream, we all scream for ice cream" would smear everything into one blurry vector. Slicing it into short chunks keeps each fragment of sound intact, so the model can later distinguish "scream" from "cream" rather than collapsing them into an average. The same applies to music, animal noises, environmental sounds, and more.</p><h3>fc_audio (trainable)</h3><p>Once the encoder has produced its tokens, <code>fc_audio</code> performs the same translation role that <code>fc_vision_2</code> does for images: It projects each audio token from the encoder's 1280-dimensional output space into <code>jina-embeddings-v5-text</code>’s hidden dimension (1024 for <code>small</code>, 768 for <code>nano</code>). Like <code>fc_vision_2</code>, it carries a "× 4" in the architecture diagram, meaning that there are four instances, each optimized for a specific task type (retrieval, text-matching, clustering, and classification).</p><h3>Delimiters (trainable)</h3><p>We need to convey to <code>jina-embeddings-v5-text</code> that this embedding represents an unexpected modality. We can do this pretty easily with delimiters. In the world of HTML, this looks like:</p>&lt;p&gt; Your text here &lt;/p&gt;<p>The first and last <code>&lt;p&gt;</code> tag conveys that everything in between them is a paragraph element.</p><p>In our case, things are a bit more complicated. For audio, the architecture diagram shows the delimiters as <code>&lt;aud_start&gt;</code> and <code>&lt;aud_end&gt;</code>, but that isn't quite accurate. The delimiters themselves are actually vectors rather than hard-coded strings.</p><p>Each task type has its own pair of delimiter vectors (hence the "4 × special tokens"). These vectors are identical every time rather than being different for each audio embedding. So a task type of retrieval with an audio input type always gets start delimiter vector X and end delimiter vector Y; text-match with audio input always gets a start delimiter vector A and an end delimiter vector B, and so on.</p><p>This is necessary because the <code>Transformer layers</code> component only understands vectors. So, by the time the audio embedding makes its way there, it looks like:</p>&lt;aud_start_vector_delimiter&gt;
&lt;aud_patch_token_1&gt;
&lt;aud_patch_token_2&gt;
&lt;aud_patch_token_3&gt;
...
&lt;aud_end_vector_delimiter&gt;<p>The same pattern applies to images:</p>&lt;vis_start_vector_delimiter&gt;
&lt;vis_patch_token_1&gt;
&lt;vis_patch_token_2&gt;
&lt;vis_patch_token_3&gt;
...
&lt;vis_end_vector_delimiter&gt;<p>Video is where this gets interesting. Up to 32 evenly spaced-out frames are pulled from the video and fed into the vision encoder. </p><p>Rather than producing one delimiter-wrapped segment, each sampled frame gets its own <code>&lt;vis_start&gt;</code> / <code>&lt;vis_end&gt;</code> wrapper, and these per-frame segments are concatenated into one long sequence:</p>&lt;vis_start&gt; [frame 1 patch tokens] &lt;vis_end&gt;
&lt;vis_start&gt; [frame 2 patch tokens] &lt;vis_end&gt;
...
&lt;vis_start&gt; [frame 32 patch tokens] &lt;vis_end&gt;<p>This is what makes multi-frame video work: Rather than averaging frames or treating them separately, the transformer receives the whole video as one token stream, allowing its attention to relate tokens across frames. This means that earlier frames can inform how later ones are interpreted. If the video does have an audio component, it's pulled out and fed into the audio encoder and ultimately prepended to the frame sequence.</p>&lt;aud_start&gt; [audio patch tokens] &lt;aud_end&gt;
&lt;vis_start&gt; [frame 1 patch tokens] &lt;vis_end&gt;
&lt;vis_start&gt; [frame 2 patch tokens] &lt;vis_end&gt;
...<p>The transformer layers component then processes this entire concatenated sequence as a single input.</p><h3>Jina text transformer layers (frozen) </h3><p>We’ve generated patch tokens for our images, videos, and audio files. We’ve also wrapped them inside of vector delimiters, all for the sake of having them understood by these layers. They’ll allow each patch token to examine the other patch tokens and determine whether they need to update themselves based on the surrounding context. I know what you're thinking:</p><p>Didn't we already do this inside of the encoder? We split up the image into 14-pixel by 14-pixel sections and generated patch tokens for each section, and then the encoder updated each patch token based on surrounding context within the same image.</p><p>And you're right! We did. But there's a key difference now.</p><p>Originally, that recontextualization ran on the attention of Qwen's vision encoder. The operation within the <code>Transformer layers</code> runs the frozen Jina text transformer's attention. It’s the same operation with different learned parameters, so it transforms the tokens differently.</p><p>Think of it like a move: The projector is the flight and the moving trucks. It physically relocates you from vision land to text land, landing you in the right city and even the right neighborhood. The transformer's attention is what happens after you've unpacked. You're already home; you spend the next few weeks figuring out exactly where you fit, meeting the neighbors, finding your bearings, and adjusting your exact spot based on who's actually around you. You did the macro move already. This is the micro fine-tuning.</p><p>Lastly, the <code>&lt;vis_end_vector_delimiter&gt;</code> will absorb all the information from the patch tokens it wraps.</p><h3>LoRA (frozen) </h3><p><a href="https://arxiv.org/abs/2106.09685">LoRA</a> is a way of fine-tuning an existing model without completely retraining it. It’s a small set of extra adjustment knobs bolted onto the transformer that nudges its behavior to optimize for one of the specific tasks (such as retrieval or classification).</p><h3>Last-token pooling </h3><p>Since <code>&lt;vis_end_vector_delimiter&gt;</code> absorbed all the other patch tokens into itself, we don't need to consider anything except it, so we throw the rest away. It acts as a stand-alone embedding that represents a summary of the whole.</p><h3>L2 normalization </h3><p><code>&lt;vis_end_vector_delimiter&gt;</code> could be any length now, which is no good. This step shrinks or stretches it so its length is exactly 1, without changing the direction it points. This is tidying so that comparing it to other vectors later is a fair, clean angle comparison. It changes only the vector’s scale, not what it means.</p><h3>Enough about ice cream</h3><p>As we've journeyed our way through <code>jina-embeddings-v5-omni</code>, we’ve been careful to outline which components are frozen and which ones are trainable. By now, you may have noticed that every single tower has been frozen. In fact, only the small projectors (translators) and delimiter tokens have been trainable. We dubbed this architecture pattern GELATO. This makes the entire training process significantly cheaper.</p><p>To be clear, we didn't invent the concept of frozen towers. Prior work on <a href="https://arxiv.org/abs/2111.07991">Locked-image Tuning (LiT)</a>, <a href="https://arxiv.org/abs/2406.04292">VISTA</a>, and <a href="https://arxiv.org/abs/2310.14037">Multi-modAl Retrieval model via Visual modulE pLugin (MARVEL)</a> froze one side or the other. What no one had done before GELATO was push the idea to its limit: text, image, video, and audio all in one model with every encoder frozen. The only trained pieces are a single projector layer per modality and a handful of delimiter tokens.</p><h2>But is it any good?</h2><h3>Benchmark results: jina embeddings vs. other multimodal embedding models</h3><p>There's no point in building out a model and releasing it if you don't even know if it's any good, especially compared to the competition. That's why we have benchmarks and evaluation frameworks. The benchmarks that <code>jina-embeddings-v5-omni</code> was run against are Massive Image Embedding Benchmark (MIEB), Massive Audio Embedding Benchmark (MAEB), Massive Multimodal Embedding Benchmark–Video (MMEB-Video), and Massive Multilingual Text Embedding Benchmark (MMTEB).</p><p>As for the models we compare against, it's important not to make apples and oranges comparisons. For that reason, we’re specifically using open-weight omni-style models with support for the same media types:</p><ul><li><p><a href="https://huggingface.co/collections/LanguageBind/languagebind-model">LanguageBind</a></p></li><li><p><a href="https://huggingface.co/nvidia/omni-embed-nemotron-3b">Omni-Embed-Nemotron-3B</a></p></li><li><p><a href="https://huggingface.co/LCO-Embedding/LCO-Embedding-Omni-3B">LCO-Embedding-Omni-3B</a></p></li><li><p><a href="https://huggingface.co/LCO-Embedding/LCO-Embedding-Omni-7B">LCO-Embedding-Omni-7B</a></p></li></ul><h3>Evaluation</h3><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt73d8f8759a643405/6a7c3c081d23d2a618b4e44d/image1.png" alt=" Benchmark table comparing jina-embeddings-v5-omni against multimodal embedding models on text, image, video and audio" /><p>The table above is sorted by parameter count. Models with the fewest parameters are clustered at the top, and models with the most parameters hug the bottom. This context is important because performance alone isn't the final variable. If a $100 ice cream with gold flakes and the finest dairy milk tastes the same as (or worse than) the average gallon of ice cream that you can buy from your local grocery store, it would be silly to buy it, because you're paying a massive premium for nothing.</p><p>A similar situation is unfolding here. <code>jina-embeddings-v5-omni-small</code> and <code>nano</code> outperform every other model on text, despite ranking low to middle in terms of parameter count.</p><p>Audio performance is strong, as well. <code>jina-embeddings-v5-omni-small</code> and <code>nano</code> beat out all other models except those from LCO, which they both trail by about 2 to 3 points.</p><p>The gap shrinks when considering image performance, particularly with <code>jina-embeddings-v5-omni-small</code>. It beats both <code>LanguageBind</code> and <code>Omni-Embed-Nemotron-3B</code><code>.</code> It lags less than a point behind both LCO models, despite the fact that they have 4.70 billion and 8.93 billion, respectively, compared to Jina’s 1.57 billion.</p><p>Video is the weakest performer for our models, though even in that case <code>jina-embeddings-v5-omni-small</code> still beats <code>Omni-Embed-Nemotron-3B</code>, which has three times as many parameters. Ultimately, when these scores are averaged out, you get the following rankings:</p><p><strong>Model</strong></p><p><strong>Number of parameters (B)</strong></p><p><strong>Average score</strong></p><p><code>LCO-Embedding-Omni-7B</code></p><p>8.93</p><p>54.43</p><p><code>jina-embeddings-v5-omni-small</code></p><p>1.57</p><p>54.04</p><p><code>LCO-Embedding-Omni-3B</code></p><p>4.70</p><p>53.83</p><p><code>jina-embeddings-v5-omni-nano</code></p><p>0.95</p><p>47.49</p><p><code>Omni-Embed-Nemotron-3B</code></p><p>4.70</p><p>41.21</p><p><code>LanguageBind</code> </p><p>1.14</p><p>35.82</p><p><code>LCO-Embedding-Omni-7B</code> has nearly six times the number of parameters as <code>jina-embeddings-v5-omni-small</code> but barely squeaks past it in average performance.</p><p>One benchmark deserves a special callout for anyone building search or retrieval augmented generation (RAG): visual document retrieval, measured on the <a href="https://huggingface.co/vidore">ViDoRe benchmark</a>. </p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt0637adcdd0cbe9fe/6a7c3c245751aa67567e250e/image11.png" alt="ViDoRe visual document retrieval scores showing jina-embeddings-v5-omni matching larger models with fewer parameters" /><p>Here, <code>jina-embeddings-v5-omni-small</code> scores 79.25 using only 0.92 billion active text-and-image parameters, ahead of <code>LCO-Embedding-Omni-3B</code> (78.24) and within striking distance of <code>LCO-Embedding-Omni-7B</code> (80.32), a model nearly 10 times its size on that path. <code>nano</code> matches that exact same 79.25 score with just 0.31 billion active parameters. The larger <code>Omni-Embed-Nemotron-3B</code> does take the top spot at 85.64, but it carries roughly five times the active parameters of <code>jina-embeddings-v5-omni-small</code>, so our models remain the most parameter-efficient of the group. If your workload is retrieving pages of documents by their layout and text, this is the number to weigh.</p><h3>Limitations of frozen-tower multimodal embeddings</h3><p>GELATO's frozen-tower design delivers strong results at low training cost, but it comes with trade-offs worth naming plainly. As already mentioned, the most consistent weak spot is video. <code>jina-embeddings-v5-omni-small</code> trails the LCO models on video, and <em>moment retrieval</em> (locating a specific event within a clip) is the weakest subtask of all. This is partly structural, since each frame produces its own token set before everything is concatenated and pooled into a single final embedding. Packing that much information into one embedding means that the early dimensions carry a heavier load, so video embeddings degrade faster than image embeddings when truncated to smaller sizes.</p><p>Audio has its own gap. While retrieval and classification scores are competitive, audio clustering is the weakest audio subtask (6.13 for <code>jina-embeddings-v5-omni-small</code>). </p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blta4ff53dd4329772f/6a7c3c43f1246402446febad/image13.png" alt=" Detailed benchmark results by task type for jina-embeddings-v5-omni across MIEB, MMEB-Video and MAEB" /><p>Cross-modal audio–text retrieval trails <code>LCO-Omni-7B</code> by 11–15 percentage points, a larger gap than the 6–7 points seen on the image–text (I-T) pair. The <code>fc_audio</code> projector is the natural next target for additional trainable parameters, suggesting the audio–text (A-T) alignment path has more room to grow than the multilayer vision pipeline. </p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/bltdc303bd9ff664c18/6a7c3cf05751aa08427e2515/image9.png" alt="Five vision ablation configurations testing frozen vs trainable encoders and projectors for multimodal embeddings" /><h3>How multimodal embeddings distribute in vector space</h3><p>We've already discussed performance via benchmarks, but what about how the actual embeddings are distributed in vector space? How does that tangibly differ from model to model?</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt225cf7e9ce7b2f98/6a7c3c570da673645857bd58/image15.png" alt="Three audio ablation configurations showing projector-first training outperforms encoder-first for audio embeddings" /><p>In the above illustration, data from video clips is funneled into each model and graphed in vector space. It’s then compressed down to two dimensions via the <a href="https://arxiv.org/abs/1802.03426">Uniform Manifold Approximation and Projection (UMAP)</a> method for easy visualization. Each modality corresponds to a different component of the video:</p><p><strong>Modality</strong></p><p><strong>Component of the video</strong></p><p>Image</p><p>Frame from the middle of the video</p><p>Video</p><p>The full video</p><p>Audio</p><p>Audio track from the video</p><p>Text</p><p>Description of the video</p><p>Immediately, some interesting patterns stand out.</p><p>Our models and the LCO models seem to have different modalities all mixed together, while <code>LanguageBind</code> and <code>Omni-Embed-Nemotron-3B</code> seem to lean more toward having their embeddings separated by modality.</p><p>Our models and the LCO models exhibit <em>interleaved geometry</em> for these vectors. This means that different modalities aren't clearly separated in vector space, but instead intermingle in similar areas. This is less pronounced with <code>Omni-Embed-Nemotron-3B</code>, since only image and video seem to occupy a similar space.</p><p><code>LanguageBind</code> is fully separated, with different modalities occupying entirely different spaces. This is known as the <em>modality-gap pattern</em>.</p><p>So which one is better? In practice, interleaved geometry tends to be the more useful of the two, and it’s worth noticing that the strongest models in our benchmarks (ours and LCO's) all exhibit it. However, there are trade-offs.</p><p>Interleaved geometry excels at cross-modal retrieval, since everything is jumbled up together in the vector space and, therefore, much closer. It's easier to find a matching picture for the text "strawberry ice cream" when the text and image embeddings sit so close together in vector space.</p><p>When you're trying to do a same-modality task though, the image that was so conveniently within reach is now in the way. However, in practice, this is easily mitigated by metadata filtering on something like a “modality” field. </p><p>No such workaround exists for the issues inherent to models that exhibit the modality-gap pattern. It’s easy to find another video of syrup poured on ice cream, since all the videos are sitting together in isolation. But having modalities confined into clusters like that makes finding an accompanying image much harder.</p><h2>Why this architecture?</h2><p>GELATO gives a lot of performance for very little training. You keep all your towers as they are and train only small projectors and delimiter tokens, which is significantly cheaper than the alternatives. To put concrete numbers on "cheaper": for <code>jina-embeddings-v5-omni-small</code>, training just the vision projector updates 4.20 million parameters instead of the 920.6 million a full fine-tune would touch. At the same 15,000-step budget, that projector-only run finishes about 1.8 times faster and peaks at 7.52 GiB of GPU memory instead of 12.96 GiB. The audio path shows an even wider gap, with projector-only training running 3.2 to 3.9 times faster than full training. But how did we conclude this was the way to go? We used a process known as <em>ablation</em>.</p><p>Ablation is when you remove or change one piece of a system to see how much it actually mattered. Imagine you've been working on an ice cream recipe. Every time you make a tweak, like doubling the milk, swapping brown sugar for white, using vanilla beans instead of extract, or taking out the chocolate chunks, that's ablation.</p><p>Ablation in ML functions much in the same way. It asks whether removing, rearranging, freezing, or unfreezing certain components makes the whole system more, less, or equally as performant. In this case, we’re particularly interested in whether unfreezing certain components, and in what order, may affect performance. We conducted five ablation studies on the <code>Qwen3.5</code> vision stack. The results are measured in mean nDCG@10 (normalized Discounted Cumulative Gain), a standard score for ranking quality where higher is better.</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blteebe4f380bb51372/6a7c3c77f6ab872458d15bc2/image7.png" alt=" Cross-modal retrieval metrics for image-text and audio-text pairs across multimodal embedding models" /><p>Overall, nearly every ablation study yielded basically identical results, except for case #3, which performed terribly. Before we explain why, let's look at the equivalent ablation diagram for audio.</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt86c48cbf40c279c1/6a7c3c881d23d290d8b4e451/image2.png" alt="UMAP plots comparing interleaved embedding geometry in jina-embeddings-v5-omni vs modality-gap patterns in competitors" /><p>In this instance, ablation case #2 performs the worst. Do you see the commonality between the worst performer here and the worst performer among the vision ablations? Across both modalities, the same rule holds: If you unfreeze the encoder before the projector has been trained, you’ll see worse performance.</p><p>For both modalities, we ultimately chose ablation case #1 for the final architecture. Both had relatively high scores. In vision's case, the configurations that edged out case #1 did so by margins too small to justify their added training stages and extra per-task artifacts. A similar story unfolds for audio, with case #3 beating out case #1 by a small margin but requiring more per-task artifacts.</p><p>Ablation validates the GELATO approach: It's cheaper and nearly identical in quality to train a dedicated translator (rather than the speaker).</p><h2>Summary: why frozen encoders make multimodal embeddings cheaper</h2><p>Rather than expensively retraining multiple towers to achieve multimodal capabilities, GELATO allows us to minimize cost by freezing our already functioning towers and training only small projectors to translate embeddings. These embeddings get funneled into <code>jina-embeddings-v5-text</code>, ultimately allowing all the output vectors to exist in the same, interleaved geometry. We can now compare text, audio, images, and video at a fraction of the cost of the competition. </p><p>Both <code>jina-embeddings-v5-omni-small</code> and <code>jina-embeddings-v5-omni-nano</code> are open-weight for personal use and available now. You can download them from the <a href="https://huggingface.co/jinaai">Jina AI collection on Hugging Face</a> and start generating multimodal embeddings today, or read the <a href="https://arxiv.org/abs/2605.08384">full technical report</a> for the complete set of benchmarks and ablations.</p>]]></content:encoded>
    <link>https://www.elastic.co/search-labs/blog/multimodal-embeddings-gelato-jina-v5-omni</link>
    <guid isPermaLink="true">https://www.elastic.co/search-labs/blog/multimodal-embeddings-gelato-jina-v5-omni</guid>
    <category><![CDATA[Jina AI]]></category>
    <category><![CDATA[ML Research]]></category>
    <dc:creator><![CDATA[Jon Avezbaki]]></dc:creator>
    <enclosure url="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt640f1072281c7143/6a7c3ae7cffa6ef6105e7b23/image14.png" length="0" type="image/png"/>
    <pubDate>Wed, 12 Aug 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[56% faster, up to 50% better retrieval performance: What's inside Jina's new 600 million parameter listwise reranker]]></title>
    <description><![CDATA[Jina Reranker 3.5 beats v3 by 50%+ on case law, closes the gap with models 7x its size on legal, medical, and financial benchmarks, and beats them outright on structured data. It's a drop-in replacement for v3, with no API changes.]]></description>
    <content:encoded><![CDATA[<p><code>jina-reranker-v3.5</code> is a 600 million parameter reranker that delivers major gains over its predecessor, <a href="https://jina.ai/models/jina-reranker-v3"><code>jina-reranker-v3</code></a>, on legal retrieval, and closes most of the gap to models seven times its size on legal, medical, and financial reranking. On long documents, it runs up to 56% faster than <code>jina-reranker-v3</code> and scores over 50% higher on case law retrieval. It also beats Qwen3-Reranker-4B on the STaRK structured data benchmark."It’s a drop-in replacement for v3 users and requires no changes to the code that accesses the model.</p><h2>What’s a reranker and how does it work?</h2><p>A <em>reranker </em>is an AI model used near the end of an information retrieval pipeline, after other modules have assembled a short list of candidate matches to a query. It’s trained to order the candidate list from best matching to least. Using a specialized model focused purely on ranking candidate matches can improve result quality dramatically.</p><p>Jina AI’s latest rerankers use a technique called <em>late interaction</em>, where queries and documents are encoded separately into lists of token embeddings that reflect each token’s semantics in context and then compared to each other.</p><p>This is an AI analog of lexical and grammatical disambiguation.</p><p>For example, consider the meaning of the word <em>match </em>in these two sentences:</p><ul><li><p>She looked for a match to light the candle.</p></li><li><p>She looked for a match on Tinder.</p></li></ul><p>The first sentence might be a match for queries about matchboxes; the second for queries about romance.</p><p>Transformer-based models do this kind of in-context disambiguation but bring much richer information into the token embeddings they produce. The word <em>match</em> might have a semantic embedding near to words like <em>fire</em> or <em>illumination</em> in the first sentence, while in the second, it might be closer to <em>smartphone</em> or <em>swipe</em>.</p><p>Late interaction rerankers generate these context-enriched token embeddings for both the query and the candidate documents and then compare them to produce sortable scores. They’re completely agnostic about how candidate match lists are created. The reranker works exactly the same when combined with lexical search schemes, like BM25, AI-driven semantic embeddings-based retrieval, or hybrid and federated search systems that may retrieve multiple candidate lists from different sources or using different algorithms. Of course, the results always depend on the quality of the candidates, so a reranker can’t fix bad first-stage retrieval, but it almost always improves whatever you’ve got.</p><p><code>jina-reranker-v3.5</code> is a <em>listwise</em> reranker using the <a href="https://jina.ai/news/jina-reranker-v3-0-6b-listwise-reranker-for-sota-multilingual-retrieval/#:~:text=query%2Ddocument%20interaction%20%22-,last%20but%20not%20late,-.%22%20It%27s%20%22last"><em>last-but-not-late</em></a> technique developed for <code>jina-reranker-v3</code>. The query and a list of candidate matches are passed into the model together and processed in one pass, returning a numerical score for each candidate. This enables the model to use context information from the query and the full candidate list to make sense of the entire input, producing better results because of the richer information available to it.</p><p><code>jina-reranker-v3</code> proved that listwise rerankers with last-but-not-late interaction can compete with the largest models on general reranking benchmarks. Only <code>jina-reranker-v3.5</code> and models with over four billion parameters beat it on <a href="https://mteb-leaderboard.hf.space/benchmark/MTEB(Multilingual%2C%20v2)">Massive Text Embedding Benchmark (MTEB) reranker tasks</a>. However, this approach places strict limits on candidate list sizes. The query and all candidate matches must fit in the input context window of the model.</p><h2>What problems does Jina Reranker v3.5 solve?</h2><p>Despite having frontier-level performance overall, <code>jina-reranker-v3</code> has some notable performance gaps:</p><h3>Domain-specific text retrieval</h3><p><code>jina-reranker-v3</code> was trained on general text corpora and, as a result, it underperforms on important use cases, particularly:</p><ul><li><p>Legal texts, like case law and contract clauses.</p></li><li><p>Medical literature, like clinical trials and patient records.</p></li><li><p>Financial datasets and other texts full of important numbers.</p></li><li><p>Computer programming and IT documentation.</p></li><li><p>Product catalogs full of technical terminology and specifications.</p></li></ul><h3>Structured data: Tables, JSON, and key-value records</h3><p>Vast quantities of essential, real-world data is encoded in spreadsheets, tables, key-value lists, and structured records, like JSON data. However, rerankers trained purely for textual comparison, like <code>jina-reranker-v3</code>, perform poorly on this kind of data.</p><h3>Compute costs for long candidate lists</h3><p>The self-attention architecture at the core of most text-processing AI models means that memory and compute requirements grow quadratically with the size of its input. This makes <code>jina-reranker-v3</code>, like other AI models, very computationally expensive to run with a full input context window. But, to make the most effective use of the model, we want to put as many match candidates as possible into its input. When it’s at its most useful, it’s also slower and more expensive to run.</p><p>We’ve developed <code>jina-reranker-v3.5</code> specifically to address these issues without reducing its performance on general purpose text retrieval.</p><h2>What’s new in Jina Reranker v3.5?</h2><p><code>jina-reranker-v3.5</code> contains a modified self-attention mechanism that enhances performance, increases processing speed, and reduces the resources required at inference time to process a full input context window. We’ve also introduced a new three-stage self-distillation training process better suited to the sliding-window architecture of large input context models.</p><p>We’ve also curated and used training data focusing on the performance gaps we identified in <code>jina-reranker-v3</code>, including:</p><ul><li><p>Multilingual legal texts drawn from diverse international sources.</p></li><li><p>Medical texts drawn largely from scientific literature and materials used for other AI projects, including a collection of Chinese medical question-answer pairs.</p></li><li><p>Financial industry data, including investment-related question-answer pairs, regulations, and tables with numbers and associated texts.</p></li><li><p>Structured data, especially from ecommerce sources and public corpora of tables.</p></li><li><p>Expanded multilingual and cross-language texts.</p></li></ul><p>For details on the data sources and technical innovations in <code>jina-reranker-v3.5</code>, see <a href="https://arxiv.org/abs/2607.18152">our technical report</a>.</p><h2>How Jina Reranker 3.5 performs on retrieval benchmarks</h2><p>Parameters</p><p>597 million</p><p>Input modalities</p><p>Text only</p><p>Context window size</p><p>131,072 tokens</p><p>Maximum number of candidate matches</p><p>No fixed limit, but all candidates and query must fit in the context window.</p><p>Languages</p><p>Training in 52 languages</p><h3>General text reranking performance (BEIR and MIRACL)</h3><p><code>jina-reranker-v3.5</code> improves on <code>jina-reranker-v3</code>’s performance on general text reranking benchmarks. On the English-language <a href="https://github.com/beir-cellar/beir">Benchmarking Information Retrieval (BEIR) benchmark</a>, the average score has increased enough to surpass the frontier <a href="https://huggingface.co/Qwen/Qwen3-Reranker-4B">Qwen3-Reranker-4B</a> and <a href="https://huggingface.co/Qwen/Qwen3-Reranker-0.6B">0.6B</a> models and Mixedbread AI’s rerankers.</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt2be1aaecef5d0cbd/6a6a33f854090e576714707e/52c2c0bd6269881eb4e2716a81442030dbf23458-2048x785.png" alt="Jina Reranker v3.5 BEIR benchmark results compared to Qwen3 and Mixedbread rerankers" /><p>We’ve also improved <code>jina-reranker-v3</code>’s multilingual reranking performance on the Multilingual Information Retrieval Across a Continuum of Languages (MIRACL) benchmark. Only the four billion parameter Qwen3 reranker regularly beats <code>jina-reranker-v3.5</code>’s score.</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blta9d9b745a3c23efd/6a6a33f899442c66f1df1d8c/cc9ba1d219b1c77ef0840c86c038088a66b40f6d-2048x785.png" alt="Jina Reranker v3.5 MIRACL multilingual benchmark results across 18 languages" /><h3>Legal, medical, and financial reranking</h3><p>The <a href="https://huggingface.co/blog/rteb">Retrieval Embedding Benchmark (RTEB) suite</a> consists of diverse domain-specific retrieval benchmarks. <code>jina-reranker-v3.5</code> outperforms <code>jina-reranker-v3</code> on all RTEB tasks related to law, medicine, and finance. Only the large Qwen3 reranker, at almost seven times as many parameters, has better average performance in those three domains.</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt6024d5c8ac1355ac/6a6a33f9b62af41d832635d8/1426abb0397e0b9d7c8115334ca5c4f899fa8fb6-2048x693.png" alt="Jina Reranker v3.5 RTEB domain-specific benchmark results for legal, medical and financial retrieval" /><p>The new model shows particularly strong improvements for legal data, beating <code>jina-reranker-v3</code>’s score by over 50% on case law retrieval tasks.</p><p>Task</p><p>Reranker v3</p><p>Reranker v3.5</p><p>Improvement v3 to v3.5</p><p>AILA-Case</p><p>20.82</p><p>32.55</p><p>+11.73 (56%)</p><p>AILA-Statute</p><p>32.15</p><p>46.16</p><p>+14.01 (44%)</p><p>LegalQuAD</p><p>81.84</p><p>83.09</p><p>+1.25 (1.5%)</p><p>LegalSum</p><p>69.64</p><p>70.99</p><p>+1.33 (1.9%)</p><h3>Structured data reranking (Struct-IR and STaRK benchmarks)</h3><p>We evaluated <code>jina-reranker-v3.5</code>'s structured data reranking on two benchmarks: <a href="https://neurips.cc/virtual/2025/loc/mexico-city/poster/121702">Struct-IR</a> and <a href="https://stark.stanford.edu/">STaRK</a>. Both benchmarks contain AI-generated JSON text data covering a variety of applications, including product records, scientific papers, and biomedical knowledge bases.</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt1deca80ed81995d5/6a6a33f79b96f21e21baf2be/b847be39e6387c4d4eaf754c5d758fc59fad4349-2048x717.png" alt="Jina Reranker v3.5 structured data benchmark results on Struct-IR and STaRK" /><p><code>jina-reranker-v3.5</code> improves substantially on <code>jina-reranker-v3</code>’s score on the Struct-IR benchmark, once again only exceeded by Qwen3-Reranker-4B. On the STaRK benchmark, <code>jina-reranker-v3.5</code> beats all the other models we tested, of any size.</p><h3>Inference speed: Latency benchmarks for short and long documents</h3><p>The longer the candidate documents get, the more significant the architectural improvements we’ve brought to <code>jina-reranker-v3.5</code> are. To verify this, we used two retrieval datasets distinguished by large differences in the average document length:</p><p>Dataset</p><p>Avg. doc length</p><p>jina-reranker-v3</p><p>jina-reranker-v3.5</p><p>Speedup</p><p>BEIR Natural Questions</p><p>145.5 tokens</p><p>371.1 ms</p><p>305.3 ms</p><p>22%</p><p>RTEB AILAcasedocs</p><p>1,904.0 tokens</p><p>16,064.9 ms</p><p>10,290.9 ms</p><p>56%</p><p><code>jina-reranker-v3.5</code> is significantly faster in both cases. On the Natural Questions benchmark, there’s a 22% speedup compared to <code>jina-reranker-v3</code> with average request latency falling from 371.1 ms to 305.3 ms. Each query from the AILAcasedocs benchmark is much larger (more than 10 times larger on average) so it naturally takes longer to rerank on average: 16,064.9 ms for <code>jina-reranker-v3</code> and 10,290.9 ms for <code>jina-reranker-v3.5</code>. This represents a 56% speedup for the newer model, representing less latency for applications and lower computer costs.</p><h2>When should you use Jina Reranker v3.5?</h2><p>Reranking improves search precision in practically every case, and <code>jina-reranker-v3.5</code> has applications in a wide variety of information retrieval contexts. However, it has some limitations. The table below summarizes our best-practice advice:</p><p>Use case</p><p>Recommendation</p><p>General text retrieval in common international languages</p><p>Use `jina-reranker-v3.5`.</p><p>Legal, financial, and medical domain retrieval</p><p>Use `jina-reranker-v3.5`.</p><p>Semi-structured data, tables, product information texts for ecommerce</p><p>Use `jina-reranker-v3.5`.</p><p>Non-text or mixed-media data</p><p>Use `jina-reranker-m0`, which supports both text and image input.</p><h2>How to use Jina Reranker 3.5 with the Elastic Inference API</h2><p><strong><code>jina-reranker-v3.5</code></strong> is available via the <a href="https://jina.ai/reranker/">Jina API</a> with free tokens to try it out. It’s also available via the <a href="https://www.elastic.co/docs/explore-analyze/elastic-inference/inference-api">Elastic Inference API</a> and <a href="https://www.elastic.co/docs/explore-analyze/elastic-inference/eis">Elastic Inference Service</a>.</p><p>If you’re already using <strong><code>jina-reranker-v3</code></strong>, all you have to do is change the name of the model in the <code>model</code> field of your request to the Jina API or <code>model_id</code> field when configuring an Elastic Inference API endpoint. The two models have completely identical interfaces.</p><p>You can install <strong><code>jina-reranker-v3.5</code></strong> as a <a href="https://www.elastic.co/search-labs/blog/on-prem-ai-jina-embedding-models">Jina On-Prem container</a> to get a completely self-contained server that runs on your own hardware. The model weights are also available to download for testing and research. Follow the instructions on the <a href="https://huggingface.co/jinaai/jina-reranker-v3.5">model’s page at Hugging Face</a>. In both cases, the model is available under a <a href="https://creativecommons.org/licenses/by-nc/4.0/deed.en">CC BY-NC-4.0 license</a>, so you’re free to try it out for testing, building prototypes, or doing scientific research. For commercial use, please contact Elastic sales.</p>]]></content:encoded>
    <link>https://www.elastic.co/search-labs/blog/jina-reranker-35-legal-medical-structured-data</link>
    <guid isPermaLink="true">https://www.elastic.co/search-labs/blog/jina-reranker-35-legal-medical-structured-data</guid>
    <category><![CDATA[Jina AI]]></category>
    <category><![CDATA[Relevance]]></category>
    <category><![CDATA[ML Research]]></category>
    <dc:creator><![CDATA[Felix Wang,Scott Martens]]></dc:creator>
    <enclosure url="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt29bbbf4463c73d8a/6a6a33fa55755baeaa2bd248/a6563ee307cc2d29722c490b043ee736c46974f3-1280x720.png" length="0" type="image/png"/>
    <pubDate>Mon, 27 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[How BBQ shrinks Jina v5 embeddings by 29x without losing recall in Elasticsearch]]></title>
    <description><![CDATA[A hands-on test comparing BBQ and float32 vector indices in Elasticsearch, measuring memory, disk and recall@10 across five languages.]]></description>
    <content:encoded><![CDATA[<p><a href="https://www.elastic.co/search-labs/blog/better-binary-quantization-lucene-elasticsearch">BBQ quantization</a> cuts the memory footprint of <a href="https://www.elastic.co/search-labs/blog/jina-embeddings-v5-text">Jina embeddings v5</a> vectors by 29x in Elasticsearch. Recall@10 holds at 0.994 against a full-precision <code>float32</code> baseline. We tested this on a multilingual news corpus across five languages, using <code>jina-embeddings-v5-text-small</code> to build a raw <code>float32</code> index and a <code>bbq_hnsw</code> index from the exact same <a href="https://www.elastic.co/what-is/vector-embedding">vectors</a>. Then we measured memory, disk usage and retrieval quality on both. Disk usage came out nearly identical between the two indices. In-memory footprint is the number that actually decides whether your cluster fits the corpus, and it dropped from 12.71 MB to 0.44 MB for this test set. Jina v5's quantization-aware training is why the recall held.</p><h2>Prerequisites</h2><ul><li><p>Elasticsearch 9.x with <code>jina-embeddings-v5-text-small</code> inference endpoint available.</p></li><li><p>Python 3.10+,</p></li><li><p>Elasticsearch API key,</p></li></ul><h2>What is quantization?</h2><p>An <em>embedding </em>is a list of numbers. By default, each number is a <code>float32</code>, which uses 4 bytes. <em>Quantization </em>stores each number with fewer bits, trading precision for space.</p><p>Like a JPEG, a <em>quantized vector</em> is a smaller, lower-fidelity copy of the original that still gets the job done.</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt1ddbaed3b3d80b67/6a54faf78f017d26529ee65c/175105f9a5059885aaf92268c2ab70b2e4e3dd6f-519x600.png" alt="Cat photo at decreasing JPEG quality, illustrating the quantization trade-off between size and detail" /><p>Name</p><p>Bytes / dim</p><p>1024-d vector</p><p>Compression</p><p>`Float` (Baseline)</p><p>4</p><p>4096 B</p><p>1x</p><p>`int8`</p><p>1</p><p>1024 B</p><p>4x</p><p>`int4`</p><p>0.5</p><p>512 B</p><p>8x</p><p>`bbq`</p><p>~0.14</p><p>142 B</p><p>~29x</p><h2>What is BBQ?</h2><p>Better Binary Quantization (BBQ) is Elasticsearch's 1-bit quantization mode for dense vectors. Each dimension of the vector is stored as a single bit, plus a few corrective bytes per vector. Then, a rescoring step is applied at query time. This keeps the final retrieval quality close to a full precision search.</p><p>For the math behind each level, see <a href="https://www.elastic.co/search-labs/blog/scalar-quantization-101">Scalar quantization 101</a>, <a href="https://www.elastic.co/search-labs/blog/optimized-scalar-quantization-elasticsearch">Optimized Scalar Quantization</a>, and the <a href="https://www.elastic.co/search-labs/blog/better-binary-quantization-lucene-elasticsearch">BBQ deep dive</a>.</p><h3>How does BBQ preserve search accuracy?</h3><p>Plain 1-bit quantization leads to too high a search quality degradation on its own. BBQ maintains high retrieval quality through three mechanisms:</p><ol><li><p><strong>Asymmetric precision:</strong> Stored vectors use 1 bit per dimension.</p></li><li><p><strong>Corrective factors:</strong> A few floats per vector record the rounding error and correct distances at scoring time.</p></li><li><p><strong>Oversample and rescore:</strong> BBQ scans candidates with the bits and then reranks the top ones with higher precision. Fetching the top 10 means scanning about 30 candidates.</p></li></ol><p>The result is the vectors that are roughly 32x smaller, with retrieval quality close to full precision. In the next section of the article, we’ll measure the memory savings and the recall on a real corpus.</p><h2>How Jina embeddings v5 works</h2><p>Jina embeddings v5 is a multilingual embedding model with quantization-aware training, which makes it a natural fit for BBQ in Elasticsearch: The 1024-dimensional vectors from <code>jina-embeddings-v5-text-small</code> sit above the dimensional floor where binary quantization stays accurate, and the model is trained so that 1-bit quantization loses little quality. Its main features are:</p><ul><li><p><strong>One model for many tasks:</strong> v5 uses small <a href="https://arxiv.org/abs/2106.09685">Low-Rank Adaptation (LoRA) adapters</a> on top of a single base model, one for each task: <em>retrieval</em>, <em>text-matching</em>, <em>clustering</em>, and <em>classification</em>. Elasticsearch <a href="https://www.elastic.co/search-labs/blog/jina-embeddings-v5-text#getting-started">picks the right adapter automatically</a> at index and query time.</p></li><li><p><a href="https://arxiv.org/abs/2205.13147"><strong>Matryoshka dimensions:</strong></a> v5 is trained so you can truncate the vector (1024, 512 to 256) and minimize search quality reduction. This is another way to shrink vectors, independent of quantization.</p></li><li><p><strong>Quantization-aware training:</strong> v5 is trained to work with BBQ, so its 1-bit vectors lose little accuracy.</p></li></ul><p>We use <code>jina-embeddings-v5-text-small</code>. This model is available through <a href="https://www.elastic.co/docs/explore-analyze/elastic-inference/eis">Elastic Inference Service</a> (EIS) and outputs 1024 dimensions with 32k token context and is multilingual across 93 languages. That puts it above the <a href="https://www.elastic.co/docs/reference/elasticsearch/mapping-reference/dense-vector#dense-vector-quantization">384-dimension threshold</a>, below which Elasticsearch no longer defaults to <code>bbq_hnsw</code>.</p><p>Full model details are in the <a href="https://www.elastic.co/search-labs/blog/jina-embeddings-v5-text">Jina v5 article on Search Labs</a>.</p><h2>Setting up the BBQ vs. float32 comparison</h2><p>We’ll create two indices: Both share mappings, and what changes is the <code>index_options.type</code> parameter, which tells Elasticsearch how to store the dense vector field (as raw <code>float32</code> HNSW or as 1-bit BBQ):</p><p>Index</p><p>`index_options`</p><p>Loaded into memory</p><p>`vectors-float32`</p><p>`hnsw`</p><p>Raw `float32` with no quantization (baseline)</p><p>`vectors-bbq`</p><p>`bbq_hnsw`</p><p>1-bit BBQ quantization + corrective factors</p><p>We then embed the corpus once with Jina v5, index those same vectors into both, and compare them on disk usage, memory footprint, and recall. You can follow along with the full <a href="https://github.com/elastic/elasticsearch-labs/blob/main/supporting-blog-content/quantizing-jina-embeddings-v5-bbq/quantization-jina-embeddings.ipynb">supporting blog content notebook</a>.</p><h3>Connect to Elasticsearch</h3>from elasticsearch import Elasticsearch, helpers

es_client = Elasticsearch(
    ELASTICSEARCH_URL, api_key=ELASTICSEARCH_API_KEY, request_timeout=120
)
es_client.info()<h3>Create the two indices</h3>DIMS = 1024
FLOAT_INDEX = "vectors-float32"
BBQ_INDEX = "vectors-bbq"


def create_index(name, index_options):
    if es_client.indices.exists(index=name):
        es_client.indices.delete(index=name)

    es_client.indices.create(
        index=name,
        mappings={
            "properties": {
                "text": {"type": "text"},
                "lang": {"type": "keyword"},
                "embedding": {
                    "type": "dense_vector",
                    "dims": DIMS,
                    "index": True,
                    "similarity": "cosine",
                    "index_options": index_options,
                },
            }
        },
    )


create_index(FLOAT_INDEX, {"type": "hnsw"})       # raw float32 baseline
create_index(BBQ_INDEX,   {"type": "bbq_hnsw"})   # 1-bit BBQ<p><em>Note: In production, you can use </em><a href="https://www.elastic.co/docs/reference/elasticsearch/mapping-reference/semantic-text"><em><code>semantic_text</code></em></a><em> to let Elasticsearch manage the mapping and inference endpoint automatically.</em></p><h3>Point at the Jina v5 inference endpoint</h3><p>We call the model <code>jina-embeddings-v5-text-small</code> directly (no need to create an <a href="https://www.elastic.co/docs/api/doc/elasticsearch/operation/operation-inference-put">inference endpoint</a>) to turn text into vectors.</p>INFERENCE_ID = ".jina-embeddings-v5-text-small"


def embed(texts, batch_size=16):
    out = []

    for i in range(0, len(texts), batch_size):
        batch = texts[i : i + batch_size]

        try:
            resp = es_client.inference.text_embedding(
                inference_id=INFERENCE_ID, input=batch
            )
        except AttributeError:  # older client versions
            resp = es_client.inference.inference(inference_id=INFERENCE_ID, input=batch)
        out.extend(item["embedding"] for item in resp["text_embedding"])

    return np.array(out, dtype=np.float32)


embed(["hello world"]).shape # testing<p>As result of the test, we got:</p>(1, 1024)<h3>Load a multilingual news dataset</h3><p>We stream real news articles from <a href="https://huggingface.co/datasets/hotchpotch/multilingual_cc_news">hotchpotch/multilingual_cc_news</a>, a parquet mirror of CC-News. We take about 1,000 articles from five languages (around 3,000 docs total), plus a small held-out set of headlines to use as search queries. Using multiple languages also lets Jina v5 show its multilingual strength.</p>from datasets import load_dataset

LANGS = ["en", "de", "ja", "pt", "ru"]
PER_LANG_DOCS = 1000
PER_LANG_QUERIES = 20

docs, queries = [], []
for lang in LANGS:
    ds = load_dataset(
        "hotchpotch/multilingual_cc_news", lang, split="train", streaming=True
    )
    rows = [
        r
        for r in ds.take(PER_LANG_DOCS + PER_LANG_QUERIES)
        if r.get("maintext") and r.get("title")
    ]

    for row in rows[:PER_LANG_DOCS]:
        text = (row["title"] + ". " + row["maintext"]).replace("\n", " ").strip()
        docs.append({"text": text[:1000], "lang": lang})

    for row in rows[PER_LANG_DOCS:]:
        queries.append({"text": row["title"], "lang": lang})  # headlines as queries

print(f"Corpus: {len(docs)} docs | Queries: {len(queries)}")

# RES: Corpus: 3102 docs | Queries: 18<h3>Generate the embeddings and bulk index</h3><p>We embed the corpus a single time and feed those exact vectors into both indices.</p>doc_vectors = embed([d["text"] for d in docs])
query_vectors = embed([q["text"] for q in queries])def index_docs(name):
    actions = (
        {
            "_index": name,
            "_id": i,
            "_source": {
                "text": d["text"],
                "lang": d["lang"],
                "embedding": doc_vectors[i].tolist(),
            },
        }
        for i, d in enumerate(docs)
    )
    helpers.bulk(es_client, actions, refresh=True)


for name in (FLOAT_INDEX, BBQ_INDEX):
    index_docs(name)
    es_client.indices.forcemerge(index=name, max_num_segments=1)
    es_client.indices.refresh(index=name)<p>We <a href="https://www.elastic.co/docs/api/doc/elasticsearch/operation/operation-indices-forcemerge">force-merge</a> to a single segment so the storage numbers are stable and comparable.</p><h2>Results: Disk versus memory</h2><p>The <a href="https://www.elastic.co/docs/api/doc/elasticsearch/operation/operation-indices-disk-usage">disk usage API</a> reports how many bytes each index spends on vectors (<code>knn_vectors</code>).</p>def vector_disk_bytes(name):
    du = es_client.indices.disk_usage(index=name, run_expensive_tasks=True)
    field = du[name]["fields"]["embedding"]
    knn = field.get("knn_vectors")
    if isinstance(knn, dict):
        return knn["size_in_bytes"]
    return field["knn_vectors_in_bytes"]


float_disk = vector_disk_bytes(FLOAT_INDEX)
bbq_disk = vector_disk_bytes(BBQ_INDEX)

N = len(docs)
float_mem = N * DIMS * 4
bbq_mem = N * (DIMS // 8 + 14)

print(f"On disk   -&gt; float32: {float_disk/1e6:6.2f} MB | BBQ: {bbq_disk/1e6:6.2f} MB")
print(f"In memory -&gt; float32: {float_mem/1e6:6.2f} MB | BBQ: {bbq_mem/1e6:6.2f} MB  ({float_mem/bbq_mem:.0f}x smaller)")<p>Result:</p>On disk   -&gt; float32:  12.80 MB | BBQ:  13.25 MB
In memory -&gt; float32:  12.71 MB | BBQ:   0.44 MB  (29x smaller)<p>On disk, the two indices are about the same size. A quantized index still keeps the raw <code>float32</code> vectors (needed for rescoring and requantization during merges) and adds the 1-bit vectors on top, so BBQ ends up slightly larger on disk.</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt480f8d535f7a67cd/6a54faf95beed09a3ec5f836/c54fff3fe10273895d7fc16e3c8f215c3538d717-583x250.png" alt=" Float32 stores near-continuous values; BBQ quantization rounds each dimension to one of two levels" /><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/bltfc4b17790ac39e76/6a54fafb9eff160936b24e85/5cede02da4a8380ef5420d993f5addac25d925b5-590x249.png" alt="BBQ quantization reduces vector storage from 4,096 bytes to 142 bytes per vector, a 29x reduction" /><p>The real savings is in memory. The HNSW scan only needs the 1-bit vectors in RAM, while the raw floats are read from disk to rescore the top candidates. We size that footprint using the documented <a href="https://www.elastic.co/docs/deploy-manage/production-guidance/optimize-performance/approximate-knn-search">kNN memory formulas</a>: <code>float</code> uses <code>num_vectors × dims × 4</code> and <code>bbq</code> uses <code>num_vectors × (dims/8 + 14)</code>.</p><p>BBQ's extra bytes on disk should match the 1-bit payload we computed for memory. Here, that’s <code>13.25 - 12.80 = 0.45 MB</code> versus the computed <code>0.44 MB</code>. They line up.</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/bltb1ae4f838c0b1c44/6a54fafdffefbe0991dd3dea/bf231b0290bb0661d0425384d69cb90d0222b44d-740x440.png" alt="BBQ quantization in Elasticsearch: similar disk usage, but memory drops from 12.7 MB to 0.4 MB" /><h2>Results: Recall</h2><p>To check whether the quantized index returns results similar to the float baseline, we use recall:</p><p><code>recall@k = | BBQ top-k ∩ float32 top-k | / k</code>, averaged over all queries.</p><p>We vary the oversampling factor (<code>num_candidates / k</code>) that’s the number of candidates BBQ scans with 1-bit vectors before reranking the top ones against the original floats to find the lowest value that still matches <code>float32</code>.</p>def search_ids(index, qvec, k=10, num_candidates=10):
    resp = es_client.search(
        index=index,
        size=k,
        _source=False,
        knn={
            "field": "embedding",
            "query_vector": qvec.tolist(),
            "k": k,
            "num_candidates": num_candidates,
        },
    )

    return [h["_id"] for h in resp["hits"]["hits"]]


K = 10

# Ground truth: full-precision float32 with a wide candidate list (~exact)
ground_truth = [
    set(search_ids(FLOAT_INDEX, qv, k=K, num_candidates=2000)) for qv in query_vectors
]

oversamples = [1, 2, 3, 5, 10]
recalls = []
for f in oversamples:
    num_candidates = max(K * f, K)
    hits = 0
    for gt, qv in zip(ground_truth, query_vectors):
        got = set(search_ids(BBQ_INDEX, qv, k=K, num_candidates=num_candidates))
        hits += len(got &amp; gt)
    recalls.append(hits / (len(query_vectors) * K))
    print(f"oversample {f:&gt;2}x -&gt; recall@{K} = {recalls[-1]:.3f}")<p>As a result, we have:</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt96c062ee246abfd4/6a54faff600d773c12e424d3/e8604775a3f9bb47c473f2ac4686b926413bf4ad-640x440.png" alt="Recall@10 for BBQ quantization stays near 0.989 versus float32 across oversample factors 1x to 10x" /><p>BBQ starts at 0.994 recall@10 at 1x oversampling, holds there up to 3x, and then settles at 0.989 at higher factors, meaning it returns at least 98.9% of the same top-10 documents as float32 across all oversampling values. For more on how recall varies across datasets under quantization, see <a href="https://www.elastic.co/search-labs/blog/recall-vector-search-quantization">Fast vs. accurate: Measuring the recall of quantized vector search</a>.</p><h2>BBQ quantization results summary</h2><p>The same vectors, two storage formats, and one experiment:</p><ul><li><p><strong>Disk:</strong> Roughly the same (<code>12.80 MB</code> versus <code>13.25 MB</code>). BBQ keeps the raw floats around for rescoring and merging.</p></li><li><p><strong>Memory:</strong> 29x smaller (<code>12.71 MB</code> versus <code>0.44 MB</code>). This is the number that decides whether your cluster fits the corpus.</p></li><li><p><strong>Recall@10:</strong> <code>0.994</code> at 1x oversampling. Quantization-aware training pays off.</p></li></ul><p>When to enable BBQ: If your dimension count is above the <a href="https://www.elastic.co/docs/reference/elasticsearch/mapping-reference/dense-vector#dense-vector-quantization">384-dim floor</a>, if your vectors are the dominant memory cost, and if you can afford a few extra candidates to rescore. For Jina v5 specifically, the model is trained for it, so the recall hit on most corpora is small.</p><h2>Further reading on BBQ and vector quantization</h2><ul><li><p>Run the full notebook from this article in the <a href="https://github.com/elastic/elasticsearch-labs/blob/main/supporting-blog-content/quantizing-jina-embeddings-v5-bbq/quantization-jina-embeddings.ipynb">supporting blog content repo</a>.</p></li><li><p>For the math behind BBQ, see <a href="https://www.elastic.co/search-labs/blog/better-binary-quantization-lucene-elasticsearch">Better Binary Quantization in Lucene and Elasticsearch</a>.</p></li><li><p>For more on Jina v5's architecture, see <a href="https://www.elastic.co/search-labs/blog/jina-embeddings-v5-text">Jina embeddings v5 on Search Labs</a>.</p></li><li><p>For a broader walkthrough on adopting BBQ, see <a href="https://www.elastic.co/search-labs/blog/bbq-implementation-into-use-case">How to implement BBQ into your use case</a>.</p></li><li><p>For the original research behind BBQ, see the paper <a href="https://arxiv.org/abs/2405.12497">RaBitQ: Quantizing High-Dimensional Vectors with a Theoretical Error Bound for Approximate Nearest Neighbor Search</a>.</p></li></ul>]]></content:encoded>
    <link>https://www.elastic.co/search-labs/blog/bbq-quantization-jina-embeddings-v5</link>
    <guid isPermaLink="true">https://www.elastic.co/search-labs/blog/bbq-quantization-jina-embeddings-v5</guid>
    <category><![CDATA[Vector Database]]></category>
    <category><![CDATA[Jina AI]]></category>
    <category><![CDATA[ML Research]]></category>
    <dc:creator><![CDATA[Jeffrey Rengifo]]></dc:creator>
    <enclosure url="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt99fb16c79484a00f/6a54fb02600d7743b9e424d9/43df5ec915eae1b9f1534d3acaf2e58732733d9b-1280x720.png" length="0" type="image/png"/>
    <pubDate>Fri, 10 Jul 2026 00:00:00 GMT</pubDate>
  </item>
  <item>
    <title><![CDATA[Elasticsearch DiskBBQ delivers 7x faster vector search than Qdrant on network-attached storage]]></title>
    <description><![CDATA[Elasticsearch DiskBBQ achieves up to 7x higher vector search throughput than Qdrant at comparable recall on network-attached storage. Explore the benchmark methodology and full results.]]></description>
    <content:encoded><![CDATA[<p>Elasticsearch DiskBBQ delivers up to 7x higher throughput than Qdrant at comparable recall, tested on network-attached persistent storage, the topology most managed-cloud deployments actually use. The gap is consistent across recall levels from 0.93 to 0.97, and it widens as recall increases. DiskBBQ keeps latency nearly flat as search breadth grows; Qdrant's latency rises sharply as <code>hnsw_ef</code> increases, driven by random reads of original vectors from disk during rescoring. If you're running vector search in Kubernetes or a managed cloud environment, this is what the tradeoff looks like.</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/bltf417e30d37bbe73a/6a46976e151035764d202f02/057e4d34719f4ca87c86f1b2a36b06d0839ad275-800x500.png" alt="Bar chart comparing throughput in queries per second between Elasticsearch 9.4.1 and Qdrant 1.18.1 at recall levels 0.93, 0.95, 0.96 and 0.97, showing Elasticsearch delivering approximately 7x higher throughput across all recall levels." /><p>Vector search is a critical foundation for large language model (LLM) applications, retrieval augmented generation (RAG), and other AI workloads. In this benchmark, Elasticsearch achieved up to 7x higher throughput than Qdrant at comparable recall on the same storage topology. Elasticsearch as a vector database offers strong vector search performance even when network-attached persistent storage remains on the query path.</p><p>The difference reflects how the two systems interact with disk. Elasticsearch DiskBBQ is designed to keep vector search efficient when persistent storage remains on the query path, using a compact quantized representation and limiting costly access to full precision vectors during search. In this setup, Qdrant relies on a graph-based search path with rescoring against original vectors stored on disk. On network-attached persistent storage, that random access cost becomes much more significant, which is why the performance gap widens as recall increases. This benchmark therefore focuses specifically on network-attached persistent storage, a common deployment model in managed cloud and Kubernetes environments.</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blte01d96b1db40d0e9/6a46977131bdbb595d8b33e7/3a39012cb09a841468a5295e226955769c4d18f0-800x500.png" alt="Line chart showing recall versus average latency in milliseconds for Elasticsearch 9.4.1 and Qdrant 1.18.1. Elasticsearch maintains low latency between 120 and 150ms across all recall levels, while Qdrant latency rises steeply from 315ms to 900ms as recall increases." /><p>The key pattern in the latency curve is not only the size of the gap but also its shape. Elasticsearch latency remains comparatively flat as recall increases, suggesting that higher recall doesn’t require a dramatic increase in expensive storage activity. Qdrant’s latency rises sharply as <code>hnsw_ef</code> increases, which is consistent with broader candidate exploration leading to more rescoring work against original vectors on disk.</p><h2>Full results table</h2><p>The table below shows the full parameter sweep for both Elasticsearch and Qdrant. Because the two engines expose different tuning controls for vector search, the results are reported using each engine’s full parameter key rather than attempting a one-to-one mapping between settings.</p><p>A few notes on the metrics:</p><ul><li><p>ParamKey: The complete parameter setting used for a given run.</p></li><li><p>Recall: Recall@100 against a ground-truth top-100 result set for the benchmark queries. Values range from 0 to 1, and higher is better.</p></li><li><p>Latency_Avg: The average end-to-end latency per query measured from the benchmarking client across the full run, in milliseconds. Lower is better.</p></li><li><p>Latency_P95: The 95th percentile query latency, in milliseconds, showing the upper range of typical slow queries. Lower is better.</p></li><li><p>Throughput: The average number of queries processed per second across the full run. Higher is better.</p></li></ul><p>Engine</p><p>ParamKey</p><p>Recall</p><p>Latency_Avg</p><p>Latency_P95</p><p>Throughput</p><p>qdrant</p><p>hnsw_ef=50, oversampling=1, size=100</p><p>0.8694</p><p>315.7849</p><p>503.4754</p><p>12.629</p><p>elasticsearch</p><p>k=100, oversample=1, size=100, visit_percentage=1</p><p>0.8789</p><p>135.0802</p><p>218.494</p><p>29.343</p><p>elasticsearch</p><p>k=100, oversample=1, size=100, visit_percentage=1.5</p><p>0.9123</p><p>127.8286</p><p>195.2318</p><p>31.1107</p><p>qdrant</p><p>hnsw_ef=100, oversampling=1, size=100</p><p>0.9287</p><p>895.9933</p><p>1213.0448</p><p>4.4493</p><p>elasticsearch</p><p>k=100, oversample=1, size=100, visit_percentage=2</p><p>0.9317</p><p>124.846</p><p>183.6314</p><p>31.8225</p><p>elasticsearch</p><p>k=100, oversample=1, size=100, visit_percentage=2.5</p><p>0.9444</p><p>123.517</p><p>180.4831</p><p>32.1883</p><p>qdrant</p><p>hnsw_ef=150, oversampling=1, size=100</p><p>0.9518</p><p>884.7236</p><p>1195.2603</p><p>4.5066</p><p>elasticsearch</p><p>k=100, oversample=1, size=100, visit_percentage=3</p><p>0.9532</p><p>123.276</p><p>183.8379</p><p>32.2364</p><p>elasticsearch</p><p>k=100, oversample=1, size=100, visit_percentage=3.5</p><p>0.9599</p><p>122.5559</p><p>184.2858</p><p>32.4469</p><p>qdrant</p><p>hnsw_ef=200, oversampling=1, size=100</p><p>0.964</p><p>883.2114</p><p>1188.6597</p><p>4.5143</p><p>elasticsearch</p><p>k=100, oversample=1, size=100, visit_percentage=4</p><p>0.965</p><p>122.7946</p><p>184.9058</p><p>32.3635</p><p>elasticsearch</p><p>k=100, oversample=1, size=100, visit_percentage=4.5</p><p>0.9689</p><p>122.7062</p><p>182.9559</p><p>32.3976</p><p>elasticsearch</p><p>k=100, oversample=1, size=100, visit_percentage=5</p><p>0.9722</p><p>122.5761</p><p>187.3536</p><p>32.4221</p><p>qdrant</p><p>hnsw_ef=256, oversampling=1, size=100</p><p>0.9722</p><p>881.9643</p><p>1185.4948</p><p>4.5192</p><p>elasticsearch</p><p>k=100, oversample=1, size=100, visit_percentage=5.5</p><p>0.9747</p><p>122.5609</p><p>184.5128</p><p>32.4176</p><p>Each row pairs the closest measured Elasticsearch and Qdrant configurations in the sweep by achieved recall.</p><h3>Matched comparisons at similar recall</h3><p>To make the comparison fair, speedup is calculated only between configurations that achieve similar recall. This avoids comparing settings that trade off accuracy very differently.</p><p>Recall band</p><p>Elasticsearch recall</p><p>Elasticsearch Latency_Avg</p><p>Elasticsearch throughput</p><p>Qdrant recall</p><p>Qdrant Latency_Avg</p><p>Qdrant throughput</p><p>Throughput speedup</p><p>~0.87</p><p>0.8789</p><p>135.0802</p><p>29.343</p><p>0.8694</p><p>315.7849</p><p>12.629</p><p>2.32x</p><p>~0.93</p><p>0.9317</p><p>124.846</p><p>31.8225</p><p>0.9287</p><p>895.9933</p><p>4.4493</p><p>7.15x</p><p>~0.95</p><p>0.9532</p><p>123.276</p><p>32.2364</p><p>0.9518</p><p>884.7236</p><p>4.5066</p><p>7.15x</p><p>~0.96</p><p>0.9599</p><p>122.5559</p><p>32.4469</p><p>0.964</p><p>883.2114</p><p>4.5143</p><p>7.19x</p><p>~0.97</p><p>0.9722</p><p>122.5761</p><p>32.4221</p><p>0.9722</p><p>881.9643</p><p>4.5192</p><p>7.17x</p><p>This matched-recall view is the clearest expression of the underlying systems difference. At similar recall levels, Elasticsearch delivers both lower latency and much higher throughput, and the gap widens as recall rises. The recall-throughput pattern matters because higher recall in this benchmark requires broader search. DiskBBQ absorbs that increase with relatively little additional cost, while Qdrant’s graph plus rescoring path becomes much more constrained by random access to original vectors on persistent storage.</p><h2>Benchmark methodology</h2><p><a href="https://github.com/elastic/jingra">Jingra</a>, the benchmarking tool used for these tests, was originally written in Python and has since been rebuilt as a Java project. For these tests, Jingra runs in a Kubernetes pod within the same cluster as the engine being measured. This helps reduce external network variability and keeps the test environment consistent across runs. For each run, Jingra executed the query set at a fixed client concurrency, recorded end-to-end client-side latency and throughput, and computed recall against a precomputed ground-truth top-100 set.</p><p>This benchmark was intentionally run on network-attached persistent storage rather than local NVMe. For the published results, the storage used the baseline performance allocation for a 200 GiB GCP Hyperdisk Balanced volume, with no explicit IOPS or throughput provisioning. We chose this topology on purpose because it’s a relevant cloud deployment model and because it keeps storage efficiency materially on the query path.</p><p>Qdrant often performs better on local NVMe, so deployments using local NVMe should expect different results than the ones shown here. This benchmark specifically tests network-attached persistent storage because that’s a common managed-cloud deployment model and because it makes storage-path efficiency visible in end-to-end query performance.</p><p>Because Elasticsearch and Qdrant expose different query parameters for controlling vector search behavior, there’s no clean one-to-one mapping between their tuning settings. Instead of comparing equivalent parameter values directly, we use recall as the primary point of comparison. The matched comparisons below therefore pair configurations that achieve similar recall, rather than configurations with superficially similar parameter values.</p><p>Recall cannot be known in advance for a given parameter setting, so we sweep across a range of search configurations for each engine and then compare results at similar recall levels. In the published results, oversampling was fixed at 1 for both engines so that recall was primarily tuned via search breadth rather than rescoring expansion.</p><h3>How does Elasticsearch configure vector search?</h3>{
  "query": {
    "knn": {
      "field": "embedding",
      "query_vector": "{{query_vector}}",
      "k": "{{k}}",
      "visit_percentage": "{{visit_percentage}}",
      "rescore_vector": {
        "oversample": "{{oversample}}"
      }
    }
  },
  "size": "{{size}}",
  "_source": false
}<ul><li><p><code>query_vector</code>: The input vector used for similarity search. Elasticsearch compares this vector against the stored vectors in the field.</p></li><li><p><code>k</code>: The number of nearest neighbors to retrieve.</p></li><li><p><code>visit_percentage</code>: Controls how much of the DiskBBQ, Elasticsearch’s disk optimized vector index, is explored during the approximate search phase. Higher values usually improve recall but increase latency.</p></li><li><p><code>oversample</code>: Controls how many extra candidate vectors are passed into rescoring relative to k. Higher values can improve recall, but usually at additional cost.</p></li><li><p><code>size</code>: The number of hits returned in the final response.</p></li><li><p><code>_source: false</code>: Disables returning the document _source field, reducing response size and avoiding extra retrieval overhead during benchmarking.</p></li></ul><p>Example</p>{
  "query": {
    "knn": {
      "field": "embedding",
      "query_vector": [ -0.0095683, 0.0072035934, ... ],
      "k": "100",
      "visit_percentage": "3",
      "rescore_vector": {
        "oversample": "1"
      }
    }
  },
  "size": "100",
  "_source": false
}<p>Params</p>  recall@100:
    - { size: 100, k: 100, visit_percentage: 1, oversample: 1 }
    - { size: 100, k: 100, visit_percentage: 1.5, oversample: 1 }
    - { size: 100, k: 100, visit_percentage: 2, oversample: 1 }
    - { size: 100, k: 100, visit_percentage: 2.5, oversample: 1 }
    - { size: 100, k: 100, visit_percentage: 3, oversample: 1 }
    - { size: 100, k: 100, visit_percentage: 3.5, oversample: 1 }
    - { size: 100, k: 100, visit_percentage: 4, oversample: 1 }
    - { size: 100, k: 100, visit_percentage: 4.5, oversample: 1 }
    - { size: 100, k: 100, visit_percentage: 5, oversample: 1 }
    - { size: 100, k: 100, visit_percentage: 5.5, oversample: 1 }<p>We keep <code>k = size = 100</code> so the search request is aligned with the benchmark target: returning the top 100 results. To improve recall, we tune <code>visit_percentage</code> rather than inflating the final result count, while keeping <code>oversample = 1</code> fixed across runs.</p><h3>How does Qdrant configure vector search?</h3>{
  "vector": "{{query_vector}}",
  "limit": "{{size}}",
  "with_payload": false,
  "with_vector": false,
  "params": {
    "hnsw_ef": "{{hnsw_ef}}",
    "quantization": {
      "rescore": true,
      "oversampling": "{{oversampling}}"
    }
  }
}<ul><li><p><code>query_vector / vector</code>: The input vector used for similarity search. Qdrant compares this vector against the stored vectors in the collection.</p></li><li><p><code>size / limit</code>: The number of nearest neighbor results returned in the response.</p></li><li><p><code>with_payload: false</code>: Disables returning payload fields, reducing response size and avoiding additional retrieval overhead during benchmarking.</p></li><li><p><code>with_vector: false</code>: Disables returning stored vectors in the response, again reducing response size and keeping the benchmark focused on search performance.</p></li><li><p>hnsw_ef: Controls the number of candidates explored during HNSW search. Higher values usually improve recall but increase latency. Like visit_percentage in Elasticsearch, it affects search breadth, but the two controls are engine-specific and not directly equivalent.</p></li><li><p><code>quantization.rescore: true</code>: Enables rescoring of the candidate set using the original vectors after quantized search.</p></li><li><p><code>oversampling</code>: Controls how many extra candidates are considered during rescoring relative to the final result count. Higher values can improve recall, but usually at additional cost.</p></li></ul><p>Example</p>{
  "vector":  [ -0.0095683, 0.0072035934, ... ],
  "limit": "100",
  "with_payload": false,
  "with_vector": false,
  "params": {
    "hnsw_ef": "150",
    "quantization": {
      "rescore": true,
      "oversampling": "1"
    }
  }
}<p>Params</p>  recall@100:
    - { size: 100, hnsw_ef: 50, oversampling: 1 }
    - { size: 100, hnsw_ef: 100, oversampling: 1 }
    - { size: 100, hnsw_ef: 150, oversampling: 1 }
    - { size: 100, hnsw_ef: 200, oversampling: 1 }
    - { size: 100, hnsw_ef: 256, oversampling: 1 }<p>We keep <code>size = 100</code> so that each request is aligned with the evaluation target, in this case top 100 retrieval. Recall is then tuned by sweeping <code>hnsw_ef</code>, which controls how many candidates are explored during search. Higher <code>hnsw_ef</code> values generally improve recall but also increase latency and reduce throughput. We keep <code>oversampling = 1</code> fixed across runs so that the main tuning variable is the search breadth rather than the rescoring expansion.</p><h2>Cluster setup and DiskBBQ configuration</h2><p>We ran the benchmark on GCP using three n4-standard-8 nodes, with each pod allocated 7 vCPUs and 26 GB of RAM, and using 200 GiB GCP Hyperdisk Balanced volumes at baseline performance allocation. The corpus contains 21 million vectors, (see dataset section below for more details and download links), which account for about 60.1 GiB of raw float vector data. With 2-bit quantization, the vector payload drops to roughly 3.8 to 4.0 GB. However, the full index footprint is much larger once graph and other index structures are included. That means the workload remains meaningfully sensitive to network-attached storage performance, especially because exact vector values still need to be read from disk during rescoring.</p><p>We chose this node size intentionally to keep the benchmark in a regime where network-attached persistent storage remains on the query path rather than allowing the full working set to remain comfortably memory-resident. Each system was therefore configured using the best-performing setup we identified for this workload within the tuning scope described in this post. In Elasticsearch, this meant <code>bbq_disk</code>. In Qdrant, the original vectors were stored on disk, while the 2-bit quantized representation used for approximate search was kept in RAM with <code>always_ram: true</code>. Because the two systems expose different search strategies and tuning controls, we compare them at matched recall rather than trying to map parameters one to one.</p><p>Elasticsearch was configured to use DiskBBQ, its disk-optimized approach for approximate nearest neighbor vector search, with 2-bit quantization. DiskBBQ uses aggressive quantization to keep the searchable index compact and then rescores with the original vectors to preserve accuracy. This helps maintain strong recall while keeping disk-based search efficient.</p><p><code>bbq_disk</code> is an Elasticsearch Enterprise feature. We used it here because the goal of this benchmark was to compare the strongest disk-oriented vector search configuration available in each engine for this workload, rather than licensing tiers or default features.</p><p>We didn’t include <code>bbq_hnsw</code> in this comparison because the benchmark was specifically designed to evaluate disk-oriented vector search under a disk-sensitive workload.</p><p>This storage topology matters because Qdrant’s rescore step reads the original <code>float32</code> vectors from disk with random access on each query. On local NVMe, those reads are much faster, and Qdrant correspondingly performs better. On network-attached persistent storage, the results are consistent with that random-read rescore path becoming a more important bottleneck. Qdrant latency rises sharply as <code>hnsw_ef</code> increases, while Elasticsearch remains comparatively flat across the same recall progression.</p><p>We chose 2-bit quantization because Qdrant couldn’t reach the target recall range with 1-bit binary quantization. Since the two systems expose different disk-oriented vector search strategies, we tuned each one to the strongest configuration available within its current feature set.</p><p>Both systems were configured with three shards distributed across the three nodes and with two total copies of each shard in the cluster. In Elasticsearch, <code>number_of_shards: 3</code> and <code>number_of_replicas: 1</code> means one primary plus one replica, for two total copies. In Qdrant, <code>shard_number: 3</code> and <code>replication_factor: 2</code> also means two total copies, since Qdrant’s replication factor refers to the total number of copies rather than the number of additional replicas. So although the field names differ, the effective replication level was the same in both systems.</p><p>Setting</p><p>Elasticsearch</p><p>Qdrant</p><p>Shards</p><p>number_of_shards: 3</p><p>shard_number: 3</p><p>Copies</p><p>number_of_replicas: 1 (1 primary + 1 replica = 2 total)</p><p>replication_factor: 2 (2 total)</p><p>Elasticsearch mapping</p>{
  "mappings": {
    "properties": {
      "embedding": {
        "type": "dense_vector",
        "element_type": "float",
        "dims": 768,
        "index": true,
        "similarity": "cosine",
        "index_options": {
          "type": "bbq_disk",
          "bits": 2
        }
      }
    }
  },
  "settings": {
    "number_of_shards": "3",
    "number_of_replicas": "1"
  }
}<p>Qdrant mapping</p>{
  "vectors": {
    "size": 768,
    "distance": "Cosine",
    "on_disk": true
  },
  "shard_number": 3,
  "replication_factor": 2,
  "hnsw_config": {
    "m": 16,
    "ef_construct": 256
  },
  "quantization_config": {
    "turbo": {
      "bits": "bits2",
      "always_ram": true
    }
  }
}<img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt6ff77646baa598e4/6a4697742d406b1032ba2bd9/8d6f3e8d3e2c620187d8d2841cc09b813cd77e57-881x401.png" alt="Architecture diagram showing the benchmark cluster setup on GCP. Two Kubernetes clusters side by side: the left contains three Elasticsearch nodes behind an ES Service, with Jingra as the benchmarking client. The right mirrors this with three Qdrant nodes behind a QD Service, also driven by Jingra." /><h2>Dataset</h2><p>For this benchmark, we used the <a href="https://huggingface.co/datasets/kenhktsui/wiki_dpr_e5"><code>kenhktsui/wiki_dpr_e5</code></a> dataset from Hugging Face, a large-scale Wikipedia passage retrieval dataset designed for dense vector search. The corpus contains 21 million embedded passages, each represented as a 768-dimensional float32 vector, or 3,072 bytes per vector. That corresponds to about 60.1 GiB of raw vector data, before accounting for additional fields and file format overhead in the source dataset. The downloadable <code>data.parquet</code> file is larger at 85.2 GB for that reason.</p><p>We chose this dataset because it reflects a common production pattern in LLM, RAG, and retrieval systems: searching a large corpus of semantically embedded text while balancing recall, latency, and throughput. At 21 million vectors and roughly 60 GiB of raw vector data, it’s large enough to make disk-based vector search a relevant operating mode to evaluate.</p><p>Both engines used 2-bit quantization, reducing each vector from 3,072 bytes to 192 bytes, a 16x reduction that brings the quantized vector corpus to around 4 GB. In Qdrant, that quantized representation was kept in RAM for search, while the original vectors remained on disk. Even so, the workload remained meaningfully sensitive to network-attached storage performance because rescoring still required access to the original vectors on disk.</p><p>You can download the dataset and query files from the links below:</p><ul><li><p><a href="https://storage.googleapis.com/elastic-benchmark-datasets/wiki-dpr-e5-768/data.parquet">data.parquet</a></p></li><li><p><a href="https://storage.googleapis.com/elastic-benchmark-datasets/wiki-dpr-e5-768/queries.parquet">queries.parquet</a></p></li></ul><h2>Jingra and recreating the benchmark</h2><p>For this benchmark, we used <a href="https://github.com/elastic/jingra/releases/tag/v0.2.3">Jingra v0.2.3</a> with the configurations described <a href="https://github.com/elastic/competitive-benchmarking-studies/tree/main/es-9.4-vs-qd-1.18-vector-search">es-9.4-vs-qd-1.18-vector-search</a>. Jingra handled data loading, query execution, parameter sweeps, and metric collection for both Elasticsearch and Qdrant, making the benchmark repeatable and easier to compare.</p><p>To reproduce the experiment, you need the published dataset, query set, engine configurations, and comparable cluster hardware. With those in place, Jingra can rerun the benchmark and generate similar recall, latency, and throughput measurements shown in this post.</p><h2>Conclusion</h2><p>At comparable recall levels, Elasticsearch DiskBBQ consistently delivered faster vector search than Qdrant in this benchmark, with higher throughput and lower latency across the recall range we tested. These results are especially notable because the comparison was made on network-attached persistent storage, where efficient storage-aware vector search becomes critical. Elasticsearch as a vector database allows organizations to achieve high recall with lower latency and higher throughput on slower persistent storage.</p><p>Just as importantly, this benchmark highlights the value of comparing engines at matched recall rather than by nominal parameter settings. Elasticsearch and Qdrant expose different controls, so the fairest comparison isn’t parameter to parameter but outcome to outcome. Across the recall range tested here, Elasticsearch maintained a clear advantage in both latency and throughput.</p><p>If you want to reproduce the experiment yourself, we’re publishing the dataset and query set used in this benchmark so others can validate the results and build on them.</p><p>Further reading:</p><ul><li><p><a href="https://www.elastic.co/search-labs/blog/diskbbq-elasticsearch-introduction">Introducing a new vector storage format: DiskBBQ</a></p></li><li><p><a href="https://www.elastic.co/search-labs/blog/elasticsearch-bbq-osq-vs-turbo">Elasticsearch BBQ vs TurboQuant</a></p></li></ul>]]></content:encoded>
    <link>https://www.elastic.co/search-labs/blog/vector-search-benchmark-elasticsearch-vs-qdrant</link>
    <guid isPermaLink="true">https://www.elastic.co/search-labs/blog/vector-search-benchmark-elasticsearch-vs-qdrant</guid>
    <category><![CDATA[Vector Database]]></category>
    <category><![CDATA[AI]]></category>
    <category><![CDATA[ML Research]]></category>
    <dc:creator><![CDATA[Sachin Frayne]]></dc:creator>
    <enclosure url="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/bltf58ffc7bd7f3c826/6a469777945073eeed30d267/0fa30e54796aeb49baaa760590fa6dd3ee863c2d-1280x720.png" length="0" type="image/png"/>
    <pubDate>Wed, 24 Jun 2026 00:00:00 GMT</pubDate>
  </item>
  <item>
    <title><![CDATA[Is your ML job's datafeed losing a race it cannot win?]]></title>
    <description><![CDATA[Learn how switching from scroll-based to aggregation-based datafeeds optimizes machine learning jobs for large-scale deployments.]]></description>
    <content:encoded><![CDATA[<p>On almost every large Elastic deployment I’ve worked with, there’s an Elastic Security or Elastic Observability anomaly detection (AD) job that looks healthy but is perpetually behind. Six hours behind. Twelve. And the gap never closes.</p><p>The datafeed isn’t broken. It’s doing exactly what it was built to do: reading every raw document, across every shard, every run. On a large cluster with cross-cluster search (CCS) and a broad index pattern, like <code>logs-*</code>, that means scanning billions of documents per bucket. There’s no hardware that makes that sustainable. The datafeed will always be chasing live data and never reaching it.</p><p>The fix is to switch from the default <a href="https://www.elastic.co/docs/api/doc/elasticsearch/operation/operation-scroll"><strong>scroll-based</strong></a> datafeed configuration to an <a href="https://www.elastic.co/docs/explore-analyze/machine-learning/anomaly-detection/ml-configuring-aggregation"><strong>aggregation-based</strong></a><a href="https://www.elastic.co/docs/explore-analyze/machine-learning/anomaly-detection/ml-configuring-aggregation"> datafeed configuration</a>: Let the data nodes summarize locally, and ship only compact bucket results to the ML node. Same detections, a fraction of the load. The speedup can be dramatic. More than you might expect. The numbers are in the next section. The explanation for <em>why</em> the gap is so large is at the end of the post, for those who want to understand the mechanics.</p><p>One catch worth knowing now: Switching requires creating a new job. The old model doesn’t transfer; weeks of learned baseline are lost. <strong>The right time to make this switch is before the job has been running for months, not after.</strong> That’s the main reason to read this before you deploy.</p><h2><strong>How much faster? Scroll vs. aggregation datafeeds for ML jobs</strong></h2><p>We ran the same job two ways on production data: first scroll-based, and then aggregation-based. The job covered 13 months of history, monitoring 836,000 log events per hour in 15-minute buckets across multiple clusters.</p><p>Training on historical data with scroll-based configuration: <strong>five days of wall-clock time</strong>, 7.9 million sequential requests, and 3.5 TB transferred; with aggregations: <strong>2.3 minutes</strong>, 23 requests, and 34 MB (a 3,374× speedup). Think of it this way: If you start the scroll backfill at 9 a.m. Monday, it will finish Saturday morning. The aggregation version is done by 9:02 a.m.</p><p>On live data, the difference is less dramatic but still meaningful: around <strong>20×</strong> fewer requests per tick. That adds up quickly when the datafeed runs every few minutes around the clock.</p><h2><strong>Before you start</strong></h2><p>Three things worth knowing before diving into the configuration.</p><p><strong>This isn't wizard territory.</strong> The standard Kibana job wizards (Single Metric, Multi-Metric, Population) don't expose aggregation configuration. To create an aggregation-based job, you need either the Elasticsearch API or Kibana's Advanced Job Wizard, with JSON edited by hand. The worked example below shows the most practical path: Configure the job in the Multi-Metric Wizard, and then click <strong>Convert to advanced job</strong> before creating it. That gets you a prefilled JSON starting point instead of a blank editor.</p><p><strong>The configuration is unforgiving and mostly silent about it.</strong> There's no schema validation that catches a misnamed aggregation key or a <code>fixed_interval</code> that doesn't match <code>bucket_span</code>. The job will run, anomalies will fire, and nothing will indicate that the results are based on the wrong data. This is why the five-step pattern exists and why the <strong>Preview </strong>tab is worth using every time: Catching a misconfiguration before the job trains is a 30-second check; catching it a week later is a much worse afternoon.</p><p><strong>The Single Metric Viewer has a known limitation with aggregated jobs.</strong> That viewer reconstructs the "actual" data curve by re-querying the index, but it can't reproduce an arbitrary, user-defined aggregation, so the actual-value line is typically missing or approximate. The Anomaly Explorer is unaffected: Anomaly scores, swim lanes, and influencer attribution all work normally. Just don't rely on the Single Metric Viewer's chart for visual validation of what the model saw.</p><h2><strong>What we can and can’t aggregate</strong></h2><p>Almost every <a href="https://www.elastic.co/docs/reference/machine-learning/machine-learning-functions">ML function</a> works with aggregated datafeeds, but the right aggregation pattern depends on the function.</p><p>Function</p><p>Pattern</p><p>`count`, `mean`, `high_mean`, `low_mean`, `sum`, `max`, `min`</p><p>Standard: `date_histogram` → `terms` → metric aggregation</p><p>`time_of_day`, `time_of_week`</p><p>Minimal: plain `date_histogram`, no `terms` or metric needed</p><p>`rare`, `freq_rare`, `info_content`</p><p>Composite: top-level composite with `date_histogram` as a source</p><p>`categorization`</p><p>`terms` on the `.keyword` subfield of the categorization field</p><p>`lat_long`, `varp`</p><p>Scroll only</p><p><code>lat_long</code> and <code>varp</code> are the genuine exceptions. If you want to use these detectors, you are required to use the scroll-based datafeed configuration.</p><p>The five-step pattern in the next section covers the standard case. We’ll walk through the remaining patterns at the end of the post.</p><h2><strong>The standard five-step pattern: Scroll-based to aggregation datafeed</strong></h2><p>Converting any scroll-based job to an aggregation-based datafeed follows the same five steps. Once you understand the pattern, applying it to any compatible job takes about 10 minutes.</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt4144dac3e60cccbf/6a16fa0514b2704508e3c412/77cd16165133374a04dbcf71210ea8d36f66b54f-1999x924.png" alt="Flowchart illustrating how to configure Elasticsearch ML datafeed aggregations, showing steps for summary fields, bucket topology, timestamp handling, field mapping, and detector metrics." /><p><strong>Step 1: Add </strong><strong><code>summary_count_field_name: </code></strong><a href="https://www.elastic.co/docs/reference/elasticsearch/mapping-reference/mapping-doc-count-field"><strong><code>"doc_count"</code></strong></a><strong> to the analysis config.</strong> This tells the ML engine that incoming data is pre-summarized. Without it, the engine treats each aggregated bucket as a single raw document and produces wrong anomaly scores.</p><p><strong>Step 2: Choose the bucket wrapper topology.</strong> For most functions (<code>count</code>, <code>mean</code>, <code>sum</code>, <code>max</code>, <code>min</code>, <code>varp</code>, <code>time_of_day</code>, <code>time_of_week</code>, and <code>categorization</code>) use a <a href="https://www.elastic.co/docs/reference/aggregations/search-aggregations-bucket-datehistogram-aggregation"><code>date_histogram</code></a> at the top level whose <code>fixed_interval</code> matches your <code>bucket_span</code> exactly to ensure accurate analysis. For <code>rare</code>, <code>freq_rare</code>, and <code>info_content</code>, use a <a href="https://www.elastic.co/docs/reference/aggregations/search-aggregations-bucket-composite-aggregation">composite</a> at the top level with a <code>date_histogram</code> as one of its sources. This routes the datafeed to the composite extractor, which paginates through all field-value combinations rather than truncating to a top-N.</p><p><strong>Step 3: Add a </strong><a href="https://www.elastic.co/docs/reference/aggregations/search-aggregations-metrics-max-aggregation"><strong><code>max</code></strong></a><strong> aggregation on </strong><strong><code>@timestamp</code></strong><strong>.</strong> The ML engine needs this to determine the precise end time of each bucket. In the standard topology (Step 2, <code>date_histogram</code> outer), it goes inside the histogram’s <code>aggregations</code>. In the composite topology, it sits as a sibling of the <code>composite</code> aggregation.</p><p><strong>Step 4: Map each analysis field to a </strong><a href="https://www.elastic.co/docs/reference/aggregations/search-aggregations-bucket-terms-aggregation"><strong><code>terms</code></strong></a><u><strong> aggregation</strong></u>, named exactly after the corresponding field in the analysis config. One categorical field → a single nested <code>terms</code>. Two or more categorical fields → a <code>composite</code> aggregation nested inside the <code>date_histogram</code>, with one <code>terms</code> source per field. For categorization jobs, use a <code>terms</code> aggregation on the <code>.keyword</code> subfield of the <code>categorization_field_name</code>. The naming rule is strict: The aggregation key must exactly match the field name in the analysis config; the ML engine uses the aggregation name, not the <code>field</code> parameter, to look up values. A mismatch produces silently wrong results; no error, just a job that appears to run while missing everything meaningful.</p><p><strong>Step 5: Map each detector’s metric field</strong> to its Elasticsearch aggregation equivalent:</p><p>ML function</p><p>Elasticsearch aggregation</p><p>`mean` / `high_mean` / `low_mean`</p><p>`avg`</p><p>`sum`</p><p>`sum`</p><p>`max`</p><p>`max`</p><p>`min`</p><p>`min`</p><p>For <code>count</code>, <code>rare</code>, <code>freq_rare</code>, <code>info_content</code>, <code>time_of_day</code>, <code>time_of_week</code>, and categorization jobs, the ML engine works from <code>doc_count</code> alone; no metric aggregation is needed, and this step can be skipped.</p><h2><strong>Step-by-step example: Building an aggregation-based ML job in Kibana</strong></h2><p>Let’s build this end to end using Kibana’s sample web logs. If you haven’t loaded them yet, go to the Kibana home page and click <strong>Integrations → Sample data → Sample web logs → Add data</strong>. This gives us a data view called <code>Kibana Sample Data Logs</code> and an index called <code>kibana_sample_data_logs</code> with fields including <code>@timestamp</code>, <code>bytes</code> (response size), and <code>geo.dest</code> (destination country).</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/bltbd1e470b4b641b8a/6a16fa072d2f505cd7c3c0e4/75b692b9f38017cd7e4e221d2e89a14f75d3b9dc-1999x1905.png" alt="Elastic “Add data” page showing sample datasets, including ecommerce orders, flight data, and web logs, with the web logs option highlighted." /><p>We’ll build a job that detects unusually large response sizes: <code>high_mean of bytes</code>, partitioned by destination country (<code>geo.dest</code>), with a 1-hour bucket span.</p><h3><strong>Creating the job with the Multi-Metric Wizard</strong></h3><p>This is how most jobs get created in practice. Navigate to <strong>Machine Learning → Anomaly Detection → Manage Jobs → Create job</strong>.</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/bltb47f8491b3a60622/6a16fa09acf088a614be98f6/b5bb2f2770a76fd535db22b97fc4f72471c43ca7-1999x587.png" alt="Kibana interface showing the “Create job” step for anomaly detection, with a panel listing available data views and the “Kibana Sample Data Logs” option selected." /><p>Select the “Kibana Sample Data Logs” data view, and set the time range to cover the full sample dataset. On the job type screen, choose <strong>Multi-metric</strong>.</p><p>In the Multi-Metric Wizard, configure the detector:</p><ul><li><p><strong>High mean</strong> of <code>bytes</code>.</p></li><li><p><strong>Split data by</strong> <code>geo.dest</code>.</p></li><li><p><strong>Bucket span:</strong> <code>1h</code>.</p></li></ul><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/bltcd0c896c1510795c/6a16fa0bab7f084ea8db9c62/b3055c91c881e4011521ba0c17cc36c6138595ee-1999x1540.png" alt="Kibana anomaly detection job summary showing a multi‑metric chart split by geographic destination and a configuration panel listing job ID, bucket span, split field, influencers, memory limit, and time range." /><p>Give the job an ID, and leave everything else at its defaults, but <strong>don’t click Create yet</strong>. On this last configuration step, click on <strong>Preview JSON</strong> and look at the datafeed section. What you’ll see is a plain scroll-based datafeed with no aggregations, just an index pattern and a <code>match_all</code> query.</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/bltfc768a5bb02a3a12/6a16fa0d92262a59d61cc0a5/32a1650958525b03a6052d480152933341acdd41-1999x1392.png" alt="Side‑by‑side JSON showing an Elasticsearch ML job configuration and its matching datafeed configuration, including detectors, influencers, index selection, query, and runtime mappings." /><p>This is the default every wizard produces. On a small cluster, it works fine. On a large cluster with CCS and a broad index pattern, this datafeed will scan every raw document on every run and never catch up with live data.</p><p>Instead of clicking <strong>Create</strong>, click <strong>Convert to advanced job</strong>. This keeps everything you just configured (the detector, the partition field, the bucket span) and drops you directly into the Advanced Wizard, where we can apply the five-step pattern.</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt883a9c3cfbe84597/6a16fa0f839dfae25edcfce1/388d279820a639c08b119753f064b3a948ace8c6-1999x1591.png" alt="Kibana multi‑metric anomaly detection job summary showing a line chart split by geographic destination and a configuration panel with job settings, time range, and creation options." /><h3><strong>Analysis configuration</strong></h3><p>The conversion prefills the detector, partition field, and bucket span. The only change needed here is <strong>Step 1</strong> of the pattern: Open the <strong>Edit JSON</strong> view, and add <code>summary_count_field_name</code> to tell the ML engine that incoming data will be pre-summarized:</p>{
  "bucket_span": "1h",
  "summary_count_field_name": "doc_count", // Step 1
  "detectors": [
    {
      "function": "high_mean",
      "field_name": "bytes",
      "partition_field_name": "geo.dest"
    }
  ],
  "influencers": ["geo.dest"]
}<h3><strong>Datafeed configuration</strong></h3><p>Switch to the <strong>Datafeed</strong> tab. This is where Steps 2 through 5 of the pattern come together. Remove <code>scroll_size</code> if it’s present, and then enter the aggregations:</p>{
  "buckets": {
    "date_histogram": {               // Step 2: bucket wrapper, interval = bucket_span
      "field": "@timestamp",
      "fixed_interval": "1h"
    },
    "aggregations": {
      "@timestamp": {                 // Step 3: max timestamp anchor
        "max": { "field": "@timestamp" }
      },
      "geo.dest": {                   // Step 4: partition field, name must match exactly
        "terms": {
          "field": "geo.dest",
          "size": 1000
        },
        "aggregations": {
          "bytes": {                  // Step 5: metric field → avg aggregation
            "avg": { "field": "bytes" }
          }
        }
      }
    }
  }
}<p>A few notes on this config:</p><ul><li><p><strong>Step 2:</strong> The <code>date_histogram</code> uses <code>fixed_interval</code>: <code>"1h"</code>, matching <code>bucket_span</code> exactly. A mismatch produces incorrect bucket timing.</p></li><li><p><strong>Step 3:</strong> The <code>max</code> aggregation on <code>@timestamp</code> must be named <code>@timestamp</code> and placed inside the histogram’s <code>aggregations</code>; without it, the ML node can’t determine the precise end of each bucket.</p></li><li><p><strong>Step 4:</strong> The <code>terms</code> aggregation for the partition field must be named <strong>exactly</strong> after the partition field: <code>geo.dest</code>, not <code>geo.dest_grouping</code> or any alias. The ML engine uses the aggregation name, not the <code>field</code> parameter, to identify which partition value each bucket belongs to. A mismatch silently drops the partition field from results entirely.</p></li><li><p><strong>Step 5:</strong> The metric aggregation key <code>bytes</code> matches <code>field_name</code> in the detector exactly. Any mismatch here produces silently wrong anomaly scores.</p></li></ul><h3><strong>Validate with the preview</strong></h3><p>Before we create the job, let’s use the <strong>Preview</strong> tab. This runs the aggregation against real data and shows exactly what the ML node will receive, a very useful sanity check before committing.</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt1a6a2a786c4bb21a/6a16fa11a6c2b90ab5e794e7/638eeb2ae5b854195dec0e468887300b9afd2c58-1999x1254.png" alt="Three‑panel view showing an ML job configuration JSON, a matching datafeed JSON with aggregations, and a datafeed preview listing timestamped, bucketed results with fields like geo.dest, bytes, and doc_count." /><p>Three things to verify in the preview output: <code>doc_count</code> should be present on every bucket and greater than 1. The <code>bytes</code> values should look like average response sizes: numbers in the hundreds to hundreds of thousands for web traffic. And each row should correspond to a distinct (<code>timestamp</code>, <code>geo.dest</code>) pair. If anything looks off, fix it in the JSON editor and rerun the preview.</p><h2><strong>Adding influencer fields</strong></h2><p>In the example above, <code>geo.dest</code> is the partition field. The ML model learns a separate baseline for each destination country, and anomalies are reported per country. But you might also want <code>machine.os</code> to appear as an <strong>influencer</strong> in anomaly results: When the detector fires, you want to see “this looks anomalous for <code>geo.dest: CN</code> and <code>machine.os: win</code> is a contributing factor.” <a href="https://www.elastic.co/docs/explore-analyze/machine-learning/anomaly-detection/ml-ad-run-jobs#ml-ad-influencers">Influencers</a> don’t drive anomaly detection; they provide context for the anomalies that are found.</p><p>To support an influencer alongside a partition field, the analysis config gains an <code>influencers</code> array:</p>“Analysis_config”: {
  "bucket_span": "1h",
  "summary_count_field_name": "doc_count",
  "detectors": [
    {
      "function": "high_mean",
      "field_name": "bytes",
      "partition_field_name": "geo.dest"
    }
  ],
  "influencers": ["geo.dest", "machine.os"]
}<p>And now the datafeed needs to aggregate on both fields simultaneously. One <code>terms</code> nested inside another <code>terms</code> won’t work; a nested <code>terms</code> surfaces only the top-N values of the inner field per outer bucket, so you’d silently lose combinations. Instead, use a <a href="https://www.elastic.co/docs/reference/aggregations/search-aggregations-bucket-composite-aggregation">composite aggregation</a> with one <code>terms</code> source per field, next to the <code>date_histogram</code>:</p>"aggregations": {
    "buckets": {
      "composite": {
        "size": 1000,
        "sources": [
          { "timestamp": { "date_histogram": { "field": "timestamp", "fixed_interval": "1h" } } },
          { "geo.dest": { "terms": { "field": "geo.dest" } } },
          { "machine.os": { "terms": { "field": "machine.os.keyword" } } }
        ]
      },
      "aggregations": {
        "timestamp": { "max": { "field": "timestamp" } },
        "bytes": { "avg": { "field": "bytes" } }
      }
    }
  }<p><code>composite</code> generates one bucket per unique (<code>geo.dest</code>, <code>machine.os</code>) combination. The ML node sees every pair and can correctly attribute which operating system was contributing when a country’s response sizes spiked. Use the preview to confirm distinct pairs appear. If you only see a handful of rows where you’d expect many, the <code>size</code> parameter on the composite may need to be raised.</p><h2><strong>Categorization</strong></h2><p>Categorization works with aggregated datafeeds: <code>summary_count_field_name</code> and <code>categorization_field_name</code> can coexist in the same job. The five-step pattern applies directly. Step 2 uses the standard <code>date_histogram</code> topology. Step 4 has one adjustment: Instead of a partition field, we aggregate the text field itself using a <code>terms</code> aggregation on its <code>.keyword</code> subfield, named to match <code>categorization_field_name</code> exactly. Step 5 is skipped. The <code>count</code> detector works from <code>doc_count</code> alone.
<strong>Analysis config:</strong></p>{
  "bucket_span": "1h",
  "summary_count_field_name": "doc_count",
  "categorization_field_name": "message",
  "detectors": [
    {
      "function": "count",
      "by_field_name": "mlcategory"
    }
  ],
  "influencers": ["mlcategory"]
}<p><strong>Datafeed aggregations:</strong></p>{
  "buckets": {
    "date_histogram": {
      "field": "@timestamp",
      "fixed_interval": "1h"
    },
    "aggregations": {
      "@timestamp": {
        "max": { "field": "@timestamp" }
      },
      "message": {
        "terms": {
          "field": "message.keyword",
          "size": 1000
        }
      }
    }
  }
}<p>The datafeed sends one bucket per unique <code>message.keyword</code> value with a <code>doc_count</code> for each. The ML node receives those strings, runs categorization on them, assigning an <code>mlcategory</code> to each, and the <code>count</code> detector tracks how many documents fall into each category per bucket. The naming rule from Step 4 applies: The <code>terms</code> aggregation must be named <code>message</code>, matching <code>categorization_field_name</code> in the analysis config exactly.</p><p>One thing to watch: Keyword fields have a default <a href="https://www.elastic.co/docs/reference/elasticsearch/mapping-reference/ignore-above"><code>ignore_above: 256</code></a> limit. Log messages longer than 256 characters won’t be indexed as <code>.keyword</code> and will be silently excluded from the aggregation. If your log messages are long, check the field mapping before using this approach. You may need to raise the limit in your index template.</p><h2><strong>The minimal pattern for </strong><strong><code>time_of_day</code></strong><strong> and </strong><strong><code>time_of_week</code></strong></h2><p><a href="https://www.elastic.co/docs/reference/machine-learning/ml-time-functions"><code>time_of_day</code></a><a href="https://www.elastic.co/docs/reference/machine-learning/ml-time-functions"> and </a><a href="https://www.elastic.co/docs/reference/machine-learning/ml-time-functions"><code>time_of_week</code></a> are the easiest functions to aggregate: They only need a timestamp and a document count. The C++ process extracts the time component from the bucket timestamp and builds a cyclical model of normal activity; <code>doc_count</code> tells it how many events fell in each bucket. No <code>terms</code> sources, no metric aggregation, no composite.
<strong>Analysis config:</strong></p>{
  "bucket_span": "15m",
  "summary_count_field_name": "doc_count",
  "detectors": [
    { "function": "time_of_day" }
  ]
}<p><strong>Datafeed aggregations:</strong></p>{
  "time": {
    "date_histogram": {
      "field": "@timestamp",
      "fixed_interval": "15m"
    },
    "aggregations": {
      "@timestamp": { "max": { "field": "@timestamp" } }
    }
  }
}<p>A plain <code>date_histogram</code> is enough; no composite needed. This makes <code>time_of_day</code> and <code>time_of_week</code> particularly CCS-friendly: one request per time chunk, minimal data over the wire. Use the same structure for <code>time_of_week</code>; only the function name changes.</p><p>If you want to add a <code>partition_field_name</code> (for example, to model time-of-day patterns per service), add a <code>terms</code> aggregation inside the histogram’s aggregations following the standard Step 4 pattern.</p><h2><strong>The composite pattern for </strong><strong><code>rare</code></strong><strong>, </strong><strong><code>freq_rare</code></strong><strong>, and </strong><strong><code>info_content</code></strong></h2><p><a href="https://www.elastic.co/docs/reference/machine-learning/ml-rare-functions"><code>rare</code></a><a href="https://www.elastic.co/docs/reference/machine-learning/ml-rare-functions">, </a><a href="https://www.elastic.co/docs/reference/machine-learning/ml-rare-functions"><code>freq_rare</code></a>, and <a href="https://www.elastic.co/docs/reference/machine-learning/ml-info-functions"><code>info_content</code></a> all need the composite extractor, the one that paginates through all unique value combinations rather than truncating to top-N. The five-step pattern applies here with a different topology in Step 2: <code>composite</code> goes at the top level (not <code>date_histogram</code>), with <code>date_histogram</code> as a source inside it. Step 3 places the <code>max</code> <code>@timestamp</code> aggregation as a sibling of the <code>composite</code>, and Step 5 is skipped since all three functions work from <code>doc_count</code> alone.</p><p>The datafeed structure is the same for all three functions: a composite at the top level, a <code>date_histogram</code> as one of its sources, and one <code>terms</code> source per analysis field. The only thing that varies is which fields you include as <code>terms</code> sources: <code>rare</code> needs one source for <code>by_field_name</code>; <code>freq_rare</code> needs sources for both <code>by_field_name</code> and <code>over_field_name</code>; <code>info_content</code> needs a source for <code>field_name</code> plus any <code>by_field_name</code> or <code>over_field_name</code> fields. None of the three require a metric aggregation.</p>{
  "buckets": {
    "composite": {
      "size": 10000,
      "sources": [
        { "@timestamp":   { "date_histogram": { "field": "@timestamp", "fixed_interval": "5m" } } },
        { "by_field":     { "terms": { "field": "by_field" } } },
        { "over_field":   { "terms": { "field": "over_field" } } }
      ]
    },
    "aggregations": {
      "@timestamp": { "max": { "field": "@timestamp" } }
    }
  }
}<p>A few notes:</p><ul><li><p>The composite aggregation must be the top-level aggregation, not nested inside a <code>date_histogram</code>. This is what routes the datafeed to the composite extractor.</p></li><li><p>The <code>date_histogram</code> is a source inside the composite, not the outer wrapper. Its <code>fixed_interval</code> must divide evenly into <code>bucket_span</code>.</p></li><li><p>The <code>max</code> aggregation on <code>@timestamp</code> sits as a sibling of the <code>composite</code> (inside <code>aggregations</code>), not nested inside it.</p></li><li><p><code>composite.size</code> controls the page size per round trip. Setting it high (10000) reduces round trips, which matters with CCS latency. With three sources and high-cardinality fields, the total combination count can be large; the extractor paginates automatically.</p></li></ul><h2><strong>Why aggregation-based datafeeds outperform scroll at scale</strong></h2><p>The gap is structural, not incidental. A scroll-based datafeed reads raw documents one page at a time: Every 1,000 documents is one request, and each waits for the previous one to complete before issuing the next. The number of requests is therefore proportional to the total document count in the time range being backfilled. At 836,000 events per hour over 13 months, that's roughly 7.9 billion events, or 7.9 million sequential round trips. Each round trip crosses the CCS boundary, waits for shard responses, and transfers matching documents in full. There’s no parallelism: The datafeed holds a <a href="https://www.elastic.co/docs/api/doc/elasticsearch/operation/operation-scroll">scroll context</a> open on the remote cluster and processes one page at a time.</p><p>An aggregation-based datafeed works differently. The data nodes summarize data locally, grouping by time bucket and categorical fields, and ship only the bucket results to the ML node. The number of requests is proportional to field cardinalities, not document count. In our example, two influencer fields with six unique combinations produce six result rows per time bucket; the datafeed pages through those in a handful of requests regardless of how many raw events fall in each bucket. Double the ingestion rate and the scroll request count doubles; the aggregation request count stays the same. This is why the gap widens at scale: The more data you have, the worse scroll looks by comparison, and the better aggregations look.</p><p>On live data, the picture is different because each real-time tick covers only one fresh bucket: Scroll issues however many pages fit in that bucket's worth of data, while aggregations issue one request. The 20× figure for live data reflects that ratio at 836,000 events per hour with a 15-minute bucket span. The practical threshold where aggregations stop being optional is when <code>(ingestion rate × bucket span) &gt; scroll_size</code>; once a single bucket contains more than <a href="https://www.elastic.co/docs/explore-analyze/machine-learning/anomaly-detection/anomaly-detection-scale#set-scroll-size">one scroll page</a> of documents, the datafeed can't keep pace with live data regardless of hardware. Below that threshold, scroll is fine and aggregations are a nice-to-have. Above it, aggregations are the only sustainable option.</p><p>Scroll-based datafeeds are the right default, and the wizards make the right call for most deployments. At scale (more shards, broader index patterns, CCS across tiers), switching to an aggregation-based datafeed is the natural next step: The data nodes summarize where the data lives, the ML node processes compact results, and the detections stay the same. The one cost to know up front is model state: Switching requires a new job, so the earlier you make the move, the less you give up.</p><p>If you hit a case not covered here, an aggregation type that doesn’t map cleanly or a composite that behaves unexpectedly, the <a href="https://discuss.elastic.co/">Elastic Discuss forums</a> are a good place to continue.</p>]]></content:encoded>
    <link>https://www.elastic.co/search-labs/blog/elastic-machine-leaning-jobs-aggregation-datafeeds</link>
    <guid isPermaLink="true">https://www.elastic.co/search-labs/blog/elastic-machine-leaning-jobs-aggregation-datafeeds</guid>
    <category><![CDATA[ML Research]]></category>
    <dc:creator><![CDATA[Valeriy Khakhutskyy]]></dc:creator>
    <enclosure url="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt584a9fa4d6ed3889/6a16fa13a6c2b97c15e794eb/023e3e6cb25891f789129d496c181113cc570f1f-1280x720.png" length="0" type="image/png"/>
    <pubDate>Wed, 15 Apr 2026 00:00:00 GMT</pubDate>
  </item>
  <item>
    <title><![CDATA[Automating log parsing in Streams with ML]]></title>
    <description><![CDATA[Learn how a hybrid ML approach achieved 94% log parsing and 91% log partitioning accuracy through automation experiments with log format fingerprinting in Streams.]]></description>
    <content:encoded><![CDATA[<p>In modern observability stacks, ingesting unstructured logs from diverse data providers into platforms like Elasticsearch remains a challenge. Reliance on manually crafted parsing rules creates brittle pipelines, where even minor upstream code updates lead to parsing failures and unindexed data. This fragility is compounded by the scalability challenge: in dynamic microservices environments, the continuous addition of new services turns manual rule maintenance into an operational nightmare.</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blte8f5bd0e4986b04c/6a170e6acdacbf612e7d2a9e/9108ec303339dd091faa3c363c7cf5c228155f49-3840x2160.png" alt="" /><p>Our goal was to transition to an automated, adaptive approach capable of handling both log parsing (field extraction) and log partitioning (source identification). We hypothesized that Large Language Models (LLMs), with their inherent understanding of code syntax and semantic patterns, could automate these tasks with minimal human intervention.</p><p>We are happy to announce that this feature is already available in <a href="http://elastic.co/elasticsearch/streams"><u>Streams</u></a>!</p><h2>Dataset description</h2><p>We chose a <a href="https://github.com/logpai/loghub"><strong>Loghub</strong></a>collection of logs for PoC purposes. For our investigation, we selected representative samples from the following key areas:</p><ul><li><p>Distributed systems: We used the HDFS (Hadoop Distributed File System) and Spark datasets. These contain a mix of info, debug, and error messages typical of big data platforms.</p></li><li><p>Server &amp; web applications: Logs from Apache web servers and OpenSSH provided a valuable source of access, error, and security-relevant events. These are critical for monitoring web traffic and detecting potential threats.</p></li><li><p>Operating systems: We included logs from Linux and Windows. These datasets represent the common, semi-structured system-level events that operations teams encounter daily.</p></li><li><p>Mobile systems: To ensure our model could handle logs from mobile environments, we included the Android dataset. These logs are often verbose and capture a wide range of application and system-level activities on mobile devices.</p></li><li><p>Supercomputers: To test performance on high-performance computing (HPC) environments, we incorporated the BGL (Blue Gene/L) dataset, which features highly structured logs with specific domain terminology.</p></li></ul><p>A key advantage of the Loghub collection is that the logs are largely unsanitized and unlabeled, mirroring a noisy live production environment with microservice architecture.</p><p>Log examples:</p>[Sun Dec 04 20:34:21 2005] [notice] jk2_init() Found child 2008 in scoreboard slot 6
[Sun Dec 04 20:34:25 2005] [notice] workerEnv.init() ok /etc/httpd/conf/workers2.properties
[Mon Dec 05 11:06:51 2005] [notice] workerEnv.init() ok /etc/httpd/conf/workers2.properties
17/06/09 20:10:58 INFO output.FileOutputCommitter: Saved output of task 'attempt_201706092018_0024_m_000083_1138' to hdfs://10.10.34.11:9000/pjhe/test/1/_temporary/0/task_201706092018_0024_m_000083
17/06/09 20:10:58 INFO mapred.SparkHadoopMapRedUtil: attempt_201706092018_0024_m_000083_1138: Committed<p>In addition, we created a Kubernetes cluster with a typical web application + database set up to mine extra logs in the most common domain.</p><p>Example of common log fields: timestamp, log level (INFO, WARN, ERROR), source, message.</p><h2>Few-shot log parsing with an LLM</h2><p>Our first set of experiments focused on a fundamental question: <strong>Can an LLM reliably identify key fields and generate consistent parsing rules to extract them?</strong></p><p>We asked a model to analyse raw log samples and generate log parsing rules in regular expression (regex) and <a href="https://www.elastic.co/docs/explore-analyze/scripting/grok">Grok</a> formats. Our results showed that this approach has a lot of potential, but also significant implementation challenges.</p><h3>High confidence &amp; context awareness</h3><p>Initial results were promising. The LLM demonstrated a strong ability to generate parsing rules that matched the provided few-shot examples with high confidence. Besides simple pattern matching, the model showed a capacity for log understanding —it could correctly identify and name the log source (e.g., health tracking app, Nginx web app, Mongo database).</p><h3>The "Goldilocks" dilemma of input samples</h3><p>Our experiments quickly surfaced a significant lack of robustness because of extreme<strong> sensitivity to the input sample.</strong> The model's performance fluctuates wildly based on the specific log examples included in the prompt. We observed a log similarity problem where the log sample needs to include <em>just diverse enough </em>logs:</p><ul><li><p>Too homogeneous (overfitting)<strong>:</strong> If the input logs are too similar, the LLM tends to <strong>overspecify</strong>. It treats variable data—such as specific Java class names in a stack trace—as static parts of the template. This results in brittle rules that cover a tiny ratio of logs and extract unusable fields.</p></li><li><p>Too heterogeneous (confusion): Conversely, if the sample contains significant formatting variance—or worse, "trash logs" like progress bars, memory tables, or ASCII art—the model struggles to find a common denominator. It often resorts to generating complex, broken regexes or lazily over-generalizing the entire line into a single message blob field.</p></li></ul><h3>The context window constraint</h3><p>We also encountered a context window bottleneck. When input logs were long, heterogeneous, or rich in extractable fields, the model's output often deteriorated, becoming "messy" or too long to fit into the output context window. Naturally, chunking helps in this case. By splitting logs using character-based and entity-based delimiters, we could help the model focus on extracting the main fields without being overwhelmed by noise.</p><h3>The consistency &amp; standardization gap</h3><p>Even when the model successfully generated rules, we noted slight inconsistencies:</p><ul><li><p>Service naming variations: The model proposes different names for the same entity (e.g., labeling the source as "Spark," "Apache Spark," and "Spark Log Analytics" in different runs).</p></li><li><p>Field naming variations: Field names lacked standardization (e.g., <code>id</code> vs. <code>service.id</code> vs. <code>device.id</code>). We normalized names using a standardized <a href="https://www.elastic.co/docs/reference/ecs/ecs-field-reference">Elastic field naming</a>.</p></li><li><p>Resolution variance: The resolution of the field extraction varied depending on how similar the input logs were to one another.</p></li></ul><h2>Log format fingerprint</h2><p>To address the challenge of log similarity, we introduce a high-performance heuristic: <strong>log format fingerprint (LFF)</strong>.</p><p>Instead of feeding raw, noisy logs directly into an LLM, we first apply a deterministic transformation to reveal the underlying structure of each message. This pre-processing step abstracts away variable data, generating a simplified "fingerprint" that allows us to group related logs.</p><p>The mapping logic is simple to ensure speed and consistency:</p><ol><li><p>Digit abstraction: Any sequence of digits (0-9) is replaced by a single ‘0’.</p></li><li><p>Text abstraction: Any sequence of alphabetical characters with whitespace is replaced by a single ‘a’.</p></li><li><p>Whitespace normalization: All sequences of whitespace (spaces, tabs, newlines) are collapsed into a single space.</p></li><li><p>Symbol preservation: Punctuation and special characters (e.g., :, [, ], /) are preserved, as they are often the strongest indicators of log structure.</p></li></ol><p>We introduce the log mapping approach. The basic mapping patterns include the following:</p><ul><li><p>Digits 0-9 of any length -&gt; to ‘0.’</p></li><li><p>Text (alphabetical characters with spaces) of any length -&gt; to ‘a’.</p></li><li><p>White spaces, tabs, and new lines -&gt; to a single space.</p></li></ul><p>Let's look at an example of how this mapping allows us to transform the logs.</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/bltf91eebab0ad79ccd/6a170e6c67045ba94f45c29c/78fa2887486eb9417804354ee3bf2a4fdb0f6383-846x252.png" alt="" /><p>As a result, we obtain the following log masks:</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt438d74dcb921578b/6a170e6d1949f74aa0e7aae3/ec439a3d3a25002498b97defcff733ea5ebc6b55-826x94.png" alt="" /><p>Notice the fingerprints of the first two logs. Despite different timestamps, source classes, and message content, their prefixes (<code>0/0/0 0:0:0 a a.a:</code>) are identical. This structural alignment allows us to automatically bucket these logs into the same cluster.</p><p>The third log, however, produces a completely divergent fingerprint (<code>0-0-0...</code>). This allows us to algorithmically separate it from the first group <em>before</em> we ever invoke an LLM.</p><h2>Bonus part: Instant implementation with ES|QL</h2><p>It’s as easy as passing this query in Discover.</p><p><strong>Query breakdown:</strong></p><p><strong>FROM</strong> loghub: Targets our index containing the raw log data.</p><p><strong>EVAL</strong> pattern = …: The core mapping logic. We chain REPLACE functions to perform the abstraction (e.g., digits to '0', text to 'a', etc.) and save the result in a “pattern” field.</p><p><strong>STATS </strong>[column1 =] expression1, …<strong> BY </strong>SUBSTRING(pattern, 0, 15):</p><p>This is a clustering step. We group logs that share the first 15 characters of their pattern and create aggregated fields such as total log count per group, list of log datasources, pattern prefix, 3 log examples</p><p><strong>SORT</strong> total_count DESC | <strong>LIMIT</strong> 100 : Surfaces the top 100 most frequent log patterns</p><p>The query results on LogHub are displayed below:</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/bltfa3960cf94ccf331/6a170e6fdc55decfa3e00e7c/b119498f124376c41d242a099bf9081fd6536be8-1600x394.png" alt="Log parsing query results on LogHub." /><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt2dbcde2a22e06367/6a170e71961e693a18c4cfb6/4dcfc0a5b7fa753497cc5def5ea3cd54449c0481-1600x719.png" alt="" /><p>As demonstrated in the visualization, this “LLM-free” approach partitions logs with high accuracy. It successfully clustered 10 out of 16 data sources (based on LogHub labels) completely (&gt;90%) and achieved majority clustering in 13 out of 16 sources (&gt;60%) —all without requiring additional cleaning, preprocessing, or fine-tuning.</p><p>Log format fingerprint offers a pragmatic, high-impact alternative and addition to sophisticated ML solutions like <a href="https://www.elastic.co/docs/reference/aggregations/search-aggregations-bucket-categorize-text-aggregation">log pattern analysis</a>. It provides immediate insights into log relationships and effectively manages large log clusters.</p><ul><li><p>Versatility as a primitive </p></li></ul><p>Thanks to <a href="https://www.elastic.co/blog/getting-started-elasticsearch-query-language">ES|QL</a> implementation, LFF serves both as a standalone tool for fast data diagnostics/visualisations, and as a building block in log analysis pipelines for high-volume use cases. </p><ul><li><p>Flexibility</p></li></ul><p>LFF is easy to customize and extend to capture specific patterns, i.e. hexadecimal numbers and IP addresses.</p><ul><li><p>Deterministic stability</p></li></ul><p>Unlike ML-based clustering algorithms, LFF logic is straightforward and deterministic. New incoming logs do not retroactively affect existing log clusters.</p><ul><li><p>Performance and mMemory</p></li></ul><p>It requires minimal memory, no training or GPU making it ideal for real-time high-throughput environments.</p><h2>Combining log format fingerprint with an LLM</h2><p>To validate the proposed hybrid architecture, each experiment contained a random 20% subset of the logs from each data source. This constraint simulates a real-world production environment where logs are processed in batches rather than as a monolithic historical dump.</p><p>The objective was to demonstrate that LFF acts as an effective compression layer. We aimed to prove that high-coverage parsing rules could be generated from small, curated samples and successfully generalized to the entire dataset.</p><h2>Execution pipeline</h2><p>We implemented a multi-stage pipeline that filters, clusters, and applies stratified sampling to the data before it reaches the LLM.</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt26635762891b3a41/6a170e73509168eea4e1bb91/b3f46ea471760b406a32fc7d4bc74cc03faaced2-3840x1660.png" alt="" /><p>1. Two-stage hierarchical clustering</p><ul><li><p>Subclasses (exact match): Logs are aggregated by identical fingerprints. Every log in one subclass shares the exact same format structure.</p></li><li><p>Outlier cleaning. We discard any subclasses that represent less than 5% of the total log volume. This ensures the LLM focuses on the dominant signal and won’t be sidetracked by noise or malformed logs.</p></li><li><p>Metaclasses (prefix match): Remaining subclasses are grouped into Metaclasses by the first N characters of the format fingerprint match. This grouping strategy effectively splits lexically similar formats under a single umbrella.We chose N=5 for Log parsing and N=15 for Log partitioning when data sources are unknown.</p></li></ul><p>2. Stratified sampling. Once the hierarchical tree is built, we construct the log sample for the LLM. The strategic goal is to maximize variance coverage while minimizing token usage.</p><ul><li><p>We select representative logs from <em>each</em> valid subclass within the broader metaclass.</p></li><li><p>To manage an edge case of too numerous subclasses, we apply random down-sampling to fit the target window size.</p></li></ul><p>3. Rule generation Finally, we prompt the LLM to generate a regex parsing rule that fits all logs in the provided sample for each Metaclass. For our PoC, we used the GPT-4o mini model.</p><h2>Experimental results &amp; observations</h2><p>We achieved 94% parsing accuracy and 91% partitioning accuracy on the Loghub dataset.</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt1b896b41b3b70e7e/6a170e757d8d67601a70e7d9/49b2b6a1401dd1f33951da68e5a3fac37d0b5aaa-1600x1506.png" alt="94% parsing accuracy and 91% partitioning accuracy on the Loghub dataset." /><p>The confusion matrix above illustrates log partitioning results. The vertical axis represents the actual data sources, and the horizontal axis represents the predicted data sources. The heatmap intensity corresponds to log volume, with lighter tiles indicating a higher count. The diagonal alignment demonstrates the model's high fidelity in source attribution, with minimal scattering.</p><h2>Our performance benchmarks insights:</h2><ul><li><p><strong>Optimal baseline:</strong> a context window of <strong>30–40 log samples</strong> per category proved to be the "sweet spot," consistently producing robust parsing with both Regex and Grok patterns.</p></li><li><p><strong>Input minimisation:</strong> we pushed the input size to 10 logs per category for Regex patterns and observed only 2% drop in parsing performance, confirming that diversity-based sampling is more critical than raw volume.</p></li></ul>]]></content:encoded>
    <link>https://www.elastic.co/search-labs/blog/log-parsing-partitioning-automation-experiments-streams</link>
    <guid isPermaLink="true">https://www.elastic.co/search-labs/blog/log-parsing-partitioning-automation-experiments-streams</guid>
    <category><![CDATA[ML Research]]></category>
    <category><![CDATA[AI]]></category>
    <dc:creator><![CDATA[Nastia Havriushenko]]></dc:creator>
    <enclosure url="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/bltc1df5a7cae463d59/6a170e76a6c2b907d7e797ab/965c58f19742361160593c38fcaa8b2f4b0d6cc5-3838x2159.png" length="0" type="image/png"/>
    <pubDate>Fri, 02 Jan 2026 00:00:00 GMT</pubDate>
  </item>
  <item>
    <title><![CDATA[Generating filters and facets using ML]]></title>
    <description><![CDATA[Exploring the pros and cons of automating the creation of filters and facets in a search experience using ML models vs the classical hard-coded approach.]]></description>
    <content:encoded><![CDATA[<p>Filters and facets are mechanisms used to refine search results, helping users find relevant content or products more quickly. In the classical approach, rules are manually defined. For example, in a movie catalog, attributes such as genre are pre-defined for use in filters and facets. On the other hand, with AI models, new attributes can be automatically extracted from the characteristics of movies, making the process more dynamic and personalized. In this blog, we explore the pros and cons of each method, highlighting their applications and challenges.</p><h2>Filters vs facets</h2><p>Before we begin, let's define what filters and facets are. <strong>Filters</strong> are predefined attributes used to restrict a set of results. In a marketplace, for example, filters are available even before a search is performed. The user can select a category, such as <strong>"Video games"</strong>, before searching for <strong>"PS5"</strong>, refining the search to a more specific subset instead of the entire database. This significantly increases the chances of obtaining more relevant results.</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt0a77b38aae238938/6a170b821949f72a52e7aa51/5ed8868fa5017d034e1273e35c884a5430afdf3c-1600x937.png" alt="Filters" /><p><strong>Facets</strong> work similarly to filters but are only available after the search is performed. In other words, the search returns results, and based on them, a new list of refinement options is generated. For example, when searching for a PS5 console, facets such as storage <strong>capacity</strong>, <strong>shipping cost</strong>, and <strong>color</strong> may be displayed to help users choose the ideal product.</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt166e356b80d423ef/6a170b840e2e494ca341a10f/c5633fcc5b6fbb916110faf32144d8d43572e33a-1600x937.png" alt="Facets " /><p>Now that we have defined filters and facets, let's discuss the impact of the classical and Machine Learning (ML)-based approaches on their implementation and usage. Each method has advantages and challenges that influence search efficiency.</p><h2>Classical approach to filters and facets</h2><p>In this approach, filters and facets are manually defined based on predefined rules. This means that the attributes available for refining the search are fixed and planned in advance, considering the catalog structure and user needs.</p><p>For example, in a marketplace, categories such as "Electronics" or "Fashion" may have specific filters like brand, format and price range. These rules are created statically, ensuring consistency in the search experience but requiring manual adjustments whenever new products or categories emerge.</p><p>Although this approach provides predictability and control over the displayed filters and facets, it can be limited when new trends arise that demand dynamic refinement.</p><p><strong>Pros:</strong></p><ul><li><p><strong>Predictability and control:</strong> Since filters and facets are manually defined, management becomes easier.</p></li><li><p><strong>Low complexity:</strong> No need to train models.</p></li><li><p><strong>Ease of maintenance:</strong> As rules are predefined, adjustments and corrections can be made quickly.</p></li></ul><p><strong>Cons</strong>:</p><ul><li><p><strong>Reindexing required for new filters:</strong> Whenever a new attribute needs to be used as a filter, the entire dataset must be reindexed to ensure that documents contain this information.</p></li><li><p><strong>Lack of dynamic adaptation:</strong> Filters are static and do not automatically adjust to changes in user behavior.</p></li></ul><h3>Implementation of filters/facets – Classical approach</h3><p>In <strong>Dev Tools, Kibana</strong>, we will create a demonstration of filters/facets using the <strong>classical approach</strong>.</p><p>First, we define the mapping to structure the index:</p>PUT videogames
{
  "mappings": {
    "properties": {
      "name": { "type": "text" },
      "brand": { "type": "keyword" },
      "storage": { "type": "keyword" },
      "price": { "type": "float" },
      "description": { "type": "text" }
    }
  }
}<p>The <strong>brand</strong> and <strong>storage</strong> fields are set as <strong>keyword</strong>, allowing them to be used directly in aggregations (<strong>facets</strong>). The <strong>price</strong> field is of type <strong>float</strong>, enabling the creation of <strong>price ranges</strong>.</p><p>In the next step, the product data will be indexed:</p>POST videogames/_bulk
{ "index": { "_id": 1 } }
{ "name": "Play Station 5", "brand": "Sony", "storage": "1TB", "price": 499.99, "description": "Stunning Gaming: Marvel at stunning graphics and experience the features of the new PS5. Breathtaking Immersion: Discover a deeper gaming experience with support for haptic feedback, adaptive triggers, and 3D Audio technology. Slim Design: With the PS5 Digital Edition, gamers get powerful gaming technology in a sleek, compact design. 1TB of Storage: Have your favorite games ready and waiting for you to play with 1TB of built-in SSD storage. Backward Compatibility and Game Boost: The PS5 console can play over 4,000 PS4 games. With Game Boost, you can even enjoy faster, smoother frame rates in some of the best PS4 console games." }
{ "index": { "_id": 2 } }
{ "name": "Xbox Series X", "brand": "Microsoft", "storage": "1TB", "price": 499.99, "description": "Fastest, most powerful Xbox console ever. Play thousands of titles: Every game looks and plays better on Xbox Series X. At the heart of Series X is the Xbox Velocity. Architecture, which combines a custom SSD and built-in software to significantly reduce load times in and out of game. Switch between multiple games in an instant with Quick Resume. Explore new worlds and experience the action like never before with an unparalleled 12 teraflops of graphics processing power. Enjoy 4K gaming at up to 120 frames per second, premium advanced 3D sound, and more. 4K at 120 FPS: requires compatible content and display X version - with disc drive" }
{ "index": { "_id": 3 } }
{ "name": "Nintendo Switch", "brand": "Nintendo", "storage": "512GB", "price": 299.99, "description": "SHARPER, VIBRANT VISUALS. The new 7-inch screen on the Nintendo Switch OLED takes your gaming to the next level: vibrant colors with sharp contrasts for every moment. INTEGRATED GAMEPLAY. Enjoy the console's many multiplayer modes and connect with other players. Online or locally, the fun on the Nintendo Switch is guaranteed. ENJOY IMMERSION FOR LONGER. In addition to delivering an unparalleled experience, thanks to its improved audio, the Nintendo Switch has a rechargeable battery while you play. From 4.5 hours to 9 hours of battery life. INCLUDES SUPER MARIO BROS. WONDER. Transform your world with the phenomenal flowers in this new Mario game, full of amazing adventures, power-ups and new abilities. NINTENDO SWITCH ONLINE SUBSCRIPTION. Access online games, play with friends and enjoy the exclusive benefits of the Nintendo Switch Online subscription." }
{ "index": { "_id": 4 } }
{ "name": "Steam Deck", "brand": "Valve", "storage": "512GB", "price": 399.99, "description": "You can save games, apps, photos and videos without worrying about space. High-Level Performance: The 4-core processor and graphics ensure a dynamic experience and fast responses. High-Definition Images: Smooth transitions and sharp images provide complete immersion in the game. Wireless Connectivity: Wi-Fi technology allows you to play wherever you want, without wires or cables limiting your fun" }
{ "index": { "_id": 5 } }
{ "name": "Nintendo Switch Lite", "brand": "Nintendo", "storage": "512GB", "price": 299.99, "description": "MADE TO BE PORTABLE. Nintendo Switch Lite is designed specifically for portable gaming. The console lets you jump into your favorite games wherever you are. COMPACT AND LIGHTWEIGHT. With its sleek, lightweight design, this console is ready to hit the road wherever you are. COMPATIBLE GAMES. The Nintendo Switch Lite system plays the library of Nintendo Switch games that work in handheld mode. A WORLD OF COLOR TO CHOOSE FROM. Available in a variety of vibrant and unique colors, Nintendo Switch Lite lets you bring even more personality wherever you go." }<p>Now, let's retrieve classic facets by grouping the results by brand, storage, and price range. In the query, size:0 was defined. In this scenario, the goal is to retrieve only the aggregation results without including the documents corresponding to the query.</p>POST videogames/_search
{
  "size": 0,
  "aggs": {
    "brands": {
      "terms": { "field": "brand" }
    },
    "storage_sizes": {
      "terms": { "field": "storage" }
    },
    "price_ranges": {
      "range": {
        "field": "price",
        "ranges": [
          { "to": 300 },   
          { "from": 300, "to": 500 },  
          { "from": 500 }  
        ]
      }
    }
  }
}<p>The response will include counts for <strong>Brand</strong>, <strong>Storage</strong>, and <strong>Price</strong>, helping to create filters and facets.</p>"aggregations": {
   "brands": {
     "doc_count_error_upper_bound": 0,
     "sum_other_doc_count": 0,
     "buckets": [
       {
         "key": "Microsoft",
         "doc_count": 1
       },
       {
         "key": "Nintendo",
         "doc_count": 1
       },
       {
         "key": "Sony",
         "doc_count": 1
       },
       {
         "key": "Valve",
         "doc_count": 1
       }
     ]
   },
   "storage_sizes": {
     "doc_count_error_upper_bound": 0,
     "sum_other_doc_count": 0,
     "buckets": [
       {
         "key": "1TB",
         "doc_count": 2
       },
       {
         "key": "512GB",
         "doc_count": 2
       }
     ]
   },
   "price_ranges": {
     "buckets": [
       {
         "key": "*-300.0",
         "to": 300,
         "doc_count": 1
       },
       {
         "key": "300.0-500.0",
         "from": 300,
         "to": 500,
         "doc_count": 3
       },
       {
         "key": "500.0-*",
         "from": 500,
         "doc_count": 0
       }
     ]
   }
 }<h2>Machine learning/AI-based approach to filters and facets</h2><p>In this approach, Machine Learning (ML) models, including Artificial Intelligence (AI) techniques, analyze data attributes to generate relevant filters and facets. Instead of relying on predefined rules, ML/AI leverages indexed data characteristics. This enables the dynamic discovery of new facets and filters.</p><p><strong>Pros</strong>:</p><ul><li><p><strong>Automatic updates:</strong> New filters and facets are generated automatically, without the need for manual adjustments.</p></li><li><p><strong>Discovery of new attributes:</strong> It can identify <strong>previously unconsidered </strong>data characteristics as filters, enriching the search experience.</p></li><li><p><strong>Reduced manual effort:</strong> The team does not need to constantly define and update filtering rules as AI learns from available data.</p></li></ul><p><strong>Cons:</strong></p><ul><li><p><strong>Maintenance complexity:</strong> The use of models may require pre-validation to ensure the consistency of the generated filters.</p></li><li><p><strong>Requires ML and AI expertise:</strong> The solution demands qualified professionals to fine-tune and monitor model performance.</p></li><li><p><strong>Risk of irrelevant filters:</strong> If the model is not well-calibrated, it may generate facets that are not useful for users.</p></li><li><p><strong>Cost:</strong> The use of ML and AI may require third-party services, increasing operational costs.</p></li></ul><p>It's worth noting that even with a well-calibrated model and a well-crafted prompt, the generated facets should still go through a review step. This validation can be manual or based on moderation rules, ensuring that the content is appropriate and safe. While not necessarily a drawback, it is an important consideration to ensure the quality and suitability of the facets before they are made available to users.</p><h3>Implementation of filters/facets – AI approach</h3><p>In this demonstration, we will use an AI model to automatically analyze product characteristics and suggest relevant attributes. With a well-structured prompt, we extract information from the catalog and transform it into filters and facets. Below, we present each step of the process.</p><p>Initially, we will use the <strong>Inference API</strong> to register an endpoint for integration with an ML service. Below is an example of integration with <strong>OpenAI's service</strong>.</p>PUT _inference/completion/generate_filter_ia
{
   "service": "openai",
   "service_settings": {
       "api_key": "your-key",
       "model_id": "gpt-4o-mini"
   }
}<p>Now, we define the pipeline to execute the prompt and obtain the new filters generated by the model.</p>PUT /_ingest/pipeline/generate_filter_ai
{
   "processors": [
     {
       "script": {
         "source": """ctx.prompt = "You are an expert in data organization for search and product categorization. Your task is to analyze the following product and identify the best dynamic facets that can be used in an e-commerce search experience. Product: " + ctx.name + "description: " + ctx.description + "Instructions: - Analyze the product name and description. - Extract only the dynamic facets (technological features or product characteristics that can be inferred from the description, try to create max 3 facets by characteristics found). Put the values into an array. Using key and value, e.g. dynamic_facets: [{ \"name\": \"Gaming Experience\", \"value\": \"Haptic Feedback\" },{ \"name\": \"Gaming Experience\", \"value\": \"Adaptive Triggers\" } - Return only a JSON."
         """
       }
     },
     {
       "inference": {
         "model_id": "generate_filter_ia",
         "input_output": {
           "input_field": "prompt",
           "output_field": "result"
         }
       }
     },
     {
       "gsub": {
         "field": "result",
         "pattern": "```json",
         "replacement": ""
       }
     },
     {
       "json" : {
         "field" : "result",
         "strict_json_parsing": false,
         "add_to_root" : true
       }
     },
     {
       "remove": {
         "field": "result"
       }
     },
     {
       "remove": {
         "field": "prompt"
       }
     }
   ]
}<p>Running a simulation of this pipeline for the "PlayStation 5" product, with the following description:</p><p><em>Stunning Gaming: Marvel at stunning graphics and experience the features of the new PS5.</em></p><p><em>Breathtaking Immersion: Discover a deeper gaming experience with support for haptic feedback, adaptive triggers, and 3D Audio technology.</em></p><p><em>Slim Design: With the PS5 Digital Edition, gamers get powerful gaming technology in a sleek, compact design.</em></p><p><em>1TB of Storage: Have your favorite games ready and waiting for you to play with 1TB of built-in SSD storage.</em></p><p><em>Backward Compatibility and Game Boost: The PS5 console can play over 4,000 PS4 games. With Game Boost, you can even enjoy faster, smoother frame rates in some of the best PS4 console games.</em></p><p>Let's observe the prompt output generated from this simulation.</p>{
 "docs": [
   {
     "doc": {
       "_index": "index",
       "_version": "-3",
       "_id": "1",
       "_source": {
         "name": "Play Station 5",
         "result": """```json
{
 "dynamic_facets": [
   { "name": "Storage Capacity", "value": "1TB SSD" },
   { "name": "Graphics Technology", "value": "Stunning Graphics" },
   { "name": "Audio Technology", "value": "3D Audio" }
 ]
}
```""",
         "description": "Stunning Gaming: Marvel at stunning graphics and experience the features of the new PS5. Breathtaking Immersion: Discover a deeper gaming experience with support for haptic feedback, adaptive triggers, and 3D Audio technology. Slim Design: With the PS5 Digital Edition, gamers get powerful gaming technology in a sleek, compact design. 1TB of Storage: Have your favorite games ready and waiting for you to play with 1TB of built-in SSD storage. Backward Compatibility and Game Boost: The PS5 console can play over 4,000 PS4 games. With Game Boost, you can even enjoy faster, smoother frame rates in some of the best PS4 console games.",
         "model_id": "generate_filter_ia",
         "prompt": """You are an expert in data organization for search and product categorization. Your task is to analyze the following product and identify the best dynamic facets that can be used in an e-commerce search experience. Product: Play Station 5description: Stunning Gaming: Marvel at stunning graphics and experience the features of the new PS5. Breathtaking Immersion: Discover a deeper gaming experience with support for haptic feedback, adaptive triggers, and 3D Audio technology. Slim Design: With the PS5 Digital Edition, gamers get powerful gaming technology in a sleek, compact design. 1TB of Storage: Have your favorite games ready and waiting for you to play with 1TB of built-in SSD storage. Backward Compatibility and Game Boost: The PS5 console can play over 4,000 PS4 games. With Game Boost, you can even enjoy faster, smoother frame rates in some of the best PS4 console games.Instructions: - Analyze the product name and description. - Extract only the dynamic facets (technological features or product characteristics that can be inferred from the description, try create max 3 facets by characteristics found). Put the values like arrays. Using key and value, e.g. dynamic_facets: [{ "name": "Gaming Experience", "value": "Haptic Feedback" },{ "name": "Gaming Experience", "value": "Adaptive Triggers" } - Return only a JSON."""
       },
       "_ingest": {
         "timestamp": "2025-03-19T22:14:32.0161803Z"
       }
     }
   }
 ]
}<p>Now a new field, <strong>dynamic_facets</strong>, will be added to the new index to store the facets generated by the AI.</p>PUT videogames_1
{
 "mappings": {
   "properties": {
     "name": { "type": "text" },
     "brand": { "type": "keyword" },
     "storage": { "type": "keyword" },
     "price": { "type": "float" },
     "description": { "type": "text" },
     "dynamic_facets": { "type": "nested",
     "properties": { "name": { "type": "keyword" },
                     "value": { "type": "keyword" } } }
   }
 }
}<p>Using the <strong>Reindex API</strong>, we will reindex the <strong>videogames</strong> index to <strong>videogames_1</strong>, applying the <strong>generate_filter_ai</strong> pipeline during the process. This pipeline will automatically generate dynamic facets during indexing.</p>POST _reindex?wait_for_completion=false
{
 "source": {
   "index": "videogames"
 },
 "dest": {
   "index": "videogames_1",
   "pipeline": "generate_filter_ai"
 }
}<p>Now, we will run a search and get the new filters:</p>GET videogames_1/_search
{
 "size": 0,
 "query": {
   "match": {
     "name": "nintendo"
   }
 },
 "aggs": {
   "dynamic_facets": {
     "nested": {
       "path": "dynamic_facets"
     },
     "aggs": {
       "facets": {
         "terms": {
           "field": "dynamic_facets.name"
         },
         "aggs": {
           "facets": {
             "terms": {
               "field": "dynamic_facets.value"
             }
           }
         }
       }
     }
   }
 }
}<p>Results:</p>"aggregations": {
   "dynamic_facets": {
     "doc_count": 3,
     "facets": {
       "doc_count_error_upper_bound": 0,
       "sum_other_doc_count": 0,
       "buckets": [
         {
           "key": "Frame Rate",
           "doc_count": 1,
           "facets": {
             "doc_count_error_upper_bound": 0,
             "sum_other_doc_count": 0,
             "buckets": [
               {
                 "key": "120 FPS",
                 "doc_count": 1
               }
             ]
           }
         },
         {
           "key": "Gaming Resolution",
           "doc_count": 1,
           "facets": {
             "doc_count_error_upper_bound": 0,
             "sum_other_doc_count": 0,
             "buckets": [
               {
                 "key": "4K",
                 "doc_count": 1
               }
             ]
           }
         },
         {
           "key": "Graphics Processing Power",
           "doc_count": 1,
           "facets": {
             "doc_count_error_upper_bound": 0,
             "sum_other_doc_count": 0,
             "buckets": [
               {
                 "key": "12 Teraflops",
                 "doc_count": 1
               }
             ]
           }
         }
       ]
     }
   }
 }<p>To symbolize the implementation of the facets, below is a simple front-end:</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/bltb0d6aa40caf7a91a/6a170b86ab7f0839afdb9eb6/12b6d9d4f4d0985848d92841545fd22b7253ae6d-1600x1288.png" alt="implementation of the facets" /><p>The UI code presented is <a href="https://gist.github.com/andreluiz1987/06d9ec1b381e942e9def0e969bd811a0">here</a>.</p><h2>Conclusion</h2><p>Both approaches to creating filters and facets have their benefits and points of concern. The classic approach, based on manual rules, offers control and lower costs but requires constant updates and does not dynamically adapt to new products or features.</p><p>On the other hand, the AI ​​and Machine Learning-based approach automates facet extraction, making the search more flexible and allowing the discovery of new attributes without manual intervention. However, this approach can be more complex to implement and maintain, requiring calibration to ensure consistent results.</p><p>The choice between the classic and AI-based approaches depends on the needs and complexity of the business. For simpler scenarios, where data attributes are stable and predictable, the classic approach can be more efficient and easier to maintain, avoiding unnecessary costs with infrastructure and AI models. On the other hand, the use of ML/AI to extract facets can add significant value, improving the search experience and making filtering more intelligent.</p><p>The important thing is to evaluate whether automation justifies the investment or whether a more traditional solution already meets the business needs effectively.</p>]]></content:encoded>
    <link>https://www.elastic.co/search-labs/blog/filters-facets-using-ml</link>
    <guid isPermaLink="true">https://www.elastic.co/search-labs/blog/filters-facets-using-ml</guid>
    <category><![CDATA[Relevance]]></category>
    <category><![CDATA[ML Research]]></category>
    <dc:creator><![CDATA[Andre Luiz]]></dc:creator>
    <enclosure url="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt4084864dcdaa25d3/6a170b880c485781f901aaa9/6f196643d573614fe5124705c7e4db9bfce004b0-1200x628.png" length="0" type="image/png"/>
    <pubDate>Thu, 03 Apr 2025 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>
  </channel>
</rss>