<?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[Vector Database - 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[Vector Database - 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/vector-database</link>
    </image>
    <link>https://www.elastic.co/search-labs/blog/category/vector-database</link>
    <atom:link href="https://www.elastic.co/search-labs/rss/category/vector-database.xml" rel="self" type="application/rss+xml"/>
    <language><![CDATA[en]]></language>
    <lastBuildDate>Tue, 15 Sep 2026 17:39:00 GMT</lastBuildDate>
  <item>
    <title><![CDATA[Elasticsearch Vector Database: Ship in minutes, scale affordably to hundreds of billions]]></title>
    <description><![CDATA[The hard parts of hybrid retrieval, already done, with optimized defaults, third party and native Jina AI models, and managed GPU inference all out of the box. Build fast, scalable AI apps, not infrastructure.]]></description>
    <content:encoded><![CDATA[<p>Elasticsearch is one of the most widely deployed platforms for vector workloads in the world, powering semantic search, retrieval augmented generation (RAG), and recommendations for companies like GitHub, Docusign, Seismic, and many others. Today we're announcing Elasticsearch Vector Database, a new serverless offering optimized for vector based applications. You bring your documents and your queries, and we handle the embeddings and index tuning, along with the infrastructure. Plus, we keep it cheap and scalable. </p><p>For new users, this is the fastest way to get high-quality vector search running. If you already use Elasticsearch, the new offering is vector search on the platform where your data already lives, with no new system to adopt. Elasticsearch Vector Database supports a range of scenarios, from grounding a large language model (LLM), to giving an AI agent retrieval and memory, to serving hundreds of billions of vectors. <a href="https://cloud.elastic.co/registration?onboarding_token=vector">Spin up a new project</a> and get started in minutes.</p><h2>One engine, every vector use case</h2><p>Elasticsearch Vector Database is built for anyone building applications using vectors:</p><ul><li><p><strong>RAG:</strong> Retrieve the right context for your LLM with dense and sparse vector retrieval, or go with hybrid search combining both vector and lexical retrieval. The quality of your generation improves with the quality of your retrieval.</p></li><li><p><strong>AI agents:</strong> Give agents fast, filtered retrieval over documents and conversation memory, with the low latencies that multistep agent loops demand.</p></li><li><p><strong>Semantic search:</strong> Match on meaning, not keywords, with one field type and zero pipeline code.</p></li><li><p><strong>Recommendations and similarity:</strong> Find nearest neighbors across products, images, or whatever content you have, at scale.</p></li></ul><h2>Everything your vector workload needs, optimized out of the box</h2><p>Building a vector-based application means wiring together several separate pieces: setting up and hosting embedding models, indexing your documents through them, storing the vectors efficiently, applying the embedding model to each query, matching against the vector store, and finally, retrieving the documents behind the matches. Elasticsearch Vector Database handles all of it for you, with no additional configuration or setup.</p><h3>Vector indexing with vectordb_document index mode</h3><p><a href="https://www.elastic.co/docs/reference/elasticsearch/mapping-reference/dense-vector#dense-vector-vectordb-document-mode"><code>vectordb_document</code></a> index mode, a new index configuration purpose-built for vector-first workloads, is on by default, so you get the settings that experts would choose. Here's what it turns on:</p><ul><li><p><strong>bfloat16 by default:</strong> Vectors are stored at half the size of float32 with negligible impact on recall, cutting your disk footprint roughly in half before quantization even enters the picture.</p></li><li><p><strong>Source vectors excluded:</strong> In Elasticsearch, your embeddings already live in the index structures used for search; keeping a second raw copy in <code>_source</code> just inflates storage and slows down fetching results. We exclude the duplicate so responses return faster and you store less.</p></li><li><p><strong>The right files preloaded into cache:</strong> The data structures that vector queries touch first are warmed into memory ahead of time, so your first (and your thousandth) query is lightning fast.</p></li><li><p><strong>Parallel merging:</strong> Merging consolidates segments into better-organized vector structures, which lifts both recall and latency, and running those merges multi-threaded means you get there faster.</p></li></ul><h3>Vector storage, compression, and auto-tuning</h3><ul><li><p>Your vectors are compressed automatically.<a href="https://www.elastic.co/search-labs/blog/better-binary-quantization-lucene-elasticsearch"> Better Binary Quantization (BBQ)</a> shrinks vector memory footprints by up to 32x while preserving recall, and DiskBBQ reduces memory requirements further for large-scale workloads.<a href="https://www.elastic.co/search-labs/blog/vector-quantization-auto-calibration-diskbbq"> </a></p></li><li><p>Opt in to<a href="https://www.elastic.co/search-labs/blog/vector-quantization-auto-calibration-diskbbq"> auto-calibration</a>, which tunes each segment's quantization to your data and retunes on every merge as data drifts. When tested across 18 datasets, queries per second (QPS) improved by an average of 16.7%, with recall gains in most of them.</p></li></ul><h3>Embeddings on managed GPU inference</h3><ul><li><p>Generate embeddings with native <a href="https://www.elastic.co/jina-search-models">Jina AI embedding and reranking models</a>, or bring third-party models, all on managed GPUs via <a href="https://www.elastic.co/docs/explore-analyze/elastic-inference/eis">Elastic Inference Service (EIS)</a> with no model servers to operate. Or self-host, if you prefer your own.</p></li><li><p>The <a href="https://www.elastic.co/docs/reference/elasticsearch/mapping-reference/semantic-text"><strong><code>semantic_text</code></strong></a> field type automatically handles chunking and embedding, along with querying, the simplest path to semantic search in the market. </p></li></ul><h3>Hybrid search and filtered vector search</h3><ul><li><p><a href="https://www.elastic.co/elasticsearch/hybrid-search">Hybrid search</a> is built in, combining full-text and vector retrieval in a single query. Blend the results with reciprocal rank fusion (RRF) or any other blending mechanism you want. Vector search is usually the hardest part of hybrid search to configure well. With Elasticsearch Vector Database, you have it handled, and your whole hybrid stack gets better. </p></li><li><p>With <a href="https://www.elastic.co/search-labs/blog/filtered-hnsw-knn-search">filtered vector search</a>, apply metadata filters as part of vector retrieval itself and not as an afterthought that wrecks recall.</p></li></ul><h3>Enterprise on day one</h3><p>You also get role-based access control (RBAC), audit logging, and the compliance certifications that pure-play vector databases generally lack.</p><h2>Affordable at scale and predictable</h2><p>Elasticsearch Vector Database is built to stay affordable as you grow: BBQ and DiskBBQ compression that keeps storage linear and memory low means scaling to hundreds of billions of vectors doesn't blow up your bill. And <a href="https://cloud.elastic.co/pricing/serverless?s=vectordb">what you do pay</a> is built from numbers you already know: how much data you store and how much you index, along with how much search capacity you need. Estimate your document count and vector dimensions, plus your query load, and you can work out what you'll pay before you create the project. You can also understand your bill line by line at the end of the month. There are no opaque compute units and no surprise charges for background operations.</p><h2>How to get started with Elasticsearch Vector Database</h2><h3>Create a serverless vector database project</h3><p>Create a new <a href="https://cloud.elastic.co/registration?onboarding_token=vector">serverless Vector Database project in Elastic Cloud</a>. Point your data at the endpoint, and you're ready to index.</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt556cbdfba551f248/6aa10b4332b53038406d321a/image1.png" alt="Elastic Cloud Serverless project types: Elasticsearch, Vector Database, Observability and Security" /><h3>Create an index using semantic_text</h3><p>Vector index mode handles the vector configuration. Using <code>semantic_text</code> means that embeddings and chunking setup are managed for you, as is index setup, on managed GPU inference, with no embedding pipeline to build.</p>PUT my-vectors
{
"mappings": {
"properties": {
"description": { "type": "semantic_text" }
    }
  }
}<h3>Ingest documents</h3><p>Index text, and the embeddings are generated for you.</p>POST /my-vectors/_doc
{
  "id": "park_rocky-mountain",
  "title": "Rocky Mountain",
  "description": "Bisected north to south by the Continental Divide, this portion of the Rockies has ecosystems varying from over 150 riparian lakes to montane and subalpine forests to treeless alpine tundra."
}<h3>Run a semantic search query</h3><p>Query the same semantic field you just created:</p>GET /my-vectors/_search
{
  "query": {
    "semantic": {
      "field": "description",
      "query": "a mountain range in the middle of north america"
    }
  }
}<p>And you get results back:</p>{
  "took": 80,
  "hits": {
    "max_score": 0.7792325,
    "hits": [
      {
        "_index": "my-vectors",
        "_score": 0.7792325,
        "_source": {
          "id": "park_rocky-mountain",
          "title": "Rocky Mountain",
          "description": "Bisected north to south by the Continental Divide, ..."
        }
      }
    ]
  }
}<p>Semantic search is just the start. Run fully textual queries or combine both into hybrid queries. You can even craft your own vector queries for full control. Follow the <a href="https://www.elastic.co/docs/solutions/vector-database/vector-full-text-search">semantic search quickstart</a> in the docs for the full instructions.</p><h2>What's next for vector search in Elasticsearch</h2><p>We're already working on the next improvements:</p><ul><li><p><strong>Better multi-tenant handling:</strong> If your data needs to stay separated per tenant, we'll give you a way to do it faster and with less code.</p></li><li><p><strong>Automatic index optimization:</strong> From "brand new index" to "fully optimized," with as little tinkering as possible.</p></li><li><p><strong>Continuous infrastructure improvements:</strong> Ongoing tuning of Vector Database's settings and infrastructure so you're always getting the best throughput and fastest responses.</p></li></ul><h2>Try Elasticsearch Vector Database on Elastic Cloud Serverless</h2><p>Go from an empty project to a hybrid, filtered vector query in minutes, with production-grade defaults doing the tuning for you. Build fast, scalable AI apps, not infrastructure.</p><p>Start on <a href="https://cloud.elastic.co/registration?onboarding_token=vector">Elastic Cloud Serverless</a>, or dive into the <a href="https://www.elastic.co/docs/solutions/vector-database">full documentation </a>and <a href="https://www.elastic.co/docs/api/doc/elastic-cloud-serverless/group/endpoint-vectordb-projects">API reference.</a> You can also access the new offering on<a href="https://aws.amazon.com/marketplace/pp/prodview-voru33wi6xs7k"> AWS Marketplace</a>,<a href="https://console.cloud.google.com/marketplace/product/elastic-prod/elastic-cloud"> Google Cloud Marketplace</a> and<a href="https://portal.azure.com/#view/Microsoft_Azure_Marketplace/GalleryItemDetailsBladeNopdl/id/elastic.ec-azure-vector/"> Microsoft Marketplace</a>.</p><p></p>]]></content:encoded>
    <link>https://www.elastic.co/search-labs/blog/vector-database-rag-serverless</link>
    <guid isPermaLink="true">https://www.elastic.co/search-labs/blog/vector-database-rag-serverless</guid>
    <category><![CDATA[Vector Database]]></category>
    <category><![CDATA[Elastic Cloud Serverless]]></category>
    <category><![CDATA[Hybrid Search]]></category>
    <dc:creator><![CDATA[Dustin Coates]]></dc:creator>
    <enclosure url="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt4def84aae6aff861/6aa10ab1ee57e53d9b05253c/cover.png" length="0" type="image/png"/>
    <pubDate>Wed, 09 Sep 2026 00:00:00 GMT</pubDate>
  </item>
  <item>
    <title><![CDATA[One setting for production vector search: How vectordb_document mode tunes Elasticsearch automatically]]></title>
    <description><![CDATA[Benchmarks across four datasets show how one index setting applies bfloat16 vector quantization, cache preloading and parallel merges to improve vector search throughput and decrease storage.]]></description>
    <content:encoded><![CDATA[<p>We’re introducing one setting for production-ready vector search. The new <code>vectordb_document</code> index mode stores raw vectors as bfloat16 to halve their disk footprint and preloads vector data structures into the filesystem cache. It also lets segment merges run unthrottled and in parallel. In our benchmarks, it delivered up to 2× the queries per second (QPS) at high recall with <code>bbq_hnsw</code> and cut time to a search-ready index by roughly 20% with <code>bbq_disk</code>, with no tuning required. It’s available in Stateful Elasticsearch 9.5 and in Elasticsearch Serverless today.</p><p>Elasticsearch supports a diverse range of use cases, including observability metrics and logs and complex geospatial analysis. However, as vector search becomes a core component of modern architectures, the need for specialized optimization has grown. Achieving peak performance for vector-heavy workloads often requires navigating a complex space of configuration knobs. To reduce this operational toil, we wanted to provide opinionated, high-performance defaults through a single setting that simplifies performance tuning for production environments. The <a href="https://www.elastic.co/docs/reference/elasticsearch/mapping-reference/dense-vector#dense-vector-vectordb-document-mode"><code>vectordb_document</code> index mode</a> is designed specifically for optimal vector search workloads.</p><h2>How to set up vectordb_document mode</h2><p>Setup is a single setting. When you’re creating an index, define the following in the index settings:</p>PUT my-index
{
  "settings" : {
    "index" : {
      "mode" : "vectordb_document"
     }
   }
}<p><code>vectordb_document</code> mode is available on all Elasticsearch subscription tiers, including Basic.</p><p>Most indices used for vector search also support other operations, such as aggregations or geo-search. They also support hybrid search. Because vector search is typically the most computationally demanding part of these mixed workloads, we recommend using the <code>vectordb_document</code> index mode to prioritize performance for your most intensive operations. An index in <code>vectordb_document</code> mode remains highly capable, supporting almost all operations available in the default standard mode, while optimizing specifically for the resource-heavy demands of vector search.</p><p>The <code>“_document”</code> suffix represents our roadmap. We're also developing a <code>“vectordb_columnar”</code> mode as another way to optimize vector search, suited to different data and access patterns.</p><h2>Elasticsearch vector search benchmarks across four datasets</h2><p>To validate these defaults, we performed extensive benchmarking across various datasets and two index types: <code>bbq_hnsw</code> and <code>bbq_disk</code>. We ran all benchmarks on a single-node Elasticsearch instance on AWS using a <code>c8gd.2xlarge</code> instance (Graviton 4, ARM64, local NVMe SSD) with a pod limited to 8GB RAM (2GB heap) and 4 CPUs, using a single shard.</p><p>Datasets:</p><p><strong>Dataset</strong></p><p><strong>Vectors</strong></p><p><strong>Dims</strong></p><p><strong>Use case</strong></p><p><code>laion-img-emb-512-20M-cosine</code></p><p>20 million</p><p>512</p><p>Pure vector search (low dim)</p><p><code>msmarco-v2-10M-jina-v5-1024</code></p><p>10 million</p><p>1024</p><p>Pure vector search (med dim)</p><p><code>dbpedia-openai-1M-3072-angular</code></p><p>1 million</p><p>3072</p><p>Pure vector search (high dim)</p><p><code>arxiv-for-fanns-large</code></p><p>2.7 million</p><p>4096</p><p>Filtered search</p><h3>bbq_hnsw: HNSW index performance with vectordb_document</h3><h4>Query throughput and recall</h4><p>On all four datasets, <code>vectordb_document</code> produced a better QPS–recall curve, and the shape of the advantage is itself informative. On <code>dbpedia-openai-1M</code>, <code>msmarco-v2-10M</code>, and <code>arxiv-for-fanns-large</code>, the curves start close together at low recall (essentially identical on arXiv) and separate as recall rises, reaching roughly 1.4×, 2.2×, and 2× at the high-recall end. On <code>laion-img-emb-512-20M</code>, the curves are apart from the start and settle at about 2× from recall 0.80 upward.</p><p>The gap widens with recall because higher recall is bought with oversampling, and oversampling is exactly where <code>vectordb_document</code> saves. The two effects compound. bfloat16 storage halves the bytes read per rescored candidate. More importantly for hierarchical navigable small world (HNSW), the oversample factor is applied to the graph search itself;  each segment is searched for k × oversample candidates, so cost scales with oversample times segment count. Unthrottled parallel merging leaves fewer, larger graphs (14–21 segments versus 22–29), and the graph and quantized vector files are preloaded into the filesystem cache, so <code>vectordb_document</code> pays far less for each increment of oversample.</p><p>Comparing identical search settings, rather than equal recall, makes the effect explicit. With rescoring switched off, the two modes are within 1%–28% of each other; at oversample 5, <code>vectordb_document</code> is 2–4× faster; at oversample 10, up to 10×. Those high-oversample settings sit off the QPS–recall frontier, which is why the curves above top out nearer 2×, but they isolate where the saving comes from.</p><p><strong>Oversample</strong></p><p><strong>DBpedia</strong></p><p><strong>LAION</strong></p><p><strong>MS MARCO</strong></p><p><strong>arXiv</strong></p><p>off</p><p>1.23×</p><p>1.28×</p><p>1.11×</p><p>1.01×</p><p>5</p><p>4.07×</p><p>2.66×</p><p>2.66×</p><p>2.05×</p><p>10</p><p>10.5×</p><p>2.33×</p><p>2.46×</p><p>4.03×</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt7be4c804a0ed7c22/6a86d603e3ec262b84519f42/1.png" alt="Elasticsearch vector search QPS vs recall for bbq_hnsw on LAION 512-dim dataset showing 2x throughput with vectordb_document" /><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt231178fa23d0726e/6a86d61ceb4ccc9c18f20f42/2.png" alt="Elasticsearch vector search QPS vs recall for bbq_hnsw on MS MARCO 1024-dim dataset with vectordb_document mode enabled" /><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt80dfbad51b8f3bb9/6a86d63feb4ccc69c2f20f48/3.png" alt="Elasticsearch vector search QPS vs recall for bbq_hnsw on DBpedia 3072-dim dataset with vectordb_document mode enabled" /><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt85e29cf189fcc51b/6a86d65173f7435dda4861ed/4.png" alt="Elasticsearch vector search QPS vs recall for bbq_hnsw on arXiv 4096-dim filtered search with vectordb_document mode" /><h4>Indexing speed and merge behavior </h4><p>In our benchmarks, <code>vectordb_document</code> increased upload time by roughly 16%. This is expected: merges now run unthrottled and parallelized across threads, so they compete with indexing for CPU while documents are still being ingested. Measured indexing time rose 9%–26% across the four datasets. HNSW graph construction is CPU-bound, so that contention is felt directly.</p><p>The same changes make the post-upload phase much cheaper. Disabling auto-throttling removes merge rate limiting entirely (in default mode, DBpedia spent 57% of its merge time throttled), and bfloat16 halves the raw vector data, cutting total bytes merged by 42%–49%. Post-ingest merging finished 75%–86% sooner on three of the four datasets, which brings total time to a search-ready index to only about 6% above the baseline. DBpedia was the exception in the other direction: Its merge tail dominated, so total time actually fell 25%.</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/bltaf89df79ce0e2185/6a86d6f4da6aead8bb379480/1.png" alt="Upload time comparison across four datasets for bbq_hnsw HNSW index with and without vectordb_document mode" /><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt9162cec285c4f2a1/6a86d708a8b3231735cbfbbd/2.png" alt="Total ingest time including merges for bbq_hnsw HNSW index comparing default mode to vectordb_document mode" /><h3>bbq_disk: disk-based vector search performance with vectordb_document</h3><h4>bbq_disk query throughput and recall</h4><p>For <code>bbq_disk</code> indices, <code>vectordb_document</code> mode's impact on search throughput varied by dataset. On the lower-dimensional datasets, QPS was essentially unchanged at equal recall: <code>laion-img-emb-512-20M-cosine</code> (512 dims) and <code>msmarco-v2-10M-jina-v5-1024</code> (1024 dims) track each other closely across the recall range, with <code>vectordb_document</code> ahead in the lower recall end and a few percent behind at the high-recall end. On the higher-dimensional datasets, we saw a consistent gain of roughly 20% at equal recall: <code>dbpedia-openai-1M-3072-angular</code> (3072 dims) and <code>arxiv-for-fanns-large</code> (4096 dims).</p><p>Our interpretation is that this is mainly a rescoring effect. <code>vectordb_document</code> stores vectors as bfloat16, so each rescored candidate reads 2× dims bytes instead of 4× dims. The sweep supports this directly: The advantage grows with the query-time oversample factor, which is exactly what sets how many candidates get rescored. On DBpedia, the QPS ratio rises from 1.16× at oversample 3 to 4.3× at oversample 8, and on arXiv from 1.20× to 1.56×, while on laion and MS MARCO, it stays flat or drifts just below 1. Rescoring is simply a much larger share of the query on the smaller, higher-dimensional datasets; on the 10 million and 20 million ones, scanning 1-bit posting lists dominates.</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/bltc3dccb980e1e3a57/6a86d7ebeb4ccc0f68f20f4c/1.png" alt="Elasticsearch vector search QPS vs recall for bbq_disk on LAION 512-dim dataset with vectordb_document mode enabled" /><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/bltffd0c65e300e7985/6a86d80462f1e2732aafe73f/2.png" alt="Elasticsearch vector search QPS vs recall for bbq_disk on MS MARCO 1024-dim dataset with vectordb_document mode" /><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt7574001d33d755e7/6a86d81a386ac33034adef66/3.png" alt="Elasticsearch vector search QPS vs recall for bbq_disk on DBpedia 3072-dim showing 20% gain with vectordb_document" /><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt45ec0bc4e1a47504/6a86d82aa1b20b40a18701ff/4.png" alt="Elasticsearch vector search QPS vs recall for bbq_disk on DBpedia 3072-dim showing 20% gain with vectordb_document" /><h4>Indexing speed and merge behavior </h4><p>Across the four datasets, <code>vectordb_document</code> cut total time to a fully merged, search-ready index by about 20%. Every dataset improved, from 6% on <code>msmarco-v2-10M</code> to 50% on <code>dbpedia-openai-1M</code>, where the post-upload merge phase alone fell from 322 seconds to 54 seconds. Upload time on its own is less clear-cut: On the day charted below, it finished 2%–15% sooner, depending on dataset, but on the earlier run set, it was marginally slower, so we read upload as unchanged to modestly faster and treat time-to-searchable as the real result.</p><p>The gain is almost entirely in merging. In default mode, Elasticsearch rate-limits how fast merges may write, and that limiter was binding hard: 75% of all merge time on DBpedia and 73% on laion was spent paused by it. <code>vectordb_document</code> disables the limiter, so paused time is zero and merge time drops 63% on DBpedia and 42% on laion. bfloat16 helps for the same reason: The limiter meters bytes written, so halving the raw vector data means less to write under the cap. The gradient across datasets follows the throttling rather than the byte count: arXiv, at 21% of merge time throttled, saw 30% less merge time, while MS MARCO, never throttled, got 55% fewer bytes but only 11% less merge time.</p><p>The two index types respond differently on ingest because merging costs something different in each. Merging <code>bbq_hnsw</code> segments means rebuilding HNSW graphs: CPU-bound work that competes directly with the equally CPU-bound graph construction on incoming documents, and on a 4-CPU pod that contention surfaces as a slower upload. <code>bbq_disk</code> merges are dominated by writing bytes rather than by CPU, so lifting the rate limiter spends I/O bandwidth that the local NVMe has to spare, and bfloat16 means that there are fewer bytes to write in the first place. Both index types reach a fully merged index far sooner; the difference is only whether the upload phase pays for it.</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt5792080b8d372fdd/6a86d85a8bbe584d8790e7cf/5.png" alt="Upload time comparison across four datasets for bbq_disk index with and without vectordb_document mode" /><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/bltfc61269e37f0d5f4/6a86d86b762d1efd795cb927/6.png" alt="Total ingest time for bbq_disk index showing 20% faster time to search-ready with vectordb_document mode" /><h2>What vectordb_document sets under the hood</h2><h3><code>element_type</code> (<code>dense_vector</code>)</h3><ul><li><p><strong>Value: </strong>bfloat16.</p></li><li><p><strong>Impact: </strong>Stores each dimension of the raw vector as bfloat16 instead of the default float32, halving the storage of raw vectors with negligible impact on recall.</p></li><li><p><strong>Benefit: </strong>Lower disk footprint (reduced total cost of ownership [TCO]), faster fetching of vectors for rescoring.</p></li></ul><h3>Dynamic float array mapping</h3><ul><li><p><strong>Value: </strong>Float arrays with 32 or more values are dynamically mapped as <code>dense_vector</code>. (In the default mode, this threshold is 128.)</p></li><li><p><strong>Impact:</strong> No need to explicitly declare the field as a dense vector field; the system identifies it automatically.</p></li><li><p><strong>Benefit: </strong>Configuration simplicity.</p></li></ul><h3><code>exclude_source_vectors</code></h3><ul><li><p><strong>Value: </strong>true.</p></li><li><p><strong>Impact: </strong>Vectors are stored once in the vector index and not duplicated in <code>_source</code>. They’re omitted from <code>_source</code> in responses, but they can still be retrieved on request.</p></li></ul><p><strong>Benefit: </strong>Lower disk footprint; faster queries, since large vectors are no longer shipped with every <code>_source</code> fetch.</p><h3><code>index.store.preload</code></h3><ul><li><p><strong>Value: </strong>[<code>"vex"</code>, <code>"veq"</code>, <code>"veb"</code>, <code>"cenivf"</code>].</p></li><li><p><strong>Impact: </strong>Preloads search-time vector data structures into the filesystem cache whenever new segments are opened.</p></li><li><p><strong>Benefit: </strong>Reduced query latency.</p></li></ul><h3><code>index.merge.intra_merge_parallelism_enabled</code></h3><ul><li><p><strong>Value: </strong>true.</p></li><li><p><strong>Impact: </strong>Use parallel threads for segment merging to achieve optimal segment sizes.</p></li><li><p><strong>Benefit: </strong>Faster convergence to fewer, larger segments leads to better recall, lower query latency.</p></li></ul><h3><code>index.merge.scheduler.auto_throttle</code></h3><ul><li><p><strong>Value: </strong>false.</p></li><li><p><strong>Impact: </strong>Allows merges to proceed at full speed.</p></li><li><p><strong>Benefit: </strong>Merges reach optimal segment sizes sooner, resulting in better recall, lower query latency.</p></li></ul><p>While these defaults are optimized for the majority of use cases, most can be overridden individually to suit unique hardware constraints or extreme performance requirements, with one exception: <code>exclude_source_vectors: true</code> is locked in and cannot be changed on a <code>vectordb_document</code> index.</p><h2>Summary</h2><p>The <code>vectordb_document</code> index mode is a production-ready foundation for vector search: By adopting high-performance defaults, teams can focus on building features rather than on hand-tuning merge or storage settings or on adjusting preload settings. </p><p>Across four datasets, the picture is consistently favorable, though the balance differs by index type. With <code>bbq_hnsw</code>, QPS recall improved on every dataset, from roughly on par at low recall to as much as double at the high-recall end, in exchange for a modest ingest cost of about 16% longer upload and 6% longer time to a search-ready index. With <code>bbq_disk</code>, the trade runs the other way: Total time to a search-ready index dropped by about 20%, while search improved by about 20% at equal recall on the higher-dimensional datasets (3072 and 4096 dims) and was essentially unchanged on the lower-dimensional ones. The split comes down to what merging costs in each: Rebuilding HNSW graphs is CPU work that competes with indexing; whereas <code>bbq_disk</code> merges are write-bound and simply run faster once they’re unthrottled.</p><p>In both cases, the new defaults move the system in the direction that most users want, with no per-index tuning required. And, for the settings that genuinely depend on the data itself, such as the degree of quantization, auto-calibration now derives them for you. (See <a href="https://www.elastic.co/search-labs/blog/vector-quantization-auto-calibration-elasticsearch">How Elasticsearch auto-tunes vector quantization to hit your recall target</a>.)</p><p>Try it out by creating an index with the <code>vectordb_document</code> index mode in Stateful Elasticsearch 9.5 or in Serverless.</p>]]></content:encoded>
    <link>https://www.elastic.co/search-labs/blog/elasticsearch-vector-search-vectordb-document-mode</link>
    <guid isPermaLink="true">https://www.elastic.co/search-labs/blog/elasticsearch-vector-search-vectordb-document-mode</guid>
    <category><![CDATA[Vector Database]]></category>
    <dc:creator><![CDATA[Mayya Sharipova,Gilad Gal,Quinn Harper]]></dc:creator>
    <enclosure url="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt24f9f391ea8b5d02/6a86ce55bed19d4545af9b54/unnamed.jpg" length="0" type="image/jpeg"/>
    <pubDate>Thu, 20 Aug 2026 00:00:00 GMT</pubDate>
  </item>
  <item>
    <title><![CDATA[How Elasticsearch's batched query phase improves search performance at scale]]></title>
    <description><![CDATA[The batched query phase can cut search execution time in half by reducing transport overhead and better distributing reduction work across the cluster.]]></description>
    <content:encoded><![CDATA[<p>The batched query phase, which is live in Elasticsearch Serverless and released in Elasticsearch 9.5.0, changes how search is coordinated across the cluster. The coordinating node now batches all queries for each data node into one, instead of a separate transport request per shard. Data nodes can then do partial result reduction themselves, rather than shipping everything back to the coordinating node. In coordination-bound workloads, this can cut search execution time in half.</p><h2>How a search is executed in Elasticsearch</h2><p>Let’s start with some Elasticsearch basics. When a search request lands on an Elasticsearch node, that node is called the <em>coordinating node</em>for the search. By default, any Elasticsearch node can act as a coordinating node. The coordinating node determines which shards need to be searched based on the index or indices specified by the search. The nodes that those shards live on are called <em>data nodes</em>. A coordinating node can also be a data node, as it may host shards that are relevant to the search.</p><p>Elasticsearch performs the search in two primary phases: the <em>query phase</em> (also known as the <em>scatter phase</em>) and the <em>fetch phase</em>(also known as the <em>gather phase</em>). The query phase is responsible for going to the data nodes and executing the query on each shard. Each shard responds with a set of document IDs (just the IDs, no data) and an associated score for each. These results are reduced. Next, the fetch phase goes back out to the data nodes to fetch the document <code>_source</code> (the data). </p><p>What exactly is a <em>reduction</em>in Elasticsearch? A reduction turns per-shard results from the query phase into a single merged result for the client. Suppose a search asks for the top five hits in a three-shard index, according to some relevance score. Elasticsearch must then get the top five hits from each targeted shard. Why? Because it’s possible that one shard contains the global top five, or, more likely, that the top five docs are spread across shards. </p><p>If three shards are being searched, the coordinating node will have 15 <code>(docID, score)</code> pairs after the query phase. These results are reduced: The documents with the top five scores are kept and the rest thrown away. Then the fetch phase reaches back out to the data nodes to get the documents’ <code>_source</code> data, which Elasticsearch then responds with.</p><p>shard1_results = [(id: 231, score: 0.871), (id: 445, score: 0.812), (id: 88, score: 0.754), (id: 312, score: 0.701), (id: 567, score: 0.643)]</p><p>shard2_results = [(id: 847, score: 0.921), (id: 76,  score: 0.843), (id: 125, score: 0.783), (id: 438, score: 0.729), (id: 590, score: 0.668)]</p><p>shard3_results = [(id: 512, score: 0.887), (id: 289, score: 0.798), (id: 74,  score: 0.741), (id: 631, score: 0.682), (id: 405, score: 0.619)]</p><p>// Take the top five scores from above (that’s the “reduction”)
reduced_result = [(id: 847, score: 0.921), (id: 512, score: 0.887), (id: 231, score: 0.871), (id: 76, score: 0.843), (id: 445, score: 0.812)]</p><h2>What is the batched query phase?</h2><h3>Without batching (shard fan-out)</h3><p>To understand the batched query phase, we must first understand how the query phase worked without batching. The following diagram represents a three-node Elasticsearch cluster with 12 index shards. Suppose a client search request lands on Node 1. That makes Node 1 the coordinating node for the search.</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/bltd8b116819f43dc50/6a79aa014423e189ba9fa08f/image3.png" alt="" /><p>The coordinating node first <em>fans out</em> to all shards, performing the client’s query on each shard. Each shard query is facilitated by a <em>transport request</em>, which is a protocol used for communication between Elasticsearch nodes. Notice that in this diagram, each shard gets its own transport request. Node 1 holds shards itself, but no network request is needed to query those shards.</p><p>After querying the shards, the coordinating node must reduce them, as described in the previous section. At this point, the coordinating node is holding 12 shard results. Once it reduces them all, it can proceed with the fetch phase and then respond back to the client.</p><h3>With batching</h3><p>So what does the batched query phase change? The batched query phase first takes effect before dispatching the shard query transport requests. Now the coordinating node batches the shards it needs to query for each data node and requests them all at once.</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt44a9f6586a927f2b/6a79aa2b2888390ea907d7c2/image2.png" alt="" /><p>Notice that only one transport request was made to Node 2 and one to Node 3. This is the first change in the batched query phase. Each transport request induces some overhead, so this change alone buys us our first performance improvement, reducing latency and CPU cycles spent on overhead.</p><p>The second change in the batched query phase has to do with reducing shard results. Now partial reductions occur on the data nodes themselves. For instance, after querying shards 2, 5, 8, and 11, Node 2 then reduces those results. The coordinating node, upon receiving partially reduced results from Node 2 and Node 3, must then perform afinal reduction. This is the second major enhancement we get from the batched query phase: the spreading out of reduction work across the data nodes. This reduces memory pressure on the coordinating node, which we’ll see measured later in the benchmarking section.</p><h2>The benefits of batching</h2><p><em>Fan-out</em> (no batching) is how search worked for a very long time. It has advantages: each shard is a separate request that returns and can be retried independently from all the others. With many shards involved, though, each shard request causes overhead due to many round trips going between the coordinating node and the data nodes.</p><p>Also, the coordinating node doesn’t have enough information to be able to determine the pace at which to send requests to each data node. It sends a maximum of five concurrent requests per data node by default, where five is a bit of a magic number which allows for some parallelism, while at the same time preventing a single query from taking over an entire data node. At the same time, data nodes also don't make distinctions between the different shard requests they receive, for instance based on what parent search request they belong to. In reality, if all shard requests involving a search request are presented in one batch to each data node, the data node can then look at its internal state and adapt its pace dynamically, removing the five concurrent shard requests artifical limit mentioned above.</p><p>The batched query phase results in several benefits, including:</p><ul><li><p>Increased efficiency, thanks to fewer round trips: less CPU spent on transport overhead and fewer bytes going through the transport layer.</p></li><li><p>Spreading out the load of reductions: the coordinating node was previously the bottleneck for reductions, and now data nodes share the work.</p></li><li><p>Better resource usage: we may be able to better max out the data node’s CPUs.</p></li></ul><p>The average search against many shards can now be served much quicker, with lower latency and higher throughput.</p><h2>Batched query phase benchmarks: Latency and memory usage</h2><p>To measure the benefits of the batched query phase, we ran a couple of benchmarks. The first displays the benefit of reduced transport overhead. This benchmark was built off the “many-shards-quantitative” <a href="https://elasticsearch-benchmarks.elastic.co/">nightly benchmark</a>. It runs on a three-node Elasticsearch cluster, running batches of searches targeting 1,000, 5,000, and 20,000 shards. The queries are <code>match_all</code> queries with <code>size: 0</code>. That means querying the shards themselves is effectively a no-op. This benchmark is meant to isolate the work of coordinating the search across the cluster.</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/bltd4a4a7aadc1632a8/6a79aa47dcb437094d2cf8da/image1.png" alt="" /><p>We ran the same benchmark with and without the batched query phase (by toggling the cluster setting <code>search.batched_query_phase</code>). At 5,000 shards, the batched query phase makes searches twice as fast. By 20,000 shards, it’s 2.2x faster. These results are a ceiling for what a realistic workload can expect to benefit; any real query work at the shard level will dilute the overall result. However, the gains are real, and the <em>coordination work</em> of your queries will benefit as shown. Note that this first benchmark did not attempt to measure any gain brought by spreading reductions across data nodes, as opposed to performing them only on the coordinating node.</p><p>Next, we benchmarked the benefits of the batched query phase on large reductions. In our benchmark, we ran a large terms aggregation over a data set called <code>http_logs</code> (which can be found in our <a href="https://github.com/elastic/rally-tracks">rally-tracks</a> repo). This data set has 247 million documents, which we indexed in seven indices each with 100 shards, again in a three-node Elasticsearch cluster. We ran a single <a href="https://www.elastic.co/docs/reference/aggregations/search-aggregations-bucket-terms-aggregation">terms aggregation</a> query for the <code>clientip</code> field with <code>size: 1000</code> and <code>shard_size: 50000</code>. That means we’re asking for the top 1,000 terms, although each shard will return 50,000 buckets to be reduced to that 1,000. Requesting so many terms from each shard increases precision, but has a cost in terms of memory usage and reduction overhead, which helps highlight the gain provided by spreading incremental reductions across data nodes.</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt978ed04cf2406b0e/6a79aa5a8bcd807043261d4b/image4.png" alt="" /><p>The <em>memory used</em> is measured by something called a <em>circuit breaker</em>, which accounts for memory usage in Elasticsearch. This field can be found in the <code>/_nodes/stats</code> response under <code>nodes.&lt;node_id&gt;.breakers.request.estimated_size_in_bytes</code>. It estimates the memory in use for processing in-flight search requests.</p><p>The memory benchmark shows how the work of reductions is now spread across the cluster. In blue is the memory used with <code>search.batched_query_phase: false</code>. We can see that the node elasticsearch-0 is the coordinating node, as it bears the entire reduction load itself. This effect is no longer the case with <code>search.batched_query_phase: true</code> in red. The node elasticsearch-1 is the coordinating node, but it uses far less memory. That’s because the data nodes elasticsearch-0 and elasticsearch-2 do reductions themselves, reducing the results before they’re returned to the coordinating node.</p><h2>Summary</h2><p>Batching shard queries per data node reduces transport overhead and allows each data node to perform partial results reduction locally. The result is lower latency for searches targeting many shards and better distribution of work across the cluster for reduction-heavy workloads. At the same time, the new model works well for all scenarios and opens the door for potential future enhancements which we’re excited to pursue, such as:</p><ul><li><p>Optimize out-of-the-box resource usage by introspecting data nodes' activity and adjusting the pace accordingly, which would replace the five concurrent shard requests per data node artifical limit.</p></li><li><p>Assign priorities to search requests and let each data node process shard requests accordingly.</p></li><li><p>Reduce the number of roundtrips further by folding the `can_match` phase into the query phase. Can match is a separate "batched" roundtrip used to shortcut the query against shards that can't possibly match based on index statistics. Now that the query phase is batched, both rounds can be executed in one go.</p></li></ul><ul><li><p>Discontinue support for minimize roundtrips in cross-cluster searches in favour of batched query execution; in hindsight, minimize roundtrips achieves a similar goal but is specific to cross-cluster execution, while batched execution is applicable to every search. </p></li></ul>]]></content:encoded>
    <link>https://www.elastic.co/search-labs/blog/elasticsearch-batched-query-phase</link>
    <guid isPermaLink="true">https://www.elastic.co/search-labs/blog/elasticsearch-batched-query-phase</guid>
    <category><![CDATA[Inside Elastic]]></category>
    <category><![CDATA[Vector Database]]></category>
    <dc:creator><![CDATA[Ben Chaplin,Luca Cavanna]]></dc:creator>
    <enclosure url="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/bltec0235a331fb0c48/6a79a9e30da673867657b299/image5.png" length="0" type="image/png"/>
    <pubDate>Mon, 10 Aug 2026 00:00:00 GMT</pubDate>
  </item>
  <item>
    <title><![CDATA[The mystery stress your heap chart can't see: AutoOps now watches vector off-heap memory]]></title>
    <description><![CDATA[Dense vectors use off-heap memory your heap chart never shows. AutoOps detects memory pressure before vector RAM stress causes OOM.]]></description>
    <content:encoded><![CDATA[<p>AutoOps now raises a <strong>Vector memory pressure</strong> insight when dense vector off-heap footprint, heap heat, and operational stress converge on the same Elasticsearch node. We validated on a 4 GiB node under sustained k-nearest neighbor (kNN) ingest: The insight fired at ~75% heap with thread-pool stress, roughly an hour before saturation. Heap charts alone still looked moderate at that point. Dense vectors for kNN live outside the Java heap, so heap monitoring and circuit breakers never show the full vector RAM picture. Below, we walk through what the insight measures and why heap on its own misses this. We also discuss what to do when it fires.</p><h2>Why dense vectors create off-heap memory pressure that heap charts miss</h2><p>Semantic search and kNN rely on <a href="https://www.elastic.co/docs/reference/elasticsearch/mapping-reference/dense-vector"><code>dense_vector</code> fields</a>. Elasticsearch stores much of that data in off-heap memory. It’s related to how the Java Virtual Machine (JVM) operates, but it isn’t the same thing as heap usage.</p><p>In production, the heap versus off-heap split shows up in a familiar pattern:</p><ul><li><p>Heap looks fine for weeks, while the dense vector off-heap footprint quietly grows.</p></li><li><p>Heap circuit breakers stay quiet or only spike late because the pressure sits outside the JVM.</p></li><li><p>kNN search and bulk ingest slow down, queues build, and nothing on the dashboard points at vector RAM as the cause.</p></li></ul><p>Heap limits protect Java allocations. They don’t tell you whether vector off-heap footprint still fits the RAM envelope that your deployment actually runs in. AutoOps already watches cluster health broadly; Vector memory pressure adds a focused read for vector-heavy nodes when memory and load signals line up.</p><h2>How AutoOps measures vector RAM, heap, and headroom</h2><p>AutoOps works from the same node stats metrics you already use for Stack Monitoring. For each node, it tracks three derived numbers:</p><p><strong>Symbol</strong></p><p><strong>Meaning</strong></p><p><strong>Source (typical)</strong></p><p><strong>Chart (see below)</strong></p><p><strong>V</strong></p><p>Vector off-heap footprint</p><p><code>indices.dense_vector.off_heap.total_size_bytes</code></p><p>First, green line</p><p><strong>A</strong></p><p>Available RAM in the product view</p><p>Delta between <code>os.mem.total_in_bytes</code> and <code>os.mem.used_in_bytes</code></p><p>Second, green line</p><p><strong>H</strong></p><p>Headroom</p><p><strong>A − V</strong> (headroom_bytes)</p><p>First, blue line</p><p>H &gt; 0 means there’s a modeled runway: Vector use still fits comfortably in that accounting. H ≤ 0 means that you’re in a <em>compression</em> regime: Vector footprint (V) meets or exceeds the free RAM (A) picture that AutoOps can align in telemetry. On small tiers, that can be common under load. The insight emphasizes trends, growth in vector off-heap footprint, and corroborating stress, not a single negative snapshot.</p><p>AutoOps also tracks a compression regime flag (fraction of recent samples where H ≤ 0), so brief flickers don’t dominate the story (see third chart below):</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt437840e9160da903/6a73173af9a79dca2a61f13e/image4.png" alt="AutoOps charts showing vector off-heap footprint growth, shrinking RAM headroom, and compression regime over 48 hours" /><h2>How vector memory pressure detection works: Expansion and compression</h2><p>Vector memory pressure is a single HIGH severity AutoOps event, which adapts to the compression regime:</p><ul><li><p><strong>Expansion (</strong><strong>H &gt; 0</strong><strong>):</strong> Emphasize shrinking headroom, hence a growing vector off-heap footprint.</p></li><li><p><strong>Compression (</strong><strong>H ≤ 0</strong><strong>):</strong> Emphasize ΔV, heap context, I/O, and latency. "Hours until H hits zero" isn’t the main narrative when headroom is already gone.</p></li></ul><p>The detector requires three layers before it fires:</p><ol><li><p><strong>Memory carriers:</strong> Compression regime, shrinking headroom, or sustained growth in vector off-heap footprint.</p></li><li><p><strong>Operational latch:</strong> Search or indexing latency versus rolling baselines, filesystem read stress (paired with latency or heap), indexing throttle, thread-pool queues or rejections, segment creep, or heap circuit breakers when paired with other stress, as circuit breakers alone don’t provide enough evidence to be escalated without corroborating stress.</p></li><li><p><strong>Heap hot:</strong> Heap usage elevated versus a 24-hour rolling median on that node, so compression alone on a calm heap doesn’t fire the insight.</p></li></ol><p>That pairing is intentional. Vector pressure without load might be capacity planning, and load without vector pressure might be a different root cause. Together, vector memory, operational stress, and heap heat surface the vector RAM story when the node is actually in trouble, not on every compressed mapping while the heap stays normal.</p><h2>Validation: Memory pressure detection on a 4 GiB node under kNN load</h2><p>We stress-tested vector memory pressure detection on 4 GiB Elastic Cloud Hosted deployments with throttled dense-vector ingest (~2,000 docs per minute) and steady kNN search (~8 queries per second). Across <a href="https://www.elastic.co/docs/reference/elasticsearch/mapping-reference/dense-vector">Hierarchical Navigable Small World (HNSW)</a>, <a href="https://www.elastic.co/docs/reference/elasticsearch/mapping-reference/bbq#bbq-hnsw">Better Binary Quantization (BBQ) HNSW</a>, and <a href="https://www.elastic.co/docs/reference/elasticsearch/mapping-reference/bbq#bbq-disk">DiskBBQ</a> mapping profiles over 24–48 hours:</p><ul><li><p>Vector off-heap footprint grew from near zero to about 6 GiB on the tightest runs (more than 4.7 million vectors indexed) in both HNSW test runs.</p></li><li><p>Nodes spent most of each run in compression (H ≤ 0), which is expected when vector footprint exceeds total RAM in this model.</p></li><li><p>Vector memory pressure stayed off while heap held near 50%, even with compression and pool stress building.</p></li><li><p>On both HNSW and BBQ HNSW, the insight fired once heap climbed past ~75% with memory compression and thread-pool queue stress, roughly an hour before heap neared saturation. Node out of memory (OOM) and circuit breakers followed in the same window, as did slow search/indexing. As we can see on the dashboard below, performance drops drastically due to corroborating stress toward the end of the test run:</p></li></ul><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt8a99d9296a444dbb/6a73176d0da67336ea57999d/image2.png" alt="HNSW validation dashboard showing heap climbing past 75% with search and indexing performance degradation" /><ul><li>On DiskBBQ, where compression was chronic but heap stayed normal, the insight didn’t fire,since storage rather than memory was the limiting factor. Disk and watermark signals are the right path to follow for that profile. As we can see on the screenshot below, all indicators stayed steady with constant performance throughout the test, even though we filled up the disk with more than 70 million vectors on the same instance type:</li></ul><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt3f30c606a7005a82/6a731789ef5befd2394f7896/image5.png" alt=" DiskBBQ validation dashboard showing stable heap, steady search and indexing performance over 10 days" /><p>That timing is the point: Operators get a vector-first read tied to real RAM stress, with subsystem context, rather than an alert on every compressed index or only a red heap chart after the node is fighting on every front.</p><h2>What to do when AutoOps raises vector memory pressure</h2><p>Here’s the insight that AutoOps now raises when it detects vector memory pressure:</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt6a5bcf4dd55dc5fb/6a7317abe35d0255150319be/image3.png" alt="AutoOps Vector memory pressure insight with detection summary and recommendations for an Elasticsearch node" /><p>Recommendations in the product map to concrete actions:</p><ol><li><p><strong>Reduce vector footprint</strong> where quality allows: Fewer dimensions, quantized mappings, archive or split indices, reindex with a leaner mapping.</p></li><li><p><strong>Tune kNN load:</strong> Lower <code>num_candidates</code>, reduce concurrent query rate, narrow filtered kNN where possible.</p></li><li><p><strong>Consider DiskBBQ</strong> when HNSW in RAM is the bottleneck (evaluate recall/latency trade-offs for your use case). If you’re already on DiskBBQ and the heap memory is calm, treat disk and watermark insights as the primary signals. Note that DiskBBQ requires an Enterprise license.</p></li><li><p><strong>Right-size RAM</strong> when vector off-heap footprint (V) trends up and headroom stays tight.</p></li></ol><p>AutoOps links affected nodes and summarizes regime and stress in plain language. Treat it as “act now, rather than waiting for red on every chart.”</p><h2>Where AutoOps vector memory pressure monitoring is available</h2><p>Vector memory pressure is available wherever AutoOps runs against Elasticsearch 9.2+, including:</p><ul><li><p>Elastic Cloud Hosted (ECH).</p></li><li><p>Elastic Cloud Serverless (coming soon).</p></li><li><p>Self-managed via <a href="https://www.elastic.co/docs/deploy-manage/monitor/autoops/cc-autoops-as-cloud-connected">Cloud Connect</a>.</p></li></ul><p>AutoOps is included at all subscription levels for supported deployment types and doesn’t consume ECUs on ECH.</p>]]></content:encoded>
    <link>https://www.elastic.co/search-labs/blog/vector-memory-pressure-autoops</link>
    <guid isPermaLink="true">https://www.elastic.co/search-labs/blog/vector-memory-pressure-autoops</guid>
    <category><![CDATA[AutoOps]]></category>
    <category><![CDATA[Vector Database]]></category>
    <category><![CDATA[Operations]]></category>
    <dc:creator><![CDATA[Valentin Crettaz]]></dc:creator>
    <enclosure url="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt1ec5b14f053bdc56/6a73170a89eb5c6c9bab24c6/image1.png" length="0" type="image/png"/>
    <pubDate>Thu, 06 Aug 2026 00:00:00 GMT</pubDate>
  </item>
  <item>
    <title><![CDATA[One field, every modality: how Elasticsearch's semantic field indexes and searches images, audio, video and PDFs automatically]]></title>
    <description><![CDATA[The semantic field turns images, audio, video, PDFs and text into multimodal embeddings at ingest time. Describe a scene and find the matching image or use a video frame to surface related clips, all from one Elasticsearch field.]]></description>
    <content:encoded><![CDATA[<p>Multimodal search in Elasticsearch now works the same way text search does: define a field, index your content, and query. The <code>semantic</code> field generates embeddings automatically at ingest time for images, audio, video, and PDFs. Every modality lands in one shared vector space, so you can retrieve an image with a text description, match audio to a phrase, or find a video with a still frame, all from a single field. Available in Elasticsearch 9.5 and serverless as a tech preview.</p><h2>The palette takes shape: how multimodal search in Elasticsearch evolved from semantic_text</h2><p>The <code>semantic</code> field is a convergence of several complementary features we've introduced over the past couple of years, bringing them together to create a cohesive multimodal search experience. Each solved an important piece of the semantic search puzzle on its own; together they enable native multimodal search.</p><p>The first brushstroke was <code>semantic_text</code>. Before it, running semantic search meant manually configuring mappings, wiring up ingest pipelines with an ML model, manually chunking content, and generating query-time embeddings yourself. The <code>semantic_text</code> field folds all of that away: it performs inference automatically at ingest time, chunks long documents for you, and simplifies the queries you write against it. <a href="https://www.elastic.co/search-labs/blog/semantic-search-simplified-semantic-text">Introduced in Elasticsearch 8.15</a> and <a href="https://www.elastic.co/search-labs/blog/elasticsearch-semantic-text-ga">released as GA in Elasticsearch 8.18</a>, it has become the foundation for semantic search on the platform.</p><p>Next came <a href="https://www.elastic.co/search-labs/blog/jina-embeddings-v5-omni-all-media-one-index">the model to power multimodal search</a>. <code>jina-embeddings-v5-omni</code> is our family of multimodal embedding models, capable of embedding text, images, video, audio, and PDFs into a shared vector space. Because those embeddings are semantically compatible across modalities, you can store diverse media in a single index and query across all of it at once, such as retrieving an image via a text description or matching audio against a written phrase, all without maintaining a separate pipeline for each content type. For more detailed information about how these embeddings are generated, see the <a href="https://jina.ai/models/jina-embeddings-v5-omni-small/">model documentation</a>.</p><p>We added the <a href="https://www.elastic.co/docs/reference/query-languages/query-dsl/query-dsl-knn-query#query-vector-builders-parameters">embedding query vector builder</a> in Elasticsearch 9.4 to handle multimodal inputs at query time. Query vector builders are general-purpose tools you can use to convert input to a vector at query time as part of your request. For example, we have the <code>text_embedding</code> query vector builder for text-only models and input, and the <code>lookup</code> query vector builder for getting a vector from an existing document. The <code>embedding</code> query vector builder is a new type that works with multimodal models and accepts multimodal input, including text or base64-encoded binaries. This allows you to pose a query in whatever modality fits, and Elasticsearch generates the matching vector on the fly.</p><p>The final piece was multimodal ingest. The <code>semantic_text</code> field brought automatic embedding to text; the <code>semantic</code> field extends that same automatic experience to images, audio, video, and PDFs from ingest through query.</p><h2>Painting the picture: creating an index with the semantic field</h2><p>Let’s create an index with a <code>semantic</code> field. This is as simple as setting the field type to semantic and defining the inference endpoint you want to use:</p>PUT example-index
{
  "mappings": {
    "properties": {
      "my_semantic_field": {
        "type": "semantic",
        "inference_id": ".jina-embeddings-v5-omni-small"
      }
    }
  }
}<p>In this example, we use the .<code>jina-embeddings-v5-omni-small</code> inference endpoint. This is our built-in <code>jina-embeddings-v5-omni</code> inference service, and it is available in all environments with access to the <a href="https://www.elastic.co/docs/explore-analyze/elastic-inference/eis">Elastic Inference Service</a> (EIS). This includes:</p><ul><li><p>Serverless.</p></li><li><p>Elastic Cloud Hosted (ECH).</p></li><li><p>Self-managed with <a href="https://www.elastic.co/docs/deploy-manage/cloud-connect">Cloud Connected Mode</a> (CCM).</p></li></ul><h3>Indexing images, audio, video and PDFs</h3><p>To index an image, provide an object with a <code>type</code> of <code>image</code> and a <code>value</code> containing the image as a base64-encoded <a href="https://developer.mozilla.org/en-US/docs/Web/URI/Reference/Schemes/data">data URL</a>:</p>PUT example-index/_doc/example_doc_1
{
  "my_semantic_field": {
    "type": "image",
    "value": "data:image/jpeg;base64,&lt;base64-encoded-image-bytes&gt;"
  }
}<p>Arrays of objects are also accepted, allowing you to index multiple images in a single field value:</p>PUT example-index/_doc/example_doc_2
{
  "my_semantic_field": [
    {
      "type": "image",
      "value": "data:image/jpeg;base64,&lt;base64-encoded-image-bytes&gt;"
    },
    {
      "type": "image",
      "value": "data:image/jpeg;base64,&lt;base64-encoded-image-bytes&gt;"
    }
  ]
}<p>The <code>semantic</code> field also supports text values, just like <code>semantic_text</code>. You can provide such values standalone or intermix them with image values:</p>PUT example-index/_doc/example_doc_3
{
  "my_semantic_field": "a cat on a windowsill"                                                                                                                                                                                                                }

PUT example-index/_doc/example_doc_4
{
  "my_semantic_field": [
    "a cat on a windowsill",
    {
      "type": "image",
      "value": "data:image/jpeg;base64,&lt;base64-encoded-image-bytes&gt;"
    },
    "a dog running in a park"
  ]
}<p>Text values are handled just like they are with <code>semantic_text</code>: long passages are chunked according to the <a href="https://www.elastic.co/docs/reference/elasticsearch/mapping-reference/semantic-field-reference#semantic-params">chunking settings configured on either the inference service or field mapping</a>. Multimodal values, such as images, are not chunked. Each multimodal value is represented as one chunk.</p><p>Other modalities are supported as well. Change the type value to match your content’s modality. Currently we support:</p><ul><li><p><code>image</code></p></li><li><p><code>audio</code></p></li><li><p><code>video</code></p></li><li><p><code>pdf</code></p></li></ul><p>For example, to index a video, the request would look like:</p>PUT example-index/_doc/example_doc_5
{
  "my_semantic_field": {
    "type": "video",
    "value": "data:video/mp4;base64,&lt;base64-encoded-video-bytes&gt;"
  }
}<p></p><h3>Image search and cross-modal retrieval with a text query</h3><p>To find multimodal content using a text description, run a <code>match</code> query on the <code>semantic</code> field:</p>GET example-index/_search
{
  "query": {
    "match": {
      "my_semantic_field": "a cat on a windowsill"
    }
  }
}<p>Just like with <code>semantic_text</code>, Elasticsearch automatically generates an embedding for the query text using the inference endpoint associated with the field. That query embedding is used to return semantically similar matches.</p><p>This query pattern enables easy text-to-image search. Just index an image and use a <code>match</code> query to retrieve it via text description! It also works for any other modality: index the multimodal input and search by description to retrieve it.</p><h3>Querying with images, video, and other multimodal inputs</h3><p>We can also search using a multimodal input by using the <code>knn</code> query with an <code>embedding</code> query vector builder. For example, we can search using an image:</p>GET example-index/_search
{
  "query": {
    "knn": {
      "field": "my_semantic_field",
      "query_vector_builder": {
        "embedding": {
          "input": {
            "type": "image",
            "value": "data:image/jpeg;base64,&lt;base64-encoded-image-bytes&gt;"
          }
        }
      }
    }
  }
}<p>The <code>input</code> object format is the same as when providing an image to index: set the <code>type</code> to <code>image</code> and <code>value</code> to a base64-encoded data URL.</p><p>Similar to when querying by text description, Elasticsearch automatically generates an embedding for the query image using the inference endpoint associated with the field. That query embedding is used to return semantically similar matches.</p><p>Just like with indexing, other modalities are supported, but are limited to those supported by your inference endpoint. For example, a search using a video clip would look like:</p>GET example-index/_search
{
  "query": {
    "knn": {
      "field": "my_semantic_field",
      "query_vector_builder": {
        "embedding": {
          "input": {
            "type": "video",
            "value": "data:video/mp4;base64,&lt;base64-encoded-video-bytes&gt;"
          }
        }
      }
    }
  }
}<h2>Extending the composition: highlighting, retrievers, and other semantic field features</h2><p>The <code>semantic</code> field didn't start from a blank canvas. It's built on the same foundation as <code>semantic_text</code>, inheriting its behavior and its ergonomics, and extending them to multimodal content. In practice, that means nearly everything you already know about working with <code>semantic_text</code> carries over unchanged. If you've built with <code>semantic_text</code> before, the <code>semantic</code> field will feel immediately familiar.</p><p>Here’s a selection of the features that come along for the ride. See <a href="https://www.elastic.co/docs/reference/elasticsearch/mapping-reference/semantic-field">the documentation</a> for a complete list.</p><h3>Highlighting the best-matching chunks</h3><p>If you index multiple values in a <code>semantic</code> field, you may want to know <em>which</em> value best matches the query. The <code>semantic</code> highlighter can be used to return the most relevant chunks as highlight fragments:</p>GET example-index/_search
{
  "query": {
    "match": {
      "my_semantic_field": "a cat on a windowsill"
    }
  },
  "highlight": {
    "fields": {
      "my_semantic_field": {
        "number_of_fragments": 2,
        "order": "score"
      }
    }
  }
}<p>Setting <code>order</code> to <code>score</code> returns the fragments ranked by relevance, while <code>number_of_fragments</code> caps how many chunks come back. The response looks like:</p>{
  "hits": {
    "hits": [
      {
        "_index": "example-index",
        "_id": "example_doc_4",
        "_source": {...},
        "highlight": {
          "my_semantic_field": [
            "a cat on a windowsill",
            "data:image/jpeg;base64,&lt;base64-encoded-image-bytes&gt;"
          ]
        }
      }
    ]
  }
}<p>Note how highlighted multimodal values are represented using their data URLs.</p><h3>Controlling vector quantisation with index options</h3><p>The <code>semantic</code> field stores its embeddings in an underlying vector field, and <code>index_options</code> lets you control how that vector field is indexed. For example, choosing a non-default quantization strategy:</p>PUT example-index
{
  "mappings": {
    "properties": {
      "my_semantic_field": {
        "type": "semantic",
        "inference_id": ".jina-embeddings-v5-omni-small",
        "index_options": {
          "dense_vector": {
            "type": "int8_hnsw"
          }
        }
      }
    }
  }
}<h3>Multi-field retrievers</h3><p>The <code>semantic</code> field participates in the <a href="https://www.elastic.co/docs/reference/elasticsearch/rest-apis/retrievers">multi-field query format</a> supported by the <code>linear</code> and <code>rrf</code> retrievers. Rather than hand-writing an inner retriever per field, you supply a single <code>query</code> and a list of <code>fields</code>, mixing lexical fields and semantic fields freely:</p>GET example-index/_search
{
  "retriever": {
    "linear": {
      "query": "a cat on a windowsill",
      "fields": ["title", "my_semantic_field"],
      "normalizer": "minmax"
    }
  }
}<p>The retriever automatically separates lexical fields from semantic fields, queries each group, and normalizes the results so that each group contributes equally to the final ranking, preventing lexical matches from drowning out semantic ones.</p><h3>Cross-cluster search</h3><p>The <code>semantic</code> field supports <a href="https://www.elastic.co/docs/solutions/search/cross-cluster-search">cross-cluster search (CCS)</a>, enabling use of the field in large, multi-cluster deployments. Simply list the indices to query using the standard <code>&lt;cluster&gt;:&lt;index&gt;</code> format:</p>GET example-index,remote-cluster:remote-index/_search
{
  "query": {
    "match": {
      "my_semantic_field": "a cat on a windowsill"
    }
  }
}<p>The fields queried across indices and clusters can use a mix of different inference endpoints that produce different query embeddings. The search request will automatically apply the proper query embedding to each individual field queried.</p><h2>Off the easel, into the world: optimising multimodal embeddings for production</h2><p>When you move multimodal search from experiment to production, the size of your multimodal inputs becomes a practical concern. Multimodal data is supplied as base64-encoded data URLs, and that data is stored in the index. Those strings can grow large in a hurry: a single high-resolution file can balloon into several megabytes of encoded text, which has several side effects:</p><ul><li><p>The index size on disk can increase significantly.</p></li><li><p>Requests and responses containing multimodal data are larger, increasing transmission time and ingress/egress costs.</p></li><li><p>Inference on larger multimodal inputs is slower.</p></li></ul><p>The good news is that you don’t need that much fidelity. Multimodal embedding models reduce each input to a compact representation before generating a vector anyway, so a smaller, lower-fidelity version of a multimodal input (such as a downscaled image or a lower-bitrate audio clip) produces a very similar embedding, and similar search quality, to its full-size original. This also applies to PDF input. PDFs are generally processed visually by multimodal models, so the quality only needs to be good enough to perform operations like image embedding and OCR. Long PDFs should be broken up into chunks of smaller inputs, so the embeddings generated more accurately represent each chunk. Feeding the model small inputs keeps your documents lean, trims index and response sizes, and speeds up ingestion, all without meaningfully affecting relevance. </p><p>Elasticsearch reinforces this practice with a guardrail: the <code>indices.inference.max_binary_input_size</code> cluster setting caps the size of each binary input, defaulting to 1 MB. Any individual value that exceeds the limit is rejected with a clear error, so oversized inputs surface as an actionable problem at index time rather than as silent bloat. This setting is adjustable in self-hosted and ECH through the <a href="https://www.elastic.co/docs/api/doc/elasticsearch/operation/operation-cluster-put-settings">cluster settings API</a>. It is not adjustable in our serverless offering, where 1 MB is the hard limit for binary sizes.</p><p>When possible, it is also advised to use <a href="https://www.elastic.co/docs/reference/elasticsearch/rest-apis/retrieve-selected-fields#source-filtering">source filtering</a> to exclude <code>semantic</code> fields from responses. For example:</p>GET example-index/_search
{ 
  "_source": {
    "excludes": ["my_semantic_field"]
  },
  "query": {
    "match": {
      "my_semantic_field": "a cat on a windowsill"
    }
  }
}<p>This makes responses smaller, more performant, and easier to parse because multimodal data is not returned with each.</p><h2>Try out the semantic field</h2><p>The <code>semantic</code> field is available in Elasticsearch 9.5 and Serverless. <a href="https://cloud.elastic.co/registration?onboarding_token=search&amp;cta=cloudregistration&amp;tech=trial&amp;plcmt=cross%20module&amp;pg=search-labs">Start a free trial</a> and try it out today.</p>]]></content:encoded>
    <link>https://www.elastic.co/search-labs/blog/semantic-field-multimodal-search-elasticsearch</link>
    <guid isPermaLink="true">https://www.elastic.co/search-labs/blog/semantic-field-multimodal-search-elasticsearch</guid>
    <category><![CDATA[Vector Database]]></category>
    <category><![CDATA[Index Data]]></category>
    <category><![CDATA[Jina AI]]></category>
    <category><![CDATA[Mappings]]></category>
    <dc:creator><![CDATA[Mike Pellegrini]]></dc:creator>
    <enclosure url="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/bltac28de8857eafbc4/6a6f090aca9a724b3c614914/image1.png" length="0" type="image/png"/>
    <pubDate>Mon, 03 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[4 NVIDIA AI tasks, 1 Elasticsearch API: Embeddings, chat, completion, and rerank]]></title>
    <description><![CDATA[Set up NVIDIA hosted models in Elasticsearch with one API key and a model ID. No custom integration code needed.]]></description>
    <content:encoded><![CDATA[<p></p><p>Elasticsearch's <a href="https://www.elastic.co/docs/api/doc/elasticsearch/group/endpoint-inference">inference API</a> now connects directly to NVIDIA-hosted models. You get text embedding, completion, chat completion, and reranking, plus access to NVIDIA's catalog of NVIDIA Inference Microservices–optimized (NIM-optimized) retrieval and generative models, without writing any custom integration code.</p><p>In practice, that's vector search and retrieval augmented generation (RAG) applications built on NVIDIA-hosted embeddings. It's also multi-turn conversations through the chat completion API and reranking with NVIDIA's cross-encoder models to push relevance past keyword matching. All four task types run natively through the inference API, with support for both streaming and non-streaming responses. How do I get an NVIDIA API key?</p><p>NVIDIA offers a broad catalog of models designed for a wide range of use cases, all of which can be explored on the <a href="https://build.nvidia.com/models">NVIDIA Build model catalog</a>. Throughout this article, we provide specific examples of high-performance models optimized for each inference task type. After identifying the model that best aligns with your application requirements, choose the deployment approach that fits your infrastructure and operational needs. This could mean running it on-premises for greater control or using a serverless option for faster experimentation and simplified scaling.</p><p>To get started quickly, you’ll first need access to NVIDIA’s model catalog and APIs. Create an account or log in at <a href="https://www.build.nvidia.com/">https://www.build.nvidia.com/</a> to explore available models, evaluate their capabilities, and compare which ones best fit your use case before proceeding toward full-scale deployment. This site provides a web-based interface for testing models, which is useful during evaluation and experimentation. For production-level requirements, you can use NVIDIA NIM to deploy endpoints on your own infrastructure.</p><p>To access NVIDIA models, you need to generate an API key. This key will serve as the authorization mechanism when making calls to NVIDIA's endpoints. You can create, access, and manage your API keys at <a href="https://build.nvidia.com/settings/api-keys">API keys</a>. To create a new key, click the <strong>Generate API Key</strong> link in the top right, and then specify a name and expiration period for the key. After generating the API key, select the appropriate model for your task and set up the corresponding Elasticsearch inference endpoint.</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt238bdc3001941d8c/6a6119f21c28938e356e5cf7/ca6c4a6322433697ad76d28f68e78c03b59095a9-2048x1104.png" alt="" /><h2>Setting up Elasticsearch inference endpoints</h2><p>Once you have set up your NVIDIA account and obtained the necessary API keys, you can create an Elasticsearch inference endpoint.</p><p>Endpoint setup can be done directly in Kibana using the console, which allows you to input the required steps into Elasticsearch even without using an API. The following sections provide examples and details on how to create and use endpoints for <a href="https://www.elastic.co/docs/api/doc/elasticsearch/operation/operation-inference-text-embedding">text embeddings</a>, <a href="https://www.elastic.co/docs/api/doc/elasticsearch/operation/operation-inference-completion">completion</a>, <a href="https://www.elastic.co/docs/api/doc/elasticsearch/operation/operation-inference-chat-completion-unified">chat completion</a>, and <a href="https://www.elastic.co/docs/api/doc/elasticsearch/operation/operation-inference-rerank">reranking</a>.For more examples and detailed information, please consult the <a href="https://www.elastic.co/docs/api/doc/elasticsearch/operation/operation-inference-put-nvidia">Elasticsearch API reference documentation</a>.</p><h3>Creating and using a text embeddings inference endpoint</h3><p>To create a text embedding inference endpoint, you first select an appropriate model that can perform embedding operations. NVIDIA lists its models in the <a href="https://build.nvidia.com/models">NVIDIA Build model catalog</a>. You can select the <strong>Text-to-Embedding</strong> or <strong>Retrieval Augmented Generation</strong> use case on the left to filter the appropriate models. You can also find NVIDIA’s text embedding models in the <a href="https://docs.api.nvidia.com/nim/reference/retrieval-apis">NVIDIA documentation</a>. NVIDIA’s retrieval APIs include <strong>text embedding</strong> and <strong>reranking</strong> models. When choosing a model, make sure it explicitly supports text embedding inference. Text embedding models typically include an API description, such as "Creates an embedding vector from the input text."</p><p>A good example of an embedding model is the <a href="https://build.nvidia.com/nvidia/nemotron-3-embed-1b">nvidia/nemotron-3-embed-1b</a> model. You can access <a href="https://build.nvidia.com/nvidia/nemotron-3-embed-1b/deploy">the deployment page for this model</a>, which allows you to deploy this model on-premises.</p><p>Once you have selected a suitable model, open its API reference page, where you’ll find the parameters required to create an Elasticsearch inference endpoint.</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/bltf15fb19a2bcf18a0/6a6119f38b1c7a7183893d18/207e483b7ac40b2acee268fc13b80819a9ecf9ed-1259x869.png" alt="NVIDIA embedding API reference showing the POST endpoint and model parameter for llama-nemotron-embed-1b-v2" /><p>Two parameters are relevant:</p><ul><li><p><strong><code>model_id</code></strong>(required): Specifies the NVIDIA model to be used for embedding inference. This parameter is named <code>model</code> on the NVIDIA side.</p></li><li><p><strong><code>url</code></strong>(optional): The endpoint URL used to send requests to the NVIDIA model (either deployed on-premises or in a serverless environment). It must be accessible from your Elasticsearch instance.</p></li></ul><p>For most text embedding models, the URL is static and NVIDIA differentiates models solely via the <code>model</code> parameter. If the <code>url</code> parameter isn’t provided during endpoint creation, the default text embedding task specific value <a href="https://integrate.api.nvidia.com/v1/embeddings">https://integrate.api.nvidia.com/v1/embeddings</a> will be used.</p><p>To generate text embeddings, set up an endpoint configured with the required NVIDIA model values in the service settings map:</p>PUT _inference/text_embedding/nvidia-text-embedding
{
    "service": "nvidia",
    "service_settings": {
        "url": "https://integrate.api.nvidia.com/v1/embeddings", // optional
        "api_key": "&lt;api_key&gt;",
	 "model_id": "nvidia/nemotron-3-embed-1b"
    }
}<p>Upon sending this request, you should receive a successful <strong>200 OK</strong> response. This response confirms that the endpoint is functioning correctly and the settings are specified accurately, and it will detail your newly created Elasticsearch endpoint for the text embedding task type.</p>{
    "inference_id": "nvidia-text-embedding",
    "task_type": "text_embedding",
    "service": "nvidia",
    "service_settings": {
        "model_id": "nvidia/nemotron-3-embed-1b",
        "url": "https://integrate.api.nvidia.com/v1/embeddings",
        "rate_limit": {
            "requests_per_minute": 3000
        },
        "dimensions": 2048,
        "similarity": "dot_product"
    },
    "chunking_settings": {
        "strategy": "sentence",
        "max_chunk_size": 250,
        "sentence_overlap": 1
    }
}<p>You can now use the newly created endpoint to generate embeddings. The request for this operation will be similar to the example shown below:</p>POST _inference/nvidia-text-embedding
{
    "input": [
        "First input.",
        "Second input."
    ]
}<p>The text embeddings will be returned, accompanied by a successful HTTP <strong>200 OK</strong> status.</p>{
    "text_embedding": [
        {
            "embedding": [
                -0.016174316,
                0.018432617,
                ...,
                -0.016723631
            ]
        },
        {
            "embedding": [
                -0.008995056,
                0.014381409,
                ...,
                -0.025314331
            ]
        }
    ]
}<p>This integration allows users to use the NVIDIA models directly within Elasticsearch, making advanced search and RAG applications easier to build. These production-ready models offer a reliable and robust foundation for enterprise-scale deployments.</p><h3>Creating and using a completion inference endpoint</h3><p>To create a completion inference endpoint, you first select an appropriate model.</p><p>NVIDIA lists its models in the <a href="https://build.nvidia.com/models">NVIDIA Build model catalog</a>. You can search for the model there, but you can also find NVIDIA’s completion models in the left-hand navigation of this <a href="https://docs.api.nvidia.com/nim/reference/llm-apis">large language model (LLM) API documentation</a>.Each entry in the list links to a general description of the model. From there, you can navigate to a nested link that opens the API reference specific to the selected model. A good example of a completion model is the <a href="https://build.nvidia.com/nvidia/nemotron-3-super-120b-a12b">nvidia/nemotron-3-super-120b-a12b</a> model. You can access <a href="https://build.nvidia.com/nvidia/nemotron-3-super-120b-a12b/deploy">the deployment page</a> for this model, which allows you to deploy this model on-premises.</p><p>Once you have selected a suitable model, open its API reference page, where you’ll find the parameters required to successfully create an Elasticsearch inference endpoint.</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt595e423887574160/6a6119f45144f7954cb98969/b3af7e17a0d310bf63c33339056e30f2960ea139-1495x779.png" alt="NVIDIA chat completions API reference showing the POST endpoint and default model nemotron-3-super-120b-a12b" /><p>Two parameters are relevant:</p><ul><li><p><strong><code>model_id</code></strong>(required): Specifies the NVIDIA model to be used for completion inference. This parameter is named <code>model</code> on the NVIDIA side.</p></li><li><p><strong><code>url</code></strong>(optional): The endpoint URL used to send requests to the NVIDIA model (either deployed on-premises or in a serverless environment). It must be accessible from your Elasticsearch instance.</p></li></ul><p>For most completion models, the URL is static, and NVIDIA differentiates between models using only the <code>model</code> parameter. If the <code>url</code> parameter isn’t specified during endpoint creation, the default value of <a href="https://integrate.api.nvidia.com/v1/chat/completions">https://integrate.api.nvidia.com/v1/chat/completions</a> will be used.</p><p>To use a generative model for the Elasticsearch inference completion task, you configure an endpoint that supports completion operations. The service settings map must include the required configuration for the selected NVIDIA model.</p>PUT _inference/completion/nvidia-completion
{
    "service": "nvidia",
    "service_settings": {
        "url": "https://integrate.api.nvidia.com/v1/chat/completions", // optional
        "api_key": "&lt;api_key&gt;",
        "model_id": "nvidia/nemotron-3-super-120b-a12b"
    }
}<p>Upon success, you’ll receive a 200 OK response. This response provides the details of your new Elasticsearch endpoint, which is configured for completion tasks.</p>{
    "inference_id": "nvidia-completion",
    "task_type": "completion",
    "service": "nvidia",
    "service_settings": {
        "model_id": "nvidia/nemotron-3-super-120b-a12b",
        "url": "https://integrate.api.nvidia.com/v1/chat/completions",
        "rate_limit": {
            "requests_per_minute": 3000
        }
    }
}<p>The created endpoint allows you to generate both streaming and non-streaming completions. These refer to how the endpoint delivers its output. <em>Non-streaming completions</em> wait until the entire response is generated before sending it back in a single block, resulting in a single, slower response time. In contrast, <em>streaming completions</em> send the generated text back in small, continuous chunks as they’re produced, which allows you to start reading the response immediately. This continuous delivery creates the perception of faster interaction and is essential for real-time conversational interfaces.</p><h4>Generating non-streaming completions</h4><p>To generate non-streaming completions, you call the newly created endpoint with a request similar to the following:</p>POST _inference/completion/nvidia-completion
{
    "input": "The sky above the port was the color of television tuned to a dead channel."
}<p>You'll receive a successful 200 OK response, with the completion result:</p>{
    "completion": [
        {
            "result": "This line uses a simile to describe the sky over a seaport."
        }
    ]
}<h4>Generating streaming completions</h4><p>To use the streaming functionality for the completion task type, you need to send the identical request used for non-streaming completions, but with <code>_stream</code> included in the URL path:</p>POST _inference/completion/nvidia-completion/_stream
{
    "input": "The sky above the port was the color of television tuned to a dead channel."
}<p>This command will initiate a continuous flow of events, delivering a sequence of outputs similar to the example provided below:</p>event: message
data: {"completion":[{"delta":"First"},{"delta":" Second"}]}

﻿event: message
data: {"completion":[{"delta":" Third"},{"delta":" Fourth"}]}

﻿event: message
data: [DONE]<p>This capability empowers users to easily integrate NVIDIA generative models directly into their Elastic applications, supporting both single-response and engaging streaming experiences for dynamic content generation.</p><h3>Creating and using a chat completion inference endpoint</h3><p>To enable more dynamic and flexible interactions than those supported by the standard completion inference endpoint, you configure a chat completion inference endpoint, specifically designed to handle chat-based completion tasks.</p><p>To identify the parameters required to construct the service settings map, refer to the completion inference endpoint section of this blog post. The same configuration principles apply to the chat completion inference endpoint.</p><p>The service settings map must include the required configuration settings for the selected NVIDIA model.</p>PUT _inference/chat_completion/nvidia-chat-completion
{
    "service": "nvidia",
    "service_settings": {
        "url": "https://integrate.api.nvidia.com/v1/chat/completions", // optional
        "api_key": "&lt;api_key&gt;",
        "model_id": "nvidia/nemotron-3-super-120b-a12b"
    }
}<p>Upon success, you’ll receive a 200 OK response, which includes the details of your new Elasticsearch endpoint specifically for the chat completion task type.</p>{
    "inference_id": "nvidia-chat-completion",
    "task_type": "chat_completion",
    "service": "nvidia",
    "service_settings": {
        "model_id": "nvidia/nemotron-3-super-120b-a12b",
        "url": "https://integrate.api.nvidia.com/v1/chat/completions",
        "rate_limit": {
            "requests_per_minute": 3000
        }
    }
}<p>You can now use the new endpoint to stream generated completions. Your request should resemble the following example:</p>POST _inference/chat_completion/nvidia-chat-completion/_stream
{
    "messages": [
        {
            "role": "user",
            "content": "What is deep learning?"
        }
    ]
}<p>The chat completion results will be delivered to you as a continuous stream of events, formatted as follows:</p>event: message
data: {
    "id": "cmpl-92346cfa1d004f65991eedf0765b622a",
    "choices": [
        {
            "delta": {
                "content": " first chunk"
            },
            "index": 0
        }
    ],
    "model": "nvidia/nemotron-3-super-120b-a12b",
    "object": "chat.completion.chunk"
}
﻿﻿event: message
data: {
    "id": "cmpl-92346cfa1d004f65991eedf0765b622a",
    "choices": [
        {
            "delta": {
                "content": " second chunk"
            },
            "finish_reason": "length",
            "index": 0
        }
    ],
    "model": "nvidia/nemotron-3-super-120b-a12b",
    "object": "chat.completion.chunk",
    "usage": {
        "completion_tokens": 10,
        "prompt_tokens": 8,
        "total_tokens": 18
    }
}

﻿event: message
data: [DONE]<p>The chat completion capability, distinct from the simpler completion API, allows users to build stateful, multi-turn conversational AI applications directly within the Elastic Stack, using the full flexibility of NVIDIA models for dynamic user interactions following Elasticsearch inference chat completion API.</p><h3>Creating and using a rerank inference endpoint</h3><p><em>Reranking</em> is the process of reordering the results from an initial search query to improve their relevance to your intent. Reranking is a second-stage relevance step that reorders the results returned by an initial retriever. In many cases, it uses a different model than the retriever itself, typically a cross-encoder model, which evaluates the query and each candidate document together to produce a more accurate relevance score. The output is a list of results ranked based on their relevancy, thereby drastically improving the quality and contextual accuracy of the search results.</p><p>To create a rerank inference endpoint, you first select an appropriate model that can perform reranking operations. NVIDIA lists its models in the <a href="https://build.nvidia.com/models">NVIDIA Build model catalog</a>. You can use the <code>reranking</code> label to select the appropriate models. You can also find NVIDIA’s reranking models in the left-hand navigation of this <a href="https://docs.api.nvidia.com/nim/reference/retrieval-apis">retrieval APIs documentation</a>. NVIDIA includes rerankingand text embedding models in the Retrieval APIs section in its API documentation. When selecting a model, ensure that it explicitly supports rerank inference requests. Rerank models typically include an API description, such as “Ranks passages by their relation to a query.” This wording indicates that the model supports the rerank task type.</p><p>A good example of a reranking model is the <a href="https://build.nvidia.com/nvidia/llama-nemotron-rerank-vl-1b-v2">nvidia/llama-nemotron-rerank-vl-1b-v2</a>. You can access <a href="https://build.nvidia.com/nvidia/llama-nemotron-rerank-vl-1b-v2/deploy">the deployment page for this model</a>, which allows you to deploy this model on-premises.</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt818a68e2286f5b73/6a6119f4f81792e7ea07f5fd/64cf92d4db85b4af88f84e2048c589457c3fb1c1-1495x772.png" alt="NVIDIA rerank API reference showing the POST endpoint and model parameter for llama-nemotron-rerank-vl-1b-v2" /><p>Once you have selected a suitable model, open its API reference page, where you’ll find the parameters required to create an Elasticsearch inference endpoint. Two parameters are relevant:</p><ul><li><p><strong><code>model_id</code></strong>(required): Specifies the NVIDIA model to be used for rerank inference.</p></li><li><p><strong><code>url</code></strong>(optional): The endpoint URL used by the inference endpoint to send requests to the NVIDIA service.</p></li></ul><p>For most models, the URL is static and NVIDIA differentiates between models using only the <code>model</code> parameter. If the <code>url</code> parameter isn’t specified during endpoint creation, the default value</p><p><a href="https://ai.api.nvidia.com/v1/retrieval/nvidia/reranking">https://ai.api.nvidia.com/v1/retrieval/nvidia/reranking</a> will be used. The <a href="https://build.nvidia.com/nvidia/llama-nemotron-rerank-vl-1b-v2">nvidia/llama-nemotron-rerank-vl-1b-v2</a> model requires a custom URL to be specified, and it will be used in the example below.</p><p>To perform reranking tasks, you configure an inference endpoint that executes reranking operations. The service settings map must include the required configuration for the selected NVIDIA model.
</p>PUT _inference/rerank/nvidia-rerank
{
    "service": "nvidia",
    "service_settings": {
        "url": "https://ai.api.nvidia.com/v1/retrieval/nvidia/llama-nemotron-rerank-vl-1b-v2/reranking", // optional
        "api_key": "&lt;api_key&gt;",
        "model_id": "nvidia/llama-nemotron-rerank-vl-1b-v2"
    }
}<p>The successful creation of your new Elasticsearch endpoint for the rerank task type will be confirmed by a 200 OK response, which will also provide the specific details of the endpoint.</p>{
    "inference_id": "nvidia-rerank",
    "task_type": "rerank",
    "service": "nvidia",
    "service_settings": {
        "model_id": "nvidia/llama-nemotron-rerank-vl-1b-v2",
        "url": "https://ai.api.nvidia.com/v1/retrieval/nvidia/llama-nemotron-rerank-vl-1b-v2/reranking",
        "rate_limit": {
            "requests_per_minute": 3000
        }
    }
}<p>You can then start using the new endpoint to perform a ranking operation with a request like the one shown below:</p>POST _inference/rerank/nvidia-rerank
{
    "input": [
        "mercury",
        "venus",
        "earth",
        "mars",
        "jupiter",
        "saturn"
    ],
    "query": "which planet is third from the sun"
}<p>A successful HTTP 200 OK status will be returned, and the ranked entries will be included in the response. Since models are not deterministic, the results you receive may vary and may be ordered differently across calls, as the same outcome isn’t guaranteed each time.</p>{
    "rerank": [
        {
            "index": 2,
            "relevance_score": -8.5
        },
        {
            "index": 1,
            "relevance_score": -8.9453125
        },
        {
            "index": 4,
            "relevance_score": -8.984375
        },
        {
            "index": 3,
            "relevance_score": -9.0078125
        },
        {
            "index": 0,
            "relevance_score": -9.5546875
        },
        {
            "index": 5,
            "relevance_score": -10.53125
        }
    ]
}<p>Integrating the rerank capability with Elasticsearch and NVIDIA elevates search applications to deliver the most accurate, contextually relevant results. By using the NVIDIA reranking models within the search infrastructure of Elasticsearch, the system moves beyond simple keyword matching. This capability prioritizes the most relevant documents after the initial search, drastically improving the user experience and the utility of the data.</p><h2>NVIDIA and Elasticsearch: What's next</h2><p>The integration of Elasticsearch's inference API with NVIDIA marks a considerable step forward for users. By providing a standardized, simpler path to access NVIDIA's high-performance, optimized models, this integration significantly expands Elastic's capabilities. Users can now work with these models for key AI tasks, including generating text embeddings for vector search, generating and streaming content with completion models, building stateful conversational AI applications with chat completion, and drastically improving search result accuracy through reranking. This simplification streamlines the development of sophisticated AI-powered applications, from advanced RAG systems to dynamic conversational interfaces, making powerful AI more accessible for Elastic users.</p><p>Ready to get started?</p><ul><li><p>Explore the <a href="https://www.elastic.co/docs/api/doc/elasticsearch/operation/operation-inference-put-nvidia">Elasticsearch API reference documentation</a> to dive deeper into setup.</p></li><li><p>Browse the <a href="https://build.nvidia.com/models">NVIDIA model catalog</a> to see all available models.</p></li><li><p>Check out the <a href="https://docs.api.nvidia.com/">NVIDIA API Documentation hub</a> for integration guides and API references.</p></li><li><p>Start your journey by creating an <a href="https://build.nvidia.com/settings/api-keys">NVIDIA account and API key</a> to begin integrating the models today.</p></li></ul><h2>Frequently asked questions</h2><p><strong>How do I connect Elasticsearch to NVIDIA hosted models?</strong></p><p>Create an NVIDIA API key at <a href="http://build.nvidia.com">build.nvidia.com</a>, and then create an Elasticsearch inference endpoint using the <code>nvidia</code> service with your API key and a <code>model_id</code>. Elasticsearch's inference API supports four NVIDIA task types: text embedding, completion, chat completion, and reranking.</p><p><strong>What's the default endpoint URL for NVIDIA text embedding models in Elasticsearch?</strong></p><p>If no <code>url</code> is specified when creating the endpoint, Elasticsearch defaults to <code>https://integrate.api.nvidia.com/v1/embeddings</code> for text embedding tasks. Completion and chat completion tasks default to <code>https://integrate.api.nvidia.com/v1/chat/completions</code> instead.</p><p><strong>Can I use NVIDIA NIM models deployed on my own infrastructure with Elasticsearch?</strong></p><p>Yes. NVIDIA NIM supports on-premises deployment, and Elasticsearch's inference endpoint accepts a custom <code>url</code> parameter pointing to your self-hosted NIM endpoint instead of NVIDIA's serverless API.</p><p><strong>How do I stream chat completion responses from NVIDIA models in Elasticsearch?</strong></p><p>Append <code>_stream</code> to the chat completion endpoint's URL path (<code>POST _inference/chat_completion/{id}/_stream</code>). Elasticsearch returns results as a continuous event stream instead of a single blocking response, ending with a <code>[DONE]</code> event.</p><p><strong>What's the difference between the completion and rerank task types in Elasticsearch's NVIDIA integration?</strong></p><p>Completion and chat completion generate new text from a prompt. Reranking takes an existing list of documents and a query and then reorders them by relevance score using a cross-encoder model; it doesn't generate text, it rescores what you already retrieved.</p><p><strong>How do NVIDIA's reranking models improve Elasticsearch search results?</strong></p><p>NVIDIA's reranking models evaluate the query and each candidate document together, producing a relevance score used to reorder results beyond keyword matching. Elasticsearch's rerank endpoint returns each document's index and relevance score, so the highest-scoring passages surface first.</p><p><strong>Do I need a paid NVIDIA account to use hosted models with Elasticsearch?</strong></p><p>You need an NVIDIA account and an API key generated at <a href="http://build.nvidia.com">build.nvidia.com</a>; NVIDIA's build platform offers both free evaluation access and paid production tiers, depending on usage. Elasticsearch itself doesn't add separate licensing for the NVIDIA service beyond your existing NVIDIA account terms.</p>]]></content:encoded>
    <link>https://www.elastic.co/search-labs/blog/elasticsearch-nvidia-inference</link>
    <guid isPermaLink="true">https://www.elastic.co/search-labs/blog/elasticsearch-nvidia-inference</guid>
    <category><![CDATA[Integrations]]></category>
    <category><![CDATA[Vector Database]]></category>
    <category><![CDATA[AI Tools ]]></category>
    <dc:creator><![CDATA[ Jan Kazlouski]]></dc:creator>
    <enclosure url="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt88526af16bafdb7c/6a17d7807f6f15825dc0998d/d11e1ba058784ec92b8953fb8db62e1bad21c210-720x420.jpg" length="0" type="image/jpeg"/>
    <pubDate>Tue, 21 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[A picture is worth 1.5x the words: What we learned benchmarking product search embeddings]]></title>
    <description><![CDATA[We benchmarked two embedding models on 5,000 real products and found that combining image and text beats either alone by up to 50%. Here's the data and the model that won.]]></description>
    <content:encoded><![CDATA[<p>A picture is worth 1.5x the words: What we learned benchmarking product search embeddings</p><p>Combining image and text into one embedding beats either alone, and the gap isn't small. In our tests, averaged image and text embeddings put the correct product in the top spot up to 1.5 times as often as image embeddings alone. We benchmarked 5,000 real apparel and footwear products in English and German across two Jina embedding models, <a href="https://jina.ai/models/jina-clip-v2/"><code>jina-clip-v2</code></a> and <a href="https://jina.ai/models/jina-embeddings-v5-omni-small/"><code>jina-embeddings-v5-omni-small</code></a>, to see which model and which indexing strategy actually wins for ecommerce search. The older, narrower Contrastive Language–Image Pre-training–style (CLIP-style) model beat the newer, more general one, and that wasn't what we expected. This post walks through the data, the method, and what we'd recommend doing with it.</p><h2>jina-clip-v2 vs. jina-embeddings-v5-omni-small: What's different</h2><p>Multimodal embedding models work by generating representative semantic vectors for inputs of different kinds in a single high-dimensional space.</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/bltf638d8046a86a96d/6a6119f95144f70015b98971/9fb7128bc42109876edbe403985a8d621d535ead-1326x973.png" alt="Diagram of a dog and a cat mapped as vectors in a shared embedding space, illustrating how multimodal embeddings represent product similarity" /><p>We used two models that do this:</p><p></p><p>jina-clip-v2</p><p>jina-embeddings-v5-omni-small</p><p>Architecture</p><p>Dual encoder (separate text + image towers)</p><p>Single shared backbone + frozen encoders</p><p>Parameters</p><p>~865M total</p><p>~1.74B total</p><p>Embedding dimensions</p><p>1024</p><p>1024</p><p>Max input</p><p>512×512 images, 8k tokens text</p><p>32k tokens</p><p>Language coverage</p><p>Broad multilingual</p><p>~100 languages</p><p>Modality handling</p><p>Text ↔ image alignment (purpose-built)</p><p>Text, image, audio, video via projectors</p><p><code>jina-clip-v2</code> is a CLIP-style dual encoder: a text tower (Jina XLM-RoBERTa, 561M parameters) and a separate image tower (EVA02-L14, 304M parameters), about 865M parameters in total. The two towers are independently trained but fine-tuned to output to a common semantic space. It produces 1024-dimensional embeddings, handles images up to 512×512 and up to 8k tokens of text, and has broad multilingual support. It has been engineered specifically to support text-to-image, image-to-text, and text-to-text matching.</p><p><code>jina-embeddings-v5-omni-small</code> has a broader scope. It extends the <a href="https://jina.ai/models/jina-embeddings-v5-text-small"><code>jina-embeddings-v5-text</code></a> model to support images, audio, and video by attaching frozen vision and audio encoders to the frozen text backbone. The encoders connect through <em>cross-modal projectors</em>, small trained layers that map each encoder's output into the text model's embedding space. These projectors are the only part of the model to receive additional training. The resulting model produces 1024-dimensional embeddings, supports a 32k-token input context, and covers roughly 100 languages. It encodes queries and documents asymmetrically: a query with the <code>retrieval.query</code> task, a document with <code>retrieval.passage</code>.</p><p>There’s an important functional difference between the two models: <code>jina-clip-v2</code> is really two separate models trained to work together, but <code>jina-embeddings-v5-omni-small</code> uses a single shared backbone that produces embeddings for all its supported media types. Every modality maps into one shared vector space. In principle, it can handle text, images, audio, or video, or combine materials of different media types into one input, yielding one embedding that encompasses all the data. However, there are two important caveats when using <code>jina-embeddings-v5-omni-small</code>: Combining image and text into one input is a documented weak spot for the model, and the Jina API only allows users to embed one modality per request. Theoretically, it can create a joint image and text vector, but in practice, you can’t with the API (and shouldn’t anyway).</p><h2>The ecommerce product dataset we used</h2><p>For this article, we downloaded the <a href="https://www.kaggle.com/datasets/paramaggarwal/fashion-product-images-dataset">Fashion Product Images</a> dataset from Kaggle. It contains roughly 44,000 catalog entries for products from a real fashion retailer, each with a high-resolution photo and structured metadata. We only used the <code>Apparel</code> and <code>Footwear</code> categories (about 30,600 products) and sampled 5,000 from them with a fixed random seed.</p><p>For each product, the dataset contains three records:</p><ul><li><p>Each product is pictured in a 1800×2400 JPEG against a clean background.</p></li><li><p>Metadata with the labels <code>gender</code>, <code>masterCategory</code>, <code>subCategory</code>, <code>articleType</code>, <code>baseColour</code>, <code>season</code>, <code>year</code>, <code>usage</code>, and <code>productDisplayName</code>.</p></li><li><p>A collection of additional informationwith labels like <code>Neckline</code>, <code>Pattern</code>, <code>Sleeve Length</code>, <code>Fit</code>, and <code>Fabric</code>, and a free-text description in English.</p></li></ul><p>For example, item #13885 is labelled "<em>Scullers Men Check Black Shirts</em>," with an accompanying image (see below) and a description that reads <em>"Black and white checked shirt, made of 100% cotton, full length buttoned placket, long sleeves with buttoned cuffs."</em></p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/bltaaf6af3ebb32e4bc/6a6119fbde9481709fd173c2/fcbc520fecf5f9decda6f6b95054ddae972b9283-1500x1999.png" alt="Product photo of a black and white checked men's shirt used as a sample item in a multimodal product search embeddings benchmark" /><h2>How we generated the search queries</h2><p>We generated our test queries without an AI language model, using rules and substitution lists. </p><p>For each product, we started from its color and article type and added one descriptive modifier, drawn at random from the product’s available metadata: neckline, pattern, sleeve length, length, surface styling, fit, fabric, season, or usage. We then reworded the query by substituting words from a fixed synonym table. This avoids making queries that reuse the catalog's own words. For example, ″<em>t-shirt″</em> becomes ″<em>tee″</em>, ″<em>regular fit″ </em>becomes ″<em>classic cut″</em>, ″<em>printed″</em> becomes ″<em>with graphic design″</em>, ″<em>sleeveless″</em> becomes <em>″no sleeves″</em>. The longest phrase with a synonym was replaced first, so we swapped <em>″sports shoes″</em> rather than <em>″shoes″</em>. Because the pipeline is rule-based and seeded, query production is reproducible and all variation is accounted for. German queries were generated the same way, using a German term table, and then a native speaker corrected them for natural retail phrasing. Some examples:</p># usage / occasion
    "in casual style": "for everyday wear",
    "in formal style": "for the office",
    "in sports style": "for working out",
    "in ethnic style": "in traditional wear",
    "in party style": "for a night out"

# article types
    "t-shirt": "tee",
    "trousers": "pants",
    "sweatshirt": "pullover",
    "sweater": "knit pullover",
    "kurta": "tunic",
    "capris": "cropped pants",
    "track pants": "joggers",
    "innerwear vest": "undershirt",
    "briefs": "underwear"
 
# colours (safe near-equivalents)
    "navy blue": "dark blue",
    "off white": "cream",
    "maroon": "deep red",<p>The German queries used their own term table, mapping the same catalog attributes to natural German retail phrasing (later checked by a native speaker). Some examples:</p># usage / occasion
"casual": "im Freizeit-Stil",
"formal": "im Business-Stil",
"sports": "zum Sport",
"party": "für die Party"

# article types
"heels": "Pumps",
"casual shoes": "Freizeitschuhe",
"track pants": "Jogginghose",
"wallets": "Geldbörse"

# patterns
"checked": "mit Karomuster",
"polka dots": "mit Punkten",
"solid": "unifarben"<h2>The six embedding configurations we tested</h2><p>We tested retrieval in six configurations, using the same text queries in each test condition and the same 5,000 product indexed dataset. For both <code>jina-clip-v2</code> and <code>jina-embeddings-v5-omni-small</code>, we tested three different ways of generating embeddings for indexing:</p><ol><li><p><strong>Image-only. </strong>We generated embeddings from the images alone without any other data.</p></li><li><p><strong>Text-only.</strong> We generated embeddings for the free text descriptions alone.</p></li><li><p><strong>Averaged image and text.</strong> For each product, we generated embeddings for the image and text description separately and then averaged the two vectors into one.</p></li></ol><p>We used the Jina API to generate document and query embeddings for product images and free text descriptions, as shown in the code below. All images were resized to fit into a 512x512px square before processing.</p>import requests

def embed(inputs, model, task=None):
    body = {"model": model, "input": inputs}
    if task:
        body["task"] = task
    response = requests.post(
        "https://api.jina.ai/v1/embeddings",
        headers={"Authorization": f"Bearer {JINA_API_KEY}"},
        json=body, timeout=120,
    )
    response.raise_for_status()
    return [d["embedding"] for d in response.json()["data"]]

# Query example using text
query_vec = embed([{"text": "T-Shirt in Grau für den Herbst"}],
                  "jina-embeddings-v5-omni-small", task="retrieval.query")

# Document example using an image. "
# image_base64" is the resized and base64 encoded PNG.
image_vec = embed([{"image": image_base64}],
                  "jina-embeddings-v5-omni-small", task="retrieval.passage")<p>We combined images and texts by embedding them separately, averaging the two vectors, and then normalizing the result so we can speed up calculating cosines. This works for multimodal models because both embeddings share the same semantic space. The sum of the two vectors is a new vector with the semantic features of both.</p><p>This is very easy to do using the numpy package in Python. We used the code below:</p>import numpy as np

def l2(x):  # includes row-wise L2 normalization
    return x / np.linalg.norm(x, axis=1, keepdims=True)

# image_vecs and text_vecs are embeddings of the same products.
# They share one space, so averaging them is meaningful.
combined = l2((l2(image_vecs) + l2(text_vecs)) / 2)<p>
For this article, we did exact retrieval, calculating the cosine between queries and all 5,000 stored product embeddings. In Elasticsearch, we would use a shortcut to approximate the same result. From the ranked results, we calculate Recall@1, Recall@5, Recall@10, Mean Reciprocal Rank (MRR), and Normalized Discounted Cumulative Gain at position 10 (nDCG@10).</p><p>Each query has exactly one correct answer, so Recall@K is the share of queries whose product lands in the top K. MRR scores the results by how close the correct answer is to the top. nDCG@10 is a standard metric that penalizes putting the best answer lower on the results list.</p><h2>Product search benchmark results</h2><p>The table below is the German cross-lingual run. We evaluated German queries to find products with English descriptions:</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt6dbfbca9f7c7884f/6a6119fcf2e1c472a0fd2a2c/3599b15b5a61b5be1de02ae6b8fedd8f1615410b-1500x700.png" alt="Table comparing jina-clip-v2 and jina-embeddings-v5-omni-small on German product search queries, showing Recall@1, Recall@5, Recall@10, MRR and nDCG@10" /><p>Query benchmark results for <code>jina-clip-v2</code> and <code>jina-embeddings-v5-omni-small</code> using German-language text queries and the images and English descriptions of garments.</p><p>The averaged image/text embeddings score the best, both using <code>jina-clip-v2</code> and <code>jina-embeddings-v5-omni-small</code>. Surprisingly, averaged vectors from <code>jina-clip-v2</code> lead the table. It ranks the correct product first about 1.5 times as often as the image-only setup and significantly more often than the text-only setup. Furthermore, it beats every <code>jina-embeddings-v5-omni-small</code> condition. Results for the same tests using English-language queries:</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/bltbb878895472badde/6a6119fdae3a7c168883ef02/5ced60965d12dd3c64961acc92317a74c17fe8eb-1500x700.png" alt="Table comparing jina-clip-v2 and jina-embeddings-v5-omni-small on English product search queries, showing Recall@1, Recall@5, Recall@10, MRR and nDCG@10" /><p>Query benchmark results for <code>jina-clip-v2</code> and <code>jina-embeddings-v5-omni-small</code> using English-language text queries and the images and English descriptions of garments.</p><p>The English run, on the same 5,000 products, tells the same story. The main difference is that the <code>jina-embeddings-v5-omni-small</code> scores are significantly closer to those of <code>jina-clip-v2</code>, although still lower.</p><h3>What the benchmark scores actually mean</h3><p>The relatively low scores in the German and English benchmark tables above are to be expected. This is a real-world dataset full of near duplicates. A search for a <em>"black tee with classic cut"</em> has to sort through dozens of basic black T-shirts, and even on the best ecommerce sites, you would expect a result like this. The important thing to understand is the difference in scores between the different conditions, not their absolute values. Our key finding is that combining text and image embeddings yields better performance than either one alone, highlighting how multimodal AI-driven search can use different information sources to produce better performance than non-multimodal strategies.</p><h3>When a picture is worth a thousand words, and when it isn’t</h3><p>The gain from adding images to text embeddings isn’t evenly distributed. We did a deep dive to see if there was a pattern to the results and discovered a few things:</p><ul><li><p><strong>Images add a lot for visually distinctive items and attributes.</strong> For example, on footwear, image-only retrieval is on par with text-only: 0.029 versus 0.028 for Recall@1. Shoe styles have distinctive shapes, so the picture does the work (image-only 0.029 versus text 0.028). The same holds for visible attributes more broadly (color, pattern, sleeve length), where fusing image and text gives the biggest lift over text-only (0.085 versus 0.073).</p></li><li><p><strong>Images help the least with things the model can’t see.</strong> For example, if we query for fabric types, adding images to embeddings adds next to nothing. Humans and AI models alike struggle to see that something is or isn’t made of linen or polyester or some other fabric type. That information is only in the text description and metadata.</p></li></ul><p>As a rule of thumb, we find that searches for clothing lean more heavily on accurate text descriptions, while footwear leans more on the semantics of images. But in both cases, merging the two embeddings either improves results or doesn’t make them worse. This highlights how use-case–specific considerations drive optimal search strategies.</p><h3>Cross-language queries gain a lot from multimodal embeddings</h3><p>The gap in performance between text-only and combined image and text embeddings using <code>jina-clip-v2</code> is much larger for German queries (0.074 versus 0.065 Recall@1) than for English ones (0.076 versus 0.075). This implies that English queries can take advantage of being in the same language as the product descriptions. Whether that’s due to overlaps in the words or that the model is simply more competent with single-language semantics than cross-language doesn’t matter. But adding images to the text embeddings compensates almost completely for the model’s shortcomings in cross-language retrieval.</p><p>This gap is even larger for <code>jina-embeddings-v5-omni-small</code>. In any kind of cross-language or multilingual context, multimodal embeddings seem to significantly improve retrieval performance.</p><h3>Can AI-generated product descriptions replace human ones?</h3><p>AI-generated descriptions scored worse than human-written ones in our tests. We tried replacing human-authored product descriptions with ones written by <a href="https://jina.ai/models/jina-vlm"><code>jina-vlm</code></a> based on the image. For this test, we used a 1,000-product random subset. The results were much worse than with the original human text. This was what we expected: The automatically generated description was less accurate and less oriented toward the salient features of the product than the human authored one.</p><p>So it turns out that not everyone’s job can be replaced by AI. People who write blurbs for catalogs ought to be safe for now.</p><h2>How should you index your ecommerce data?</h2><p>Our tests aren’t totally scientifically rigorous, but they do offer some insights into the issues you might face if you have similar data. We offer the following as provisional conclusions:</p><ul><li><p><strong>If you have aligned texts and images (and most catalogs do), combine them in your embeddings.</strong> In every case, using a multimodal embedding model like the ones Jina AI by Elastic provides and then averaging the image and text embeddings significantly outperformed all other options. The combination adds no computing costs at inference time but does create additional costs at embedding time. For each product, you’ll need to generate two embeddings and combine them, roughly doubling the cost.</p></li><li><p><strong>Use the right model.</strong> You need to identify a model that supports all the modalities and languages you plan to use. All the inputs have to be embedded in the same semantic vector space or none of this will work. It won’t do to get two single-modality models or multiple language-specific text models, average their outputs, and hope for the best. Jina AI by Elastic currently supports texts in up to 100 languages, including computer code and technical terminology images of all kinds, such as scans and infographics, as well as audio and video data. You can change your mind about your models later, but only if you’re willing to reindex all your data.</p></li><li><p><strong>Whatever you pick, test it on your own data.</strong> The only way to know what the best model is for you, your data, and your use case is to try them out. We were very surprised that our older CLIP-style model outperformed our latest on this dataset, but it was trained almost specifically for this use case. Your data and use case could easily show the opposite. This result is from one catalog, with one style of photography and one kind of query. The ranking between two models can flip with a different domain, image style, or query mix. Run the same sort of benchmark on a sample of your own products before you commit. It’s the only way to know which model really fits your case.</p></li><li><p><strong>Use generated descriptions to fill gaps, not to replace good text. </strong>AI isn’t a replacement for good work done by conscientious people. Replacing human-made descriptions with machine-made ones made results worse. AI should only replace humans when it has to, like when data is missing or needs to be augmented and it’s impractical to have humans fill in the gaps. Yes, we have tools that work in those situations, but they aren’t necessarily good substitutes. They’re OK substitutes, sometimes.</p></li></ul><p><strong>Average your embeddings.</strong> Semantic embeddings are very robust, and averaging them is a relatively cheap solution that doesn’t affect inference-time costs at all. This is a real boon over methods that index each modality separately and require multiple queries to satisfy a single request. But they do require compatible multimodal models.</p><h2>Limitations of this product search benchmark</h2><p>A few things to keep in mind before generalizing too much from this experiment:</p><p>This article doesn’t perfectly match real-world use cases. Human users make messier queries and have more ambiguous matching criteria. The queries we used were generated specifically for this data. A test with actual customer-made queries from system logs would be a better one.</p><p>Embedding averaging isn’t the same as a true joint embedding. Embedding models rely on the different parts of their input to interact in order to extract a semantic representation of the whole. The approach used here is a bit of a hack, one that relies on the robust nature of semantic embedding spaces to get the job done. We expect future models from Jina AI to produce better embeddings by supporting more than one input modality at a time.</p><p>This is one dataset in one domain with distinctive features. Fashion photography is very foreground-focused and the descriptions are attribute-rich. Other kinds of materials, even for ecommerce, may look very different. It’s important to test as much as possible with your own data or something very similar.</p><h2>How to get started with multimodal product search embeddings</h2><p><code>jina-clip-v2</code> and <code>jina-embeddings-v5-omni-small</code> are available through the <a href="https://jina.ai/api-dashboard">Jina API</a>, <a href="https://www.elastic.co/docs/explore-analyze/elastic-inference/eis">Elastic Inference Service</a> (EIS), and <a href="https://huggingface.co/jinaai">Hugging Face</a>. The omni models are free to download under a CC-BY-NC-4.0 license and free for noncommercial use, with commercial licensing through Elastic. If you use Elasticsearch, EIS exposes both models through the <code>semantic</code> field type, with non-text media in Base64 encoding.</p><p>The takeaway from this article is intended to be practical and actionable: For product search, a picture and its description aren’t the same signal. Both add information, and you don’t have to pick one. Average your multimodal embeddings, and benchmark the results with your own data to get a good picture of the kinds of results you can expect.</p>]]></content:encoded>
    <link>https://www.elastic.co/search-labs/blog/multimodal-embeddings-ecommerce-product-search</link>
    <guid isPermaLink="true">https://www.elastic.co/search-labs/blog/multimodal-embeddings-ecommerce-product-search</guid>
    <category><![CDATA[Vector Database]]></category>
    <category><![CDATA[Relevance]]></category>
    <category><![CDATA[Jina AI]]></category>
    <dc:creator><![CDATA[Sofia Vasileva]]></dc:creator>
    <enclosure url="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt93506c893fb19bb9/6a6119fea2ee17d20274edab/b08939addf8cfa92dd0b31211ff0a86511eb582c-1280x720.png" length="0" type="image/png"/>
    <pubDate>Thu, 16 Jul 2026 00:00:00 GMT</pubDate>
  </item>
  <item>
    <title><![CDATA[The disk that never woke up: what actually decided our Qdrant vector search benchmark rematch]]></title>
    <description><![CDATA[On the same hardware, Elasticsearch and Qdrant land in the same range at 56 QPS. The io_uring disk scorer and memory claims turned out to be the two things that mattered least.]]></description>
    <content:encoded><![CDATA[<p>Vector search vendors like a good benchmark, and lately Elasticsearch and Qdrant have been trading them. Earlier this year we <a href="https://www.elastic.co/search-labs/blog/vector-search-benchmark-elasticsearch-vs-qdrant">published one</a> comparing Elasticsearch's <code>bbq_disk</code> against Qdrant on a disk-rescore workload, where full-precision vectors live on disk and get read back to rescore the top candidates. Qdrant <a href="https://qdrant.tech/blog/benchmark-elastic-diskbbq/">replied</a> with better numbers on their side and a set of explanations for why: an io_uring-based async disk scorer, and remarks about Elasticsearch needing more memory.</p><p>We didn't want to fire back with another round of numbers. Reproducing a benchmark is the easy part; understanding why it produces the numbers it does is the useful part, and it is the part both original posts skipped. So we stood up Qdrant's exact setup on our own cluster, loaded the same <a href="https://huggingface.co/datasets/kenhktsui/wiki_dpr_e5">21 million vectors</a>, reproduced their result, and then traced every number back to its cause.</p><p>Here is what we found. The result comes down to a few setup choices: how many segments you build, whether the data is warm in memory, how you return the result ids, and which hardware you run on. Control those, and it cuts both ways: matched to Qdrant's setup the two engines are on par on query speed, and with fast default ingestion Elasticsearch is faster at search and faster to index. The big multipliers in both posts, our 7x and their reply, are artifacts of those choices, not a verdict on either engine. The two reasons Qdrant leaned on hardest, an io_uring disk scorer and Elasticsearch's memory use, turn out to be the two that mattered least: the disk is never read during the run, and the memory gap is a difference in labels, not in bytes.</p><p>We hold our own original post to the same standard. Both sides changed several things at once and reported the result rather than the reason. One rule for the rest of this post: every claim gets a number and a mechanism, or it doesn't ship.</p><h2>Vector search benchmark setup: hardware, config and query set</h2><p>Here is exactly what each side ran.</p><p></p><p></p>﻿<p></p><p></p><p>Elastic (original post)</p><p>Qdrant (response)</p><p>This post (apples-to-apples)</p><p>Nodes</p><p>3 × n4-standard-8 (7 vCPU / 26 GB), GCP</p><p>3 × m6g.large (2 vCPU / 8 GB), AWS</p><p>3 × m6g.large (2 vCPU / 8 GB)</p><p>﻿</p><p></p><p></p><p>Elasticsearch</p><p><code>bbq_disk</code> 2-bit, <code>replicas: 1</code></p><p>(cited original)</p><p><code>bbq_disk</code> 2-bit, <code>vectordb_document</code>, bfloat16, <code>replicas: 0</code></p><p>﻿</p><p></p><p></p><p>Qdrant</p><p>2-bit, async scorer off, RF=2</p><p>TurboQuant 4-bit, async scorer on, RF=1</p><p>TurboQuant 4-bit, async scorer on, RF=1</p><p>﻿</p><p></p><p></p><p>Copies per shard</p><p>2 = 2 (matched)</p><p>1</p><p>1 = 1 (matched)</p><p>﻿</p><p></p><p></p><p>Query set</p><p>10k fixed, recall@100</p><p>10k fixed, recall@100</p><p>10k fixed, recall@100</p><p>Three things are worth pinning down, because all three have been used as talking points.</p><ol><li><p>Replica count was matched in both rounds: An Elasticsearch index with <code>replicas: 1</code> keeps two copies of each shard, which is exactly what Qdrant's RF=2 does. In this round, both sides ran a single copy. It was never a thumb on the scale in either direction.</p></li><li><p>We changed a few things on our side on purpose, and we will own them: <code>vectordb_document</code> index mode, float32 to bfloat16 for stored full-precision vectors (3,072 down to 1,536 bytes each), and a merge policy tuned for vector data. Those choices matter later, so we are flagging them up front rather than burying them.</p></li><li>Memory is compared like for like. Qdrant's post sets Elasticsearch's JVM heap allocation against Qdrant's total memory use, but those are different accounting categories, since heap size is not total memory consumption. On these 8 GB nodes we give the JVM heap 2 GB, 25% of node RAM, deliberately: that follows Elastic's Vector Search Optimized profile, which leaves the other 75% for the OS page cache where the vectors actually live, rather than the general 50%-heap upper bound. The honest comparison is resident index plus runtime plus active page cache, and on an 8 GB node both engines live inside the same envelope.</li></ol><p>Look at the middle column, though. Between our original run and their response, Qdrant changed almost everything at once: new hardware, 4-bit quantization instead of 2-bit, the async scorer on, a different replication factor. Then it credited the win to one of those changes. Drawing meaningful conclusions from many simultaneous changes is hard, and pinning down which change actually moved the number is the whole job of the sections that follow.</p><h2>What does disk rescore cost in a vector search benchmark?</h2><p>In this benchmark, the answer is almost nothing, because the disk is never read. Two back-of-the-envelope budgets show why, and the live measurements later in the post confirm it.</p><p>The design under test is built around one idea: keep a small quantized copy of each vector in RAM for approximate search, keep the full-precision originals on disk, and read a handful of those originals back to rescore the top candidates for accuracy.</p><p>Two budgets determine performance: disk I/O for rescore reads and RAM for what has to stay resident. Both are computable on the back of an envelope, so let us compute them before measuring anything.</p><h3>The disk I/O budget for vector search rescoring</h3><p>Rescoring reads the top 100 candidates per query. A 768-dimensional float32 vector is 768 × 4 = 3,072 bytes. Because those reads are not page-aligned, the real cost per vector is closer to an 8 KB page read. Each query needs roughly 800 KB and 100 random reads. Scale that up:</p><p>target QPS (cold)</p><p>rescored vectors/s</p><p>reads/s (IOPS) per node</p><p>MB/s per node</p><p>10</p><p>1,000</p><p>333</p><p>3</p><p>100</p><p>10,000</p><p>3,333</p><p>27</p><p>1,000</p><p>100,000</p><p>33,333</p><p>273</p><p>10,000</p><p>1,000,000</p><p>333,333</p><p>2,731</p><p>Two things fall out immediately: First, cold rescore is an IOPS problem, never a bandwidth one. Even at 10,000 QPS, you need only about 2.7 GB/s per node, but a third of a million IOPS per node. Second, the total working set for the benchmark is tiny: <code>100 candidates × 10,000 queries × 8 KB = 8 GB across the cluster, or 2.7 GB per node</code>. That fits in the free page cache on an 8 GB node with room to spare. The first time the benchmark cycles through its 10,000 queries, the originals it touches are pulled into page cache, and every read after that is a memory copy.</p><p>Put a ceiling on it too. On gp3 at baseline (3,000 IOPS/node), if the reads did go to disk, the workload would top out around 90 QPS from IOPS alone; provisioned gp3 or local NVMe would push that to roughly 480 or 3,000. Hold that number. It is the ceiling that would matter in a benchmark that actually touched the disk.</p><h3>The RAM budget for a disk-based vector index</h3><p>What has to stay resident is the quantized copy plus the search structure. Everything else is page cache. At 4-bit quantization and an HNSW graph with m=16:</p><p>component</p><p>bytes/vector</p><p>per node @ 21M</p><p>4-bit quantized vectors (<code>always_ram</code>)</p><p>384</p><p>2.69 GB</p><p>HNSW graph, m=16 (2m links × 4 B)</p><p>136</p><p>0.95 GB</p><p>resident total</p><p>520</p><p>3.64 GB</p><p>raw float32 originals (on disk)</p><p>3,072</p><p>21.5 GB (on disk)</p><p>We measured the resident footprint on the running nodes at 3.4 to 3.9 GB, sitting right on the 3.64 GB estimate. The point to hold is that at 21 million vectors, the entire searchable index (quantized vectors plus graph) is about 3.6 GB per node and it fits in RAM, leaving a couple of gigabytes to spare. Nothing in this benchmark forces the disk-backed design to actually use the disk. We come back to that at the end, because it is the real story.</p><h2>Does io_uring make vector search faster?</h2><p>Qdrant's reply credits two levers it says we omitted: a two-stage prefetch-and-rescore retrieval pattern and an async, io_uring-based disk scorer. They are two faces of the same rescore step, and the same evidence answers both. Take the async scorer first, since it is the one Qdrant makes the centerpiece: io_uring to parallelize disk reads during rescoring. It is a good feature, and in this benchmark it had exactly one job: parallelize the disk reads during rescoring. It got to do none of them. We verified that in three independent ways.</p><p>First, the arithmetic. From the RAM budget, the quantized index and graph are pinned in RAM. From the I/O budget, the rescore working set is 2.7 GB per node and lives in page cache after the first pass. That leaves no disk reads on the hot path to accelerate.</p><p>Second, their own methodology guarantees it. Each operating point in the harness runs a full 10,000-query recall pass, followed by a timed throughput window that cycles through the same 10,000 queries again. By the time the stopwatch starts, every vector those queries will touch is already resident. The measurement is warm by construction.</p><p>Third, and this is the part we insisted on doing rather than arguing, we measured it. We were careful here because it is easy to accidentally test io_uring in the off state and not notice. Running Qdrant in Docker, we found that io_uring was not even initializing: the default seccomp profile blocks the io_uring syscalls, so it silently fell back to synchronous reads (<code>failed to initialize io_uring instance: Operation not permitted</code>). We fixed that, ran the container with seccomp unconfined, confirmed zero io_uring errors in the logs, and re-ran. This honestly means we have both states:</p><ul><li>Async scorer effectively off (synchronous fallback): 31.6 QPS at ef=50.</li><li>Async scorer on (io_uring confirmed working): 35.8 QPS at ef=50.</li></ul><p>A 13% move, and even that is within the run-to-run noise for a measurement whose ceiling is set by CPU, not disk. Put plainly, switching on the feature that the entire result was credited to changed about as much as running the benchmark a second time. To close the loop, during the actual throughput window we watched the block device on all three nodes with <code>iostat</code>: read throughput held at 0 MB/s and 0 IOPS, while CPU sat at 60-70% and climbed to 100% as we added concurrency. The bottleneck is the CPU doing <a href="https://www.elastic.co/search-labs/blog/bbq-vector-comparison-simd-instructions">quantized distance computations</a> and graph traversal. It is not the disk, because the disk is asleep.</p><p>The two-stage prefetch-and-rescore pattern, the lever Qdrant lists first, is answered by the same run. Every Qdrant number here comes from Qdrant's own reproduction script, which uses their two-stage query throughout, so two-stage was on for the whole benchmark, including the io_uring comparison, and the disk still held at 0 IOPS. It is also not something we omitted: prefetch is the approximate search over the quantized vectors, which lives in RAM, and rescore reads the originals, which live in page cache here. That is the disk-rescore workload, and <code>bbq_disk</code> runs the identical shape. The one part of two-stage that is not about disk, how many candidates you rescore, is CPU rather than I/O; it is already included in these numbers and available to both engines, so it is not a hidden lever either.</p><p>There is a smaller detail worth noting. Qdrant's post does not say which disk they ran on. That would normally matter in a benchmark whose premise is disk access, but here it does not, for the same reason the async scorer does not: you cannot be bottlenecked on a device you never read from. The disk is left unspecified, and as it happens it is also beside the point.</p><p>Here's what I've seen, as an engineer who has sat through a lot of these conversations: io_uring gets treated as a universal fix far more often than it is the bottleneck. It is excellent when the working set overflows RAM, and the cold I/O budget above shows exactly that case, where parallelizing reads buys you throughput. This benchmark never enters that regime. Async disk reads are a real answer to a real question, and this benchmark just never asked it. Of everything that changed between the two runs, io_uring is the one that got the headline and moved the number the least.</p><h2>Why does faster ingestion mean slower vector search queries?</h2><p>This is where reproducing the number got interesting, and where the honest driver of the gap turned out to live.</p><p>Our first load produced 128 segments and roughly half the throughput we expected. Qdrant's published provenance, right there in <a href="https://github.com/qdrant-labs/wiki-dpr-disk-rescore-benchmark/blob/main/results/published/qdrant_4vcpu16gb.json">their results JSON</a>, records 67 segments. That one difference explained almost everything. When we merged our collection down to match their segment count, all three metrics moved together toward their numbers:</p><p>segments</p><p>recall@100</p><p>QPS</p><p>avg latency</p><p>128 (our first, parallel load)</p><p>0.9745</p><p>35.8</p><p>112 ms</p><p>66 (merged to match theirs)</p><p>0.9531</p><p>53.3</p><p>75 ms</p><p>67 (Qdrant published)</p><p>0.9596</p><p>67.2</p><p>59.5 ms</p><p>Look at what moves. Recall falls as segments drop, because with fewer segments each query examines fewer total candidates. That is the tell that segment count, not tuning, was inflating our recall. And throughput rises, because HNSW query cost is very sensitive to the number of segments: every query fans out across every segment's graph, so twice the segments is close to twice the per-query work, plus the contention of doing all that on two cores. The small remaining gap between our 53 and their 67 is warm-cache completeness in our shorter run, not the engine.</p><p>Here is the part worth stating carefully, because it is architectural and it cuts against us as much as for us. We use the same segmented design; this is not a Qdrant quirk. The question is how you get to few segments. You get there by ingesting single-threaded, so data concentrates into fewer, larger segments. Fewer segments means faster queries, but you pay for it with slow ingestion. Our parallel load was fast and produced many segments; their single-threaded load was slower and produced few. It is a real tradeoff, and it is the same one on both sides.</p><p>Where the two designs genuinely differ is how much that tradeoff hurts. Building an HNSW graph is expensive, and its query cost scales poorly with segment count. On these 2-vCPU nodes, constructing the graph at <code>ef_construct=256</code> took about 1.6 hours pinned at 100% CPU. The IVF layout behind <a href="https://www.elastic.co/search-labs/blog/better-binary-quantization-101"><code>bbq_disk</code></a> is cheap to build and far less sensitive to segment count, so we can ingest with many threads while keeping query latency low. We do not have to choose. That is not a benchmark trick. It is the IVF vs. HNSW tradeoff, stated honestly.</p><h2>Why document retrieval, not vector search, explains the rest of the latency</h2><p>Now turn the same lens on our own result, because the same accounting applies to our own numbers too.</p><p>At the search-light end of the sweep, a large share of the per-query time is not vector search at all. It is the fetch phase assembling the response. <code>vectordb_document</code> returns full source documents, which is exactly what you want for retrieval and hybrid search, where you actually need the document back. A pure vector benchmark only needs the top-N ids, and today <code>_id</code> lives in the same stored-fields column as <code>_source</code>, the text and the vector. So fetching an id pulls that entire compressed block through the decompressor for every hit. When we isolate the id from that column, Elasticsearch steps straight up into Qdrant's low-segment throughput range. That gap is document retrieval, not vector search.</p><p>The fix is structural, not a tuning flag: give <code>_id</code> its own doc-values field so returning an id never touches <code>_source</code>. That is what the <a href="https://www.elastic.co/search-labs/blog/elasticsearch-columnar-storage"><code>vectordb_columnar</code></a> mode we are building automatically does. We are calling it out here because good benchmarking means naming your own costs as clearly as anyone else's, and this one is a document-retrieval cost sitting inside a vector-search score.</p><h2>Elasticsearch vs Qdrant vector search benchmark: our numbers, on the same box</h2><p>Elasticsearch on the same three m6g.large nodes, <code>vectordb_document</code> with bfloat16, warm, shown both as it runs today and with the id isolated:</p><p>visit %</p><p>recall@100</p><p>QPS (default)</p><p>QPS (id-isolated)</p><p>1</p><p>0.894</p><p>44</p><p>89</p><p>2</p><p>0.939</p><p>39</p><p>73</p><p>3</p><p>0.956</p><p>35</p><p>56</p><p>5</p><p>0.970</p><p>31</p><p>40</p><p>The id-isolated column is measured by dropping the stored-fields fetch entirely, which is a near-upper-bound proxy for a doc-values <code>_id</code> (the doc-values read is cheap but not literally free). At around 0.96 recall, id-isolated Elasticsearch does about 56 QPS, sitting right alongside Qdrant's 53 to 67 on identical hardware. Same ballpark. The gaps in both posts came from setup and document retrieval, not from the vector engine: segment counts, the id fetch, the hardware. Their reply we can account for in full here; the larger 7x from our own first post we cannot yet, and we take that up below. When you control for those, two well-built systems doing the same work on the same box land close together, which is what you would expect.</p><p>So here is the plain claim: performance depends on the setup. Match Qdrant's slow, single-threaded ingestion, the one that gives them their 67-segment configuration, and the two engines are on par on the search itself: id-isolated Elasticsearch does about 56 QPS against their 53 to 67 at the same recall. But that low segment count is bought with slow ingestion. Let both engines ingest fast, which is the natural default, and Qdrant lands back at 128 segments and about 35 QPS, while Elasticsearch degrades less as the segment count climbs, because the IVF layout behind <code>bbq_disk</code> is less sensitive to segment count than HNSW. So with default, fast ingestion, Elasticsearch is faster at search too, and it reached a queryable index faster to begin with. The one place we still trail is returning the ids, which is document retrieval rather than search, and it is exactly what <code>vectordb_columnar</code> removes.</p><p>We deliberately did not run the 4 vCPU / 16 GB tier. It lifts both engines together and shows the identical trend, so it would add cost without adding insight.</p><h2>Our original 7x Elasticsearch vs Qdrant benchmark number, explained</h2><p>The rule in this post is that every claim gets a number and a mechanism. That rule was also applied to our <a href="https://www.elastic.co/search-labs/blog/vector-search-benchmark-elasticsearch-vs-qdrant">original benchmark</a>.</p><p>That post measured a 7x throughput advantage for Elasticsearch, and the number is real for that configuration. The setup was disclosed in full, including that Qdrant does better on NVMe. But the post attached a mechanism to the number: Qdrant was bottlenecked by random disk reads of the original vectors during rescoring, a problem made worse by network-attached storage. The arithmetic in this post undercuts that explanation. Those round-1 nodes had 26 GB of RAM, more headroom than the 8 GB nodes here where we watched the disk sit at 0 IOPS, so if the disk was idle here, it was almost certainly idle there too.</p><p>So what actually held round-1 Qdrant to 4.5 QPS at 0.97 recall? We cannot claim to know yet, but the original post's own configuration points the way: it ran Qdrant on 2-bit quantization with oversampling pinned at 1. Two-bit codes are coarse, and with oversampling fixed at 1 the only lever left to recover recall is ef. Reaching 0.97 recall on ef alone means a very large ef, and a very large ef makes each query expensive on its own, before disk enters the picture at all. That is still a hypothesis, but the mechanism the original post named, random disk reads during rescore, does not survive the same arithmetic we just applied to Qdrant.</p><h2>Benchmarking, not benchmarketing</h2><p>The numbers reproduce; the explanations don't. A benchmark that made "disk reads" its headline ran with the disk asleep, and we watched it sit at 0 IOPS for the whole window. The honest differences we could actually find were a segment count (an ingest-speed-versus-query-speed tradeoff where the IVF layout lets <code>bbq_disk</code> ingest fast and query fast) and a stored-field retrieval cost on our side (document retrieval, not vector search, and something <code>vectordb_columnar</code> removes). None of it is io_uring, and none of it is "Java is heavy."</p><p>The original and arguably bigger point got lost in the io_uring discussion: putting vectors on disk was never really about the rescore, it is about memory. The key was keeping the searchable index, the quantized vectors and the IVF or HNSW structure, compact and disk-resident enough to serve more vectors per gigabyte of RAM than a design that pins everything in memory. Qdrant's setup pins the 4-bit vectors with <code>always_ram</code> and keeps the HNSW graph in RAM, about 3.6 GB per node for 21 million vectors. <code>bbq_disk</code> keeps the quantized IVF on disk. At 21 million vectors on 8 GB nodes, everything fits in RAM either way, which is precisely why this benchmark cannot tell the two designs apart. It is measuring the case where the interesting variable has been held constant.</p><p>The interesting question is what happens when the searchable index stops fitting. Scale the corpus until the quantized vectors and the structure exceed page cache, and the two designs diverge: one keeps serving from disk, the other needs more RAM per node. That is the regime <code>bbq_disk</code> was built for, and it is the one neither post has measured. The rescore-under-disk-pressure case, many more distinct queries than fit in cache, is worth measuring too, but it is the second question, not the first.</p><p>We tried to get there, and our first attempt still fit in cache, because 21 million vectors on these nodes do not spill. So we are not done. We are building a benchmark with a corpus large enough that the searchable index no longer fits in RAM, and we will publish those numbers, with the reasons attached and checked the same way.</p><p>The ask here is to the reader, not to Qdrant: when a benchmark hands you a clean multiplier, chase every number back to a cause before you believe the story around it. Half the time it is a warm cache or a segment count.</p><p>The Qdrant team built a good engine, and we're not questioning that. The honest verdict is not about who is faster: matched to their setup, the two are on par, and on the fast default path, Elasticsearch is quicker for both indexing and searching. The numbers were fine. The reasons attached to them were the part worth checking, starting with our own. And the benchmark that would actually stress a disk-backed index, a working set that does not fit in RAM, is still to be written. We will bring the numbers.</p>]]></content:encoded>
    <link>https://www.elastic.co/search-labs/blog/vector-search-benchmark-elasticsearch-qdrant</link>
    <guid isPermaLink="true">https://www.elastic.co/search-labs/blog/vector-search-benchmark-elasticsearch-qdrant</guid>
    <category><![CDATA[Vector Database]]></category>
    <dc:creator><![CDATA[Jim Ferenczi]]></dc:creator>
    <enclosure url="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt39d5755c76e56db3/6a7084c1c2c8ed02a308b1bb/image1.png" length="0" type="image/png"/>
    <pubDate>Mon, 13 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[How we doubled vector search throughput on Elasticsearch Serverless]]></title>
    <description><![CDATA[How we brought Elasticsearch's native SIMD scoring engine to serverless, and why serverless is where vector search innovation happens next.]]></description>
    <content:encoded><![CDATA[<p>We've brought simdvec, Elasticsearch's native single instruction, multiple data (SIMD) vector scoring engine, to serverless. Search throughput nearly doubled under concurrent load, and p99.9 tail latency dropped from 237 ms to 30 ms. By giving simdvec direct access to the blob cache's memory-mapped regions, serverless now runs the same zero-copy SIMD kernels as stateful, with identical recall and zero heap overhead. And because serverless gives us control over the entire storage layer, we believe it's where vector search will be fastest. Here's how we got there.</p><h2>Vector Search on Elasticsearch Serverless</h2><p><a href="https://www.elastic.co/search-labs/blog/elasticsearch-serverless-stateless-architecture">Elasticsearch Serverless</a> is built on Stateless Elasticsearch, a fully decoupled compute and storage architecture where index data lives in remote object storage and search nodes maintain only a local cache. For vector search to be fast on this architecture, the scoring engine needs to work directly with the local cache, not copy it to the heap first.</p><p>Elasticsearch <a href="https://www.elastic.co/search-labs/blog/elasticsearch-vector-search-simdvec-engine">simdvec</a> is the engine behind every vector distance computation in Elasticsearch. It provides hand-tuned AVX-512 and NEON kernels, bulk scoring with explicit prefetching, and off-heap memory access that keeps data flowing from storage straight to CPU registers. On stateful Elasticsearch, simdvec has always had a direct fuel line: Memory-mapped files feed native pointers straight into SIMD intrinsics. On serverless, the data was sitting right there in the blob cache's memory-mapped regions, in exactly the right form, but there was no path connecting it to the scoring engine.</p><p>We've now built that path. simdvec runs on Serverless with the same off-heap, native SIMD scoring as stateful. And because serverless gives us control over the entire storage layer, this is just the beginning.</p><h2>Premium fuel only: why simdvec requires off-heap memory for vector scoring</h2><p>simdvec's speed comes from working directly with off-heap memory. It takes a native pointer to memory-mapped data and passes it straight to C++ SIMD intrinsics. No intermediate copies, no heap allocations. The data flows from storage straight to CPU registers. This matters more than it sounds: simdvec's kernels process vectors faster than the data can be copied, so any copy in the path becomes the bottleneck, not the scoring itself.</p><p>On stateful Elasticsearch, this just works. Lucene memory-maps index files from local disk, and the scorer extracts a native pointer directly from the mapped region. This is the path that delivers the <a href="https://www.elastic.co/search-labs/blog/elasticsearch-vector-search-simdvec-engine#thousands-at-a-time">benchmark numbers</a> we've published, and it's what we wanted to bring to serverless. To see how, we first need to understand how serverless stores and accesses data.</p><h2>The serverless blob cache: how Elasticsearch stores vector data</h2><p>In the stateless architecture, the primary copy of all index data lives in remote object storage, such as S3. Each search node maintains a local cache (called the <em>blob cache</em>) that keeps recently and frequently accessed portions of the index data on local SSD. The frozen tier on stateful Elasticsearch uses the same architecture: Searchable snapshots are backed by a similar blob cache that memory-maps regions from remote storage onto local disk. When a search hits cached data, it's served from fast local storage. When it misses, the blob cache fetches the data from the remote store and caches it for future queries.</p><p>The blob cache is organized into fixed-size memory-mapped regions, 16MB by default. It manages its own lifecycle: tracking which regions are in use, applying a <a href="https://www.elastic.co/search-labs/blog/searchable-snapshots-benchmark">least-frequently-used eviction policy</a> when the cache is full, and reference counting to ensure regions aren't evicted while being read. The regions are still memory-mapped through the OS, but the blob cache controls which regions exist, which are populated, and when they're reclaimed. On stateful, those decisions are left entirely to the OS.</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/bltd17ed30cb6a59275/6a46944fde977731a4ca54b5/e7a1b50ef019d1b5a12d49c7457d63a026e1edd0-727x496.png" alt="Diagram showing data flow between Remote Object Storage and simdvec. The top box labeled “Remote Object Storage” lists S3, GCS, and Azure Blob, with an arrow marked “fetch on miss” pointing to a larger box labeled “Blob Cache.” Inside the Blob Cache are regions numbered 0–5 plus two empty slots, each 16 MB. Regions 0, 1, 3, 4 are green and labeled “cached,” Region 2 is blue and labeled “in use,” Region 5 is yellow and labeled “evicting,” and two gray boxes are labeled “empty.” A legend explains the color codes. A downward arrow labeled “direct memory” connects Blob Cache to a dark box labeled “simdvec – native SIMD scoring.”" /><p>Crucially, because each region is memory-mapped, the blob cache already holds vector data in exactly the form simdvec needs. But before <a href="https://github.com/elastic/elasticsearch/pull/141718">we built the connection</a>, there was no way to get at it. Every vector comparison was copied into a heap array and handed to a slower scorer. No direct memory pointers, no SIMD, and garbage collection pressure on every call.</p><h2>Unified scoring: one SIMD path for all storage tiers</h2><p>We introduced a new abstraction that lets the scorer safely borrow direct memory from whatever storage layer is underneath, just long enough to run the SIMD computation. If the data is available as direct memory, simdvec's native kernels run. If not (data not yet cached or spanning a region boundary), the scorer falls back to a heap copy. In practice, the fallback is rare.</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/bltadef4930fe865721/6a469452a65a6b4e1dbeff0f/fd9583ffce724665018af46ce0409dc4e0825078-828x259.png" alt="Side‑by‑side comparison diagram labeled “Before” and “After.” The “Before” section shows four boxes: blue “Stateful – local mmap,” green “simdvec – native SIMD ✓,” yellow “Serverless – blob cache,” and red “Java scorer – no SIMD ✗.” Arrows indicate a green “direct ptr” from Stateful to simdvec and a red “heap copy” from Serverless to Java scorer, with the caption “two paths, two implementations.” The “After” section shows three boxes: blue “Stateful – local mmap,” yellow “Serverless – blob cache,” and green “simdvec – native SIMD ✓,” with two green arrows labeled “direct” pointing to simdvec and the caption “one engine, one code path, all tiers.&quot;" /><p>This gave us a single scoring entry point across all tiers:</p><ol><li><p><strong>Stateful</strong> (local disk): The scorer extracts a native pointer from the OS memory map.</p></li><li><p><strong>Blob cache</strong> (serverless, frozen tier): The scorer borrows a direct memory slice from a cache region.</p></li><li><p><strong>Fallback</strong>: The scorer copies bytes to the heap. Rare in practice.</p></li></ol><p>The scorer doesn't know which tier it's running on, and it doesn't need to. It also means we no longer maintain separate scoring implementations; previously, there was a fast native path for stateful and a slower path for everything else. Now every improvement to simdvec benefits all tiers automatically, including its most powerful capability: bulk scoring.</p><h2>Bulk vector scoring across blob cache regions</h2><p>A single query may score thousands of candidate vectors. simdvec's <a href="https://www.elastic.co/search-labs/blog/elasticsearch-vector-search-simdvec-engine#thousands-at-a-time">bulk scoring</a> processes these in batches with multi-accumulator inner loops, query amortization, and cache-line prefetching, up to 4x faster than single-vector alternatives when data exceeds CPU cache.</p><p>Search over an Inverted file (IVF) index is where bulk scoring has the most impact. The query selects a set of candidate posting lists and sweeps through the quantized vectors, scoring them in large batches against the query vector. On stateful, those vectors live in one contiguous memory-mapped file, so bulk scoring resolves them with straightforward pointer arithmetic and scores a batch in a single native call.</p><p>On serverless, a sweep through a posting list may cross blob cache region boundaries. We extended the direct memory abstraction with a bulk access method that resolves multiple vector offsets to their respective cache regions in a single call. If all vectors in the batch are cached and none cross a region boundary, the scorer gets a direct memory slice and passes the whole batch to simdvec's native bulk kernel with the same prefetching and pipelining as stateful. When a vector does cross a boundary, the system falls back to per-vector scoring: still zero-copy, just without the batching benefit. With 16MB regions and 1024-byte vectors, that happens roughly once every 16,000 vectors.</p><p>simdvec's bulk scoring architecture, the key differentiator highlighted in the simdvec <a href="https://www.elastic.co/search-labs/blog/elasticsearch-vector-search-simdvec-engine">benchmarks</a>, now operates on serverless with the same characteristics that make it fast on stateful. So how does it perform in practice?</p><h2>simdvec on Elasticsearch Serverless: vector search lap times</h2><p>We benchmarked with an 18 million vector <a href="https://github.com/elastic/rally-tracks/tree/master/msmarco-v2-vector">MSMARCO</a> dataset at 1024 dimensions, using IVF with Better Binary Quantization (BBQ) 1-bit quantization. All results are on a warm blob cache with the full dataset resident in local cache regions, so we're measuring the scoring path rather than remote fetch latency.</p><p><strong>Throughput.</strong> Under concurrent load, search throughput nearly doubled, jumping from 398 to 739 ops/s. Single-client gains were 23-39%, but the real difference shows up under concurrency: The improvement was 2-3x larger because eliminating heap copies removes the GC pressure and allocation contention that previously throttled concurrent scoring.</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt277d31993bac780c/6a4694559450737b7530d222/fc152731c4b99b85a448e8bd4e01915fefcb55a3-919x533.png" alt="Bar chart titled “Search Throughput — Baseline vs Zero‑Copy (Median ops/s).” It compares median throughput between Baseline (heap‑copy) and Zero‑Copy (DirectAccessInput) across eight knn configurations. Each group shows a gray Baseline bar and a taller green Zero‑Copy bar with percentage improvements labeled above. The y‑axis shows median throughput in operations per second, ranging up to 900. The subhead notes that percentage labels indicate improvement." /><p><strong>Tail latency.</strong> The direct memory path transformed tail latency under load:</p><ul><li><p><em>p99.9</em> dropped from 237 ms to 30 ms (87% reduction).</p></li><li><p><em>p99.99</em> dropped from 9.1 seconds to 55 ms (99.4% reduction).</p></li></ul><p><em>p100</em> dropped from 11.4 seconds to under 100 ms.</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt6002a4d4bf8cbd8f/6a469458c71ec49c21b98453/003874d85261507d95274504a83bc016af0beb13-818x555.png" alt="Line graph titled “Tail Latency Collapse — knn‑10‑10 Multi‑Client.” The chart compares Baseline (heap‑copy) and Zero‑Copy (DirectAccessInput) latency across percentiles p50 to p100 on a logarithmic scale. The red Baseline line rises sharply, while the green Zero‑Copy line remains low. Labels mark key points. The caption notes Rally benchmark details and log scale." /><p>The worst-case outliers that previously took seconds now complete in tens of milliseconds. The heap-copy-induced queueing that caused latency spikes is gone.</p><p>Recall is identical. The same vectors are scored, producing the same results. And we're just getting started.</p><h2>Beyond parity: what Elasticsearch Serverless can do for vector search that stateful can't</h2><p>Reaching parity with stateful was the goal. But the more interesting realization is what the stateless architecture lets us do that stateful can’t.</p><p>On stateful, the OS controls memory-mapped file behavior: which pages stay resident, when to evict, how aggressively to read ahead. The application can offer hints, but they apply to entire file mappings, and the kernel may ignore them. Worse, search and indexing happen concurrently on the same node, so a hint that benefits one access pattern can hurt another. In practice, to balance different needs, you have to be conservative.</p><p>On serverless, two things are fundamentally different. The blob cache manages its own memory-mapped regions with full application-level control. And serverless <a href="https://github.com/elastic/elasticsearch/issues/147626">separates indexing and search onto dedicated tiers</a>: Search nodes never merge, indexing nodes never serve queries. No conflicting access patterns means we can be aggressive with memory advice. Here’s what we’re working on:</p><ul><li><p><strong>Per-region memory advice.</strong> The blob cache knows what type of data each region holds. It can issue <a href="https://github.com/elastic/elasticsearch/issues/147625">random-access hints for rescoring regions</a>, where raw float32 vectors are read in unpredictable order and the kernel’s default readahead would waste memory on pages that will never be used. It can apply sequential readahead for scans through quantized vectors. On the indexing tier, merges read data sequentially, so aggressive readahead brings pages in before they're needed, with no risk of harming concurrent random reads that simply aren't happening on that node.</p></li><li><p><strong>Cache-aware prefetching.</strong> simdvec already prefetches at the CPU cache-line level. On serverless, we can coordinate this with the blob cache's knowledge of region residency, prefetching at multiple levels: remote store to cache, OS pages to RAM, and cache lines to CPU. The blob cache can <a href="https://github.com/elastic/elasticsearch/pull/147964">tell the scorer</a> which regions are resident before scoring begins, avoiding work on data that would trigger a remote fetch.</p></li><li><p><strong>Workload-aware eviction.</strong> The blob cache can prioritize retaining data that vector search depends on: IVF centroid indexes that are checked on every query or quantized vectors that are scored in bulk, over data that's accessed infrequently. The OS page cache evicts based on generic heuristics with no understanding of what the data represents. On serverless, eviction policy can be tuned to the workload.</p></li></ul><p>The blob cache gives us a level of control over the memory hierarchy that the OS page cache simply can’t. This is why we see serverless as the most promising platform for the next generation of vector search performance work. Not just matching stateful, but surpassing it. And vectors are just the beginning.</p><h2>Vector search on Elasticsearch Serverless: what we shipped and what's next</h2><p>simdvec now runs everywhere Elasticsearch runs (stateful, serverless, and frozen tier) with the same native SIMD scoring, the same bulk scoring, and the same off-heap efficiency. The abstraction we built is general-purpose and already wired through every layer in the storage chain, so the same approach could benefit term lookups, aggregations, sorting, and stored field retrieval in the future.</p><p>Elasticsearch Serverless is where we're investing most heavily in vector search performance. Every improvement to simdvec, every optimization to the blob cache, and every new storage-level improvement lands here first. If you're choosing where to run your vector workloads, serverless is the platform that keeps getting faster. You can get started with a free <a href="https://cloud.elastic.co/registration">Elastic Cloud trial</a>.</p>]]></content:encoded>
    <link>https://www.elastic.co/search-labs/blog/vector-search-serverless-simdvec-throughput</link>
    <guid isPermaLink="true">https://www.elastic.co/search-labs/blog/vector-search-serverless-simdvec-throughput</guid>
    <category><![CDATA[Vector Database]]></category>
    <category><![CDATA[Inside Elastic]]></category>
    <dc:creator><![CDATA[Chris Hegarty,Lorenzo Dematte]]></dc:creator>
    <enclosure url="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/bltd17ed30cb6a59275/6a46944fde977731a4ca54b5/e7a1b50ef019d1b5a12d49c7457d63a026e1edd0-727x496.png" length="0" type="image/png"/>
    <pubDate>Thu, 28 May 2026 00:00:00 GMT</pubDate>
  </item>
  <item>
    <title><![CDATA[Cutting Elasticsearch DiskBBQ query quantization time by 5x]]></title>
    <description><![CDATA[See how asymmetric quantization cuts DiskBBQ query quantization overhead from about 20% to 4% with little recall impact.]]></description>
    <content:encoded><![CDATA[<p>Asymmetric quantization cuts the time Elasticsearch DiskBBQ spends quantizing queries by 5x. We discovered that too much time was spent quantizing queries. DiskBBQ started off quantizing queries with the same centroids as the indexed documents. However, we can make this cheaper by quantizing the queries with coarser-grained centroids. This improves query latency with very little observed recall impact in our tests.</p><h2>How DiskBBQ uses two centroid tiers for asymmetric quantization</h2><p>DiskBBQ now uses two centroid tiers (fine-grained document centroids and coarser query centroids) so queries are quantized once per parent centroid instead of once per document centroid.</p><p>The old mental model is "one centroid does everything for a posting list." The new model splits responsibilities:</p><ul><li><p>Document centroids (fine-grained): Still used for posting-list structure and document centering.</p></li><li><p>Query centroids (coarser): A parent centroid reused across multiple document centroids.</p></li></ul><p>So instead of quantizing the query independently for every document centroid we visit, we quantize per parent centroid and reuse that work across all of its children. Since we were already using two-tier clustering logic as the index size grew, it was a natural fit. We can reuse the work we already do during querying.</p><p>These images are a simple representation of our goal: Quantizing per centroid gives us overhead per centroid. Let’s get rid of it!</p><p>The goal is to significantly reduce the number of times we actually need to quantize a given query.</p><h2>The math behind asymmetric BBQ in Elasticsearch</h2><p>To center the data prior to computing quantized query and document vectors,  and , we rewrite the dot product  as  and expand. We can perform exactly the same operation but using different centroids for the query vector  and document vector . Specifically,</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/bltd01f666c5dfa4f24/6a4695b015103586c4202edb/f395654cf3cc965254dfc8d7e66b57743d29cf41-1566x206.png" alt="" /><p>As for standard Better Binary Quantization (BBQ), we quantize  and  in order to estimate the per (document, query) pair component of the dot product. The quantities   and  are scalars so just two extra additions per dot product we compute. For , we compute naturally when finding the nearest centroid. For , this can be stored with the quantized document vectors, which are just 4 bytes overhead. Below, we’ll discuss how to manage the other term on the fly.</p><h3>Asymmetric BBQ in DiskBBQ</h3><p>We cluster the document centroids (using k-means, for example) into  clusters, for  and  the query and document centroid count, respectively. This means there’s a many-to-one mapping from document centroids to query centroids. We’ll denote the document centroids by their index  and define this mapping to the query centroids as →.</p><p>Since there’s a unique query centroid for each document centroid, we only need to cache one value for  per quantized document vector, that is, for each document vector  in posting list , we need to cache  with the quantized document vector.</p><p>When we come to compute the dot products between a query and the document vectors in a cluster, we look up the quantized query vector corresponding to  and we compute  once and use it to process the whole posting list. The quantization process is significantly more expensive than computing the dot product, so this is a big net win.</p><p>The  term is estimated using the usual <a href="https://www.elastic.co/search-labs/blog/scalar-quantization-optimization">BBQ machinery</a>, that is, these vectors will be quantized and the dot product value estimated from the quantized vectors. Then we can use (1) to compute the final dot product estimate. Notice that this means we only need to quantize the query at most  times. Furthermore, we typically visit many centroids from the same parent centroid in a search because they’re close to one another.</p><h3>Euclidean distance corrections for asymmetric quantization</h3><p>For Euclidean, we can write  and treat the  term exactly as above. In fact, there’s a slightly nicer form. Substituting, we have that:</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/bltf7cc1ab13a3dc512/6a4695b274bff735d8a05b44/8faf7cd146c3221f3a3929e07286ceb82ac95a04-1598x122.png" alt="" /><p>We can rewrite this as follows:</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/bltce5ca0055196ceff/6a4695b62d406b3c77ba2bac/91d700d38d0e7842f2efef5d7778a6a34142c384-1172x362.png" alt="" /><p>The corrective terms are the norm of query vector  minus the document centroid , the norm of the document vector  minus the query centroid , and the norm of the difference of query and document centroids. As before,  can be stored as a single float with each document.</p><h2>What changed in DiskBBQ indexing and scoring</h2><p>At indexing/merge time, centroids can be clustered into parent groups when centroid count is large enough. Posting metadata moved from "centroid ordinal + centroid score" to a shape that explicitly carries query-centroid ordinal and document-centroid score. That decoupling is what lets scoring read documents and query centering from different places. For Euclidean, let’s break it down further by our mathematics above:</p><p> &lt;- This is the distance from a “query vector ” to “document centroid ”. We already gather this when we find the nearest centroids during querying. No new work.</p><p> &lt;- This is the distance from “document vector ” to “query centroid ”. However, recalling our <a href="https://www.elastic.co/search-labs/blog/scalar-quantization-optimization">original quantization work</a>, this can simply replace a previously stored float value. No new storage is required.</p><p> &lt;- This is just the distance between query centroid  and document centroid . This is just a single extra floating point value per postings list.</p><p>The practical change for dot product spaces is even simpler; the only correction value change is  being stored instead of .</p><p>These changes don’t introduce new computation costs and marginally reduce storage costs because we no longer quantize queries with document centroids. Those raw centroids don’t need to be present with the posting lists.</p><p>One cost we did add is a small cache of quantized query values. This is to account for clustering edge cases. For example, it's possible that query  is very close to query centroid  but not quite as close as . That said, the actual nearest three document centroids could have a relative order: . So, to prevent the query from being quantized twice, we keep a limited cache of the most recent quantized values for a given query.</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt1aeb75c07434e77f/6a4695b9c71ec47ccbb9846c/d0df2be8b601fb46005667bfa81fc89b2fdaee48-1538x1092.png" alt="Diagram showing a blue circle labeled “q” connected by colored arrows to two dashed oval regions. The green oval contains orange circles labeled dc_0–dc_2 and a green diamond labeled qc_0; and the purple oval contains pink circles labeled dc_3–dc_5 and a purple diamond labeled qc_1. Arrows illustrate relationships between q and the cluster components." /><p>Here’s a visualization of the situation described above. In the typical iteration scenario, we don’t want to risk unnecessarily quantizing the query against the same query centroid multiple times.</p><h2>DiskBBQ asymmetric quantization: performance results</h2><p>The flame graphs below show a before and after comparison. Before, about 20% of the time was spent quantizing queries when we visited each cluster. After our adjustment, it dropped to about 4%.</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt8e2b9d73c7ab998a/6a4695bc91c425d3b5732cb1/a17c3618a29b83d4196088d8422a7ede6eba5c3e-1999x655.png" alt="Flame graph showing computational costs using symmetric quantization, with stacked colored blocks labeled for Elasticsearch and JDK vectorization functions. Each block’s width represents relative processing time, and the tooltip highlights quantization activity within Elasticsearch query code." /><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt7bfd2b99e2fec276/6a4695bf5f1d903f44e3ba97/bb47abf8a7c7da5f5e8990beaaf3319741abfc52-1999x661.png" alt="Flame graph showing reduced computational time spent on quantization after introducing asymmetric quantization, with stacked colored blocks labeled for Elasticsearch and JDK vectorization functions. Each block’s width represents relative processing time, and a tooltip highlights quantization activity within Elasticsearch query code." /><p>Of course, the bulk of the cost is still just scoring the vectors in each cluster. But every little bit helps.</p><p>Here’s a better view of the full end-to-end performance and recall. The data set was 1 million <a href="https://github.com/iai-group/DBpedia-Entity/">DBpedia</a> docs encoded with the <a href="https://huggingface.co/thenlper/gte-base">GTE-Base</a> model. Here, “sec” indicates the number of clusters per secondary (parent) cluster. Note that symmetric quantization is still impacted by the secondary cluster size as it also impacts the two-tier clustering indexing we do already.</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/bltefa647d89e1eb323/6a4695c1a3096d631b9ce7bf/92ec17f1d4ca18a98d3c20b430af72cdf8d8be8a-1260x900.png" alt="Line chart titled “Latency vs Recall Pareto (sec = 16),” comparing asymmetric and symmetric quantization. The blue asymmetric line shows higher recall at each latency value than the red symmetric line, indicating improved latency with minimal recall impact. Axes are labeled “Latency (ms)” and “Recall.”" /><p>However, the impact on our current index structure is still dominated by centroid scoring and scoring vectors in the cluster. Asymmetric quantization removes a frustratingly expensive part of our scoring overhead, but the impact isn’t dramatic given our current structure.</p><h2>What's next for DiskBBQ quantization</h2><p>This simple piece of mathematics decouples our query quantization from our document quantization, giving us better storage efficiency and faster queries. This is in Elasticsearch Serverless now and will be in Elastic Stack version 9.4.0.</p><p>This now means that query quantization time isn’t a direct concern for future decisions. We can make larger index changes without worrying about the consistent overhead of quantization directly with document centroids.</p><p>This was a nerdy one. I hope you survived all the math (and that I copied it all down correctly). It’s always fun to be able to tackle complex problems with simple mathematics, and the results are actually positive in real use cases and data.</p>]]></content:encoded>
    <link>https://www.elastic.co/search-labs/blog/diskbbq-asymmetric-query-quantization</link>
    <guid isPermaLink="true">https://www.elastic.co/search-labs/blog/diskbbq-asymmetric-query-quantization</guid>
    <category><![CDATA[Vector Database]]></category>
    <dc:creator><![CDATA[Benjamin Trent,Thomas Veasey]]></dc:creator>
    <enclosure url="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt9fe4355576b40f9a/6a4695a774bff7d11ba05b40/265ce999fd38f21943d91e29c0bc49ab01f0196d-1999x1546.png" length="0" type="image/png"/>
    <pubDate>Wed, 27 May 2026 00:00:00 GMT</pubDate>
  </item>
  <item>
    <title><![CDATA[How to measure and improve Elasticsearch search recall: from 0.43 to 0.75 with hybrid search]]></title>
    <description><![CDATA[Learn how to measure and improve search recall in Elasticsearch by combining BM25 lexical search with Jina AI vector embeddings, using the rank_eval API to validate the improvement with real numbers.]]></description>
    <content:encoded><![CDATA[<p><a href="https://www.elastic.co/docs/solutions/search/full-text">Lexical search</a> using the <a href="https://www.elastic.co/blog/practical-bm25-part-1-how-shards-affect-relevance-scoring-in-elasticsearch">BM25 ranking algorithm</a> is cheap, fast, and very effective for a wide range of queries. But it has a blind spot: queries that don't share tokens with your documents. In this article, you’ll measure exactly where BM25 falls short. We'll use Elasticsearch's <a href="https://www.elastic.co/docs/reference/elasticsearch/rest-apis/search-rank-eval">ranking evaluation API</a> (<code>rank_eval</code>) and close that gap by adding <a href="https://www.elastic.co/search-labs/es/blog/jina-embeddings-v3-elastic-inference-service">Jina AI embeddings</a> through <a href="https://www.elastic.co/docs/explore-analyze/elastic-inference/eis">Elastic Inference Service</a> (EIS). You’ll see the recall score go from <code>0.43</code> to <code>0.75</code> and understand why.</p><h2>What is recall?</h2><p><a href="https://www.elastic.co/docs/reference/elasticsearch/rest-apis/search-rank-eval#k-recall">Recall</a> measures on a scale from <code>0</code> to <code>1</code> how many of the documents that your users actually want appear somewhere in your search results. If a query should surface three products and your search returns only two of them in the top 10, <code>recall@10 = 0.67</code> for that query. It’s a set-based metric: It doesn’t care about the position of the relevant documents within those <em>k</em> results. A relevant document in position 10 counts the same as one in position 1. Having a high recall means that you’re not losing relevant results.</p><p>
</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt5ffd147b13705680/6a170a6fe8fbce11a539fc22/b13af2a5d0ca055535d8bfe3dfe4b3d1093ee6da-1457x796.png" alt="Venn diagram illustrating how Recall@10 is calculated by showing the overlap between all relevant documents and the top 10 results retrieved by BM25, resulting in a Recall@10 score of 0.40." /><p>The diagram shows two sets: all relevant documents (left) and what BM25 actually retrieved (top 10, right). Only the intersection counts toward recall, <code>prod_1</code> and <code>prod_2</code> were found, while <code>prod_3</code>, <code>prod_4</code>, and <code>prod_6</code> were missed entirely. Result: <code>Recall@10 = 2/5 = </code><strong><code>0.40</code></strong>.</p><h2>Prerequisites</h2><p>Let's get down to business to better understand how recall works. This demonstration uses Python. You can follow along with it on the companion notebook (<a href="https://github.com/elastic/elasticsearch-labs/blob/main/supporting-blog-content/relevance-tuning-improving-recall-adding-vectors/notebook.ipynb">notebook.ipynb</a>), where every code block is a cell ready to run.</p><p>The code provided uses the following:</p><ul><li><p>Elasticsearch 9.3+</p></li><li><p>Python 3.10+</p></li></ul>pip install elasticsearch pandas plotly python-dotenv<ul><li><p>A <code>.env</code> file with your Elasticsearch credentials</p></li></ul>ELASTICSEARCH_URL=https://your-cluster-url
ELASTICSEARCH_API_KEY=your-api-key<h2>The dataset</h2><p>We’ll use a product catalog of 1,000 products, spanning categories such as footwear, electronics, tools, and more.</p><p>Each document has four fields:</p><p>Field</p><p>Type</p><p>`title`</p><p>text</p><p>`description`</p><p>text</p><p>`brand`</p><p>keyword</p><p>`category`</p><p>keyword</p><p>The dataset is loaded from <a href="https://github.com/elastic/elasticsearch-labs/blob/main/supporting-blog-content/relevance-tuning-improving-recall-adding-vectors/dataset.csv"><code>dataset.csv</code></a>.</p><h2>The power and limits of lexical search</h2><p>BM25 is the default ranking algorithm in Elasticsearch and most search engines. It scores documents by how often your query terms appear in them, adjusted for document length and the frequency of those terms across the entire index. You get <a href="https://www.elastic.co/docs/reference/text-analysis/analyzer-reference">analyzers</a> on top: lowercase normalization, stemming, and stopword removal. A query for "running shoes" will match "Running Shoes" and likely "run" as well.</p><p>This works well for a large class of queries:</p><ul><li><p>"running shoes" immediately matches products with those exact tokens in the title.</p></li><li><p>"bluetooth speaker" surfaces portable audio products because the tokens appear verbatim.</p></li></ul><p>The results are deterministic and explainable: A document ranks highly because the query terms appear in it. Debugging relevance is straightforward.</p><h3>Where it breaks</h3><p>Now let’s try these queries against the same catalog:</p><ul><li><p><strong>"skincare routine":</strong> The word "routine" doesn’t appear in any product title. BM25 can partially match on "skincare," but face serums, body oils, and moisturizers are described using terms like "vitamin C," "retinol," or "brightening," none of which overlap with the query. Products that form a complete skincare routine are scattered across the index with no shared token to anchor them.</p></li></ul>ID: B06XX6DS3P, Score: 9.0552, Title: Replenix Retinol Smooth + Tighten Body Lotion - Collagen-Boosting, Regenerating Anti-Aging Body Cream, Reduces Appearance of Stretch Marks, 6.7 oz.

  ID: B08XMPKJ1L, Score: 5.2699, Title: Bio-Oil Skincare Body Oil (Natural) Serum for Scars and Stretchmarks, Face and Body Moisturizer Hydrates Skin, with Organic Jojoba Oil and Vitamin E, For All Skin Types, 6.7 oz

  ID: B01CY764KQ, Score: 5.0057, Title: Nike Up Or Down Men Deodorant - Pack of 2 | Long-Lasting Fragrance, Body Spray Combo for Men | Deodorant for Active Living | Nike Men's Deo Set | Ultimate Odor Protection | Grooming Essentials | Signature Nike Scent | High-Performance Men's Deodorant<ul><li><p><strong>"pet travel accessories":</strong> This is a use-case grouping, not a product category. A dog sling carrier, a pet car seat, and a travel crate are all relevant, but their descriptions talk about portability, safety, and comfort rather than "travel accessories." BM25 matches "pet" broadly but has no signal to distinguish travel-specific products from the rest of the pet catalog.</p></li></ul>ID: B0BVV7BKTW, Score: 7.4371, Title: Large Foldable Travel Duffel Bag with Shoes Compartment

ID: B07TNPHYNV, Score: 6.6455, Title: 40 Pieces Christmas Bronze Jingle Bells Craft Small Bells

ID: B08R8FRW53, Score: 6.6335, Title: CUBY Dog and Cat Sling Carrier
ID: B08QMCQYGM, Score: 6.5259, Title: YTFGGY Whiteboard Pinstripe Tape 6 Rolls 1/8"
ID: B0CP3LQSWM, Score: 6.2994, Title: Portable Dog Water Bottle 32 Oz<p>This is a <strong>recall problem</strong>. The relevant documents exist in your index. BM25 just cannot find them because the user's words and the document's words do not match closely enough.</p><p>Adding synonyms helps for known cases. But you cannot enumerate every way a user might express an intent. That is where vectors come in.</p><h2>Why you should measure recall</h2><p>Before fixing a problem, you need to quantify it.</p><p><a href="https://www.elastic.co/docs/reference/elasticsearch/rest-apis/search-rank-eval#k-recall"><strong>Recall@k</strong></a> measures how many of the documents that your users actually want appear somewhere in your search results. Formally:</p>Recall@k = (relevant documents found in top k) / (total relevant documents)<p><a href="https://www.elastic.co/docs/reference/elasticsearch/rest-apis/search-rank-eval#k-precision"><strong>Precision@k</strong></a> measures the top k results and how many are actually relevant:</p>Precision@k = (relevant documents in top k) / k<p>High precision means that the results you do return are good. In ecommerce, missing a relevant product (low recall) is often worse than showing a slightly imperfect result (lower precision), because a hidden product is a lost sale.</p><p>Elasticsearch's <code>rank_eval</code> API lets you measure both systematically. You provide a list of queries, each with a set of rated documents, and Elasticsearch computes the metrics for you across all queries.</p><h2>Setting up the evaluation</h2><p>The <code>rank_eval</code> API needs a <strong>ratings dataset</strong>: a mapping of queries to the documents that are relevant for each one, along with a relevance grade (0 = not relevant, 1 = relevant, 2 = highly relevant).</p><p>In the notebook, this is the <a href="https://www.elastic.co/docs/solutions/search/ranking/learning-to-rank-ltr#learning-to-rank-judgement-list">judgments list</a>:</p>judgments = [
    # Query 1: "running shoes" BM25 handles well (tokens appear in product titles) 
    {"query_id": "q1", "doc_id": "B09NQJFRW6", "grade": 2, "query": "running shoes"},
    {"query_id": "q1", "doc_id": "B08JMD4LMM", "grade": 2, "query": "running shoes"},
    {"query_id": "q1", "doc_id": "B08VRJ6F2Q", "grade": 2, "query": "running shoes"},
    {"query_id": "q1", "doc_id": "B07S8NRRWR", "grade": 2, "query": "running shoes"},
    {"query_id": "q1", "doc_id": "B01HD620I8", "grade": 2, "query": "running shoes"},
    {"query_id": "q1", "doc_id": "B07DX86321", "grade": 2, "query": "running shoes"},
    {"query_id": "q1", "doc_id": "B0968YVLQ8", "grade": 1, "query": "running shoes"},
    {"query_id": "q1", "doc_id": "B093QJ39ZS", "grade": 1, "query": "running shoes"},
    {"query_id": "q1", "doc_id": "B096FGSC39", "grade": 1, "query": "running shoes"},
    {"query_id": "q1", "doc_id": "B01GVQWVV2", "grade": 1, "query": "running shoes"},

    # Query 2: "skincare routine" intent-based, "routine" never appears in product titles
    {"query_id": "q2", "doc_id": "B08XMPKJ1L", "grade": 2, "query": "skincare routine"},
    {"query_id": "q2", "doc_id": "B0BN3WQB92", "grade": 2, "query": "skincare routine"},
    {"query_id": "q2", "doc_id": "B0BT7B7P5T", "grade": 2, "query": "skincare routine"},
    {"query_id": "q2", "doc_id": "B00NPA2WEY", "grade": 2, "query": "skincare routine"},
    {"query_id": "q2", "doc_id": "B06XX6DS3P", "grade": 1, "query": "skincare routine"},
    {"query_id": "q2", "doc_id": "B07PDRD1KT", "grade": 1, "query": "skincare routine"},
    {"query_id": "q2", "doc_id": "B074J7869B", "grade": 1, "query": "skincare routine"},
    {"query_id": "q2", "doc_id": "B08JV31QW4", "grade": 1, "query": "skincare routine"},
    {"query_id": "q2", "doc_id": "B00K3TVJMQ", "grade": 1, "query": "skincare routine"},

    # Query 3: "study desk setup" intent-based, products are desks/stands/organizers
    {"query_id": "q3", "doc_id": "B08CS35J2T", "grade": 2, "query": "study desk setup"},
    {"query_id": "q3", "doc_id": "B09B3LFDXJ", "grade": 2, "query": "study desk setup"},
    {"query_id": "q3", "doc_id": "B07W58LMND", "grade": 1, "query": "study desk setup"},
    {"query_id": "q3", "doc_id": "B0CHYDX91L", "grade": 1, "query": "study desk setup"},

    # Query 4: "pet travel accessories" use-case grouping, products are carriers/crates/seats
    {"query_id": "q4", "doc_id": "B08R8FRW53", "grade": 2, "query": "pet travel accessories"},
    {"query_id": "q4", "doc_id": "B01MYUYX33", "grade": 2, "query": "pet travel accessories"},
    {"query_id": "q4", "doc_id": "B003C5RKE4", "grade": 2, "query": "pet travel accessories"},
    {"query_id": "q4", "doc_id": "B09GF8GBF6", "grade": 1, "query": "pet travel accessories"},
    {"query_id": "q4", "doc_id": "B0CP3LQSWM", "grade": 1, "query": "pet travel accessories"},
]<p>The mix is intentional: <code>q1</code> is a query that BM25 handles well (exact tokens in product titles), while <code>q2</code>, <code>q3</code>, and <code>q4</code> are intent-based queries where the user's intent is expressed as a concept rather than specific product keywords.</p><h2>Measuring BM25 baseline recall</h2><p>First, set up the Elasticsearch client and index the raw text data:</p>import os
import json
import pandas as pd
import plotly.graph_objects as go
from elasticsearch import Elasticsearch, helpers
from dotenv import load_dotenv

load_dotenv()

es = Elasticsearch(
    os.getenv("ELASTICSEARCH_URL"),
    api_key=os.getenv("ELASTICSEARCH_API_KEY")
)

INDEX_NAME = "ecommerce-products"<p>Now build the <code>rank_eval</code> request for BM25. Each request in the list combines a query with its ratings:</p>judgments_df = pd.DataFrame(judgments)

bm25_requests = []
for query_id, query_text in (
    judgments_df[["query_id", "query"]].drop_duplicates().values
):
    relevant_docs = judgments_df[judgments_df["query_id"] == query_id]
    ratings = [
        {"_index": INDEX_NAME, "_id": row["doc_id"], "rating": row["grade"]}
        for _, row in relevant_docs.iterrows()
    ]

    bm25_requests.append({
        "id": query_id,
        "request": {
            "query": {
                "multi_match": {
                    "query": query_text,
                    "fields": ["title", "description"]
                }
            }
        },
        "ratings": ratings,
    })

bm25_eval = {
    "requests": bm25_requests,
    "metric": {"recall": {"k": 10, "relevant_rating_threshold": 1}},
}

bm25_result = es.rank_eval(index=INDEX_NAME, body=bm25_eval)
print("BM25 Recall@10:", bm25_result.body["metric_score"])<p>Result:</p>BM25 Recall@10: 0.43<p><code>0.43</code> means that across all four queries, BM25 finds only 43% of the documents it should find. The shortfall is concentrated in the intent-based queries: "skincare routine" misses face serums and body oils because "routine" never appears in product titles, and "pet travel accessories" retrieves off-topic pet products while missing carriers and crates described in terms of portability and safety rather than "travel accessories."</p><p>This is our baseline. Now we have a number to beat.</p><h2>Adding vector search with Jina embeddings</h2><p><a href="https://www.elastic.co/docs/solutions/search/vector"><code>Vector search</code></a> encodes documents and queries as high-dimensional vectors, a type of vector made up of hundreds or thousands of numerical values, each encoding a specific feature of the data it represents. Documents with similar meaning end up close together in vector space, even if they share no words. "Gym equipment" and "dumbbell set" will be nearby because the concepts are related. I chose Elasticsearch as my vector database because it supports hybrid search, giving me both semantic understanding and keyword precision out of the box.</p><p><a href="https://www.elastic.co/docs/explore-analyze/elastic-inference/eis">EIS</a> includes out-of-the-box support for embedding models through its <a href="https://www.elastic.co/docs/api/doc/elasticsearch/group/endpoint-inference">inference API</a>.</p><h3>Step 1: Using Jina embeddings v5 as an inference endpoint</h3>INFERENCE_ENDPOINT_ID = ".jina-embeddings-v5-text-small"<p>If your cluster has GPU resources (available in Elastic Cloud and Elasticsearch 9.3+), the embeddings are generated on GPU, which is significantly faster than CPU inference and removes the performance trade-off that historically made vectors expensive at scale.</p><p>Why Jina embeddings specifically? <a href="https://www.elastic.co/search-labs/blog/jina-embeddings-v5-text">jina-embeddings-v5-text</a> is a multilingual model (119+ languages) with a 32,000-token context window and support for task-specific <a href="https://arxiv.org/abs/2106.09685">Low-Rank Adaptation (LoRA) adapters</a>. It works well for short product descriptions out of the box. Read more about <code>jina-embeddings-v5-text</code> model <a href="https://huggingface.co/jinaai/jina-embeddings-v5-text-small">here</a>.</p><h3>Step 2: Create the index with a semantic field</h3>index_mappings = {
    "mappings": {
        "properties": {
            "title": {"type": "text", "copy_to": "semantic_field"},
            "description": {"type": "text", "copy_to": "semantic_field"},
            "brand": {"type": "keyword"},
            "category": {"type": "keyword"},
            "semantic_field": {
                "type": "semantic_text",
                "inference_id": INFERENCE_ENDPOINT_ID,
            },
        }
    }
}

if not es.indices.exists(index=INDEX_NAME):
    es.indices.create(index=INDEX_NAME, body=index_mappings)
    print(f"Created index: {INDEX_NAME}")<p>The <a href="https://www.elastic.co/docs/solutions/search/semantic-search/semantic-search-semantic-text"><code>semantic_text</code></a> field type is the key here. It’s a higher-level abstraction over <a href="https://www.elastic.co/docs/reference/elasticsearch/mapping-reference/dense-vector"><code>dense_vector</code></a>: You point it at an inference endpoint, and Elasticsearch takes care of generating embeddings automatically.</p><p>The <a href="https://www.elastic.co/docs/reference/elasticsearch/mapping-reference/copy-to"><code>copy_to</code></a> property on <code>title</code> and <code>description</code> means content from both fields flows into <a href="https://www.elastic.co/docs/reference/elasticsearch/mapping-reference/semantic-text"><code>semantic_field</code></a> for embedding, so a single vector captures the full product representation.</p><h3>Step 3: Index the products</h3>def bulk_index(products, index_name):
    actions = []
    for product in products:
        doc_id = product.get("_id")
        source = {k: v for k, v in product.items() if k != "_id"}
        action = {"_index": index_name, "_source": source}
        if doc_id:
            action["_id"] = doc_id
        actions.append(action)

    success, failed = helpers.bulk(es, actions, raise_on_error=False)
    if failed:
        for error in failed:
            print(f"Error: {error}")
    else:
        print(f"Successfully indexed {success} documents")

bulk_index(products, INDEX_NAME)<p>At index time, Elasticsearch calls the inference endpoint for each document and stores the resulting embedding in <code>semantic_field</code>. No extra code on your side.</p><h2>Hybrid search: Combining BM25 and vectors with RRF</h2><p>Adding vectors improves recall, but using vectors alone risks losing precision on exact-match queries; "running shoes" should still rank verbatim matches first. Hybrid search retains the lexical component specifically to preserve that precision.</p><p>Hybrid search with <a href="https://www.elastic.co/docs/reference/elasticsearch/rest-apis/reciprocal-rank-fusion">Reciprocal Rank Fusion</a> (RRF) keeps the best of both:</p><ul><li><p>BM25 handles exact and near-exact queries with high precision.</p></li><li><p>Semantic search handles intent-based and multilingual queries with high recall.</p></li><li><p>RRF combines the two ranked lists into a single ranking.</p></li></ul><p>The RRF formula assigns each document a score based on its rank in each result list:</p>score = sum(1 / (rank_constant + rank))<p>A document that ranks highly in both lists gets a higher combined score. The <code>rank_constant</code> controls how much weight lower-ranked documents receive.</p>hybrid_requests = []

for query_id, query_text in (
    judgments_df[["query_id", "query"]].drop_duplicates().values
):
    relevant_docs = judgments_df[judgments_df["query_id"] == query_id]
    ratings = [
        {"_index": INDEX_NAME, "_id": row["doc_id"], "rating": row["grade"]}
        for _, row in relevant_docs.iterrows()
    ]

    hybrid_requests.append({
        "id": query_id,
        "request": {
            "retriever": {
                "rrf": {
                    "retrievers": [
                        {
                            "standard": {
                                "query": {
                                    "multi_match": {
                                        "query": query_text,
                                        "fields": ["title", "description"],
                                    }
                                }
                            }
                        },
                        {
                            "standard": {
                                "query": {
                                    "match": {
                                        "semantic_field": {"query": query_text}
                                    }
                                }
                            }
                        },
                    ],
                    "rank_window_size": 50,
                    "rank_constant": 5,
                }
            }
        },
        "ratings": ratings,
    })

hybrid_eval = {
    "requests": hybrid_requests,
    "metric": {"recall": {"k": 10, "relevant_rating_threshold": 1}},
}

hybrid_result = es.rank_eval(index=INDEX_NAME, body=hybrid_eval)
print("Hybrid Recall@10:", hybrid_result.body["metric_score"])<p>Result:</p>Hybrid Recall@10: 0.75<p>Hybrid improves substantially over BM25 (<code>0.43</code>) and preserves precision for exact-match queries like "running shoes."</p><h2>Results: Before and after</h2><p>Here’s the full comparison across all three approaches:</p>methods = {
    "BM25 (Lexical)": bm25_requests,
    "Hybrid (BM25 + Vectors)": hybrid_requests,
}

recall_metric = {"recall": {"k": 10, "relevant_rating_threshold": 1}}

comparison_data = []
for method_name, requests in methods.items():
    result = es.rank_eval(
        index=INDEX_NAME,
        body={"requests": requests, "metric": recall_metric}
    )
    comparison_data.append({
        "method": method_name,
        "recall@10": result.body["metric_score"]
    })

comparison_df = pd.DataFrame(comparison_data)
print(comparison_df.to_string(index=False))<p>Result:</p><p>Method</p><p>Recall@10</p><p>BM25 (Lexical)</p><p>0.43</p><p>Hybrid (BM25 + Vectors)</p><p>0.75</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt5a1d72b57056fe64/6a170a71c1e8a56c58f882ab/e49f6c10516b0a48a0ad75962c6590ee07311407-700x500.png" alt="Bar chart comparing Recall@10 between BM25 lexical search and hybrid search combining BM25 with vectors, showing hybrid search achieving significantly higher recall." /><p>Breaking it down by query:</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt871347f754c866d0/6a170a73839dfa40abdcfeb4/40e36dcb7b34cbf4649c512bcb60cef60f1778a6-700x500.png" alt="Grouped bar chart comparing Recall@10 between BM25 lexical and hybrid search across four product queries, showing hybrid search consistently outperforming lexical search for each query." /><h2>Conclusion</h2><p>Throughout this post, we saw that BM25 lexical search is reliable when users type exact queries, but it loses recall when they search by intent rather than keywords. Using <code>rank_eval</code>, we established a reproducible baseline to measure that gap with real numbers. From there, we added a <code>semantic_text</code> field powered by Jina embeddings and ran the evaluation again. The result: Hybrid search improved recall from <code>0.43</code> to <code>0.75</code> while preserving precision on exact-match queries, though the actual margin will depend on your query mix.</p><p>The pattern scales beyond this example: Collect judgments from your users' actual queries, run <code>rank_eval</code> as a baseline, add <code>semantic_text</code>, and measure again. You'll know exactly what improved and by how much.</p><h2>Next steps</h2><ul><li><p>Dive deeper into recall and vector search: <a href="https://www.elastic.co/search-labs/blog/recall-vector-search-quantization">Recall and vector search quantization</a> by Jeff Vestal</p></li><li><p>Add reranking for even better precision on the top results</p></li><li><p>Explore <a href="https://www.elastic.co/guide/en/elasticsearch/reference/current/rrf.html">Elasticsearch hybrid search documentation</a></p></li><li><p>Read more about the <a href="https://www.elastic.co/guide/en/elasticsearch/reference/current/search-rank-eval.html"><code>rank_eval</code></a><a href="https://www.elastic.co/guide/en/elasticsearch/reference/current/search-rank-eval.html"> API</a></p></li></ul>]]></content:encoded>
    <link>https://www.elastic.co/search-labs/blog/elasticsearch-relevance-tuning-improve-recall</link>
    <guid isPermaLink="true">https://www.elastic.co/search-labs/blog/elasticsearch-relevance-tuning-improve-recall</guid>
    <category><![CDATA[Hybrid Search]]></category>
    <category><![CDATA[Vector Database]]></category>
    <dc:creator><![CDATA[Jeffrey Rengifo]]></dc:creator>
    <enclosure url="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt37c9d2971b5a2db3/6a170a75cf4f254223b2d149/492c9b5432a2b9e40cebb3b60f0df019a8c7bf6d-1280x720.png" length="0" type="image/png"/>
    <pubDate>Mon, 04 May 2026 00:00:00 GMT</pubDate>
  </item>
  <item>
    <title><![CDATA[LINQ to Elasticsearch ES|QL: Write C#, query Elasticsearch]]></title>
    <description><![CDATA[Exploring the new LINQ to Elasticsearch ES|QL provider in the Elasticsearch .NET client, which allows you to write C# code that’s automatically translated to ES|QL queries.]]></description>
    <content:encoded><![CDATA[<p>Starting with <strong>v9.3.4</strong> and <strong>v8.19.18</strong>, the Elasticsearch .NET client includes a <a href="https://learn.microsoft.com/en-us/dotnet/csharp/linq/">Language Integrated Query (LINQ) </a>provider that translates C# LINQ expressions into <a href="https://www.elastic.co/guide/en/elasticsearch/reference/current/esql.html">Elasticsearch Query Language (ES|QL)</a> queries at runtime. Instead of writing ES|QL strings by hand, you compose queries using <code>Where</code>, <code>Select</code>, <code>OrderBy</code>, <code>GroupBy</code>, and other standard operators. The provider takes care of translation, parameterization, and result deserialization, including per-row streaming that keeps memory usage constant, regardless of result set size.</p><h2>Your first query</h2><p>Start by defining a plain old CLR object (POCO) that maps to your Elasticsearch index. Property names are resolved to ES|QL column names through standard <code>System.Text.Json</code> attributes, like <code>[JsonPropertyName]</code>, or through a configured <code>JsonNamingPolicy</code>. The same <a href="https://www.elastic.co/docs/reference/elasticsearch/clients/dotnet/source-serialization">source serialization</a> rules that apply across the rest of the client apply here as well.</p>using System.Text.Json.Serialization;

public class Product
{
    [JsonPropertyName("product_id")]
    public string Id { get; set; }

    public string Name { get; set; }

    public string Brand { get; set; }

    [JsonPropertyName("price_usd")]
    public double Price { get; set; }

    [JsonPropertyName("in_stock")]
    public bool InStock { get; set; }
}<p>With the type in place, a query looks like this:</p>var minPrice = 100.0;
var brand = "TechCorp";

await foreach (var product in client.Esql.QueryAsync&lt;Product&gt;(q =&gt; q
    .From("products")
    .Where(p =&gt; p.InStock &amp;&amp; p.Price &gt;= minPrice &amp;&amp; p.Brand == brand)
    .OrderByDescending(p =&gt; p.Price)
    .Take(10)))
{
    Console.WriteLine($"{product.Name}: ${product.Price}");
}<p>The provider translates this into the following ES|QL:</p><p>A few details to note:</p><ul><li><p><strong>Property name resolution:</strong> <code>p.Price</code> becomes <code>price_usd</code> because of the <code>[JsonPropertyName]</code> attribute, and <code>p.Brand</code> becomes <code>brand</code> following the default camelCase naming policy.</p></li><li><p><strong>Parameter capturing:</strong> The C# variables <code>minPrice</code> and <code>brand</code> are captured as named parameters (<code>?minPrice</code>, <code>?brand</code>). They’re sent separately from the query string in the JSON payload, which prevents injection and enables server-side query plan caching.</p></li><li><p><strong>Streaming:</strong> <code>QueryAsync&lt;T&gt;</code> returns <code>IAsyncEnumerable&lt;T&gt;</code>. Rows are materialized one at a time as they arrive from Elasticsearch.</p></li></ul><p>You can also inspect the generated query and its parameters without executing it:</p>var query = client.Esql.CreateQuery&lt;Product&gt;()
    .Where(p =&gt; p.InStock &amp;&amp; p.Price &gt;= minPrice &amp;&amp; p.Brand == brand)
    .OrderByDescending(p =&gt; p.Price)
    .Take(10);

Console.WriteLine(query.ToEsqlString());
// FROM products | WHERE (in_stock == true AND price_usd &gt;= 100) | SORT price_usd DESC | LIMIT 10

Console.WriteLine(query.ToEsqlString(inlineParameters: false));
// FROM products | WHERE (in_stock == true AND price_usd &gt;= ?minPrice AND brand == ?brand) | SORT price_usd DESC | LIMIT 10

var parameters = query.GetParameters();
// { "minPrice": 100.0, "brand": "TechCorp" }<h2>How does this work? A quick LINQ refresher</h2><p>The mechanism that makes LINQ providers possible is the distinction between <code>IEnumerable&lt;T&gt;</code> and <code>IQueryable&lt;T&gt;</code>.</p><p>When you call <code>.Where(p =&gt; p.Price &gt; 100)</code> on an <code>IEnumerable&lt;T&gt;</code>, the lambda compiles to a <code>Func&lt;Product, bool&gt;</code>, a regular delegate that the runtime executes in-process. This is LINQ-to-Objects.</p><p>When you call the same method on an <code>IQueryable&lt;T&gt;</code>, the C# compiler wraps the lambda in an <code>Expression&lt;Func&lt;Product, bool&gt;&gt;</code> instead. This is a data structure that represents the <em>structure</em> of the code rather than its executable form. The expression tree can be inspected, analyzed, and translated into another language at runtime.</p>// IEnumerable: the lambda is a compiled delegate
IEnumerable&lt;Product&gt; local = products.Where(p =&gt; p.Price &gt; 100);

// IQueryable: the lambda is an expression tree, a data structure
IQueryable&lt;Product&gt; remote = queryable.Where(p =&gt; p.Price &gt; 100);<p>The <code>IQueryProvider</code> interface is the extension point. Any provider can implement <code>CreateQuery&lt;T&gt;</code> and <code>Execute&lt;T&gt;</code> to translate these expression trees into a target language. Entity Framework uses this to emit SQL. The LINQ to ES|QL provider uses it to emit ES|QL.</p><p>The expression tree for the query above looks like this:</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt521838e8b9c36649/6a1705b1839dfa5f40dcfdfe/f864cd18a390831f8d28503a29b5835efb1842f7-1000x720.png" alt="Expression tree for the example query." /><p><em>Expression tree for the example query.</em></p><p>The tree is nested inside out: <code>Take</code> wraps <code>OrderByDescending</code>, which wraps <code>Where</code>, which wraps <code>From</code>, which wraps the root <code>EsqlQueryable&lt;Product&gt;</code> constant. The <code>Where</code> predicate is itself a subtree of <code>BinaryExpression</code> nodes for the <code>&amp;&amp;</code>, <code>&gt;=</code>, and <code>==</code> operators, with <code>MemberExpression</code> leaves for property accesses and closure captures for the <code>minPrice</code> and <code>brand</code> variables. This is the data structure that the provider walks to produce the final ES|QL.</p><h2>Under the hood: The translation pipeline</h2><p>The path from a LINQ expression to query results follows a six-stage pipeline:</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt930670a505dd61ea/6a1705b3b339d58a54769ecf/2a2c772b63d720f61fc9a28b2f85668fa2db8d38-1999x1036.png" alt="Translation pipeline overview." /><p><em>Translation pipeline overview.</em></p><h3>1. Expression tree capture</h3><p>When you chain <code>.Where()</code>, <code>.OrderBy()</code>, <code>.Take()</code> and other operators on an <code>IQueryable&lt;T&gt;</code>, the standard LINQ infrastructure builds an expression tree. <code>EsqlQueryable&lt;T&gt;</code> implements <code>IQueryable&lt;T&gt;</code> and delegates to <code>EsqlQueryProvider</code>.</p><h3>2. Translation</h3><p>When the query is executed (by enumerating, calling <code>ToList()</code>, or using <code>await foreach)</code>, the <code>EsqlExpressionVisitor</code> walks the expression tree inside out. It dispatches each LINQ method call to a specialized visitor:</p><p>Visitor</p><p>Translates</p><p>Into</p><p>WhereClauseVisitor</p><p>.Where(predicate)</p><p>WHERE condition</p><p>SelectProjectionVisitor</p><p>.Select(selector)</p><p>EVAL + KEEP + RENAME</p><p>GroupByVisitor</p><p>.GroupBy().Select()</p><p>STATS ... BY</p><p>OrderByVisitor</p><p>.OrderBy() / .ThenBy()</p><p>SORT field [ASC\|DESC]</p><p>EsqlFunctionTranslator</p><p>EsqlFunctions.*, Math.*, string methods</p><p>80+ ES|QL functions</p><p>During translation, C# variables referenced in expressions are captured as named parameters.</p><h3>3. Query model</h3><p>The visitors don’t produce strings directly. Instead, they produce <code>QueryCommand</code> objects, an immutable intermediate representation. A <code>FromCommand</code>, a <code>WhereCommand</code>, a <code>SortCommand</code>, and a <code>LimitCommand</code>, each representing one ES|QL processing command. These are collected into an <code>EsqlQuery</code> model.</p><p></p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt788c9936976f2f62/6a1705b50e2e4910da419ff0/2adc349b6cf655b96b7b3e826a134e8a17fe42fd-1999x1036.png" alt="Query model and command pattern." /><p><em>Query model and command pattern.</em></p><p>This intermediate model is decoupled from both the expression tree and the output format. It can be inspected, intercepted (via <code>IEsqlQueryInterceptor</code>), or modified before formatting.</p><h3>4. Formatting</h3><p><code>EsqlFormatter</code> visits each <code>QueryCommand</code> in order and produces the final ES|QL string. Each command becomes one line, separated by the pipe (|) operator that ES|QL uses to chain processing commands. Identifiers containing special characters are automatically escaped with backticks.</p><h3>5. Execution</h3><p>The formatted ES|QL string and captured parameters are sent to Elasticsearch’s <code>/_query</code> endpoint as a JSON payload. The <code>IEsqlQueryExecutor</code> interface abstracts the transport layer, which is where the layered package architecture comes into play.</p><h3>6. Materialization</h3><p><code>EsqlResponseReader</code> streams the JSON response without buffering the entire result set into memory. A <code>ColumnLayout</code> tree, precomputed once per query, maps flat ES|QL column names (like <code>address.street</code>, <code>address.city</code>) to nested POCO properties. Each row is assembled into a <code>T</code> instance and yielded one at a time via <code>IEnumerable&lt;T&gt;</code> or <code>IAsyncEnumerable&lt;T&gt;</code>.</p><h2>The layered architecture</h2><p>The LINQ to ES|QL functionality is split across three packages:</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt662bd0dd8861b6b6/6a1705b7a929cf7086ae08a2/41b8aae860ecdc2480edcb1c1d4cc9b03cfb78c9-1999x1036.png" alt="Package architecture." /><p><em>Package architecture.</em>
<a href="https://www.nuget.org/packages/Elastic.Esql"><strong><code>Elastic.Esql</code></strong></a> is the pure translation engine. It has zero HTTP dependencies and contains the expression visitors, query model, formatter, and response reader. You can use it stand alone to build and inspect ES|QL queries without an Elasticsearch connection, which is useful for testing, query logging, or building your own execution layer.</p>// Translation-only: no Elasticsearch connection needed
var provider = new EsqlQueryProvider();
var query = new EsqlQueryable&lt;Product&gt;(provider)
    .From("products")
    .Where(p =&gt; p.InStock)
    .OrderByDescending(p =&gt; p.Price);

Console.WriteLine(query.ToEsqlString());
// FROM products | WHERE in_stock == true | SORT price_usd DESC<p><a href="https://www.nuget.org/packages/Elastic.Clients.Esql"><strong><code>Elastic.Clients.Esql</code></strong></a> is a lightweight stand-alone ES|QL client. It adds HTTP execution on top of <code>Elastic.Esql</code> via <code>Elastic.Transport</code>. If your application only needs ES|QL and none of the other Elasticsearch APIs, this is the minimal dependency option.</p><p><a href="https://www.nuget.org/packages/Elastic.Clients.Elasticsearch"><strong><code>Elastic.Clients.Elasticsearch</code></strong></a> is the full Elasticsearch .NET client. It also builds on <code>Elastic.Esql</code> and exposes the LINQ provider through the <code>client.Esql</code> namespace. This is the recommended entry point for most applications.</p><p>Both execution-layer packages provide their own implementation of <code>IEsqlQueryExecutor</code>, the strategy interface that bridges translation and transport.</p><p>All three packages are compatible with Native AOT when used with a source-generated <code>JsonSerializerContext</code>. For the full client, see the <a href="https://www.elastic.co/docs/reference/elasticsearch/clients/dotnet/source-serialization#native-aot">Native AOT documentation</a>.</p><h2>Beyond the basics</h2><p>The example above covered filtering, sorting, and pagination. The provider supports a broader set of operations.</p><h3>Aggregations</h3><p><code>GroupBy</code>, combined with aggregate functions in <code>Select</code>, translates to ES|QL <a href="https://www.elastic.co/docs/reference/query-languages/esql/commands/stats-by"><code>STATS ... BY</code></a>:</p>var stats = client.Esql.Query&lt;Product, object&gt;(q =&gt; q
    .GroupBy(p =&gt; p.Brand)
    .Select(g =&gt; new
    {
        Brand = g.Key,
        Count = g.Count(),
        AvgPrice = g.Average(p =&gt; p.Price),
        MaxPrice = g.Max(p =&gt; p.Price)
    }));

// -&gt; FROM products | STATS COUNT(*), AVG(price_usd), MAX(price_usd) BY brand<h3>Projections</h3><p><code>Select</code>, with anonymous types generates <a href="https://www.elastic.co/docs/reference/query-languages/esql/commands/eval"><code>EVAL</code></a>, <a href="https://www.elastic.co/docs/reference/query-languages/esql/commands/keep"><code>KEEP</code></a>, and <a href="https://www.elastic.co/docs/reference/query-languages/esql/commands/rename"><code>RENAME</code></a> commands:</p>var query = client.Esql.CreateQuery&lt;Product&gt;()
    .Select(p =&gt; new { ProductName = p.Name, p.Price, p.InStock });

// -&gt; FROM products | KEEP name, price_usd, in_stock | RENAME name AS ProductName<h3>Rich function library</h3><p>Over 80 ES|QL functions are available through the <code>EsqlFunctions</code> class, covering date/time, string, math, IP, pattern matching, and scoring. Standard <code>Math.*</code> and <code>string.*</code> methods are also translated:</p>.Where(p =&gt; p.Name.Contains("Pro"))       // -&gt; WHERE name LIKE "*Pro*"
.Where(p =&gt; EsqlFunctions.CidrMatch(      // -&gt; WHERE CIDR_MATCH(ip, "10.0.0.0/8")
    p.IpAddress, "10.0.0.0/8"))<h3>LOOKUP JOIN</h3><p>Cross-index lookups translate to ES|QL <a href="https://www.elastic.co/docs/reference/query-languages/esql/commands/lookup-join"><code>LOOKUP JOIN</code></a>:</p>var enriched = client.Esql.Query&lt;Product, object&gt;(q =&gt; q
    .LookupJoin&lt;Product, CategoryLookup, string, object&gt;(
        "category-lookup-index",
        product =&gt; product.Id,
        category =&gt; category.CategoryId,
        (product, category) =&gt; new { product.Name, category!.CategoryLabel }));<h3>Raw ES|QL escape hatch</h3><p>For ES|QL features not yet covered by the LINQ provider, you can append raw fragments:</p>var results = client.Esql.Query&lt;Product&gt;(q =&gt; q
    .Where(p =&gt; p.InStock)
    .RawEsql("| EVAL discounted = price_usd * 0.9"));<h3>Server-side async queries</h3><p>For long-running queries, submit them for background processing on the server:</p>await using var asyncQuery = await client.Esql.SubmitAsyncQueryAsync&lt;Product&gt;(
    q =&gt; q.Where(p =&gt; p.InStock),
    asyncQueryOptions: new EsqlAsyncQueryOptions
    {
        WaitForCompletionTimeout = TimeSpan.FromSeconds(5),
        KeepAlive = TimeSpan.FromMinutes(10)
    });

await asyncQuery.WaitForCompletionAsync();
await foreach (var product in asyncQuery.AsAsyncEnumerable())
    Console.WriteLine(product.Name);<p>Server-side async queries are especially useful for long-running analytical queries / large dataset processing that might exceed typical timeout thresholds, or in timeout-sensitive environments with load balancers, API gateways, or proxies that enforce strict HTTP timeouts. Async queries avoid connection drops by decoupling submission from result retrieval.</p><h2>Getting started</h2><p>LINQ to ES|QL is available starting from:</p><ul><li><p><strong>Elastic.Clients.Elasticsearch v9.3.4</strong> (9.x branch)</p></li><li><p><strong>Elastic.Clients.Elasticsearch v8.19.18</strong> (8.x branch)</p></li></ul><p>Install from NuGet:</p><p><code>dotnet add package Elastic.Clients.Elasticsearch</code></p><p>The entry points are on <code>client.Esql</code>:</p><p>Method</p><p>Returns</p><p>Use case</p><p>Query&lt;T&gt;(...)</p><p>IEnumerable&lt;T&gt;</p><p>Synchronous execution</p><p>QueryAsync&lt;T&gt;(...)</p><p>IAsyncEnumerable&lt;T&gt;</p><p>Async streaming</p><p>CreateQuery&lt;T&gt;()</p><p>IEsqlQueryable&lt;T&gt;</p><p>Advanced composition and inspection</p><p>SubmitAsyncQueryAsync&lt;T&gt;(...)</p><p>EsqlAsyncQuery&lt;T&gt;</p><p>Long-running server-side queries</p><p>For the full feature reference, including query options, multifield access, nested objects, and multivalue field handling, see the <a href="https://www.elastic.co/docs/reference/elasticsearch/clients/dotnet/linq-to-esql">LINQ to ES|QL documentation</a>.</p><h2>Conclusion</h2><p>LINQ to ES|QL brings the full expressiveness of C# LINQ to Elasticsearch's ES|QL query language, letting you write strongly typed, composable queries without handcrafting query strings. With automatic parameter capturing, streaming materialization, and a layered package architecture that scales from stand-alone translation to the full Elasticsearch client, it fits naturally into .NET applications of any size. Install the latest client, point your LINQ expressions at an index, and let the provider handle the rest.</p>]]></content:encoded>
    <link>https://www.elastic.co/search-labs/blog/linq-esql-c-elasticsearch-net-client</link>
    <guid isPermaLink="true">https://www.elastic.co/search-labs/blog/linq-esql-c-elasticsearch-net-client</guid>
    <category><![CDATA[ES|QL]]></category>
    <category><![CDATA[Vector Database]]></category>
    <dc:creator><![CDATA[Florian Bernd,Martijn Laarman]]></dc:creator>
    <enclosure url="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/bltdfa35fbcbbf4959f/6a1705b9dc55de19a4e00d07/e54132e915217063e9ed0ec45059c6cfc38e31dd-1280x720.png" length="0" type="image/png"/>
    <pubDate>Wed, 01 Apr 2026 00:00:00 GMT</pubDate>
  </item>
  <item>
    <title><![CDATA[Fast vs. accurate: Measuring the recall of quantized vector search]]></title>
    <description><![CDATA[Explaining how to measure recall for vector search in Elasticsearch with minimal setup.]]></description>
    <content:encoded><![CDATA[<p>Everyone wants vector search to be instant. But high-dimensional vectors are heavy. A single 1,024-dimension float-32 vector takes up significant memory, and comparing it against millions of others is computationally expensive.</p><p>To solve this, search engines like Elasticsearch use two main optimization strategies:</p><ol><li><p><strong>Approximate search (hierarchical navigable small world [HNSW]):</strong> Instead of scanning every document, we build a navigation graph to jump quickly to the likely neighborhood of the answer.</p></li><li><p><strong>Quantization:</strong> We compress the vectors (for example, from 32-bit floats to 8-bit integers or even 1-bit binary values) to reduce memory usage and speed up calculations.</p></li></ol><p>But optimization often comes with a tax: <strong>accuracy</strong>.</p><p>The fear is valid: "If I compress my data and take shortcuts during the search, will I miss the best results?" "Does this optimization degrade the relevance of my search engine?"</p><p>To prove that Elastic’s quantization doesn’t degrade results, we built a repeatable test harness using the <a href="https://huggingface.co/datasets/fancyzhx/dbpedia_14"><strong>DBPedia-14</strong></a><a href="https://huggingface.co/datasets/fancyzhx/dbpedia_14"> dataset</a> to calculate exactly how much accuracy (specifically, <strong>recall)</strong> we trade for speed when using default optimizations in Elasticsearch.</p><p>tldr: It’s likely much less than you think. Check out the <a href="https://github.com/elastic/elasticsearch-labs/blob/main/supporting-blog-content/fast_vs_accurate_measuring_the_recall_of_quantized_vector_search/vector_recall_notebook.ipynb">notebook here</a>, and try it yourself</p><h2><strong>The definitions (for the non-experts)</strong></h2><p>Before we look at the code, let’s level-set on some terms.</p><ul><li><p><strong>Relevance versus recall:</strong> <strong>Relevance</strong> is subjective (did I find good stuff?). <strong>Recall</strong> is mathematical. If there are 10 documents in the database that are the <em>perfect</em> mathematical matches for your query, and the search engine finds nine of them, your recall is 90% (or 0.9).</p></li><li><p><strong>Exact search (flat):</strong> Sometimes called the "brute force" method. The search engine scans every single document in an index and calculates the distance.</p><ul><li><p><em>Pros:</em> 100% perfect recall.</p></li><li><p><em>Cons:</em> Computationally expensive and slow at scale.</p></li></ul></li><li><p><strong>Approximate search (HNSW):</strong> The "shortcut" method. The search engine builds an <a href="https://www.elastic.co/search-labs/blog/hnsw-graph">HNSW</a> graph. It traverses the graph to find the nearest neighbors.</p><ul><li><p><em>Pros:</em> Extremely fast and scalable.</p></li><li><p><em>Cons:</em> You might miss a neighbor if the graph traversal stops too early.</p></li></ul></li></ul><h2><strong>The experiment: Exact versus approximate</strong></h2><p>To test recall, we used the <strong>DBPedia-14</strong> dataset, a large dataset of titles and abstracts across 14 ontology classes, commonly used for training and evaluating text categorization models. Specifically, we’ll focus on the "Film" category. We wanted to compare the optimized production settings against a mathematically perfect ground truth.</p><p>For this experiment, we are using the <a href="https://www.elastic.co/search-labs/blog/jina-embeddings-v5-text">jina-embeddings-v5-text-small</a> model, a state-of-the-art multilingual model that leads industry benchmarks for text representation. We chose this model because it defines the current standard for high-performance embeddings. By combining Jina v5’s elite accuracy with Elasticsearch’s native quantization, we can demonstrate a search architecture that is both computationally efficient and uncompromising on retrieval quality.</p><p>We set up an index with dual mapping. We ingested the same text into two different fields simultaneously:</p><ol><li><p><strong><code>content.raw</code></strong>with type: <code>flat</code>. This forces Elasticsearch to perform a brute-force scan of the full Float32 vectors. This returns exact match results and will be used for our baseline.</p></li><li><p><strong><code>content</code></strong>with type <code>semantic_text</code>. With defaults using HNSW + Better Binary Quantization (BBQ). This is the standard, optimized production setting for approximate match.</p></li></ol><h3><strong>The Recall@10 test</strong></h3><p>For our metric, we used Recall@10.</p><p>We picked 50 random movies and ran the same query against both fields.</p><ul><li><p>If the <strong>exact (flat)</strong> search says the top 10 neighbors are IDs [1, 2, 3... 10].</p></li><li><p>And the <strong>approximate (HNSW)</strong> search returns IDs [1, 2, 3... 9, 99].</p></li><li><p>We found nine out of the top 10 correctly. The score is <strong>0.9</strong>.</p></li></ul><p>Here’s the mapping we used:</p># The "Control Group": Forces exact brute-force scan
"raw": {
    "type": "semantic_text",
    "inference_id": ".jina-embeddings-v5-text-small",
    "index_options": {
        "dense_vector": {
            "type": "flat"
        }
    }
}<p><strong>The results: The "flat line" of success</strong></p><p>We ran a scale test, reloading the full dataset and testing against index sizes of 1,000 to 40,000 documents.</p><p>Here’s what happened to the recall score:</p><p>Documents</p><p>Recall@10 score</p><p>1,000</p><p>1.000 (100%)</p><p>5,000</p><p>0.998 (100%)</p><p>10,000</p><p>0.992 (99.4%)</p><p>20,000</p><p>0.999 (99.0%)</p><p>40,000</p><p>0.992 (98.8%)</p><p>The results were incredibly stable. Even as we scaled up, the approximate search matched the brute-force exact search <strong>&gt;99% of the time</strong>.</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt8168a0a4946bade7/6a170e154a531b61b536a9eb/a4bfacb1d0cce6fdf6df0e1a9d4fc5d4007a66da-1999x1209.png" alt="vector search stability: Recall vs  Index Size" /><h2><strong>Why did it work so well?</strong></h2><p>You might expect that compressing vectors to binary values would hurt accuracy more than this. The reason it doesn't lies in how Elasticsearch handles the retrieval.</p><p>Most embedding models today output Float32 vectors, which are large. To make search efficient, Elasticsearch uses quantization for high-dimensional vectors. Specifically, since 9.2, it uses <a href="https://www.elastic.co/search-labs/blog/elasticsearch-9-1-bbq-acorn-vector-search">BBQ</a> by default.</p><p>BBQ uses a <strong>rescoring</strong> mechanism:</p><ol><li><p><strong>Traversal:</strong> The search engine uses the compressed (quantized) vectors to traverse the HNSW graph quickly. Because the vectors are small, it can efficiently over-sample, gathering a larger list of candidates (for example, the top 100 roughly similar docs) without a performance penalty.</p></li><li><p><strong>Rescore:</strong> Once it has those candidates, it retrieves the full-precision values for just those few documents to calculate the final, precise ranking.</p></li></ol><p>This gives you the best of both worlds, the speed of quantization for the heavy lifting, and the precision of floats for the final sort.</p><h2><strong>Can we do better?</strong></h2><p>It’s worth noting that the results we’re seeing here are using default settings and a random sampling of data. Think of this as a high-performance starting point. While Jina v5 is a beast, these recall scores aren't a "one size fits all" guarantee for every dataset. Every data collection has its own quirks, and while you can definitely tune things further to squeeze out even more performance, you should always benchmark against your own specific data to see where your ceiling is.</p><h2><strong>Conclusion</strong></h2><p>This is a very small-scale test. But the point of the exercise is not to measure the embedding model or BBQ specifically, it’s to demonstrate how you can easily measure the recall of your dataset with minimal setup.</p><p>If you want to run this test on your own data, you can check out the <a href="https://github.com/elastic/elasticsearch-labs/blob/main/supporting-blog-content/fast_vs_accurate_measuring_the_recall_of_quantized_vector_search/vector_recall_notebook.ipynb">notebook here</a> and try it yourself.</p>]]></content:encoded>
    <link>https://www.elastic.co/search-labs/blog/recall-vector-search-quantization</link>
    <guid isPermaLink="true">https://www.elastic.co/search-labs/blog/recall-vector-search-quantization</guid>
    <category><![CDATA[Vector Database]]></category>
    <dc:creator><![CDATA[Jeff Vestal]]></dc:creator>
    <enclosure url="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt198c7085db96aa04/6a170e17cdacbfe88c7d2a86/09f03b9239d66c36763cdab3fafcdac207ff6d83-1280x720.png" length="0" type="image/png"/>
    <pubDate>Fri, 20 Mar 2026 00:00:00 GMT</pubDate>
  </item>
  <item>
    <title><![CDATA[Adaptive early termination for HNSW in Elasticsearch]]></title>
    <description><![CDATA[Introducing a new adaptive early termination strategy for HNSW in Elasticsearch.]]></description>
    <content:encoded><![CDATA[<p>Elasticsearch uses the <a href="https://www.elastic.co/search-labs/blog/hnsw-graph">Hierarchical Navigable Small World</a> (HNSW) algorithm to perform vector search over a proximity graph. HNSW is known to provide a nice trade-off between the quality of k-nearest neighbor (KNN) results and the associated cost.</p><p>In HNSW, search proceeds by iteratively expanding candidate nodes in the graph, maintaining a bounded set of nearest neighbors discovered so far. Each expansion has a cost (vector operations, random seeks to disk, and more), and the marginal benefit of that cost tends to decrease as the search progresses.</p><p>One way to optimize HNSW graph traversal is to stop searching when the marginal likelihood of finding new true neighbors doesn’t increase. For this reason, in <a href="https://www.elastic.co/docs/reference/elasticsearch/index-settings/index-modules#index-dense-vector-hnsw-early-termination">Elasticsearch 9.2</a> we introduced a new <a href="https://www.elastic.co/search-labs/blog/hnsw-knn-search-early-termination">early termination mechanism</a>. This stops the search process when visiting graph nodes doesn’t provide enough new nearest neighbors, consecutively, for a fixed number of times.</p><p>This article guides you through how we improved over the mentioned early termination mechanism in HNSW to make it better suited for different datasets and data distributions.</p><h2><strong>Early termination in HNSW</strong></h2><p>In HNSW, search proceeds by iteratively expanding candidate nodes in the proximity graph, maintaining a bounded set of nearest neighbors discovered so far, until it either has visited the whole graph or meets some early stop criteria.</p><p>Early termination is therefore not necessarily always an optimization, it’s <strong>part of the search algorithm itself</strong>. The moment we decide to stop determines the balance between efficiency and recall. In Elasticsearch, there are already a number of ways a query on HNSW can early terminate:</p><ul><li><p>A fixed maximum number of nodes is visited.</p></li><li><p>A fixed timeout is reached.</p></li></ul><p>While simple and predictable, these rules are largely <strong>agnostic to what the search is actually doing</strong>. Also they’re used mostly to make sure that the query finishes in reasonable time for the end user.</p><p>In a <a href="https://www.elastic.co/search-labs/blog/hnsw-knn-search-early-termination">previous blogpost</a>, we introduced the concept of redundancy in HNSW. In short, redundant computations occur when HNSW continues to evaluate new candidate nodes that don’t result in finding more nearest neighbors.</p><h2><strong>Patience: Measuring progress instead of effort</strong></h2><p>The notion of <em>patience</em> reframes early termination around <strong>progress rather than effort</strong>.</p><p>Instead of asking:</p><p>“How many steps have we taken?”</p><p>The new question becomes:</p><p>“What is the amount of computation we accept to waste, until we lose hope?”</p><p>During HNSW search, early exploration typically produces peak improvements to the top-k candidate set. During first steps of the HNSW graph exploration, the set of neighbors is continuously updated as the algorithm keeps discovering nearer and nearer neighbors to the query vector. Over time, these improvements become rarer as the search converges. <a href="https://cs.uwaterloo.ca/~jimmylin/publications/Teofili_Lin_ECIR2025.pdf">Patience-based termination</a> monitors this pattern and terminates the search once improvements have ceased for a sustained period.</p><p>In practice, while visiting the HNSW graph we also compute the queue saturation ratio as we hop through candidate nodes. This measures the percentage of nearest neighbors that were left unchanged while visiting the most recent graph node (or the inverse of the number of new neighbors introduced during the last iteration). When such a ratio becomes too big for too many consecutive iterations, we stop visiting the graph.</p><p>Conceptually, patience treats HNSW search as a <strong>diminishing returns process</strong>. When returns flatten out, continuing to explore the graph yields little benefit.</p><p>This framing is powerful because it ties termination directly to <em>observable outcomes</em> rather than to arbitrary fixed limits.</p><p>The benefit of using this smart early termination technique is that HNSW graph explorations tend to visit a smaller number of graph nodes while retaining an almost perfect relative recall.</p><p>To visualize this, we can plot the amount of recall per visited node that we got with the patience based early termination (labeled as <em><code>et=static</code></em>), when compared to the default HNSW behavior (labeled as <em><code>et=no</code></em>) on a couple of datasets, FinancialQA and Quora, and models, JinaV3 and E5-small.</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/bltfd0d692b9beb476a/6a170ef4dc55debf0be00e97/a9d07c5153ea64a2426c82487c36846030692bb9-1600x945.png" alt="Adaptive Early Termination for HNSW " /><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt93509c251a1b641e/6a170ef6dc55dea2b3e00e9b/dac56125c4b16d1b596c9876b6ca9ac7b2dc87fa-1600x944.png" alt="Adaptive Early Termination for HNSW es" /><h2><strong>Static thresholds and HNSW dynamics</strong></h2><p>In practice, in Elasticsearch this is implemented using <strong>static thresholds</strong>. One threshold refers to the <strong>saturation threshold</strong>: that is, the ratio of saturation that we consider suboptimal. The other threshold refers to the number of consecutive graph nodes that we allow to be visited while still having a suboptimal queue saturation: that is, the <strong>patience threshold</strong>.</p><p>When we introduced this early termination strategy in Elasticsearch 9.2, we decided to opt for conservative defaults, so as to let the recall as much as possible, while still gaining in terms of latency and memory consumption. For this reason, we set the saturation threshold to be 100% and the patience threshold to be set as a (bounded) 30% of the <a href="https://www.elastic.co/docs/reference/query-languages/query-dsl/query-dsl-knn-query#knn-query-top-level-parameters:~:text=search%20request%20size.-,num_candidates,-(Optional%2C%20integer)%20The"><em><code>num_candidates</code></em></a> in the KNN query.</p><p>In many scenarios, these settings resulted to work nicely; however, two queries requesting the same number of neighbors might have radically different convergence behaviors. Some queries encounter dense local neighborhoods and saturate quickly; others must traverse long, sparse paths before finding competitive candidates. The latter resulted to be the most difficult to handle effectively.</p><p>As a result, we sometimes noticed:</p><ul><li><p>Over-exploration for easy queries.</p></li><li><p>Premature termination for hard queries.</p></li></ul><p>Therefore, we figured that fixed threshold values encode global assumptions about convergence, whereas we could make HNSW better adapt to different dynamics.</p><h2><strong>Making HNSW early termination adaptive</strong></h2><p>Adaptive early termination approaches this problem from a different angle. Instead of enforcing predefined stopping thresholds, the algorithm <strong>infers when to stop from the search dynamics themselves</strong>.</p><p>So instead of comparing the queue saturation ratio between two consecutive candidates, we decided to introduce both an instant smoothed discovery rate   (how many new neighbors were introduced for a query <em>q</em>, in the last visit <em>i</em>) together with rolling mean  and standard deviation  of such a discovery rate during the graph visit (using <a href="https://en.wikipedia.org/wiki/Algorithms_for_calculating_variance#Welford's_online_algorithm">Welford’s algorithm</a>). These statistics about the discovery rate are calculated per query, so that this information can be used to decide different degrees of patience for each query.</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/bltdbfb1e123f1b026d/6a170ef7cf4f25d9bab2d216/1958be7ca4425ade66eaf621ada3533173183598-694x118.png" alt="" /><p>The previously static thresholds become adaptive to the discovery rate statistics: The saturation threshold becomes the rolling mean plus the standard deviation; whereas we make the patience adapt and scale inversely with the standard deviation.</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt7d4d91f464a9dc9d/6a170ef8d7c0223420de656a/f7ee4a55c24853b657df26052b275e8bd76cf0f9-654x156.png" alt="" /><p>The early exit rules remain the same; the saturation happens when the instant discovery rate is lower than the adaptive saturation threshold. The graph visit stops if the saturation persists for a number of consecutive candidate visits that’s larger than the adaptive patience.</p><p>This way, we obtain a behavior that doesn’t depend on the <em><code>num_candidates</code></em> parameter in the KNN query (which might be always set or left as the default, regardless of early exit) and that better adapts to each query and vector distribution dynamically.</p><p>The recall per visited node on FinancialQA and Quora with the adaptive strategy (labeled as <em><code>et=adaptive</code></em>) reports a higher recall per visited node, when compared to the static strategy (<em><code>et=static</code></em>) and the default HNSW behavior (<em><code>et=no</code></em>).</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blteab7ba53ae14da0e/6a170ef9961e69e072c4cfd5/2a906997d9a25d74c7038bd9661bc97581e7258e-1600x938.png" alt=" adaptive strategy and the default HNSW behavior" /><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt6fb9e672d3698200/6a170efb67045b7b2b45c2ab/3a114911e232c351dbb814cea20e8b0f1415a717-1600x925.png" alt="" /><p>Adaptive early termination is turned on by default in Elasticsearch 9.3 for HNSW dense vector fields (and it can eventually be turned off via the <a href="https://www.elastic.co/docs/reference/elasticsearch/index-settings/index-modules#index-dense-vector-hnsw-early-termination">same index level setting</a>).</p>]]></content:encoded>
    <link>https://www.elastic.co/search-labs/blog/hnsw-elasticsearch-adaptive-early-termination</link>
    <guid isPermaLink="true">https://www.elastic.co/search-labs/blog/hnsw-elasticsearch-adaptive-early-termination</guid>
    <category><![CDATA[Vector Database]]></category>
    <category><![CDATA[Inside Elastic]]></category>
    <dc:creator><![CDATA[Tommaso Teofili]]></dc:creator>
    <enclosure url="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt27b746cc1995e6b7/6a170efda29299de8ad010c6/e6d3186f609dd56dc5ffe33d70fa9e5cfa05b51f-1280x720.png" length="0" type="image/png"/>
    <pubDate>Mon, 02 Mar 2026 00:00:00 GMT</pubDate>
  </item>
  <item>
    <title><![CDATA[Elasticsearch vector search is up to 8x faster than OpenSearch]]></title>
    <description><![CDATA[Exploring filtered vector search benchmarks of OpenSearch vs. Elasticsearch and why vector search performance is critical for context-engineered systems.]]></description>
    <content:encoded><![CDATA[<h2>Why search speed matters for AI agents and context engineering</h2><p>Our benchmarks on a 20M document corpus show that Elasticsearch delivers up to 8x higher throughput than OpenSearch for filtered vector search, while also achieving higher Recall@100 across the configurations we tested. Context engineering depends on more than fast vector retrieval. Teams also need strong relevance controls, like hybrid search and filtering, operational simplicity, and predictable performance, as workflows iterate. But because agents often run retrieve, reason, retrieve loops many times per request, retrieval latency becomes a multiplier, so improvements here translate directly into better end-to-end responsiveness and lower cost.</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt9daec868eed84658/6a170bef6234e0c322db1a19/d5a52a07773f0942c2baa732dacfe782aac0f415-1600x683.png" alt="OpenSearch vs. Elasticsearch​: Throughput for filtered vector search benchmark" /><p>For context engineering, retrieval isn’t a one-time step. Agents and applications repeatedly run loops, such as retrieve → reason → retrieve, to refine queries, verify facts, assemble grounded context, and complete tasks. This pattern is common in agentic workflows and iterative retrieval augmented generation (RAG). Because retrieval may be invoked many times per user request, it adds delay to the response and/or increases infrastructure costs.</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt718c72fb858e4c98/6a170bf00e2e496e7241a132/54ac476ff20a3cf93484298c9ae47612c12fc110-800x417.png" alt="Context engineering turns a large context pool into a limited LLM context window." /><h2>Why is vector search performance critical?</h2><p></p><p>Imagine a shopping assistant answering the question, “I need a carry-on backpack under $60 that fits a 15-inch laptop, is water resistant, and can arrive by Friday.”</p><p>In production, the assistant rarely issues one vector query and stops. It runs a retrieval loop to build the right context, and each step is typically constrained by filters, like availability, region, shipping promise, brand rules, and policy eligibility.</p><p><strong>Step 1: Interpret intent and translate to constraints.</strong></p><p>The agent turns the request into structured filters and a semantic query, such as:</p><ul><li><p>Filters: In stock, deliverable to the user’s postcode, delivery by Friday, price under $60, valid listing</p></li><li><p>Vector query: “Carry-on backpack 15-inch laptop water resistant”</p></li></ul><p><strong>Step 2: Retrieve candidates, and then refine.</strong></p><p>It often repeats retrieval with variations to avoid missing good matches:</p><ul><li><p>“travel backpack carry on laptop sleeve”</p></li><li><p>“water resistant commuter backpack 15 inch”</p></li><li><p>“lightweight cabin backpack”</p></li></ul><p>Each query uses the same eligibility filters, because retrieving irrelevant or unavailable items is wasted context.</p><p><strong>Step 3: Expand to confirm details and reduce risk.</strong></p><p>The agent then retrieves again to verify key attributes that affect the final answer:</p><ul><li><p>Material and water resistance wording</p></li><li><p>Dimensions and laptop compartment fit</p></li><li><p>Return policy or warranty constraints</p></li><li><p>Alternate options if inventory is low</p></li></ul><p>This is multistep context engineering: Retrieve, reason, retrieve, assemble.</p><h2>Why latency and recall matter for context engineering</h2><p>These interactions can involve dozens of filtered retrieval calls per user session. That makes per-call latency a direct multiplier on end-to-end response time, and low recall forces extra retries or causes the agent to miss eligible items, degrading answer quality.</p><p>Takeaway: In context-engineered systems, filtered approximate nearest neighbors (ANN) isn’t a single lookup. It’s a repeated operation under constraints, so vector search performance shows up immediately in latency, throughput, and cost, even when the large language model (LLM) is the most visible component.</p><h2>Benchmarking</h2><h3>Results</h3><p>In Graph 2, each dot represents one test configuration. The best results appear toward the top left, meaning higher recall with lower latency. Elasticsearch’s results are consistently closer to the top left than OpenSearch’s, indicating better speed and accuracy under the same workload settings.</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/bltb562b30a600cc8e1/6a170bf2cf4f253582b2d1b2/c50d1df00968cac18149a2799e6242fbe49b66a0-1600x990.png" alt=" Graph 2: Recall versus average latency (rescore 1)." /><h4>Some key insights</h4><ul><li><p><code>s_n_r_value</code>: Shorthand for <code>size_numCandidates_rescoreOversample</code> (k and numCandidates set equal to numCandidates in these tests), for example, <code>100_500_1</code> means size=100, numCandidates=500 and k=500, rescore oversample=1</p></li><li><p>Recall: Measured Recall@100 for that configuration</p></li><li><p>Avg latency (ms): Average end-to-end latency per query</p></li><li><p>Throughput: Queries per second</p></li><li><p>Recall %: Relative recall lift of Elasticsearch versus OpenSearch (Elasticsearch minus OpenSearch) / OpenSearch</p></li><li><p>Latency Xs: OpenSearch average latency divided by Elasticsearch average latency</p></li><li><p>Throughput Xs: Elasticsearch throughput divided by OpenSearch throughput</p></li></ul><p>Engine</p><p>`s_n_r_value`</p><p>Recall</p><p>Avg Latency (ms)</p><p>Throughput</p><p>Recall %</p><p>Latency Xs</p><p>Throughput Xs</p><p>Elasticsearch</p><p>100_250_1</p><p>0.7704</p><p>25</p><p>534.75</p><p>9.70%</p><p>2.28</p><p>1.91</p><p>OpenSearch</p><p>100_250_1</p><p>0.7023</p><p>57.08</p><p>279.58</p><p></p><p></p><p></p><p>Elasticsearch</p><p>100_500_1</p><p>0.8577</p><p>25.42</p><p>524.14</p><p>7.20%</p><p>2.4</p><p>2</p><p>OpenSearch</p><p>100_500_1</p><p>0.8001</p><p>60.9</p><p>262.12</p><p></p><p></p><p></p><p>Elasticsearch</p><p>100_750_1</p><p>0.8947</p><p>29.67</p><p>528.09</p><p>5.72%</p><p>2.25</p><p>2.21</p><p>OpenSearch</p><p>100_750_1</p><p>0.8463</p><p>66.76</p><p>239.11</p><p></p><p></p><p></p><p>Elasticsearch</p><p>100_1000_1</p><p>0.9156</p><p>29.65</p><p>534.5</p><p>4.66%</p><p>2.46</p><p>2.44</p><p>OpenSearch</p><p>100_1000_1</p><p>0.8748</p><p>72.88</p><p>219.01</p><p></p><p></p><p></p><p>Elasticsearch</p><p>100_1500_1</p><p>0.9386</p><p>31.84</p><p>497.3</p><p>3.38%</p><p>2.71</p><p>2.68</p><p>OpenSearch</p><p>100_1500_1</p><p>0.9079</p><p>86.16</p><p>185.4</p><p></p><p></p><p></p><p>Elasticsearch</p><p>100_2000_1</p><p>0.9507</p><p>34.69</p><p>457.2</p><p>2.57%</p><p>2.98</p><p>2.96</p><p>OpenSearch</p><p>100_2000_1</p><p>0.9269</p><p>103.36</p><p>154.55</p><p></p><p></p><p></p><p>Elasticsearch</p><p>100_2500_1</p><p>0.9582</p><p>37.9</p><p>418.43</p><p>1.99%</p><p>3.28</p><p>3.26</p><p>OpenSearch</p><p>100_2500_1</p><p>0.9395</p><p>124.29</p><p>128.53</p><p></p><p></p><p></p><p>Elasticsearch</p><p>100_3000_1</p><p>0.9636</p><p>41.86</p><p>379.4</p><p>1.62%</p><p>3.46</p><p>3.44</p><p>OpenSearch</p><p>100_3000_1</p><p>0.9482</p><p>144.67</p><p>110.34</p><p></p><p></p><p></p><p>Elasticsearch</p><p>100_4000_1</p><p>0.9705</p><p>50.28</p><p>316.21</p><p>1.06%</p><p>3.87</p><p>3.85</p><p>OpenSearch</p><p>100_4000_1</p><p>0.9603</p><p>194.36</p><p>82.22</p><p></p><p></p><p></p><p>Elasticsearch</p><p>100_5000_1</p><p>0.9749</p><p>58.77</p><p>270.91</p><p>0.73%</p><p>4.43</p><p>4.41</p><p>OpenSearch</p><p>100_5000_1</p><p>0.9678</p><p>260.33</p><p>61.38</p><p></p><p></p><p></p><p>Elasticsearch</p><p>100_6000_1</p><p>0.9781</p><p>66.75</p><p>238.59</p><p>0.52%</p><p>4.91</p><p>4.89</p><p>OpenSearch</p><p>100_6000_1</p><p>0.973</p><p>327.44</p><p>48.81</p><p></p><p></p><p></p><p>Elasticsearch</p><p>100_7000_1</p><p>0.9804</p><p>74.64</p><p>213.49</p><p>0.38%</p><p>5.28</p><p>5.27</p><p>OpenSearch</p><p>100_7000_1</p><p>0.9767</p><p>394.24</p><p>40.53</p><p></p><p></p><p></p><p>Elasticsearch</p><p>100_8000_1</p><p>0.9823</p><p>82.28</p><p>193.59</p><p>0.27%</p><p>6.86</p><p>6.83</p><p>OpenSearch</p><p>100_8000_1</p><p>0.9797</p><p>564.14</p><p>28.33</p><p></p><p></p><p></p><p>Elasticsearch</p><p>100_9000_1</p><p>0.9837</p><p>90.08</p><p>176.96</p><p>0.16%</p><p>7.63</p><p>7.61</p><p>OpenSearch</p><p>100_9000_1</p><p>0.9821</p><p>687.25</p><p>23.25</p><p></p><p></p><p></p><p>Elasticsearch</p><p>100_10000_1</p><p>0.9848</p><p>97.64</p><p>163.31</p><p>0.08%</p><p>8.38</p><p>8.36</p><p>OpenSearch</p><p>100_10000_1</p><p>0.984</p><p>818.64</p><p>19.53</p><p></p><p></p><p></p><p>For example, at <code>100_9000_1</code>, OpenSearch averages 687 milliseconds per retrieval versus 90 milliseconds on Elasticsearch, and in a 10-step retrieval loop that’s about 10 x (687 - 90) = six seconds of additional waiting time. </p><p>See the <a href="https://github.com/elastic/competitive-benchmarking-studies/tree/main/es-9.3-vs-os-3.5-vector-search/jingra/results/20260220">full results</a>.</p><h3>Methodology</h3><p>Using Python to send the queries and track the response timing and other statistics, we sent the following queries to the engines. Bear in mind that the performance of any vector search engine depends on how you tune its core parameters: how many candidates to consider, how aggressively to rescore, and how much context to return. These settings directly affect both recall (the likelihood of finding the right answer) and latency (how fast you get results).</p><p>In our benchmarks, we used the same candidate, rescore, and result-size settings you’d typically tune in an agentic retrieval loop, and we measured how Elasticsearch performs under that workload. We then ran OpenSearch with the same settings as a reference.</p><p>OpenSearch</p>GET &lt;INDEX_NAME&gt;/_search
{
  "query": {
    "knn": {
      "&lt;DENSE_VECTOR_FIELD_NAME&gt;": {
        "vector": [...],
        "k": &lt;NUMBER_OF_CANDIDATES&gt;,
        "method_parameters": {
          "ef_search": &lt;NUMBER_OF_CANDIDATES&gt;
        },
        "rescore": {
          "oversample_factor": &lt;OVERSAMPLE&gt;
        },
        "filter": {
          &lt;SOME_FILTER&gt;
        }
      }
    }
  },
  "size": &lt;RESULT_SIZE&gt;,
  "_source": {
    "excludes": [
      "&lt;DENSE_VECTOR_FIELD_NAME&gt;"
    ]
  }
}<ul><li><p><code>"size": &lt;RESULT_SIZE&gt;</code>: Number of hits returned to the client. In this benchmark, result size is 100 to compute Recall@100.</p></li><li><p><code>"k": &lt;NUMBER_OF_CANDIDATES&gt;</code>: The number of nearest neighbor candidates.</p></li><li><p><code>"ef_search": &lt;NUMBER_OF_CANDIDATES&gt;</code>: The number of vectors to examine.</p></li><li><p><code>"oversample_factor": &lt;OVERSAMPLE&gt;</code>: How many candidate vectors are retrieved before rescoring.</p></li></ul><p>Elasticsearch</p>GET &lt;INDEX_NAME&gt;/_search
{
  "query": {
    "knn": {
      "field": "&lt;DENSE_VECTOR_FIELD_NAME&gt;",
      "query_vector": [...],
      "k": &lt;NUMBER_OF_CANDIDATES&gt;,
      "num_candidates": &lt;NUMBER_OF_CANDIDATES&gt;,
      "rescore_vector": {
        "oversample": &lt;OVERSAMPLE&gt;
      },
      "filter": {
        &lt;SOME_FILTER&gt;
      }
    }
  },
  "size": &lt;RESULT_SIZE&gt;,
  "_source": {
    "excludes": [
      "&lt;DENSE_VECTOR_FIELD_NAME&gt;"
    ]
  }
}<ul><li><p><code>"size": &lt;RESULT_SIZE&gt;</code>: Number of hits returned to the client. In this benchmark, result size is 100 to compute Recall@100.</p></li><li><p><code>"k": &lt;NUMBER_OF_CANDIDATES&gt;</code>: Number of nearest neighbors to return from each shard.</p></li><li><p><code>"num_candidates": &lt;NUMBER_OF_CANDIDATES&gt;</code>: Number of nearest neighbor candidates to consider per shard while doing <code>knn</code> search.</p></li><li><p><code>"oversample": &lt;OVERSAMPLE&gt;</code>: How many candidate vectors are retrieved before rescoring.</p></li></ul><p>Example</p><p><code>Knn</code> query, (<code>100_500_1</code>), would be as follows:</p><p>OpenSearch</p>GET search_catalog_128/_search
{
  "query": {
    "knn": {
      "search_catalog_embedding": {
        "vector": [...],
        "k": 500,
        "method_parameters": {
          "ef_search": 500
        },
        "rescore": {
          "oversample_factor": 1
        },
        "filter": {
          "term": {
            "valid": true
          }
        }
      }
    }
  },
  "size": 100,
  "_source": {
    "excludes": [
      "search_catalog_embedding"
    ]
  }
}<p>Elasticsearch</p>GET search_catalog_128/_search
{
  "query": {
    "knn": {
      "field": "search_catalog_embedding",
      "query_vector": [...],
      "k": 500,
      "num_candidates": 500,
      "rescore_vector": {
        "oversample": 1
      },
      "filter": {
        "term": {
          "valid": true
        }
      }
    }
  },
  "size": 100,
  "_source": {
    "excludes": [
      "search_catalog_embedding"
    ]
  }
}<p>The full configuration, alongside Terraform scripts, Kubernetes manifests and the benchmarking code is available in this <a href="https://github.com/elastic/competitive-benchmarking-studies">repository</a> in the folder <a href="https://github.com/elastic/competitive-benchmarking-studies/tree/main/es-9.3-vs-os-3.5-vector-search">es-9.3-vs-os-3.5-vector-search</a>.</p><h3>Cluster setup</h3><p>We ran our tests on six e2-standard-16 cloud servers, each with 16 vCPUs and 64 GB RAM. On each server, we allocated 15 vCPUs and 56 GB RAM to each Kubernetes pod running the search engine node, with 28 GB reserved for the JVM heap.</p><p>The clusters ran Elasticsearch 9.3.0 and OpenSearch 3.5.0 (Lucene 10.3.2). Because both systems use the same Lucene version in this benchmark, the throughput and latency differences we observe cannot be attributed to Lucene alone and instead reflect differences in how each engine integrates and executes filtered k-nearest neighbor (kNN) retrieval and rescoring. We used a single index with three primary shards and one replica (so 6 shards total, 1 per node).</p><p>We also used a separate server in the same region to run the benchmark client and collect timing statistics.</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt7c7bdd567395d5b7/6a170bf3c1e8a56ee2f882f6/f81002c9186e4c2d3e92f49d72418fee9860fc5e-761x401.png" alt="Cluster setup for Elasticsearch and for OpenSearch benchmarks" /><h3>The dataset</h3><p></p><p>For this benchmark, we used a large-scale ecommerce-style catalog embedding dataset with 20 million documents, designed to reflect real-world filtered vector retrieval at scale.</p><p></p><p>Each document represents a catalog item and includes:</p><p></p><ul><li><p>A 128-dimensional dense vector embedding used for approximate kNN retrieval.</p></li><li><p>Structured metadata fields used for filtering (for example, item validity and availability plus other catalog constraints) enabling the common production pattern of retrieving the nearest neighbors but only within an eligible subset.</p></li></ul><p></p><p>We chose this dataset because it captures the core performance challenge we see in agentic and RAG-style systems in production: Vector similarity alone is not enough, retrieval is frequently constrained by filters, and the system must maintain high recall while keeping latency low under those constraints. Compared to smaller QA-style datasets, a 20M document corpus also better reflects the scale and candidate pressure that filtered ANN systems face in practice.</p><h2>Conclusion</h2><p>In modern AI architectures, especially those built around context engineering, vector search speed isn’t a minor implementation detail. It’s a multiplier. When agents and workflows iterate through retrieve → reason → retrieve, retrieval performance directly shapes end-to-end latency, throughput, and the quality of the context fed into the model.</p><p>In our benchmarks, Elasticsearch consistently delivered higher recall at lower latency than OpenSearch in scenarios where correctness depends on retrieving the right document, not just a similar vector. On a controlled dataset, the difference is clear, and in production those gains accumulate across large volumes of retrieval calls, improving responsiveness, increasing capacity headroom, and reducing infrastructure costs.</p><h3>Further reading</h3><ol><li><p><a href="https://www.elastic.co/search-labs/blog/context-engineering-overview">What is context engineering?</a></p></li><li><p><a href="https://www.elastic.co/search-labs/blog/series/context-engineering-hybrid-search-evolution">The evolution of hybrid search and context engineering</a></p></li><li><p><a href="https://www.elastic.co/search-labs/blog/context-engineering-relevance-ai-agents-elasticsearch">The impact of relevance in context engineering for AI agents</a></p></li></ol>]]></content:encoded>
    <link>https://www.elastic.co/search-labs/blog/opensearch-vs-elasticsearch-filtered-vector-search</link>
    <guid isPermaLink="true">https://www.elastic.co/search-labs/blog/opensearch-vs-elasticsearch-filtered-vector-search</guid>
    <category><![CDATA[Vector Database]]></category>
    <dc:creator><![CDATA[Sachin Frayne]]></dc:creator>
    <enclosure url="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt4b38e5114bbf098c/6a170bf560084b3a6c3c459d/fb7ee623925ca6696d643e437ce8efe5fe749079-1280x720.png" length="0" type="image/png"/>
    <pubDate>Wed, 25 Feb 2026 00:00:00 GMT</pubDate>
  </item>
  <item>
    <title><![CDATA[Elasticsearch 9.3 adds bfloat16 vector support]]></title>
    <description><![CDATA[Exploring the new Elasticsearch element_type: bfloat16, which can halve your vector data storage.]]></description>
    <content:encoded><![CDATA[<p>Elasticsearch 9.3 brings with it several important improvements to vector data, including a new <code>element_type: bfloat16</code>. This has the potential to <em>halve</em> your vector data storage, with minimal reductions in recall and runtime performance for most use cases.</p><h2>Storage formats in dense_vector fields</h2><p>Prior to 9.3, <code>dense_vector</code> fields support vectors of single bits, 1-byte integers, and 4-byte floats. We store the original vectors on top of any quantization and/or hierarchical navigable small world (HNSW) graph used for indexing, and the original vectors make up the vast majority of the required disk space of the vector indices. If your vectors are floating point, then the only option versions of Elasticsearch prior to 9.3 provide is to store 4 bytes per vector value: That’s 4kB for a single 1024-dimensional vector.</p><p>There are other floating-point sizes available, of course: <a href="https://en.wikipedia.org/wiki/IEEE_754">IEEE-754</a> specifies floating-point sizes of many different lengths, including the 4-byte <code>float32</code> and 8-byte <code>float64</code> used by Java <code>float</code> and <code>double</code> types. It also specifies a <code>float16</code> format, which only uses 2 bytes per value. However, this only has a maximum value of 65,504, compared to the 3.4x1038 of 4-byte <code>float32</code> values, and the conversion between the two involves several arithmetic operations.</p><p>As an alternative, many machine learning (ML) applications now use <a href="https://en.wikipedia.org/wiki/Bfloat16_floating-point_format">bfloat16</a>, which is a modification of IEEE-754 <code>float32</code> to only use 2 bytes. It does this by discarding the lowest 2 bytes of the fractional part of the value, leaving the sign and exponent unchanged. This effectively reduces the precision of the floating-point value <em>without</em> a corresponding reduction in range. The conversion from <code>float32</code> to <code>bfloat16</code> is a simple bitwise truncation on the <code>float32</code> value, with a bit of jiggling to account for rounding.</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/bltc2f2b97b3d6911e6/6a170e2e67045b22db45c280/b0f8d88cfb8c696b8ef805424d7dc7a242023484-913x394.png" alt="bfloat16" /><h2>bfloat16 in Elasticsearch 9.3</h2><p>Elasticsearch 9.3 now supports storing vector element types as bfloat16. In memory, it will still process every vector value as a 4-byte float32, as Java does not have built-in support for bfloat16. As it writes vector data to disk, it will simply truncate and round each float32 value to a 2-byte bfloat16, and zero-expand each bfloat16 value back to float32 on reading the value into memory.</p><p>This effectively <em>halves</em> your vector index sizes, as it uses 2 bytes per value rather than 4 bytes. There may be a small performance cost during reading and writing data as Elasticsearch performs the necessary conversions, but this is often counterbalanced by a significant reduction in the I/O required, as the OS now has to read half as much data. And, for most datasets, there is a minimal effect on search recall.</p><p>As an example, this is the difference in sizes for bfloat16 on our <code>dense_vector</code> dataset:</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt1570058a6fed8296/6a170e30b0367d770272bdb1/d7d2459cead1b8d29d107a1b6bbbe21be1fc7315-1600x990.png" alt="bfloat16 in Elasticsearch" /><p>So, if your input vectors are already at bfloat16 precision, then happy days! Elasticsearch accepts raw bfloat16 vectors as float values, and as <a href="https://www.elastic.co/search-labs/blog/base64-encoded-strings-vector-ingestion">Base64-encoded vectors</a>. The vectors are persisted to disk with the same precision as your original source data, immediately halving your data storage requirements.</p><p>If your input vectors are at 4-byte precision, then you can also use bfloat16 format to halve your index data sizes. Elasticsearch will truncate and round each value to 2-byte precision, throwing away the least significant bits of the fraction. This means that the vector values you get back from Elasticsearch won’t be exactly the same as what you originally indexed, so don’t use bfloat16 if you need to maintain the full 4-byte precision of float32.</p><p>Starting in Elasticsearch 9.3, and on Elasticsearch Serverless, you can specify <code>element_type: bfloat16</code> with all <code>dense_vector</code> index types on any newly created indices. If you wish to use bfloat16 with existing indices, you can reindex into an index with <code>element_type: bfloat16</code> and Elasticsearch will automatically convert your existing float vectors to bfloat16.</p>]]></content:encoded>
    <link>https://www.elastic.co/search-labs/blog/bfloat16-vector-support-elasticsearch</link>
    <guid isPermaLink="true">https://www.elastic.co/search-labs/blog/bfloat16-vector-support-elasticsearch</guid>
    <category><![CDATA[Vector Database]]></category>
    <dc:creator><![CDATA[Simon Cooper]]></dc:creator>
    <enclosure url="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt1b9dfebf4a2e59f2/6a170e31a6c2b95f67e7978e/f80f988d758f53742f6b4cd13b04d0cb27af7a17-1280x720.png" length="0" type="image/png"/>
    <pubDate>Mon, 16 Feb 2026 00:00:00 GMT</pubDate>
  </item>
  <item>
    <title><![CDATA[Speed up vector ingestion using Base64-encoded strings]]></title>
    <description><![CDATA[Introducing Base64-encoded strings to speed up vector ingestion in Elasticsearch.]]></description>
    <content:encoded><![CDATA[<p>We’re improving the ingestion speed of vectors in Elasticsearch. Now, in <a href="https://www.elastic.co/cloud/serverless">Elastic Cloud Serverless</a> and in v9.3, you can send your vectors to Elasticsearch encoded as Base64 strings, which will provide immediate benefits to your ingestion pipeline.</p><p>This change reduces the overhead of parsing vectors in JSON by an order of magnitude, which translates to almost a 100% improvement on indexing throughput for DiskBBQ and around 20% improvement for hierarchical navigable small world (HNSW) workloads. In this blog, we’ll take a closer look at Base64-encoded strings and the improvements it brings to vector ingestion.</p><h2>What’s the problem?</h2><p>At Elastic, we’re always looking for ways to improve our vector search capabilities, whether that’s enhancing existing storage formats or introducing new ones. Recently, for example, we added a new disk-friendly storage format called <a href="https://www.elastic.co/search-labs/blog/diskbbq-elasticsearch-introduction">DiskBBQ</a> and enabled vector indexing with <a href="https://www.elastic.co/search-labs/blog/elasticsearch-gpu-accelerated-vector-indexing-nvidia">NVIDIA cuVS</a>.</p><p>In both cases, we expected to see major gains in ingestion speed. However, once these changes were fully integrated into Elasticsearch, the improvements weren’t as large as we had hoped. A flamegraph of the ingestion process made the issue clear: JSON parsing had become one of the main bottlenecks.</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt7d4cd9d8628b64b3/6a170c5b2867148d5e93e353/a286408afc85ff1cd3dd448b8fdf59dd3e11d599-1600x675.png" alt="Vector ingestion before using Base64-encoded strings  " /><p>Parsing JSON requires walking through every element in the arrays and converting numbers from text format into 32-bit floating-point values, which is very expensive.</p><h3>Why Base64-encoded strings?</h3><p>The most efficient way to parse vectors is directly from their binary representation, where each element uses a 32-bit floating-point value. However, JSON is a text-based format, and the way to include binary data in it is by using <a href="https://en.wikipedia.org/wiki/Base64">Base64</a>-encoded strings. Base64 is just a binary-to-text encoding schema.</p>{
  “emb” : [1.2345678, 2.3456789, 3.4567891]
}<p>We can now send vectors encoded as Base64 strings:</p>{
  “emb” : ”P54GUUAWH5pAXTwI”
}<p>Is it worth it? Our benchmarks suggest yes. When parsing 1,000 JSON documents, using Base64 encoded strings instead of float arrays resulted in performance improvements of more than an order of magnitude, at the cost of a small encode/decode trade-off (client-side Base64 encoding and a temporary byte array on the server for decoding) in exchange for eliminating expensive per-element numeric parsing.</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blta9a1662fdf7d5849/6a170c5d839dfaf624dcff29/86e5a926e13b07bb3b0abe80bd4930464e8f6f9b-1200x742.png" alt="Base64 vs. Float32 parsing time" /><h3>Give me some ingestion numbers</h3><p>We can see these improvements in practice when running the <a href="https://github.com/elastic/rally-tracks/blob/master/so_vector/README.md"><code>so_vector</code></a> rally track with the different approaches. The actual gains depend on how fast indexing is for each storage format. For <code>bbq_disk</code>, indexing throughput increases by about 100%, while for <code>bbq_hnsw</code>, the improvement is closer to 20%, since indexing is inherently slower there.</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blte35ffc920ae863f5/6a170c5e509168f193e1bb1c/4277057ee59cb84d068176b56bb7fa00b66e1cb3-1200x742.png" alt="Base64 vs Float32 indexing throughput" /><p>Starting with Elasticsearch v9.2, <a href="https://www.elastic.co/search-labs/blog/elasticsearch-exclude-vectors-from-source">vectors are excluded from </a><a href="https://www.elastic.co/search-labs/blog/elasticsearch-exclude-vectors-from-source"><code>_source</code></a> by default and are stored internally as 32-bit floating-point values. This behavior also applies to Base64-encoded vectors, making the choice of indexing format completely transparent at search time.</p><h2>Client support</h2><p>Adding a new format for indexing vectors might require changes on ingestion pipelines. To help this effort, in v9.3, Elasticsearch official clients can transform vectors with 32-bit floating-point values into Base64-encoded strings and the other way around. You might need to check the client documentation for the specific implementation.</p><p>For example, here’s a snippet for implementing bulk loading using the Python client:</p>from elasticsearch.helpers import bulk, pack_dense_vector

def get_next_document():
    for doc in dataset:
        yield {
            "_index": "my-index",
            "_source": {
                "title": doc["title"],
                "text": doc["text"],
                "emb": pack_dense_vector(doc["emb"]),
            },
        }

result = bulk(
    client=client,
    chunk_size=chunk_size,
    actions=get_next_document,
    stats_only=True,
)<p>The only difference from a bulk ingest using floats is that the embedding is wrapped with the <code>pack_dense_vector()</code> auxiliary function.</p><h2>Conclusion</h2><p>By switching from JSON float arrays to Base64-encoded vectors, we remove one of the largest remaining bottlenecks in Elasticsearch’s vector ingestion pipeline: numeric parsing. The result is a simple change with outsized impact: up to 2× higher throughput for DiskBBQ workloads and meaningful gains even for slower indexing strategies, like HNSW.</p><p>Because vectors are already stored internally in a binary format and excluded from <code>_source</code> by default, this improvement is completely transparent at search time. With official client support landing in v9.3, adopting Base64 encoding requires only minimal changes to existing ingestion code, while delivering immediate performance benefits.</p><p>If you’re indexing large volumes of embeddings, especially in high-throughput or serverless environments, Base64-encoded vectors are now the fastest and most efficient way to get your data into Elasticsearch.Those interested in the implementation details can follow the related Elasticsearch issues and pull requests: #<a href="https://github.com/elastic/elasticsearch/issues/111281">111281</a> and #<a href="https://github.com/elastic/elasticsearch/issues/135943">135943</a>.</p>]]></content:encoded>
    <link>https://www.elastic.co/search-labs/blog/base64-encoded-strings-vector-ingestion</link>
    <guid isPermaLink="true">https://www.elastic.co/search-labs/blog/base64-encoded-strings-vector-ingestion</guid>
    <category><![CDATA[Vector Database]]></category>
    <category><![CDATA[Inside Elastic]]></category>
    <dc:creator><![CDATA[Jim Ferenczi,Benjamin Trent,Ignacio Vera Sequeiros]]></dc:creator>
    <enclosure url="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/bltc5ffc7ac4c2b9d93/6a170c5f839dfa007ddcff2d/4c1ebbd7a1071e8e1721a9871cba87f6aed140e9-1280x720.png" length="0" type="image/png"/>
    <pubDate>Wed, 04 Feb 2026 00:00:00 GMT</pubDate>
  </item>
  <item>
    <title><![CDATA[Elasticsearch open inference API adds support for IBM watsonx.ai rerank models]]></title>
    <description><![CDATA[Explore how to use IBM watsonx™ reranking when building semantic search experiences in Elasticsearch.]]></description>
    <content:encoded><![CDATA[<p>Elastic announced Elastic Rerank in December 2024, which brings powerful semantic search capabilities with no required reindexing—delivering high relevance, top performance, and efficiency. The core set of capabilities powered by Elastic is now even more flexible, allowing developers to bring their own models from Cohere, Vertex AI, Hugging Face, <a href="https://www.elastic.co/search-labs/blog/jina-ai-embeddings-rerank-model-open-inference-api">Jina AI</a> and now IBM watsonx.ai. With our open Inference API, you get the control and choice to integrate, test, and optimize reranking for your needs.</p><p>Along with support for IBM watsonx™ <a href="https://www.elastic.co/search-labs/blog/ibm-watsonx-elasticsearch-inference-api">Slate embedding models</a>, Elasticsearch vector database powers <a href="https://www.elastic.co/blog/ibm-elasticsearch-partnership-conversational-search-watsonx-assistant">watsonx Assistant for Conversational Search</a>—now with semantic reranking for even better answer quality.</p><p>Reranking refines LLM responses by prioritizing the most relevant documents using advanced scoring methods, ensuring accurate responses in multi-stage retrieval, and making it broadly applicable even for datasets that you don’t want to reindex or remap.</p><p><a href="https://dataplatform.cloud.ibm.com/docs/content/wsj/analyze-data/fm-models-embed.html?context=wx#rerank">IBM watsonx offers high-quality reranker models</a> that accurately score and prioritize passages based on query relevance, helping refine search results for better precision. These models enhance tasks like semantic search and document comparison, making them essential for delivering highly relevant answers in AI-driven retrieval systems.</p><p>In this blog, we’ll explore how to use IBM watsonx™ reranking when building search experiences in the Elasticsearch vector database to reorder search results by meaning, giving you sharper, more context-aware answers without altering your existing index.</p><h2>How reranking can create powerful search experiences</h2><p>Semantic reranking is crucial because users expect the best answers at the top, and GenAI models require accurate results to avoid generating incorrect information. Semantic reranking provides consistent scoring, ensuring the most relevant documents are used by AI models and enabling effective cutoff points to prevent hallucinations.</p><h2>Prerequisites &amp; Creation of an Inference Endpoint</h2><p><a href="https://www.elastic.co/guide/en/serverless/current/elasticsearch-get-started.html"><strong>Create an Elasticsearch Serverless Project</strong></a>.</p><p>Elasticsearch Cloud Serverless offers fast query execution and seamless integration with the open Inference API, making it ideal for deploying reranking without infrastructure overhead.</p><h2>Generate an API key in IBM Cloud</h2><ul><li><p>Go to IBM watsonx.ai <a href="https://dataplatform.cloud.ibm.com/registration/stepone?context=wx">Cloud</a> and log in using your credentials. You will land on the welcome page.</p></li></ul><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blta7ddd843ddb47a32/6a17e14b3e9e454aaaba13a6/f4039b1ee68fa91fcffc01708099dd302302ff19-1600x551.png" alt="Generating an API key in IBM Cloud." /><ul><li><p>Go to the <a href="https://cloud.ibm.com/iam/apikeys">API keys</a> page.</p></li><li><p>Create an API key.</p></li></ul><h2>Steps in Elasticsearch</h2><p>Using DevTools in Kibana, create an inference endpoint using the watsonxai service for reranking. This example uses the MS Marco MiniLM L-12 v2 model which is supported by IBM, for ensuring high relevance in passage retrieval.</p>PUT _inference/rerank/ibm_watsonx_rerank
{
    "service": "watsonxai",
    "service_settings": {
        "api_key": "&lt;api_key&gt;",
        "url": "xxx.ml.cloud.ibm.com",
        "model_id": "cross-encoder/ms-marco-minilm-l-12-v2",
        "project_id": "&lt;project_id&gt;",
        "api_version": "2024-05-02"
    }
}<p>You will receive the following response on the successful creation of the inference endpoint:</p>{
  "inference_id": "ibm_watsonx_rerank",
  "task_type": "rerank",
  "service": "watsonxai",
  "service_settings": {
    "url": "xxx.ml.cloud.ibm.com",
    "api_version": "2024-05-02",
    "model_id": "cross-encoder/ms-marco-minilm-l-12-v2",
    "project_id": "&lt;project_id&gt;",
    "rate_limit": {
      "requests_per_minute": 120
    }
  }
}<p>Let us now create an index.</p>PUT quotes-index
{
  "mappings": { 
    "properties": {
      "movie_title": {
        "type": "text"
      },
      "quotes": {
        "type": "text"
      }
    }
  }
}<p>Next, insert data into the created index.</p>PUT quotes-index/_doc/1
{
  "movie_title": "The Big Lebowski",
  "quotes": [
    "That rug really tied the room together",
    "Yeah, well, you know, that's just like, uh, your opinion, man"
  ]
}

PUT quotes-index/_doc/2
{
  "movie_title": "Star Wars",
  "quotes": [
    "These are not the droids you're looking for",
    "I have a bad feeling about this",
    "Do. Or do not. There is no try."
  ]
}

PUT quotes-index/_doc/3
{
  "movie_title": "The Avengers",
  "quotes": [
    "What's the matter, scared of a little lightning?",
    "Superheroes? In New York? Give me a break!"
  ]
}<p>Next, let’s search using a <code>text_similarity_reranker</code> retriever, which enhances search results by reranking documents based on semantic similarity to a specified inference text, using an ML model.</p><p>The retriever helps you configure both the retrieval and reranking of search results in a single API call.</p>POST quotes-index/_search
{
  "retriever": {
    "text_similarity_reranker": {
      "retriever": {
        "standard": {
          "query": {
            "match": {
              "quotes": "feeling lightning"
            }
          }
        }
      },
      "field": "quotes",
      "inference_id": "ibm_watsonx_rerank",
      "inference_text": "feeling lightning",
      "rank_window_size": 50
    }
  },
  "size": 50
}<p>Next, let’s verify the returned result.</p>{
  "took": 718,
  "timed_out": false,
  "_shards": {
    "total": 1,
    "successful": 1,
    "skipped": 0,
    "failed": 0
  },
  "hits": {
    "total": {
      "value": 2,
      "relation": "eq"
    },
    "max_score": 0.003072956,
    "hits": [
      {
        "_index": "quotes-index",
        "_id": "3",
        "_score": 0.003072956,
        "_source": {
          "movie_title": "The Avengers",
          "quotes": [
            "What's the matter, scared of a little lightning?",
            "Superheroes? In New York? Give me a break!"
          ]
        }
      },
      {
        "_index": "quotes-index",
        "_id": "2",
        "_score": 0.000024473073,
        "_source": {
          "movie_title": "Star Wars",
          "quotes": [
            "These are not the droids you're looking for",
            "I have a bad feeling about this",
            "Do. Or do not. There is no try."
          ]
        }
      }
    ]
  }
}<p>The passages are now reordered to show the passages with the highest scores first. In this example, lexical retrieval initially selected The Avengers (_id: 3) and Star Wars (_id: 2) based on word matches—“lightning” in one and “feeling” in the other. This approach considers surface-level overlaps and keywords.</p><p>IBM watsonx.ai rerank then re-evaluated the results based on context, ranking The Avengers higher because “lightning” directly aligned with the query "feeling lightning." This demonstrates that by prioritizing meaning over simple keyword matches, reranking ensures more relevant search results.</p><h2>Try semantic reranking with watsonx and Elasticsearch today</h2><p>With the integration of <a href="https://www.elastic.co/search-labs/integrations/ibm-watsonx">IBM watsonx™</a> rerank models, the Elasticsearch Open Inference API continues to empower developers with enhanced capabilities for building powerful and flexible AI-powered search experiences. Explore more supported <a href="https://dataplatform.cloud.ibm.com/docs/content/wsj/analyze-data/fm-models-embed.html?context=wx">encoder foundation models available with watsonx.ai</a>.</p><p>Additionally, use IBM watsonx Assistant’s new Conversational Search feature and IBM watsonx Discovery today. Visit <a href="https://www.ibm.com/docs/en/watsonx/saas?topic=models-retrieval-augmented-generation">IBM watsonx Discovery</a> to learn more about this new capability using Elasticsearch. You can follow <a href="https://github.com/watson-developer-cloud/assistant-toolkit/blob/master/integrations/extensions/docs/elasticsearch-install-and-setup/watsonx_discovery_install_and_setup.md">these steps</a> for setup and integration with IBM watsonx Assistants.</p>]]></content:encoded>
    <link>https://www.elastic.co/search-labs/blog/ibm-watsonx-elasticsearch-inference-api-rerank-models</link>
    <guid isPermaLink="true">https://www.elastic.co/search-labs/blog/ibm-watsonx-elasticsearch-inference-api-rerank-models</guid>
    <category><![CDATA[AI]]></category>
    <category><![CDATA[Vector Database]]></category>
    <dc:creator><![CDATA[Saikat Sarkar]]></dc:creator>
    <enclosure url="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt6abcdba172164e4b/6a17e14c1480090ff7b48697/34c9034c09f965630567d796380c7d57cb836b32-700x420.png" length="0" type="image/png"/>
    <pubDate>Mon, 16 Jun 2025 00:00:00 GMT</pubDate>
  </item>
  <item>
    <title><![CDATA[Exploring GPU-accelerated vector search in Elasticsearch with NVIDIA: Chapter I]]></title>
    <description><![CDATA[Powered by NVIDIA cuVS, the collaboration looks to provide developers with GPU-acceleration for vector search in Elasticsearch.]]></description>
    <content:encoded><![CDATA[<p>We in the Elastic Engineering org have been busy optimizing vector database performance for a while now. Our mission: making Lucene and Elasticsearch the best vector database. Through hardware accelerated <a href="https://www.elastic.co/blog/accelerating-vector-search-simd-instructions">CPU SIMD instructions</a>, introducing new vector data compression innovations (<a href="https://www.elastic.co/search-labs/blog/better-binary-quantization-lucene-elasticsearch">Better Binary Quantization a.k.a BBQ</a>), and then exceeding expectations by updating the algorithmic approach to BBQ for even more benefits, and also <a href="https://www.elastic.co/search-labs/blog/filtered-hnsw-knn-search">making Filtered HNSW faster</a>. You get the gist—we’re building a faster, better, efficient(er?) vector database for the developers as they solve those RAG-gedy problems!</p><p>As part of our mission to leave no efficiencies behind, we are exploring acceleration opportunities with these curious computer chips, which you may have heard of—NVIDIA GPUs! (Seriously, have you not?).</p><p>When obsessing over performance, we have several problem spaces to explore—how to index exponentially more data, how to retrieve insights from it, and how to do it when your ML models are involved. You should be able to eke out every last benefit available when you have GPUs.</p><p>In this post, we dive into our collaboration with the NVIDIA vector search team as we explore GPU-accelerated vector search in Elasticsearch. This work paves the way for use cases where developers could use a mix of GPUs and CPUs for real-world Elasticsearch-powered apps. Exciting times!</p><h2>Elasticsearch GPUs</h2><p>We are excited to share that the Elasticsearch engineering team is helping build the open-source cuVS Java API experience for developers, which exposes bindings for vector search algorithms. This work leverages our previous experience with Panama FFI. Elasticsearch and Apache Lucene use the NVIDIA cuVS API to build the graph during indexing. Okay, we are jumping ahead; let’s rewind a bit.</p><p><a href="https://developer.nvidia.com/cuvs">NVIDIA cuVS</a>, an open-source C++ library, is at the heart of this collaboration. It aims to bring GPU acceleration to vector search by providing higher throughput, lower latency, and faster index build times. But Elasticsearch and Apache Lucene are written in Java; how will this work?</p><p>Enter <a href="https://github.com/SearchScale/lucene-cuvs">lucene-cuvs</a> and the Elastic-NVIDIA-SearchScale collaboration to bring it into the Lucene ecosystem to explore GPU-accelerated vector search in Elasticsearch. In the recent NVIDIA cuVS 25.02 release, we added a Java API for cuVS. The new API is experimental and will continue to evolve, but it’s currently available for use. The question may arise: aren’t Java to native function calls slow? Not anymore! We’re using the new <a href="https://openjdk.org/projects/panama/">Panama FFI</a> (Foreign Function Interface) for the bindings, which has minimal overhead for Java to native downcalls.</p><p>We’ve been using <a href="https://www.elastic.co/search-labs/blog/lucene-and-java-moving-forward-together">Panama FFI in Elasticsearch and Lucene</a> for a while now. It’s awesome! But... there is always a “but”, isn’t there? FFI has availability challenges across Java versions. We overcame this by compiling the cuVS API to Java 21 and encapsulating the implementation within a multi-release jar targeting Java 22. This allows the use of cuVS Java directly in Lucene and Elasticsearch.</p><p>Ok, now that we have the cuVS Java API, what else would we need?</p><h2>A tale of two algorithms for CPU</h2><p>Elasticsearch supports the <a href="https://arxiv.org/abs/1603.09320">HNSW algorithm</a> for scalable approximate KNN search. However, to get the most out of the GPU, we use a different algorithm, <a href="https://arxiv.org/pdf/2308.15136">CAGRA [</a><a href="https://arxiv.org/pdf/2308.15136"><strong>C</strong></a><a href="https://arxiv.org/pdf/2308.15136"><em>UDA</em></a> <a href="https://arxiv.org/pdf/2308.15136"><strong>A</strong></a><a href="https://arxiv.org/pdf/2308.15136"><em>NN</em></a> <a href="https://arxiv.org/pdf/2308.15136"><strong>GRA</strong></a><a href="https://arxiv.org/pdf/2308.15136"><em>ph</em></a><a href="https://arxiv.org/pdf/2308.15136">]</a>, which has been specifically designed for the high levels of parallelism offered by the GPU.</p><p>Before we get into how we look to add support for CAGRA, let’s look at how Elasticsearch and Lucene access index data through a “codec format”. This consists of</p><ol><li><p>the on-disk representation,</p></li><li><p>the interfaces for reading and writing data,</p></li><li><p>and the machinery for dealing with Lucene’s segment-based architecture.</p></li></ol><p>We are implementing a new KNN (k-nearest neighbors) <a href="https://lucene.apache.org/core/10_1_0/core/org/apache/lucene/codecs/KnnVectorsFormat.html">vector format</a> that internally uses the cuVS Java API to index and search on the GPU. From here, we “plumb” this codec type through Elasticsearch’s mappings to a field type in the index. As a result, your existing KNN queries continue to work regardless of whether the backing index is using a CAGRA or HNSW graph. Of course, this glosses over many details, which we plan to cover in a future blog. The following is the high-level architecture for a GPU-accelerated Elasticsearch.</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/bltb6197b631a34f8b6/6a170b0da6c2b9e60ce7970b/be6b7356c03df4dee7230625c2c9af3b019f93be-756x510.png" alt="" /><p>This new codec format defaults to CAGRA. However, it also supports converting a CAGRA graph to an HNSW graph for search on the CPU.</p><h2>Indexing and searching on the GPU: Making some “core” decisions</h2><p>With the stateless <a href="https://www.elastic.co/search-labs/blog/stateless-your-new-state-of-find-with-elasticsearch">architecture</a> for Elasticsearch Serverless, which separates indexing and search, there is now a clear delineation of responsibilities. We pick the best hardware profile to fulfill each of these independent responsibilities.</p><p>We anticipate users to consider two main deployment strategies:</p><ol><li><p>Index and search on the GPU: During indexing, build a CAGRA graph and use it during search - ideal when extremely low latency search is required.</p></li><li><p>Index on GPU and search on CPU: During indexing, build a CAGRA graph and convert it to an HNSW graph. The HNSW graph is stored in the index, which can later be used on the CPU for searching.</p></li></ol><p>This flexibility provides different deployment models, offering tradeoffs between cost and performance. For example, an indexing service could use GPU to efficiently build and merge graphs in a timely manner while using a lower-powered CPU for searching.</p><h2>So here is the plan for GPU-accelerated vector search in Elasticsearch</h2><p>We are looking forward to bringing performance gains and flexibility with deployment strategies to users, offering various knobs to balance cost and performance. <a href="https://www.nvidia.com/gtc/session-catalog/?tab.catalogallsessionstab=16566177511100015Kus&amp;search=Lucene#/">Here is the NVIDIA GTC 2025 session</a> where this work was presented in detail.</p><p>We’d like to thank the engineering teams at NVIDIA and SearchScale for their fantastic collaboration. In an upcoming blog, we will explore the implementation details and performance analysis in greater depth. Hold on to your curiosity hats 🎩!</p>]]></content:encoded>
    <link>https://www.elastic.co/search-labs/blog/gpu-accelerated-vector-search-elasticsearch-nvidia</link>
    <guid isPermaLink="true">https://www.elastic.co/search-labs/blog/gpu-accelerated-vector-search-elasticsearch-nvidia</guid>
    <category><![CDATA[Vector Database]]></category>
    <dc:creator><![CDATA[Chris Hegarty,Hemant Malik]]></dc:creator>
    <enclosure url="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt298e839e708ca11c/6a170b0fb339d560c2769fc2/38bc0377a6adce7eae0099f61902fdbbe644eb4a-1440x960.jpg" length="0" type="image/jpeg"/>
    <pubDate>Wed, 19 Mar 2025 00:00:00 GMT</pubDate>
  </item>
  <item>
    <title><![CDATA[Elasticsesarch semantic search, leveled up: now with native match, knn and sparse_vector support]]></title>
    <description><![CDATA[Semantic text search becomes even more powerful, with native support for match, knn and sparse_vector queries. This allows us to keep the simplicity of the semantic query while offering the flexibility of the Elasticsearch query DSL. ]]></description>
    <content:encoded><![CDATA[<p>Elasticsearch’s <a href="https://www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl-semantic-query.html">semantic query</a> is incredibly powerful, allowing users to perform semantic search over data configured in <a href="https://www.elastic.co/guide/en/elasticsearch/reference/current/semantic-text.html">semantic_text</a> fields. Much of this power lies in simplicity: just set up a <code>semantic_text</code> field with the inference endpoint you want to use, and then ingest content as if indexing content into a regular <code>text</code> field. The inference happens automatically and transparently, making it simple to set up and use a search index with semantic functionality.</p><p></p><p>This ease of use does come with some tradeoffs: we simplified semantic search with <code>semantic_text</code> by making judgments on default behavior that fit the majority of use cases. Unfortunately, this means that some customizations available for traditional vector search queries aren’t present in the <a href="https://www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl-semantic-query.html">semantic query</a>. We didn’t want to add all of these options directly to the <code>semantic</code> query, as that would undermine the simplicity that we strive for. Instead, we expanded the queries that support the <code>semantic_text</code> field, leaving it up to you to choose the best query that meets your needs.</p><p></p><p>Let’s walk through these changes, starting with creating a simple index with a semantic_text field:</p>PUT index-songs-semantic
{
  "mappings": {
    "properties": {
      "song_title": {
        "type": "text"
      },
      "artist": {
        "type": "keyword"
      },
      "lyric": {
        "type": "semantic_text"
      }
    }
  }
}

// Now index a sample document
POST index-songs-semantic/_doc/1
{
  "song_title": "...Baby One More Time",
  "artist": "Britney Spears",
  "lyric": "When I'm with you, I lose my mind, give me a sign"
}
<p></p><h2>We made match happen in semantic search!</h2><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/bltc5e9a7041fc40292/6a17eeda414c646ff494524f/9b15e17822e297e35a46393ef2a7c1e6d55fedd9-1792x1024.png" alt="" /><p></p><p>First and most importantly, the <a href="https://www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl-match-query.html">match query</a> will now work with <code>semantic_text</code> fields!</p><p></p><p>This means that you can change your old semantic query:</p>GET index-songs-semantic/_search
{
  "query": {
    "semantic": {
      "field": "lyric",
      "query": "song lyrics about love"
    }
  }
}
<p></p><p>Into a simple <code>match</code> query:</p>GET index-songs-semantic/_search
{
  "query": {
    "match": {
      "lyric": "song lyrics about love"
    }
  }
}
<p></p><p>We can see the benefits of semantic search here because we’re searching for “song lyrics about love”, none of which appears in the indexed document. This is because of ELSER’s text expansion.</p><p></p><p>But wait, it gets better!</p><p></p><p>If you have multiple indices, and the same field name is <code>semantic_text</code> in one field and perhaps text in the other field, you can still run <code>match</code> queries against these fields. Let’s create another index, with the same field names, but different types (<code>text</code> instead of <code>semantic_text</code>). Here’s a simple example to illustrate:</p>// Setup - Create a similar index without semantic fields
PUT index-songs-lexical
{
  "mappings": {
    "properties": {
      "song_title": {
        "type": "text"
      },
      "artist": {
        "type": "keyword"
      },
      "lyric": {
        "type": "text"
      }
    }
  }
}

POST index-songs-lexical/_doc/2
{
  "song_title": "Crazy",
  "artist": "Britney Spears",
  "lyric": "You drive me crazy, I just can't sleep, I'm so excited, I'm in too deep"
}

GET index-songs-semantic,index-songs-lexical/_search
{
  "query": {
    "match": {
      "lyric": "crazy"
    }
  }
}
<p></p><p>Here, searching for “crazy” brings up both the lexical match that has “crazy” in the title, and the semantic lyric “lose my mind.”</p><p></p><p>There are some caveats to keep in mind when using the <code>match</code> functionality with <code>semantic_text</code>:</p><ul><li><p>The underlying <code>semantic_text</code> field has a limitation where you can’t use multiple inference IDs on the same field. This limitation extends to <code>match</code><strong>—</strong>meaning that if you have two semantic_text fields with the same name, they need to have the same inference ID or you’ll get an error. You can work around this by creating different names and querying them in a <a href="https://www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl-bool-query.html">boolean query</a> or a compound <a href="https://www.elastic.co/guide/en/elasticsearch/reference/current/retrievers-overview.html">retriever</a>.</p></li><li><p>Depending on what model you use, the scores between lexical (text) matches and semantic matches will likely be very different. In order to get the best ranking of results, we recommend using second stage rerankers such as <a href="https://www.elastic.co/guide/en/elasticsearch/reference/current/semantic-reranking.html">semantic reranking</a> or <a href="https://www.elastic.co/guide/en/elasticsearch/reference/current/rrf.html">RRF</a>.</p></li></ul><p></p><p>Semantic search using the <code>match</code> query is also available in <a href="https://www.elastic.co/guide/en/elasticsearch/reference/current/esql-functions-operators.html#esql-match">ES|QL</a>! Here’s the same example as above, but using ES|QL:</p><p></p><h2>Expert-level semantic search with knn and sparse_vector</h2><p>Match is great, but sometimes you want to specify more vector search options than the semantic query supports. Remember, the tradeoff of making the semantic query as simple as it is involved making some decisions on default behavior.</p><p></p><p>This means that if you want to take advantage of some of the more advanced vector search features, perhaps <code>num_candidates</code> or <code>filter</code> from the <a href="https://www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl-knn-query.html">knn query</a> or <a href="https://www.elastic.co/search-labs/blog/text-expansion-pruning">token pruning</a> in the <a href="https://www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl-sparse-vector-query.html">sparse_vector query</a>, you won’t be able to do so using the semantic query.</p><p></p><p>In the past, we provided some workarounds to this, but they were convoluted and required knowing the inner workings and architecture of the <code>semantic_text</code> field and constructing a corresponding nested query. If you’re doing that workaround now, it will still work—however, we now support query DSL using <code>knn</code> or <code>sparse_vector</code> queries on <code>semantic_text</code> fields.</p><p></p><h3>All about that dense (vector), no trouble</h3><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/bltf6651a79c3e56345/6a17eedcb1e113820979f31d/aa3ea406fc072698db510a0b2ab64eb0266948c1-1792x1024.png" alt="" /><p>Here’s an example script that populates a <code>text_embedding</code> model and queries a <code>semantic_text</code> field using the knn query:</p>PUT index-dense-semantic-songs
{
  "mappings": {
    "properties": {
      "song_title": {
        "type": "text"
      },
      "artist": {
        "type": "keyword"
      },
      "lyric": {
        "type": "semantic_text",
        "inference_id": ".multilingual-e5-small-elasticsearch"
      }
    }
  }
}

// Index sample documents
POST index-dense-semantic-songs/_doc/4
{
  "song_title": "Oops! ...I Did It Again",
  "artist": "Britney Spears",
  "lyric": "Oops, I did it again, I played with your heart, got lost in the game."
}

POST index-dense-semantic-songs/_doc/5
{
  "song_title": "Poker Face",
  "artist": "Lady Gaga",
  "lyric": "Can't read my, can't read my, no, he can't read my poker face"
}

GET index-dense-semantic-songs/_search
{
  "query": {
    "knn": {
      "field": "lyric",
      "k": 10,
      "num_candidates": 100,
      "query_vector_builder": {
        "text_embedding": {
          "model_text": "game"
        }
      }
    }
  }
}
<p></p><p>The <code>knn</code> query can be modified with extra options to enable more advanced queries against the semantic_text field. Here, we perform the same query but add a pre-filter against the <code>semantic_text</code> field:</p>GET index-dense-semantic-songs/_search
{
  "query": {
    "knn": {
      "field": "lyric",
      "k": 10,
      "num_candidates": 100,
      "query_vector_builder": {
        "text_embedding": {
          "model_text": "game"
        }
      },
      "filter": {
        "term": {
          "artist": "Britney Spears"
        }
      }
    }
  }
}
<p></p><h3>Keepin’ it sparse (vector), keepin’ it real</h3><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt540cbe07b8eeb546/6a17eedfe8fbce91343a1a09/1af6680a3bae23b0f1b3fd4605ba6cb3184318e2-1792x1024.png" alt="" /><p></p><p>Similarly, sparse embedding models can be queried more specifically using <code>semantic_text</code> fields as well. Here’s an example script that adds a few more documents and uses the <code>sparse_vector</code> query:</p>POST index-songs-semantic/_doc/6
{
  "song_title": "Crazy In Love",
  "artist": "Beyoncé",
  "lyric": "Looking so crazy, your love's got me looking, got me looking so crazy in love"
}


POST index-songs-semantic/_doc/7
{
  "song_title": "Complicated",
  "artist": "Avril Lavigne",
  "lyric": "Why'd you have to go and make things so complicated?, I see the way you're acting like you're somebody else"
}

GET index-songs-semantic/_search
{
  "query": {
    "sparse_vector": {
      "field": "lyric",
      "query": "crazy"
    }
  }
}
<p></p><p>The <code>sparse_vector</code> query can be modified with extra options, to enable more advanced queries against the <code>semantic_text</code> field. Here, we perform the same query but add <a href="https://www.elastic.co/search-labs/blog/text-expansion-pruning">token pruning</a> against a <code>semantic_text</code> field:</p>GET index-songs-semantic/_search
{
  "query": {
    "sparse_vector": {
      "field": "lyric",
      "query": "crazy",
      "prune": true,
      "pruning_config": {
        "tokens_freq_ratio_threshold": 1,
        "tokens_weight_threshold": 0.4,
        "only_score_pruned_tokens": false
      }
    }
  }
}
<p>This example significantly decreases the token frequency ratio required to pruning, which helps us show differences with such a small dataset, though they’re probably more aggressive than you’d want to see in production (remember, token pruning is about pruning irrelevant tokens to improve performance, not drastically change recall or relevance). You can see in this example that the Avril Lavigne song is no longer returned, and the scores have changed due to the pruned tokens. (Note that this is an illustrative example, and we still <a href="https://www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl-sparse-vector-query.html#sparse-vector-query-with-pruning-config-and-rescore-example">recommend a rescore adding pruned tokens back into scoring</a> for most use cases).</p><p></p><p>You’ll note that with all of these queries if you’re only querying a <code>semantic_text</code> field, you no longer need to specify the inference ID in <code>knn</code>’s <code>query_vector_builder</code> or in the <code>sparse_vector</code> query. This is because it will be inferred from the <code>semantic_text</code> field. You <em>can</em> specify it if you want to override with a different (compatible!) inference ID for some reason or if you’re searching combined indices that have both <code>semantic_text</code> and <a href="https://www.elastic.co/guide/en/elasticsearch/reference/current/sparse-vector.html">sparse_vector</a> or <a href="https://www.elastic.co/guide/en/elasticsearch/reference/current/dense-vector.html">dense_vector</a> fields though.</p><h2>Try it out yourself</h2><p>We’re keeping the original <code>semantic</code> query simple, but expanding our semantic search capabilities to power more use cases and seamlessly integrate semantic search with existing workflows. These power-ups are native to Elasticsearch and are already available in Serverless. They’ll be available in stack-hosted Elasticsearch starting with version 8.18.</p><p></p><p>Try it out today!</p><p></p>]]></content:encoded>
    <link>https://www.elastic.co/search-labs/blog/semantic-search-match-knn-sparse-vector</link>
    <guid isPermaLink="true">https://www.elastic.co/search-labs/blog/semantic-search-match-knn-sparse-vector</guid>
    <category><![CDATA[Relevance]]></category>
    <category><![CDATA[Vector Database]]></category>
    <dc:creator><![CDATA[Kathleen DeRusso]]></dc:creator>
    <enclosure url="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/bltf6651a79c3e56345/6a17eedcb1e113820979f31d/aa3ea406fc072698db510a0b2ab64eb0266948c1-1792x1024.png" length="0" type="image/png"/>
    <pubDate>Thu, 06 Mar 2025 00:00:00 GMT</pubDate>
  </item>
  <item>
    <title><![CDATA[High Quality RAG with Aryn DocPrep, DocParse and Elasticsearch vector database]]></title>
    <description><![CDATA[Learn how to achieve high-quality RAG with effective data preparation using  Aryn.ai DocParse, DocPrep, and Elasticsearch vector database.]]></description>
    <content:encoded><![CDATA[<p>Organizations rely on natural language queries to gain insights from unstructured data, but achieving high-quality answers starts with effective data preparation. <a href="https://www.aryn.ai/">Aryn DocParse and DocPrep</a> streamline this process by converting complex documents into structured JSON or markdown, <a href="https://www.aryn.ai/post/an-evaluation-of-pdf-segmentation-and-layout-analysis-models">delivering up to 6x better data chunking and 2x improved recall</a> for hybrid search and Retrieval-Augmented Generation (RAG) applications. Powered by the open-source Aryn Partitioner and <a href="https://huggingface.co/Aryn/deformable-detr-DocLayNet">effective, deep learning DETR AI</a> model trained on 80K+ enterprise documents, these tools ensure higher accuracy and relevance <a href="https://www.aryn.ai/post/an-evaluation-of-pdf-segmentation-and-layout-analysis-models">compared to off-the-shelf solutions</a>.</p><p>In this blog, we’ll demonstrate how to use DocParse and DocPrep to prepare and load a dataset of complex PDFs into Elasticsearch for a RAG application. We will use ~75 PDF reports from the National Transportation Safety Board (NTSB) about aircraft incidents. An example document from the collection is <a href="https://data.ntsb.gov/carol-repgen/api/Aviation/ReportMain/GenerateNewestReport/103753/pdf">here</a>.</p><h2>What is Aryn DocParse and DocPrep</h2><p>Aryn DocParse segments and labels documents, extracts tables, and images, and does OCR – turning 30+ document types into structured JSON. It runs the open-source Aryn Partitioner and its <a href="https://huggingface.co/Aryn/deformable-detr-DocLayNet">open-source deep learning DETR AI model</a> trained on 80k+ enterprise documents. This leads to <a href="https://www.aryn.ai/post/an-evaluation-of-pdf-segmentation-and-layout-analysis-models">up to 6x more accurate data chunking and 2x improved recall</a> on hybrid search or RAG compared to off-the-shelf systems.</p><p><a href="https://docs.aryn.ai/docprep/getting_started">Aryn DocPrep</a> is a tool for creating document ETL pipelines to prepare and load this data into vector databases and hybrid search indexes like Elasticsearch. The first step in a pipeline is using DocParse to process each document. DocPrep creates Python code using <a href="https://sycamore.readthedocs.io/en/stable/">Sycamore</a>, an open-source, scalable, LLM-powered document ETL library. Though DocPrep can easily create ETL pipelines using Sycamore code, you will likely need to customize the pipeline using additional <a href="https://sycamore.readthedocs.io/en/stable/">Sycamore data transforms, chunking/merging, extraction, and cleaning functions</a>.</p><p>As can be seen, these documents are complex, containing tables, images, section headings, and complicated layouts. Let’s begin!</p><h2>Building high-quality RAG apps with effective data preparation</h2><h3>Launch an Elasticsearch vector database container</h3><p>We’ll install Elasticsearch locally using a Docker container for the demo RAG application. Follow <a href="https://github.com/elastic/start-local">these instructions</a> to deploy it.</p><h3>Prepare data for RAG using Aryn DocPrep and DocParse</h3><p>Aryn <a href="https://docs.aryn.ai/docprep/getting_started">DocPrep</a> is a tool for creating document ETL pipelines that prepare and load data into vector databases and hybrid search indexes like Elasticsearch. The first step in a pipeline is using DocParse to process each document.</p><p>We will use Aryn DocParse in Aryn Cloud to generate our initial ETL pipeline code. You can <a href="https://www.aryn.ai/get-started">sign up for free</a> to use Aryn Cloud and go to the <a href="https://console.aryn.cloud/docprep/">DocPrep UI in the Aryn Cloud console</a>.</p><p>You can also write an ETL pipeline and run a version of the Aryn Partitioner (used in DocParse) locally. <a href="https://sycamore.readthedocs.io/en/stable/">Visit the Sycamore documentation</a> to learn more.</p><h3>Create ETL pipeline with Aryn DocPrep</h3><p>DocPrep creates Python code using <a href="https://sycamore.readthedocs.io/en/stable/">Sycamore</a>, an open-source, scalable, LLM-powered document ETL library. While DocPrep can easily create ETL pipelines using Sycamore code, you may need to customize the pipeline further with additional <a href="https://sycamore.readthedocs.io/en/stable/">Sycamore data transforms, extraction, and cleaning functions</a>.</p><p>DocPrep simplifies the creation of a base ETL pipeline to prepare unstructured data for RAG and semantic search.</p><p>First, we provide the document type (PDF) and the source location of our PDFs in Amazon S3 (<code>s3://aryn-public/ntsb/</code>):</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt688c73411f105c89/6a17e25a63173062e45859f0/799421fc6a5544dbba666a81fe88845bfe5532d2-738x410.png" alt="Select document type and source" /><p></p><p>Next, we will select MiniLM for our embedding model to create our vector embeddings locally. DocPrep uses DocParse for document segmentation, extraction, and other processing, but we don’t need to change the default configuration.</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt0b3959439d5478d1/6a17e25be31791b3a42d5759/4f84fe3e9d0130f815ecd380e011c036c5c506dd-936x452.png" alt="Select chunking options" /><p>Finally, we select Elasticsearch as our target database and add the Host URL and Index name. Note that the URL is set to “localhost” because we are running Elasticsearch locally. We will also run DocPrep/Sycamore ETL pipeline locally so it can easily load the cluster.</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt31d8160e920700df/6a17e25da2929981afd02bda/345545b1264db36bb113071e5baf69c51fe90fb1-936x728.png" alt="Configure Elasticsearch Connector" /><p>Now, click “Generate pipeline” to create the ETL pipeline. Click “Download notebook” on the next page to download the code as a Jupyter notebook file.</p><p></p><h3>Install Jupyter and Sycamore</h3><p>We will run the ETL pipeline locally in a Jupyter notebook and use the Sycamore document ETL library. As a first step, install Jupyter and Sycamore with the Elasticsearch connector and local inference libraries to create vector embeddings.</p>pip install jupyter
pip install 'sycamore-ai[elasticsearch,local-inference]'<p></p><h3>Run Pipeline</h3><p>Run Jupyter and open the notebook with the ETL pipeline downloaded in the earlier step.</p><p>If you haven’t set your Aryn Cloud API key as an environmental variable called <code>ARYN_API_KEY</code>, you can set it directly in the notebook.</p><p>In the second-to-last cell, update the Elasticsearch loading configuration. Replace the es_client_args from setting an Elasticsearch password to the Elasticsearch basic auth config from your container:</p><p></p>es_client_args={"basic_auth": (“&lt;YOUR-USERNAME&gt;”, os.getenv("ELASTIC_PASSWORD"))}<p></p><p>If the password isn’t set as an environment variable, you can add it directly here.</p><p>Now, run the cells in the notebook. Each of the ~75 PDFs is sent to DocParse for processing, and this step in the pipeline will take a few minutes. One of the cells will output three pages of a document with bounding boxes to show how DocParse segments the data.</p><p>The final cell runs a read query to verify if the data has been loaded correctly. Now, you can use the prepared data in the Elasticsearch index with your RAG application.</p><p></p><h3>Add additional data enrichment and transforms</h3><p>The code generated in DocPrep is great for a basic ETL pipeline, however, you may want to extract metadata and perform data cleaning. The pipeline code is fully customizable, and you can use additional transformations in Sycamore or arbitrary Python code.</p><p><a href="https://github.com/elastic/elasticsearch-labs/blob/main/supporting-blog-content/Aryn-elasticsearch-RAG-data-preparation-demo/aryn-elasticsearch-blog-dataprep.ipynb">Here is an example notebook</a> with additional data transforms, metadata extraction, and data cleaning steps. You can use this metadata in your RAG applications to filter your results.</p><h2>Conclusion</h2><p>This blog used Aryn DocParse, DocPrep, and Sycamore to parse, extract, enrich, clean, embed, and load data into vector and keyword indexes in the Elasticsearch vector database. We used DocPrep to create an initial ETL pipeline and then used a notebook with additional Sycamore code to demonstrate additional data enrichment and cleaning.</p><p>How your documents are parsed, enriched, and processed significantly impacts the quality of your RAG queries. Use the examples in this blog post to quickly and easily build your own RAG systems with Aryn and Elasticsearch and iterate on the processing and retrieval strategies as you build your GenAI application.</p><p>Below are some resources for your next steps:</p><ul><li><p><a href="https://github.com/elastic/elasticsearch-labs/blob/main/supporting-blog-content/Aryn-elasticsearch-RAG-data-preparation-demo/aryn-elasticsearch-blog-dataprep.ipynb">Sample notebook with Aryn DataPrep and Elasticsearch</a></p></li><li><p><a href="https://www.elastic.co/guide/en/elasticsearch/reference/current/run-elasticsearch-locally.html">start-local with Elasticsearch vector database</a></p></li><li><p><a href="https://www.aryn.ai/get-started">Get started with Aryn Cloud DocPrep</a></p></li><li><p><a href="https://sycamore.readthedocs.io/en/stable/">Sycamore documentation</a></p></li><li><p><a href="https://www.elastic.co/search-labs/integrations">Elasticsearch vector database ecosystem integrations</a></p></li></ul>]]></content:encoded>
    <link>https://www.elastic.co/search-labs/blog/rag-aryn-elasticsearch-data-prep</link>
    <guid isPermaLink="true">https://www.elastic.co/search-labs/blog/rag-aryn-elasticsearch-data-prep</guid>
    <category><![CDATA[AI]]></category>
    <category><![CDATA[Vector Database]]></category>
    <dc:creator><![CDATA[Hemant Malik,Jonathan Fritz]]></dc:creator>
    <enclosure url="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blte3979255ddfc7f45/6a17e25ffaa913812f93c7cb/92c517a2e7b36122a18feee317a0215981b62b6b-720x420.jpg" length="0" type="image/jpeg"/>
    <pubDate>Tue, 21 Jan 2025 00:00:00 GMT</pubDate>
  </item>
  <item>
    <title><![CDATA[Using Ollama and Go to build a RAG application]]></title>
    <description><![CDATA[Building a RAG application with Go using Ollama to leverage local models.]]></description>
    <content:encoded><![CDATA[<p>There’s a lot to say about a wide variety of open models. Some of them are known as the <code>Mixtral</code> family, in all of their sizes, and a maybe less known kind are the <code>openbiollm</code>, a Llama 3 adaptation for the medical field. Testing all of them by implementing their APIs would take a lot of work. However, Ollama allows us to test them all using a friendly interface and a straightforward command line.</p><p>In this article, we’ll build a RAG application in Golang, using Ollama as the LLM server and Elasticsearch as the vector database.</p><h3>Steps for building RAG application with Ollama &amp; Go</h3><ol><li><p><a href="https://www.elastic.co/search-labs/blog/rag-ollama-go-elasticsearch#install-ollama">Install Ollama</a></p></li><li><p><a href="https://www.elastic.co/search-labs/blog/rag-ollama-go-elasticsearch#ingesting-data">Ingesting data</a></p></li><li><p><a href="https://www.elastic.co/search-labs/blog/rag-ollama-go-elasticsearch#rag-app-in-go">RAG App in Go</a></p></li></ol><h2>Install Ollama</h2><h3>What is Ollama?</h3><p>Ollama is a framework that allows you to download and access models locally with a CLI. With simple commands we can download, chat, and set up a server with the model we want to consume from our app.</p><p>Download the Ollama installer here:</p><p><a href="https://ollama.com/">https://ollama.com/</a></p><p>The library with the available models is here:</p><p><a href="https://ollama.com/library">https://ollama.com/library</a></p><p>Once you have installed Ollama, we can test that everything works by running one of the available models. Let’s install <code>llama3.2</code> with 3B parameters. The library includes the necessary commands to download and run the model:</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt0447917eb80c2fbb/6a170ab96f7f04331d914833/5c059422975604e4341eec8f14d43e5a468f546d-770x243.png" alt="ollama_library" /><p>We’ll run the command for the 3B version:</p>ollama run llama3.2:latest<p>The first time, it will download the model and then open a chat in the terminal:</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt1b03cb5712103581/6a170aba964cea122e08bb6b/e8281d3bf4e08d8f427e3f1ccc98a4741a3e263d-722x267.png" alt="ollama_chat" /><p>Now we can type <code>/exit</code> to exit and use the server set up in this location: <code>http://localhost:11434</code>. Let's test the endpoints to make sure everything is working as expected.</p><p>Ollama offers two answer modes: <code>generate</code> to provide a single answer and <code>chat</code> to have conversations with the model:</p><h3>Generate</h3><p>We use generate when we expect a single answer to a single question and nothing else.</p>curl http://localhost:11434/api/generate -d '{
  "model": "llama3.2",
  "stream": false,
  "prompt":"Why is Elastic so cool?"
}'<h3>Chat</h3><p>We use chat when we expect to continue the conversation after the first question, and we want the LLM to remember the previous interactions.</p>curl http://localhost:11434/api/chat -d '{
  "model": "llama3.2",
  "stream": false,
  "messages": [
    { "role": "user", "content": "Why Elastic is so cool?" }
  ]
}' <p>By default, the answer is generated as <code>stream: true</code> but we’ll use <code>stream: false</code> so that the answer is generated in just one message and it’s easier to read. <code>stream: true</code> is useful in UI applications as tokens are sent as they are generated, as opposed to blocking until the entire response is complete.</p><p>Let’s move on to the data.</p><h3>Ingesting data</h3><p>Let's index some medical documents in Elasticsearch as text and vectors. We'll use these to test the quality of the answers from a medical-oriented model like <code>openbiollm</code> compared to a general one.</p><p>Before we start, make sure that we have created the inference endpoint to use <a href="https://www.elastic.co/guide/en/machine-learning/current/ml-nlp-elser.html">ELSER</a> as our embeddings model:</p>PUT _inference/sparse_embedding/my-elser-model 
{
  "service": "elser", 
  "service_settings": {
    "num_allocations": 1,
    "num_threads": 1
  }
}<p>Now, let’s continue creating the index using the <code>semantic_text</code> field type that will allow us to control the chunking size, as well as vector configuration. This allows our index to support full text, semantic, and hybrid searches.</p>PUT rag-ollama 
{
  "mappings": {
    "properties": {
      "semantic_field": {
        "type": "semantic_text",
        "inference_id": "my-elser-model"
      },
      "content": {
        "type": "text",
        "copy_to": "semantic_field"
      }
    }
  }
}<p>Now, let’s index the documents:</p>POST _bulk
{"index":{"_index":"rag-ollama"}}
{"title":"JAK Inhibitors vs. Monoclonal Antibodies in Rheumatoid Arthritis Treatment","content":"This article compares the mechanisms of action, efficacy, and safety profiles of JAK inhibitors and monoclonal antibodies in rheumatoid arthritis treatment, including recent clinical trial data and real-world evidence. It discusses the intracellular signaling pathways targeted by JAK inhibitors, their rapid onset of action, and oral administration advantages. The article also covers the specific targets of various monoclonal antibodies, their long-term safety profiles, and the criteria for choosing between these two classes of drugs based on patient characteristics and disease severity."}
{"index":{"_index":"rag-ollama"}}
{"title":"Diagnostic Approach to Resistant Hypertension: Focus on Primary Aldosteronism","content":"This guide outlines the step-by-step diagnostic process for resistant hypertension, with a particular emphasis on screening and confirming primary aldosteronism. It details the use of aldosterone-renin ratio (ARR) testing as an initial screening tool, explaining proper patient preparation and interpretation of results. The guide also covers confirmatory tests such as the saline infusion test and captopril challenge test, their protocols, and diagnostic criteria. Additionally, it discusses the role of imaging studies in localizing aldosterone-producing adenomas and the importance of adrenal vein sampling in subtype classification of primary aldosteronism."}
{"index":{"_index":"rag-ollama"}}
{"title":"Gut Microbiota Diversity and Inflammatory Cytokine Production in IBD","content":"This study examines the relationship between gut microbiota diversity and the production of pro-inflammatory cytokines in inflammatory bowel diseases (IBD). It explores how reduced microbial diversity correlates with increased levels of cytokines such as TNF-α, IL-1β, and IL-6 in both Crohn's disease and ulcerative colitis. The research discusses specific bacterial species associated with anti-inflammatory effects and their mechanisms of action. Furthermore, it delves into potential therapeutic implications, including the use of prebiotics, probiotics, and fecal microbiota transplantation to modulate the gut microbiome and influence cytokine production. The study also touches on emerging microbiome-based interventions and their potential to complement existing IBD treatments."}
{"index":{"_index":"rag-ollama"}}
{"title":"Biological Therapy Selection in Rheumatoid Arthritis After csDMARD Failure","content":"This article provides a comprehensive framework for selecting appropriate biological therapy in rheumatoid arthritis patients who have not responded adequately to conventional synthetic Disease-Modifying Antirheumatic Drugs (csDMARDs). It discusses the various classes of biologics available, including TNF inhibitors, IL-6 inhibitors, B-cell depleting agents, and T-cell costimulation modulators. The article outlines key factors to consider in the decision-making process, such as disease activity scores, extra-articular manifestations, comorbidities, and patient preferences. It also addresses the importance of biomarkers and predictors of treatment response in guiding therapy selection. The piece concludes with a discussion on cycling versus switching mechanisms of action when faced with inadequate response to an initial biologic agent."}
{"index":{"_index":"rag-ollama"}}
{"title":"Hypertension Management in Chronic Kidney Disease: Special Considerations","content":"This review discusses the unique challenges in managing hypertension in patients with chronic kidney disease (CKD). It outlines the current recommendations for blood pressure targets in CKD patients, explaining how these differ based on the presence and degree of albuminuria. The article explores the preferred classes of antihypertensive medications in CKD, with a focus on renin-angiotensin system blockers and their renoprotective effects. It addresses the complexities of managing volume status in CKD and the role of diuretics. The review also covers the impact of proteinuria on treatment decisions and the need for more aggressive blood pressure control in heavily proteinuric patients. Finally, it discusses considerations for patients on dialysis and the phenomenon of reverse epidemiology in end-stage renal disease."}<p>Done! Now that we have the model and data ready, we can put everything together with our Go app.</p><h3>RAG App in Go</h3><p>For our Go application, we could make calls to the Ollama server directly but I’ve decided to use <a href="https://github.com/parakeet-nest/parakeet">parakeet</a> instead. Parakeet is a library to create GenAI applications based on Go text. It provides Go interfaces to abstract the HTTP communication, besides giving helpers for embeddings, chunking, and memory, among others, so it makes it very easy to create an application.</p><p>We’ll begin by creating our work folder and setting up dependencies:</p>mkdir ollama-rag
cd ollama-rag
go mod init ollama-rag
go get github.com/parakeet-nest/parakeet
go get github.com/elastic/go-elasticsearch/v8@latest<p>Now, create a <code>main.go</code> file with the minimum you need to test that everything is configured correctly:</p><p><em>main.go</em></p>package main

import (
	"github.com/parakeet-nest/parakeet/completion"
	"github.com/parakeet-nest/parakeet/enums/option"
	"github.com/parakeet-nest/parakeet/llm"

	"fmt"
)

func main() {
	ollamaUrl := "http://localhost:11434"
	model := "llama3.2:latest"

	options := llm.SetOptions(map[string]interface{}{
		option.Temperature: 0.5,
	})

	question := llm.GenQuery{
		Model:   model,
		Prompt:  "Why Elastic is so cool?, answer in one sentence",
		Options: options,
	}

	// We use generate because we are going to run this script to ask a single question
	answer, err := completion.Generate(ollamaUrl, question)
	if err != nil {
        log.Fatal("😡:", err)
    }
	fmt.Println(answer.Response)
}<p>Run it:</p>go run main.go<p>In the terminal, you should see an answer similar to this:</p><p><em>Elastic, a company known for its innovative and user-friendly software solutions, has disrupted the traditional IT industry by empowering businesses to create, deploy, and manage applications quickly and reliably.</em></p><p>This answer is based on the LLM's training data, which is not something that we can provide or control and present some disadvantages:</p><ul><li><p>Information may be wrong</p></li><li><p>Information may be outdated</p></li><li><p>There is no way to get citations of the source</p></li></ul><p>Now, let’s create a file called <code>elasticsearch/elasticsearch.go</code> to connect with Elasticsearch using the <a href="https://github.com/elastic/go-elasticsearch">Go’s official client</a> and be able to use the information in our documents to generate grounded answers based on our data.</p>package elasticsearch

import (
	"context"
	"encoding/json"
	"fmt"
	"strings"

	"github.com/elastic/go-elasticsearch/v8"
	"github.com/elastic/go-elasticsearch/v8/typedapi/types"
)

// Initializing elasticsearch client

func EsClient() (*elasticsearch.TypedClient, error) {
	var cloudID = "" // your Elastic Cloud ID Here
	var apiKey = "" // your Elastic ApiKey Here

	es, err := elasticsearch.NewTypedClient(elasticsearch.Config{
		CloudID: cloudID,
		APIKey:  apiKey,
	})

	if err != nil {
		return nil, fmt.Errorf("unable to connect: %w", err)
	}
	return es, nil
}

// Searching for documents and building the context
func SemanticRetriever(client *elasticsearch.TypedClient, query string, size int) (string, error) {
	// Perform the semantic search
	res, err := client.Search().
		Index("rag-ollama").
		Query(&amp;types.Query{
			Semantic: &amp;types.SemanticQuery{
				Field: "semantic_field",
				Query: query,
			},
		}).
		Size(size).
		Do(context.Background())

	if err != nil {
		return "", fmt.Errorf("semantic search failed: %w", err)
	}

	// Prepare to format the results
	var output strings.Builder
	output.WriteString("Documents found\n\n")

	// Iterate through the search hits
	for i, hit := range res.Hits.Hits {
		// Define a struct to unmarshal each document
		var doc struct {
			Title   string `json:"title"`
			Content string `json:"content"`
		}

		// Unmarshal the document source into our struct
		if err := json.Unmarshal(hit.Source_, &amp;doc); err != nil {
			return "", fmt.Errorf("failed to unmarshal document %d: %w", i, err)
		}

		// Append the formatted document to our output
		output.WriteString(fmt.Sprintf("Title\n%s\n\nContent\n%s\n", doc.Title, doc.Content))

		// Add a separator between documents, except for the last one
		if i &lt; len(res.Hits.Hits)-1 {
			output.WriteString("\n-----\n\n")
		}
	}

	// Return the formatted output as a string
	return output.String(), nil
}<p>The <code>EsClient</code> function initializes the Elasticsearch client using the Cloud Credentials provided, and <code>SemanticRetriever</code> performs a semantic query to build the context the LLM needs to answer a question.</p><p>To find your Cloud ID and API Key, <a href="https://www.elastic.co/search-labs/tutorials/install-elasticsearch/elastic-cloud">go to this link</a>.</p><p>Let’s go back to our <code>main.go</code> file and update with the above features to call Elasticsearch and run a semantic query: This builds the LLM context:</p><p><em>main.go</em></p>package main

import (
	"fmt"
	"log"
	"ollama-rag/elasticsearch"

	"github.com/parakeet-nest/parakeet/completion"
	"github.com/parakeet-nest/parakeet/enums/option"
	"github.com/parakeet-nest/parakeet/llm"
)

func main() {

	ollamaUrl := "http://localhost:11434"
	chatModel := "llama3.2:latest"
	question := `Summarize document: JAK Inhibitors vs. Monoclonal Antibodies in Rheumatoid Arthritis Treatment`
	size := 3

	esClient, err := elasticsearch.EsClient()

	if err != nil {
		log.Fatalln("😡:", err)
	}

	// Retrieve documents from semantic query to build context
	documentsContent, nil := elasticsearch.SemanticRetriever(esClient, question, size)

	systemContent := `You are a helpful medical assistant. Only answer the questions based on found documents.
	Add references to the base document titles and be succint in your answers.`

	options := llm.SetOptions(map[string]interface{}{
		option.Temperature: 0.0,
	})

	queryChat := llm.Query{
		Model: chatModel,
		Messages: []llm.Message{
			{Role: "system", Content: systemContent},
			{Role: "system", Content: documentsContent},
			{Role: "user", Content: question},
		},
		Options: options,
	}

	fmt.Println()
	fmt.Println("🤖 answer:")

	// Answer the question
	_, err = completion.ChatStream(ollamaUrl, queryChat,
		func(answer llm.Answer) error {
			fmt.Print(answer.Message.Content)
			return nil
		})
	if err != nil {
		log.Fatal("😡:", err)
	}

	fmt.Println()
}<p>As you can see, we send the user’s question together with all of the documents related to it. This is how we get an answer based on documents in Elasticsearch.</p><p>We can test by running the code:</p>go run .<p>You should see something like:</p>According to the article "JAK Inhibitors vs. Monoclonal Antibodies in Rheumatoid Arthritis Treatment", JAK inhibitors and monoclonal antibodies are two classes of drugs used to treat rheumatoid arthritis (RA). The main difference between them lies in their mechanisms of action:<ul><li>JAK inhibitors target intracellular signaling pathways, specifically the Janus kinase (JAK) pathway, which is involved in inflammation and immune response. They have a rapid onset of action and are administered orally.</li><li>Monoclonal antibodies target specific proteins involved in the inflammatory process, such as tumor necrosis factor-alpha (TNF-α), interleukin-6 (IL-6), and interleukin-17 (IL-17).</li></ul>The article highlights that JAK inhibitors have a more favorable safety profile compared to monoclonal antibodies, with fewer gastrointestinal side effects. However, the choice between these two classes of drugs depends on patient characteristics and disease severity."References:"<ul><li>"JAK Inhibitors vs. Monoclonal Antibodies in Rheumatoid Arthritis Treatment" (document title)</li></ul><p>Parakeet will handle the Ollama interaction for us, including the token stream! From now on, we can test different models quite easily without making changes to the code.</p><p>Besides the models in the main library, we also have access to the ones uploaded by community members.</p><p>To use one, we just have to make sure to download into Ollama first. For example, let’s test openbiollm:</p><p><code>ollama run taozhiyuai/openbiollm-llama-3:8b_q8_0</code></p><p>Once installed, we can use it with our Go code:</p>chatModel := "taozhiyuai/openbiollm-llama-3:8b_q8_0"<p>Let’s run it again with the same question. Do you notice any differences?</p>In rheumatoid arthritis treatment, JAK inhibitors and monoclonal antibodies are commonly used. This article discusses the benefits and drawbacks of both therapies. JAK inhibitors work by targeting intracellular signaling pathways involved in the immune response. They have a rapid onset of action and can be administered orally, making them convenient for patients. Recent clinical trial data has shown that JAK inhibitors are effective at reducing inflammation and slowing joint damage progression in rheumatoid arthritis. However, there is still ongoing research to fully understand their long-term safety profile. Monoclonal antibodies, on the other hand, specifically target molecules involved in the immune system. These drugs have been found to be highly effective in managing symptoms of rheumatoid arthritis and improving joint function. They can provide prolonged symptom control and are often used as first-line treatment options. However, due to their complexity and unique administration requirements, monoclonal antibodies may not be suitable for all patients. In conclusion, both J AK inhibitors and monoclonal antibodies have their own advantages and disadvantages in treating rheumatoid arthritis. The choice of therapy depends on individual patient characteristics and disease severity. Ongoing research will contribute to a deeper understanding of the efficacy and safety profiles of these treatments, ultimately leading to improved care for patients with rheumatoid arthritis.<p>It seems that the openbiollm model provided more details with the technical lingo, but it didn’t follow instructions about referencing the documents provided in the context and giving brief answers. In comparison, Llama3.2 followed instructions better.</p><p>You can find the full working example <a href="https://github.com/elastic/elasticsearch-labs/tree/main/supporting-blog-content/ollama-and-go-for-rag">here</a></p><h2>Conclusion</h2><p>Ollama provides a very straightforward and simple way to download and test different open models, from the better-known to those fine-tuned by community members. Pairing it with Parakeet and the official Elasticsearch Go client, makes it very easy to create a RAG application. In addition, by using the <code>semantic_text</code> field type, you can create a semantic-query-ready index that uses ELSER–the Elastic Sparse embeddings model–without any additional configurations, thus simplifying the chunking, indexing, and vector querying process, too.</p>]]></content:encoded>
    <link>https://www.elastic.co/search-labs/blog/rag-ollama-go-elasticsearch</link>
    <guid isPermaLink="true">https://www.elastic.co/search-labs/blog/rag-ollama-go-elasticsearch</guid>
    <category><![CDATA[AI]]></category>
    <category><![CDATA[Vector Database]]></category>
    <dc:creator><![CDATA[Gustavo Llermaly]]></dc:creator>
    <enclosure url="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt9dc96557970f2395/6a170abcd7c022a0bdde6462/47f7bc050fd9b7d21648138f0c003ec6bb015c9a-1198x804.png" length="0" type="image/png"/>
    <pubDate>Thu, 26 Dec 2024 00:00:00 GMT</pubDate>
  </item>
  <item>
    <title><![CDATA[How to use Elasticsearch Vector Store Connector for Microsoft Semantic Kernel for AI Agent development]]></title>
    <description><![CDATA[Microsoft Semantic Kernel is a lightweight, open-source development kit that lets you easily build AI agents and integrate the latest AI models into your C#, Python, or Java codebase. With the release of Semantic Kernel Elasticsearch Vector Store Connector, developers using Semantic Kernel for building AI agents can now plugin Elasticsearch as a scalable enterprise-grade vector store while continuing to use Semantic Kernel abstractions.]]></description>
    <content:encoded><![CDATA[<p>In collaboration with the <a href="https://learn.microsoft.com/en-us/semantic-kernel/overview/">Microsoft Semantic Kernel</a> team, we are announcing the availability of <a href="https://github.com/elastic/semantic-kernel-net/">Semantic Kernel Elasticsearch Vector Store Connector</a>, for <a href="https://learn.microsoft.com/en-us/semantic-kernel/overview/">Microsoft Semantic Kernel</a> (.NET) users. Semantic Kernel simplifies building enterprise-grade AI agents, including the capability to enhance large language models (LLMs) with more relevant, data-driven responses from a Vector Store. Semantic Kernel provides a seamless abstraction layer for interacting with Vector Stores like Elasticsearch, offering essential features such as creating, listing, and deleting collections of records and uploading, retrieving, deleting individual records.</p><p>The <a href="https://learn.microsoft.com/en-us/semantic-kernel/concepts/vector-store-connectors/out-of-the-box-connectors/elasticsearch-connector?pivots=programming-language-csharp">out-of-the-box Semantic Kernel Elasticsearch Vector Store Connector</a> supports the Semantic Kernel <a href="https://learn.microsoft.com/en-us/semantic-kernel/concepts/vector-store-connectors/?pivots=programming-language-csharp#the-vector-store-abstraction">vector store abstractions</a> which make it very easy for developers to plugin Elasticsearch as a vector store while building AI agents.</p><p>Elasticsearch has a strong foundation in the open-source community and recently adopted the <a href="https://www.elastic.co/blog/elasticsearch-is-open-source-again">AGPL license</a>. Combined with the open-source Microsoft Semantic Kernel, these tools offer a powerful, enterprise-ready solution. You can get started locally by spinning up Elasticsearch in a few minutes by running this command <code>curl -fsSL https://elastic.co/start-local | sh </code>(refer <a href="https://www.elastic.co/guide/en/elasticsearch/reference/current/run-elasticsearch-locally.html">start-local</a> for details) and move to <a href="https://cloud.elastic.co/registration?onboarding_token=vectorsearch&amp;utm_source=semantickernel&amp;utm_content=documentation">cloud-hosted</a> or <a href="https://www.elastic.co/guide/en/elasticsearch/reference/8.16/install-elasticsearch.html">self-hosted</a> versions while productionizing your AI agents.</p><p>In this blog we look at how to use <a href="https://github.com/elastic/semantic-kernel-net/">Semantic Kernel Elasticsearch Vector Store Connector</a> when using Semantic Kernel. A Python version of the connector will be made available in the future.</p><h2>High-level scenario: Building a RAG app with Semantic Kernel &amp; Elasticsearch</h2><p>In the following section we go through an example. At a high-level we are building a RAG (Retrieval Augmented Generation) application which takes a user's question as input and returns an answer. We will use Azure OpenAI (<a href="https://devblogs.microsoft.com/semantic-kernel/introducing-new-ollama-connector-for-local-models/">local LLM</a> can be used as well) as the LLM, Elasticsearch as the vector store and Semantic Kernel (.net) as the framework to tie all components together.</p><p>If you are not familiar with RAG architectures, you can have a quick introduction with this article: <a href="https://www.elastic.co/search-labs/blog/retrieval-augmented-generation-rag">https://www.elastic.co/search-labs/blog/retrieval-augmented-generation-rag</a>.</p><p>The answer is generated by the LLM which is fed with context, relevant to the question, retrieved from Elasticsearch vectorstore. The response also includes the source that was used as the context by the LLM.</p><h3>RAG example</h3><p>In this specific example, we build an application that allows users to ask questions about hotels stored in an internal hotel database. The user could e.g. search for a specific hotel, based on different criteria, or ask for a list of hotels.</p><p>For the example database, we generated a <a href="https://github.com/elastic/semantic-kernel-net/blob/main/Elastic.SemanticKernel.Playground/hotels.csv">list of hotels</a> containing 100 entries. The sample size is intentionally small to allow you to try out the connector demo as easily as possible. In a real-world application, the Elasticsearch connector would show its advantages over other options, such as the `InMemory` vector store implementation, especially when working with extremely large amounts of data.</p><p>The complete demo application can be found in the Elasticsearch vector store connector <a href="https://github.com/elastic/semantic-kernel-net/tree/main/Elastic.SemanticKernel.Playground">repository</a>.</p><p>Let’s start with adding the required NuGet packages and using directives to our project:</p>dotnet add package "Elastic.Clients.Elasticsearch" -v 8.16.2
dotnet add package "Elastic.SemanticKernel.Connectors.Elasticsearch" -v 0.1.2
dotnet add package "Microsoft.Extensions.Hosting" -v 9.0.0
dotnet add package "Microsoft.SemanticKernel.Connectors.AzureOpenAI" -v 1.30.0
dotnet add package "Microsoft.SemanticKernel.PromptTemplates.Handlebars" -v 1.30.0using System;
using System.IO;
using System.Linq;
using System.Threading.Tasks;

using Elastic.Clients.Elasticsearch;
using Elastic.Transport;

using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.VectorData;
using Microsoft.SemanticKernel;
using Microsoft.SemanticKernel.Data;
using Microsoft.SemanticKernel.Embeddings;
using Microsoft.SemanticKernel.PromptTemplates.Handlebars;<p>We can now create our data model and provide it with Semantic Kernel specific attributes to define the storage model schema and some hints for the text search:</p>/// &lt;summary&gt;
/// Data model for storing a "hotel" with a name, a description, a  description embedding and an optional reference link.
/// &lt;/summary&gt;
public sealed record Hotel
{
	[VectorStoreRecordKey]
	public required string HotelId { get; set; }

	[TextSearchResultName]
	[VectorStoreRecordData(IsFilterable = true)]
	public required string HotelName { get; set; }

	[TextSearchResultValue]
	[VectorStoreRecordData(IsFullTextSearchable = true)]
	public required string Description { get; set; }

	[VectorStoreRecordVector(Dimensions: 1536, DistanceFunction.CosineSimilarity, IndexKind.Hnsw)]
	public ReadOnlyMemory&lt;float&gt;? DescriptionEmbedding { get; set; }

	[TextSearchResultLink]
	[VectorStoreRecordData]
	public string? ReferenceLink { get; set; }
}<p>The Storage Model Schema attributes (`VectorStore*`) are most relevant for the actual use of the Elasticsearch Vector Store Connector, namely:</p><p></p><ul><li><p><code>VectorStoreRecordKey</code> to mark a property on a record class as the key under which the record is stored in a vector store.</p></li><li><p><code>VectorStoreRecordData</code> to mark a property on a record class as 'data'.</p></li><li><p><code>VectorStoreRecordVector</code> to mark a property on a record class as a vector.</p></li></ul><p>All of these attributes accept various optional parameters that can be used to further customize the storage model. In the case of <code>VectorStoreRecordKey </code>, for example, it is possible to specify a different distance function or a different index type.</p><p>The text search attributes (<code>TextSearch*</code>) will be important in the last step of this example. We will come back to them later.</p><p>In the next step, we initialize the Semantic Kernel engine and obtain references to the core services. In a real world application, <a href="https://learn.microsoft.com/en-us/dotnet/core/extensions/dependency-injection">dependency injection</a> should be used instead of directly accessing the service collection. The same thing applies to the hardcoded configuration and secrets, which should be read using a <a href="https://learn.microsoft.com/en-us/dotnet/core/extensions/configuration">configuration provider</a> instead:</p>var builder = Host.CreateApplicationBuilder(args);

// Register AI services.
var kernelBuilder = builder.Services.AddKernel();

kernelBuilder.AddAzureOpenAIChatCompletion("gpt-4o", "https://my-service.openai.azure.com", "my_token");

kernelBuilder.AddAzureOpenAITextEmbeddingGeneration("ada-002", "https://my-service.openai.azure.com", "my_token");

// Register text search service.
kernelBuilder.AddVectorStoreTextSearch&lt;Hotel&gt;();

// Register Elasticsearch vector store.
var elasticsearchClientSettings = new ElasticsearchClientSettings(new Uri("https://my-elasticsearch-instance.cloud"))
    .Authentication(new BasicAuthentication("elastic", "my_password"));

kernelBuilder.AddElasticsearchVectorStoreRecordCollection&lt;string, Hotel&gt;("skhotels", elasticsearchClientSettings);

// Build the host.
using var host = builder.Build();

// For demo purposes, we access the services directly without using a DI context.

var kernel = host.Services.GetService&lt;Kernel&gt;()!;
var embeddings = host.Services.GetService&lt;ITextEmbeddingGenerationService&gt;()!;
var vectorStoreCollection = host.Services.GetService&lt;IVectorStoreRecordCollection&lt;string, Hotel&gt;&gt;()!;

// Register search plugin.
var textSearch = host.Services.GetService&lt;VectorStoreTextSearch&lt;Hotel&gt;&gt;()!;
kernel.Plugins.Add(textSearch.CreateWithGetTextSearchResults("SearchPlugin"));<p>The <code>vectorStoreCollection</code> service can now be used to create the collection and to ingest a few <a href="https://github.com/elastic/semantic-kernel-net/blob/main/Elastic.SemanticKernel.Playground/hotels.csv">demo records</a>:</p>await vectorStoreCollection.CreateCollectionIfNotExistsAsync();

// CSV format: ID;Hotel Name;Description;Reference Link
var hotels = (await File.ReadAllLinesAsync("hotels.csv"))
    .Select(x =&gt; x.Split(';'));

foreach (var chunk in hotels.Chunk(25))
{
    var descriptionEmbeddings = await embeddings.GenerateEmbeddingsAsync(chunk.Select(x =&gt; x[2]).ToArray());
    
    for (var i = 0; i &lt; chunk.Length; ++i)
    {
        var hotel = chunk[i];
        await vectorStoreCollection.UpsertAsync(new Hotel
        {
            HotelId = hotel[0],
            HotelName = hotel[1],
            Description = hotel[2],
            DescriptionEmbedding = descriptionEmbeddings[i],
            ReferenceLink = hotel[3]
        });
    }
}<p>This shows how Semantic Kernel reduces the use of a vector store with all its complexity to a few simple method calls.</p><p>Under the hood, a new index is created in Elasticsearch and all the necessary property mappings are created. Our data set is then mapped completely transparently into the storage model and finally stored in the index. Below is how the mappings look in Elasticsearch.</p>{
  "mappings": {
    "properties": {
      "descriptionEmbedding": {
        "dims": 1536,
        "index": true,
        "index_options": {
          "type": "hnsw"
        },
        "similarity": "cosine",
        "type": "dense_vector"
      },
      "hotelName": {
        "type": "keyword"
      },
      "description": {
        "type": "text"
      }
    }
  }
}<p>The <code>embeddings.GenerateEmbeddingsAsync()</code> calls transparently called the configured Azure AI Embeddings Generation service.</p><p>Even more magic can be observed in the last step of this demo.</p><p>With just a single call to <code>InvokePromptAsync</code>, all of the following operations are performed when the user asks a question about the data:</p><p>1. An embedding for the user's question is generated</p><p>2. The vector store is searched for relevant entries</p><p>3. The results of the query are inserted into a prompt template</p><p>4. The actual query in the form of the final prompt is sent to the AI chat completion service</p>// Invoke the LLM with a template that uses the search plugin to
// 1. get related information to the user query from the vector store
// 2. add the information to the LLM prompt.
var response = await kernel.InvokePromptAsync(
    promptTemplate: """
                    Please use this information to answer the question:
                    {{#with (SearchPlugin-GetTextSearchResults question)}}
                      {{#each this}}
                        Name: {{Name}}
                        Value: {{Value}}
                        Source: {{Link}}
                        -----------------
                      {{/each}}
                    {{/with}}
                    
                    Include the source of relevant information in the response.

                    Question: {{question}}
                    """,
    arguments: new KernelArguments
    {
        { "question", "Please show me all hotels that have a rooftop bar." },
    },
    templateFormat: "handlebars",
    promptTemplateFactory: new HandlebarsPromptTemplateFactory());<p>Remember the <code>TextSearch*</code> attributes, we previously defined on our data model? These attributes enable us to use corresponding placeholders in our prompt template which are automatically populated with the information from our entries in the vector store.</p><p>The final response to our question "Please show me all hotels that have a rooftop bar." is as follows:</p>Console.WriteLine(response.ToString());

// &gt; The hotel that has a rooftop bar is Skyline Suites. You can find more information about this hotel [here](https://example.com/yz567).<p>The answer correctly refers to the following entry in our hotels.csv</p>9;
Skyline Suites;
Offering panoramic city views from every suite, this hotel is perfect for those who love the urban landscape. Enjoy luxurious amenities, a rooftop bar, and close proximity to attractions. Luxurious and contemporary.;
https://example.com/yz567<p>This example shows very well how the use of Microsoft Semantic Kernel achieves a significant reduction in complexity through its well thought abstractions, as well as enabling a very high level of flexibility. By changing a single line of code, for example, the vector store or the AI services used can be replaced without having to refactor any other part of the code.</p><p>At the same time, the framework provides an enormous set of high-level functionality, such as the `InvokePrompt` function, or the template or search plugin system.</p><p>The complete demo application can be found in the Elasticsearch vector store connector repository.</p><h2>What else is possible with Elasticsearch</h2><ul><li><p><a href="https://www.elastic.co/search-labs/blog/semantic-search-simplified-semantic-text">Elasticsearch new semantic_text mapping: Simplifying semantic search</a></p></li><li><p><a href="https://www.elastic.co/search-labs/blog/semantic-reranking-with-retrievers">Semantic reranking in Elasticsearch with retrievers</a></p></li><li><p><a href="https://www.elastic.co/search-labs/blog/advanced-rag-techniques-part-1">Advanced RAG techniques part 1: Data processing</a></p></li><li><p><a href="https://www.elastic.co/search-labs/blog/advanced-rag-techniques-part-2">Advanced RAG techniques part 2: Querying and testing</a></p></li><li><p><a href="https://www.elastic.co/search-labs/blog/elasticsearch-rag-with-llama3-opensource-and-elastic">Building RAG with Llama 3 open-source and Elastic</a></p></li><li><p><a href="https://www.elastic.co/search-labs/blog/local-rag-agent-elasticsearch-langgraph-llama3">A tutorial on building local agent using LangGraph, LLaMA3 and Elasticsearch vector store from scratch</a></p></li></ul><h2>Elasticsearch &amp; Semantic Kernel: What's next?</h2><ul><li><p>We showed how the Elasticsearch vector store can be easily plugged into Semantic Kernel while building GenAI applications in .NET. Stay tuned for a Python integration next.</p></li><li><p>As Semantic Kernel builds abstractions for advanced search features like <a href="https://www.elastic.co/search-labs/tutorials/search-tutorial/vector-search/hybrid-search">hybrid search</a>, the Elasticsearch connect will enable .NET developers to easily implement them while using Semantic Kernel.</p></li></ul>]]></content:encoded>
    <link>https://www.elastic.co/search-labs/blog/elasticsearch-connector-microsoft-semantic-kernel</link>
    <guid isPermaLink="true">https://www.elastic.co/search-labs/blog/elasticsearch-connector-microsoft-semantic-kernel</guid>
    <category><![CDATA[AI]]></category>
    <category><![CDATA[.NET]]></category>
    <category><![CDATA[Vector Database]]></category>
    <dc:creator><![CDATA[Florian Bernd,Srikanth Manvi]]></dc:creator>
    <enclosure url="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt2d8725035e86f8a8/6a17fe447f6f1564f8c09d74/0564fe794e4c66d0507317822d7aa71826183d20-1311x762.jpg" length="0" type="image/jpeg"/>
    <pubDate>Fri, 06 Dec 2024 00:00:00 GMT</pubDate>
  </item>
  <item>
    <title><![CDATA[Vector embeddings made simple with the Elasticsearch-DSL client for Python]]></title>
    <description><![CDATA[Learn how to ingest and search dense vectors in Python using the Elasticsearch-DSL client.]]></description>
    <content:encoded><![CDATA[<p>In this article we'll take a look at the <a href="https://elasticsearch-dsl.readthedocs.io/en/latest/index.html">Elasticsearch-DSL</a> client for Python, with a focus on how it simplifies the task of building a vector search solution.</p><p>The <a href="https://github.com/miguelgrinberg/quotes">code</a> that accompanies this article implements a database of famous quotes. It includes a back end written in Python with the <a href="https://fastapi.tiangolo.com/">FastAPI</a> web framework, and a front end written in <a href="https://www.typescriptlang.org/">TypeScript</a> and <a href="https://react.dev/">React</a>. Regarding vector search, this application demonstrates how to:</p><ul><li><p>run a local Elasticsearch service using Docker,</p></li><li><p>bulk-ingest a large number of documents efficiently,</p></li><li><p>generate vector embeddings for documents as they are ingested,</p></li><li><p>leverage the power of a GPU to accelerate the generation of vector embeddings through parallelization,</p></li><li><p>run vector search queries using the <a href="https://www.elastic.co/guide/en/elasticsearch/reference/current/knn-search.html#approximate-knn">approximate kNN algorithm</a>,</p></li><li><p>aggregate results from vector search,</p></li><li><p>compare vector search results against those resulting from a standard <a href="https://www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl-match-query.html">match</a> (BM25) query.</p></li></ul><p>Below you can see a screenshot of the application. In this article you will find a detailed explanation of how the ingest and search features work. You then have the option to install and run the code on your own computer to experiment and learn!</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt4a680a6bf95f4421/6a17122b66c4f9358ef8c157/1225861f71cf9ccbb2102216a9365dd07ff71e9c-1440x858.png" alt="Application screenshot" /><h2>What is the Elasticsearch-DSL client for Python?</h2><p>Sometimes called the "high-level" Python client, <a href="https://elasticsearch-dsl.readthedocs.io/en/latest/index.html">Elasticsearch-DSL</a> offers idiomatic (or "Pythonic") access to your Elasticsearch database, in contrast with the official (or "low-level") Python client, which provides direct access to the complete range of Elasticsearch features and endpoints.</p><p>When using Elasticsearch-DSL, the structure (or "mappings") of Elasticsearch indices are defined as classes, with a syntax that is similar to that of Python <a href="https://docs.python.org/3/library/dataclasses.html">dataclasses</a>. The documents stored in these indices are represented by instances of these classes. All the transformations that are necessary to map between Python objects and Elasticsearch documents are automatically and transparently carried out, resulting in application code that is simple and idiomatic.</p><p>To add Elasticsearch-DSL to your Python project, you can install it with <code>pip</code>:</p>pip install elasticsearch-dsl
<p>If your project is asynchronous, then there are additional dependencies that need to be installed, so in that case use the following command instead:</p>pip install "elasticsearch-dsl[async]"
<h2>Index definition</h2><p>As stated above, with Elasticsearch-DSL the structure of an Elasticsearch index is defined as a Python class. The example application featured in this article uses a dataset of famous quotes that have the following fields:</p><ul><li><p><code>quote</code>: the text of the quote, as a string</p></li><li><p><code>author</code>: the name of the author, as a string</p></li><li><p><code>tags</code>: a list of tag names that apply to the quote, each a string</p></li></ul><p>As part of this application we are going to add one additional field, the vector embedding that we will use to search for quotes:</p><ul><li><p><code>embedding</code>: a list of floating point numbers representing a vector embedding for the quote</p></li></ul><p>Let's write an initial document class to describe our famous quotes index:</p>import elasticsearch_dsl as dsl

class QuoteDoc(dsl.AsyncDocument):
    quote: str
    author: str
    tags: list[str]
    embedding: list[float]

    class Index:
        name = 'quotes'
<p>The <code>AsyncDocument</code> class that is used as a base class for our <code>QuoteDoc</code> class implements all the functionality to connect the class to an Elasticsearch index. The choice of an asynchronous document base class was made because this examples uses the FastAPI web framework, which is also asynchronous. For projects that do not use asynchronous Python, the <code>Document</code> base class must be used when declaring document classes.</p><p>The <code>name</code> attribute given in the <code>Index</code> inner class defines the name of the Elasticsearch index that will be used with documents of this class.</p><p>If you have used Python dataclasses before, you likely find the way fields are defined very familiar, with each field being given a Python type hint. These Python types are mapped to the closest Elasticsearch type, so for example, in the case of <code>str</code>, the corresponding field in the Elasticsearch index will be given the type <a href="https://www.elastic.co/guide/en/elasticsearch/reference/current/text.html#text-field-type"><code>text</code></a>, the standard type that is used for text that needs to be indexed for full-text search, while <code>float</code> is mapped to the equally named <a href="https://www.elastic.co/guide/en/elasticsearch/reference/current/number.html"><code>float</code></a> on the Elasticsearch side.</p><p>While it can be useful to leave the <code>quote</code> field as is so that we can use it for both vector and full-text searches, the <code>author</code> and <code>tags</code> fields do not really need all the extra work associated with full-text search. The best Elasticsearch type for these fields is <a href="https://www.elastic.co/guide/en/elasticsearch/reference/current/keyword.html#keyword-field-type"><code>keyword</code></a>, which just stores the text, without doing any indexing. Likewise, the <code>embedding</code> field is not just a simple list of floating point numbers, we are going to use it for vector search, which is a behavior associated with the <a href="https://www.elastic.co/guide/en/elasticsearch/reference/current/dense-vector.html"><code>dense_vector</code></a> type in Elasticsearch.</p><p>To assign a type override to a field, we add an assignment with the <code>mapped_field()</code> function, as shown in the improved version of the <code>QuoteDoc</code> class that follows:</p>class QuoteDoc(dsl.AsyncDocument):
    quote: str
    author: str = dsl.mapped_field(dsl.Keyword())
    tags: list[str] = dsl.mapped_field(dsl.Keyword())
    embedding: list[float] = dsl.mapped_field(dsl.DenseVector(), init=False)

    class Index:
        name = 'quotes'
<p>As you can see in this updated version, the <code>elasticsearch_dsl</code> package includes classes such as <code>Keyword</code> and <code>DenseVector</code> to represent all the native Elasticsearch field types.</p><p>Did you notice the <code>init=False</code> argument given in this new definition of the <code>embedding</code> field? If you are familiar with Python dataclasses you may recognize <code>init</code> as one of the options available in the dataclasses <a href="https://docs.python.org/3/library/dataclasses.html#dataclasses.field"><code>field()</code></a> function, used to indicate that the given attribute should be omitted from the constructor for instances of the class. The behavior is the same here, which means that when creating an instance of <code>QuoteDoc</code>, this argument should not be given.</p><p>How will the vector embeddings be generated if they will not be passed down to the document constructor? Elasticsearch-DSL always calls the <code>clean()</code> method in all documents before serializing them and sending them to Elasticsearch. This method is a convenience entry point where the application can add any custom field processing logic. For example, fields that are optional or auto-generated can be added in this method. Here is the final version of the <code>QuoteDoc</code> document class, including the logic that generates the embeddings:</p>from sentence_transformers import SentenceTransformer

model = SentenceTransformer("all-MiniLM-L6-v2")

class QuoteDoc(dsl.AsyncDocument):
    quote: str
    author: str = dsl.mapped_field(dsl.Keyword())
    tags: list[str] = dsl.mapped_field(dsl.Keyword())
    embedding: list[float] = dsl.mapped_field(dsl.DenseVector(), init=False)

    class Index:
        name = 'quotes'

    def clean(self):
        if not self.embedding:
            self.embedding = model.encode(self.quote).tolist()
<p>For this example we are going to use embeddings from a <a href="https://sbert.net/">SentenceTransformers</a> model. These embeddings are easy to generate locally and being open source and free they are convenient to use when experimenting. The <a href="https://huggingface.co/sentence-transformers/all-MiniLM-L6-v2">all-MiniLM-L6-v2</a> model is a great general purpose embedding model for English text. There are many other models that are also compatible with the SentenceTransformers framework, so feel free to use a different one if you prefer.</p><p>The <code>clean()</code> method can be used for more advanced use cases as well. For example, it is common when working with large bodies of text to split the text into smaller chunks, and then generate embeddings for each chunk. Elasticsearch accommodates this use case through nested objects. If you want to see an advanced example that implements this type of solution, check out the <a href="https://github.com/elastic/elasticsearch-dsl-py/blob/main/examples/vectors.py">vectors</a> example in the Elasticsearch-DSL repository.</p><h2>Document ingestion</h2><p>With the structure of the index in place, we can now create the index. This is done with the <code>init()</code> class method:</p>async def ingest_quotes():
    await QuoteDoc.init()
<p>In many cases it is useful to delete a previously existing index to make sure an ingest process begins from a clean starting point. This can be done using the <code>_index</code> class attribute, which provides access to the Elasticsearch index, along with its <code>exists()</code> and <code>delete()</code> methods:</p>async def ingest_quotes():
    if await QuoteDoc._index.exists():
        await QuoteDoc._index.delete()
    await QuoteDoc.init()
<p>The example dataset used by the example application is a collection of almost 37,000 famous quotes. It comes as a CSV file with the <code>quote</code>, <code>author</code> and <code>tags</code> columns. The tags are given as a comma-separated string. The dataset is available for <a href="https://raw.githubusercontent.com/miguelgrinberg/quotes/main/backend/quotes.csv">download</a> from the example GitHub repository.</p><p>To ingest the data contained in this dataset, Python's <code>csv</code> module can be used:</p>import csv

async def ingest_quotes():
    if await QuoteDoc._index.exists():
        await QuoteDoc._index.delete()
    await QuoteDoc.init()

    with open('quotes.csv') as f:
        reader = csv.DictReader(f)
        for row in reader:
            q = QuoteDoc(quote=row['quote'], author=row['author'],
                         tags=row['tags'].split(','))
            await q.save()
<p>The <code>csv.DictReader</code> class creates a CSV file importer that returns a dictionary for each row in the data file. For each row, we create a <code>QuoteDoc</code> instance and pass the <code>quote</code>, <code>author</code> and <code>tags</code> in the constructor. For the tags, the string that is read from the CSV file has to be split into a list, which is how it will be stored in the Elasticsearch index.</p><p>To write a document to the index, the <code>save()</code> method is invoked. This method will call the document's <code>clean()</code> method, which in turn will generate the vector embedding for the quote.</p><h3>Starting an Elasticsearch instance</h3><p>Before the above ingest script can be executed, you need to have access to a running instance of Elasticsearch. By far the easiest (and also 100% free) way to do this is with a <a href="https://www.docker.com/">Docker</a> container.</p><p>To start a single-node Elasticsearch service on your computer first make sure you have Docker running, and then execute the following command:</p>docker run -p 127.0.0.1:9200:9200 -d --name elasticsearch \
  -e "discovery.type=single-node" \
  -e "xpack.security.enabled=false" \
  -e "xpack.license.self_generated.type=basic" \
  -v "./data:/usr/share/elasticsearch/data" \
  docker.elastic.co/elasticsearch/elasticsearch:8.15.0
<p>To make sure you are running the latest and greatest version, open the <a href="https://www.elastic.co/guide/en/elasticsearch/reference/current/es-release-notes.html">release notes</a> page to find out what is the current version, then replace the version number in the last line of the above command.</p><p>The <code>-v</code> option in the command above sets up a mapping between a directory named <code>data</code> in your local system and the data directory in the Elasticsearch container. All the data files used by Elasticsearch will be saved in this directory, so that in case you need to restart your container you do not lose any data. If you prefer to not store the data files in your computer, then you can remove the <code>-v</code> line and the data will be stored ephemerally in the container.</p><p>Note that deploying Elasticsearch using this method is only adequate for local experimentation. If you intend to deploy Elasticsearch on a production server, consider using our <a href="https://www.elastic.co/blog/getting-started-with-the-elastic-stack-and-docker-compose">Elasticsearch on Docker Compose</a> or <a href="https://www.elastic.co/guide/en/cloud-on-k8s/current/k8s-deploy-eck.html">Elasticsearch on Kubernetes</a> guides.</p><h3>Connecting to Elasticsearch</h3><p>The ingestion script needs to know how to connect to Elasticsearch. If you are running a Docker container as demonstrated in the previous section, add the following line between the imports and the definition of the <code>QuoteDoc</code> class:</p>dsl.async_connections.create_connection(hosts=['http://localhost:9200'])
<p>To complete the script, the <code>ingest_quotes()</code> function should be called. Add the following snippet at the bottom of your source file:</p>if __name__ == '__main__':
    asyncio.run(ingest_quotes())
<p>The <code>asyncio.run()</code> function will launch the asynchronous application. If your application is not asynchronous, then you would just call the ingest function directly.</p><p>For your convenience, below you can find the complete code for the script up to this point. You can save this file as <em>search.py</em>. You can find an example of this file <a href="https://github.com/miguelgrinberg/quotes/blob/main/backend/search.py">here</a>.</p>import asyncio
import csv
import elasticsearch_dsl as dsl
from sentence_transformers import SentenceTransformer

model = SentenceTransformer("all-MiniLM-L6-v2")
dsl.async_connections.create_connection(hosts=['http://localhost:9200'], serializer=OrjsonSerializer())


class QuoteDoc(dsl.AsyncDocument):
    quote: str
    author: str = dsl.mapped_field(dsl.Keyword())
    tags: list[str] = dsl.mapped_field(dsl.Keyword())
    embedding: list[float] = dsl.mapped_field(dsl.DenseVector(), init=False)

    class Index:
        name = 'quotes'

    def clean(self):
        if not self.embedding:
            self.embedding = model.encode(self.quote).tolist()

async def ingest_quotes():
    if await QuoteDoc._index.exists():
        await QuoteDoc._index.delete()
    await QuoteDoc.init()

    with open('quotes.csv') as f:
        reader = csv.DictReader(f)
        for row in reader:
            q = QuoteDoc(quote=row['quote'], author=row['author'],
                         tags=row['tags'].split(','))
            await q.save()

if __name__ == '__main__':
    asyncio.run(ingest_quotes())
<p>Create a virtual environment for your project using the tool of your choice, and then install the dependencies on it:</p>pip install "elasticsearch-dsl[async]" sentence-transformers
<p>Make sure you have the <a href="https://raw.githubusercontent.com/miguelgrinberg/quotes/main/backend/quotes.csv">quotes.csv</a> file in the current directory, and then start the ingest by running the script:</p>python search.py
<p>The script does not print anything, so it will run for a while adding the quotes from the CSV file into your Elasticsearch index. The file has about 37,000 quotes, so expect the process to run for several minutes.</p><p>Luckily you do not need to wait that long. If you start the script and no error appears, that is confirmation that everything is working. You can press Ctrl-C to stop it and continue reading to learn about ingest performance.</p><h3>Performance tuning part 1: bulk processing</h3><p>If your dataset is small, then the above ingest solution will work just fine, and it has the benefit that it is simple to code and easy to understand.</p><p>For larger ingest jobs, however, it is necessary to sacrifice code clarity and pay attention to performance, so let's see what optimizations can be done in this application.</p><p>First of all, to evaluate performance we need to be able to measure the performance of the existing solution. Below is the updated <code>ingest_quotes()</code> function, which now calls <code>ingest_progress()</code> every 100 ingested documents to show how many documents have been ingested, along with an average document per second.</p>from time import time

# ...

def ingest_progress(count, start):
    elapsed = time() - start
    print(f'\rIngested {count} quotes. ({count / elapsed:.0f}/sec)', end='')

async def ingest_quotes():
    if await QuoteDoc._index.exists():
        await QuoteDoc._index.delete()
    await QuoteDoc.init()

    with open('quotes.csv') as f:
        reader = csv.DictReader(f)
        count = 0
        start = time()
        for row in reader:
            q = QuoteDoc(quote=row['quote'], author=row['author'],
                         tags=row['tags'].split(','))
            await q.save()
            count += 1
            if count % 100 == 0:
                ingest_progress(count, start)
        ingest_progress(count, start)

# ...
<p>This version of the ingest is nicer than the previous one because it prints regular status updates. If you let the script run for a while you may see an output similar to the one below:</p>❯ python search.py
Ingested 4900 quotes. (97/sec)
<p>The data file has close to 37,000 quotes, so now you can have a good idea of how long the ingest will take. Assuming the average of 97 ingested documents per second holds throughout the entire ingest job, it should take less than 7 minutes to ingest the entire dataset. You can press Ctrl-C to stop this ingest process, there is no need to let it run to completion yet.</p><p>Elasticsearch offers a very flexible bulk ingest feature, which is made available in the Elasticsearch-DSL package's <code>bulk()</code> method. Instead of saving each document, the entire import loop can be moved into a generator function which is given to the <code>bulk()</code> method as an argument:</p>async def ingest_quotes():
    if await QuoteDoc._index.exists():
        await QuoteDoc._index.delete()
    await QuoteDoc.init()

    async def get_next_quote():
        with open('quotes.csv') as f:
            reader = csv.DictReader(f)
            count = 0
            start = time()
            for row in reader:
                q = QuoteDoc(quote=row['quote'], author=row['author'],
                             tags=row['tags'].split(','))
                yield q
                count += 1
                if count % 100 == 0:
                    ingest_progress(count, start)
            ingest_progress(count, start)

    await QuoteDoc.bulk(get_next_quote())
<p>Here the <code>get_next_quote()</code> inner generator function yields <code>QuoteDoc</code> instances. The <code>QuoteDoc.bulk()</code> method will run the generator and issue batch updates to Elasticsearch. With this change, you can expect to see a small speed improvement:</p>❯ python s.py
Ingested 5500 quotes. (108/sec)
<p>For another small improvement, the JSON serializer used by the Elasticsearch client can be changed to the <a href="https://pypi.org/project/orjson/">orjson</a> library, which performs better than Python's own:</p>from elasticsearch import OrjsonSerializer
# ...

dsl.async_connections.create_connection(hosts=['http://localhost:9200'],
                                        serializer=OrjsonSerializer())

# ...
<p>This should lead to another small performance improvement:</p>❯ python s.py
Ingested 5100 quotes. (111/sec)
<h3>Performance tuning part 2: GPU accelerated embeddings</h3><p>You have seen in the previous section that we have obtained some modest performance improvements by processing ingest requests in bulk. But while ingestion requests are now being grouped, the embeddings continue to be generated one by one in the <code>clean()</code> method of the <code>QuoteDoc</code> class.</p><p>Is there a way to optimize embedding generation? The SentenceTransformers model uses PyTorch, which in turn uses a GPU if one is available. But the embeddings are generated individually, which does not lead to an optimal utilization of the GPU hardware. GPUs are very good at parallelization, so we can reorganize the ingest function to generate embeddings in batches. And once again the price we pay for this comes in increased code complexity.</p><p>So we are going to stop using the <code>clean()</code> method to generate document embeddings, and instead we are going to accumulate the <code>QuoteDoc</code> instances in a list, and once we reach a good number we'll generate embeddings for all of them in a single operation.</p><p>Let's start by writing a helper function that generates embeddings for a list of <code>QuoteDoc</code> instances:</p>def embed_quotes(quotes):
    embeddings = model.encode([q.quote for q in quotes])
    for q, e in zip(quotes, embeddings):
        q.embedding = e.tolist()
<p>Note how now the <code>model.encode()</code> method is given a list of quotes to embed instead of a single one. When the input argument is a list, the model generates an embedding for each list element. The method accepts an optional <a href="https://sbert.net/docs/package_reference/sentence_transformer/SentenceTransformer.html#sentence_transformers.SentenceTransformer.encode"><code>batch_size</code></a> argument (not used in the example above) that defaults to 32 that can be used to control the size of each batch of samples that are sent to the model for computation. Depending on the GPU hardware you may find that different values of this argument help tune performance to the best possible. Once the embeddings are generated, they are assigned to each quote using a for-loop.</p><p>Now the ingest function can be refactored to accumulate quotes and use the helper function to generate embeddings:</p>async def ingest_quotes():
    if await QuoteDoc._index.exists():
        await QuoteDoc._index.delete()
    await QuoteDoc.init()

    async def get_next_quote():
        quotes = []
        with open('quotes.csv') as f:
            reader = csv.DictReader(f)
            count = 0
            start = time()
            for row in reader:
                q = QuoteDoc(quote=row['quote'], author=row['author'],
                             tags=row['tags'].split(','))
                quotes.append(q)
                if len(quotes) == 512:
                    embed_quotes(quotes)
                    for q in quotes:
                        yield q
                    count += len(quotes)
                    ingest_progress(count, start)
                    quotes = []
            if len(quotes) &gt; 0:
                embed_quotes(quotes)
                for q in quotes:
                    yield q
            ingest_progress(count, start)
<p>In this version of <code>ingest_quotes()</code>, each <code>QuoteDoc</code> instance is added to the <code>quotes</code> list, and when 512 elements have accumulated the <code>embed_quotes()</code> function added above is used to generate the embeddings more efficiently. Once the objects have their embeddings, they are yielded, so that the <code>bulk()</code> method from Elasticsearch-DSL can add them to the index as before.</p><p>What is the significance of the 512 number? There isn't any. We know that the model uses a batch size of 32, so it makes sense to accumulate at least that many documents. Starting from 32, you can try if larger powers of 2 provide better performance. With the hardware available to me, I've found 512 to give the best performance.</p><p>Here is an example run using batched embeddings:</p>❯ python search.py
Ingested 36864 quotes. (481/sec)
<p>And now the ingestion process runs much faster, with the entire dataset ingested in about 1 minutes and 16 seconds.</p><p>If you decide to try to optimize your ingest, you are encouraged to try different options and see what works best with your hardware.</p><h2>Querying the index</h2><p>If you are following along, by now you have an Elasticsearch index called <code>quotes</code> that is populated with about 37K famous quotes, each with a searchable vector embedding. Now it is time to learn how to query this index.</p><p>When using Elasticsearch-DSL, the document classes return a search object from their <code>search()</code> method:</p>s = QuoteDoc.search()
<p>The search object has a large number of methods that map to the query options in the Elasticsearch <a href="https://www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl.html">query DSL</a>.</p><p>The simplest query that can be issued is the <a href="https://www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl-match-all-query.html">match all</a> query, which returns all the elements. With the class-based approach used by Elasticsearch-DSL, this is how to run the query:</p>s = QuoteDoc.search()
s = s.query(dsl.query.MatchAll())
async for q in s:
    print(q.quote)
<p>This would obviously print a listing of the entire list of quotes stored in the index, up to 10,000, which is the maximum number of results Elasticsearch returns by default.</p><p>In many cases it is useful to request a subset of the results. The search object uses Python style slicing for this. Here is how to request the first 25 results only:</p>async for q in s[:25]:
    print(q.quote)
<p>Here is how to request the second page of results, at 25 results per page:</p>async for q in s[25:50]:
    print(q.quote)
<p>Elasticsearch offers approximate and exact vector search queries, also called <a href="https://www.elastic.co/guide/en/elasticsearch/reference/current/knn-search.html">k-nearest neighbor (kNN) queries</a>. To run a vector search query with the approximate k-nearest neighbor algorithm, the <code>Knn</code> query should be used:</p>s = QuoteDoc.search()
s = s.query(dsl.query.Knn(field=QuoteDoc.embedding, query_vector=model.encode(q).tolist()))
<p>The <code>Knn</code> query class accepts the field that stores the embeddings and a search vector as arguments. In the above snippet the variable <code>q</code> has the search text entered by the user.</p><p>If instead you prefer to run a regular full-text search, the <code>Match</code> query class is used:</p>s = QuoteDoc.search()
s = s.query(dsl.query.Match(quote=q))
<h3>Filters</h3><p>One of the most important benefits of using Elasticsearch as a vector database is that it is a robust database system, and all the options you can expect to have from a database nicely integrates with your vector search queries.</p><p>A great example of this is <em>filters</em>. The famous quotes database stores a list of tags for each quote, so it is only natural to have the option to restrict a query to quotes that have a specific tag.</p><p>Given a list of tag filters stored in a <code>tags</code> variable, the following snippet configures a search object to only return results that include the given tags using a "terms" filter:</p>for tag in tags:
    s = s.filter(dsl.query.Terms(tags=[tag]))
<h3>Aggregations</h3><p>Another example of a useful database function that is fully integrated with vector search is <em>aggregations</em>. Given a query, Elasticsearch can aggregate the tags and provide the counts of quotes per tag.</p><p>The next snippet shows how to add a <a href="https://www.elastic.co/guide/en/elasticsearch/reference/current/search-aggregations-bucket-terms-aggregation.html">Terms</a> aggregation to an existing query, which will return the 100 most referenced tags in the results:</p>s.aggs.bucket('tags', dsl.aggs.Terms(field=QuoteDoc.tags, size=100))
<p>Recall that the <code>tags</code> field was declared with the <code>Keyword()</code> type, which means that the tags will be stored as is on the index, without any processing. This is required by the Terms aggregation, which will count the occurrences of each tag in the results.</p><h3>A complete query example</h3><p>You have seen a few isolated query examples. In this section you can see how they can all be integrated into a function that performs a query in the example application.</p><p>The <code>search_quotes()</code> function shown below accepts a query string <code>q</code>, a list of filters <code>tags</code> and a <code>use_knn</code> flag to choose between kNN or full-text search query. It also accepts <code>start</code> and <code>size</code> pagination arguments.</p><p>The function decides which of the three queries you've seen above to issue depending on the input arguments. If <code>q</code> is empty, then it selects a "match all" query, and in any other case it selects a kNN or match query depending on the <code>use_knn</code> flag, which the user can control from a checkbox in the application's user interface.</p><p>The function returns three results as a tuple:</p><ul><li><p>a list of <code>QuoteDoc</code> instances that are the search results,</p></li><li><p>the tag aggregations as a list of tuples, each with tag name and document count,</p></li><li><p>the total number of results, which is useful to show in paginated queries</p></li></ul><p>Here is the complete code of this function:</p>async def search_quotes(q, tags, use_knn=True, start=0, size=25):
    s = QuoteDoc.search()
    if q == '':
        s = s.query(dsl.query.MatchAll())
    elif use_knn:
        s = s.query(dsl.query.Knn(field=QuoteDoc.embedding, query_vector=model.encode(q).tolist()))
    else:
        s = s.query(dsl.query.Match(quote=q))
    for tag in tags:
        s = s.filter(dsl.query.Terms(tags=[tag]))
    s.aggs.bucket('tags', dsl.aggs.Terms(field=QuoteDoc.tags, size=100))
    r = await s[start:start + size].execute()
    tags = [(tag.key, tag.doc_count) for tag in r.aggs.tags.buckets]
    return r.hits, tags, r['hits'].total.value
<p>To be able to access both the search results and the aggregation results, we now issue the request explicitly through the <code>execute()</code> method and store the response is stored in <code>r</code>. The <code>hits</code> attribute of the response object contains the actual search results, and the <code>aggs</code> attribute provides access to the aggregations. The format in which the aggregation results is provided is described in the <a href="https://www.elastic.co/guide/en/elasticsearch/reference/current/search-aggregations-bucket-terms-aggregation.html">terms aggregation documentation</a>.</p><h2>Conclusion</h2><p>The complete quotes example is available in a <a href="https://github.com/miguelgrinberg/quotes">GitHub repository</a> that you can install and run on your computer. Follow the instructions on the <code>README.md</code> file to set it up.</p><p>You are welcome to use this example to experiment with vector embeddings and Elasticsearch!</p>]]></content:encoded>
    <link>https://www.elastic.co/search-labs/blog/elasticsearch-dsl-python-vectors</link>
    <guid isPermaLink="true">https://www.elastic.co/search-labs/blog/elasticsearch-dsl-python-vectors</guid>
    <category><![CDATA[Vector Database]]></category>
    <category><![CDATA[AI]]></category>
    <category><![CDATA[Python]]></category>
    <dc:creator><![CDATA[Miguel Grinberg]]></dc:creator>
    <enclosure url="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt4a680a6bf95f4421/6a17122b66c4f9358ef8c157/1225861f71cf9ccbb2102216a9365dd07ff71e9c-1440x858.png" length="0" type="image/png"/>
    <pubDate>Fri, 16 Aug 2024 00:00:00 GMT</pubDate>
  </item>
  <item>
    <title><![CDATA[Advanced RAG techniques part 2: Querying and testing]]></title>
    <description><![CDATA[Discussing and implementing techniques which may increase RAG performance. Part 2 of 2, focusing on querying and testing an advanced RAG pipeline.]]></description>
    <content:encoded><![CDATA[<p><em>All code may be found </em><a href="https://github.com/elastic/elasticsearch-labs/tree/advanced-rag-techniques/supporting-blog-content/advanced-rag-techniques"><em>in the Searchlabs repo, in the advanced-rag-techniques branch</em></a><em>.</em></p><p>Welcome to Part 2 of our article on Advanced RAG Techniques! In <a href="https://www.elastic.co/search-labs/blog/advanced-rag-techniques-part-1">part 1 of this series</a>, we set up, discussed, and implemented the data processing components of the advanced RAG pipeline:</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt9a4691874a19d8da/6a170b3f47d49c99f22d8a24/72b51ba2ae5e5977b56e5b915674753d6cfd0e56-1440x840.jpg" alt="Advanced RAG pipeline" /><p>In this part, we're going to proceed with querying and testing out our implementation. Let's get right to it!</p><h3>Table of contents</h3><ul><li><p><a href="https://www.elastic.co/search-labs/blog/advanced-rag-techniques-part-2#searching-and-retrieving,-generating-answers">Searching and retrieving, generating answers</a></p><ul><li><p><a href="https://www.elastic.co/search-labs/blog/advanced-rag-techniques-part-2#enriching-queries-with-synonyms">Enriching queries with synonyms</a></p></li><li><p><a href="https://www.elastic.co/search-labs/blog/advanced-rag-techniques-part-2#hyde-hypothetical-document-embedding">HyDE (Hypothetical Document Embedding)</a></p></li><li><p><a href="https://www.elastic.co/search-labs/blog/advanced-rag-techniques-part-2#hybrid-search">Hybrid search</a></p></li></ul></li><li><p><a href="https://www.elastic.co/search-labs/blog/advanced-rag-techniques-part-2#experiments">Experiments</a></p><ul><li><p><a href="https://www.elastic.co/search-labs/blog/advanced-rag-techniques-part-2#summary-of-results">Summary of results</a></p></li><li><p><a href="https://www.elastic.co/search-labs/blog/advanced-rag-techniques-part-2#test-1-who-audits-elastic">Test 1: Who audits Elastic?</a></p><ul><li><p><a href="https://www.elastic.co/search-labs/blog/advanced-rag-techniques-part-2#advancedrag">AdvancedRAG</a></p></li><li><p><a href="https://www.elastic.co/search-labs/blog/advanced-rag-techniques-part-2#simplerag">SimpleRAG</a></p></li></ul></li><li><p><a href="https://www.elastic.co/search-labs/blog/advanced-rag-techniques-part-2#test-2--total-revenue-2023">Test 2: total revenue 2023</a></p><ul><li><p><a href="https://www.elastic.co/search-labs/blog/advanced-rag-techniques-part-2#advancedrag-1">AdvancedRAG</a></p></li><li><p><a href="https://www.elastic.co/search-labs/blog/advanced-rag-techniques-part-2#simplerag-1">SimpleRAG</a></p></li></ul></li><li><p><a href="https://www.elastic.co/search-labs/blog/advanced-rag-techniques-part-2#test-3-what-product-does-growth-primarily-depend-on-how-much">Test 3: What product does growth primarily depend on? How much?</a></p><ul><li><p><a href="https://www.elastic.co/search-labs/blog/advanced-rag-techniques-part-2#advancedrag-2">AdvancedRAG</a></p></li><li><p><a href="https://www.elastic.co/search-labs/blog/advanced-rag-techniques-part-2#simplerag-2">SimpleRAG</a></p></li></ul></li><li><p><a href="https://www.elastic.co/search-labs/blog/advanced-rag-techniques-part-2#test-4-describe-employee-benefit-plan">Test 4: Describe employee benefit plan</a></p><ul><li><p><a href="https://www.elastic.co/search-labs/blog/advanced-rag-techniques-part-2#advancedrag-3">AdvancedRAG</a></p></li><li><p><a href="https://www.elastic.co/search-labs/blog/advanced-rag-techniques-part-2#simplerag-3">SimpleRAG</a></p></li></ul></li><li><p><a href="https://www.elastic.co/search-labs/blog/advanced-rag-techniques-part-2#test-5-which-companies-did-elastic-acquire">Test 5: Which companies did Elastic acquire?</a></p><ul><li><p><a href="https://www.elastic.co/search-labs/blog/advanced-rag-techniques-part-2#advancedrag-4">AdvancedRAG</a></p></li><li><p><a href="https://www.elastic.co/search-labs/blog/advanced-rag-techniques-part-2#simplerag-4">SimpleRAG</a></p></li></ul></li></ul></li><li><p><a href="https://www.elastic.co/search-labs/blog/advanced-rag-techniques-part-2#conclusion">Conclusion</a></p></li><li><p><a href="https://www.elastic.co/search-labs/blog/advanced-rag-techniques-part-2#appendix">Appendix</a></p><ul><li><p><a href="https://www.elastic.co/search-labs/blog/advanced-rag-techniques-part-2#prompts">Prompts</a></p><ul><li><p><a href="https://www.elastic.co/search-labs/blog/advanced-rag-techniques-part-2#rag-question-answering-prompt">RAG question answering prompt</a></p></li><li><p><a href="https://www.elastic.co/search-labs/blog/advanced-rag-techniques-part-2#elastic-query-generator-prompt">Elastic query generator prompt</a></p></li><li><p><a href="https://www.elastic.co/search-labs/blog/advanced-rag-techniques-part-2#potential-questions-generator-prompt">Potential questions generator prompt</a></p></li><li><p><a href="https://www.elastic.co/search-labs/blog/advanced-rag-techniques-part-2#hyde-generator-prompt">HyDE generator prompt</a></p></li></ul></li><li><p><a href="https://www.elastic.co/search-labs/blog/advanced-rag-techniques-part-2#sample-hybrid-search-query">Sample hybrid search query</a></p></li></ul></li></ul><h2>Searching and retrieving, generating answers</h2><p>Let's ask our first query, ideally some piece of information found primarily in the annual report. How about:</p>Who audits Elastic?"
<p>Now, let's apply a few of our techniques to enhance the query.</p><h3>Enriching queries with synonyms</h3><p>Firstly, let's enhance the diversity of the query wording, and turn it into a form that can be easily processed into an Elasticsearch query. We'll enlist the aid of GPT-4o to convert the query into a list of OR clauses. Let's write this prompt:</p>
ELASTIC_SEARCH_QUERY_GENERATOR_PROMPT = '''
You are an AI assistant specialized in generating Elasticsearch query strings. Your task is to create the most effective query string for the given user question. This query string will be used to search for relevant documents in an Elasticsearch index.

Guidelines:
1. Analyze the user's question carefully.
2. Generate ONLY a query string suitable for Elasticsearch's match query.
3. Focus on key terms and concepts from the question.
4. Include synonyms or related terms that might be in relevant documents.
5. Use simple Elasticsearch query string syntax if helpful (e.g., OR, AND).
6. Do not use advanced Elasticsearch features or syntax.
7. Do not include any explanations, comments, or additional text.
8. Provide only the query string, nothing else.

For the question "What is Clickthrough Data?", we would expect a response like:
clickthrough data OR click-through data OR click through rate OR CTR OR user clicks OR ad clicks OR search engine results OR web analytics

AND operator is not allowed. Use only OR.

User Question:
[The user's question will be inserted here]

Generate the Elasticsearch query string:
'''
<p>When applied to our query, GPT-4o generates synonyms of the base query and related vocabulary.</p>'audits elastic OR 
elasticsearch audits OR 
elastic auditor OR 
elasticsearch auditor OR 
elastic audit firm OR 
elastic audit company OR 
elastic audit organization OR 
elastic audit service'
<p>In the <code>ESQueryMaker</code> class, I've defined a function to split the query:</p>def parse_or_query(self, query_text: str) -&gt; List[str]:
    # Split the query by 'OR' and strip whitespace from each term
    # This converts a string like "term1 OR term2 OR term3" into a list ["term1", "term2", "term3"]
    return [term.strip() for term in query_text.split(' OR ')]
<p>Its role is to take this string of OR clauses and split them into a list of terms, allowing us do a multi-match on our key document fields:</p>["original_text", 'keyphrases', 'potential_questions', 'entities']
<p>Finally ending up with this query:</p> 'query': {
    'bool': {
        'must': [
            {
                'multi_match': {
                'query': 'audits Elastic Elastic auditing Elastic audit process Elastic compliance Elastic security audit Elasticsearch auditing Elasticsearch compliance Elasticsearch security audit',
                'fields': [
                    'original_text',
                'keyphrases',
                'potential_questions',
                'entities'
                ],
                'type': 'best_fields',
                'operator': 'or'
                }
            }
      ]
<p>This covers many more bases than the original query, hopefully reducing the risk of missing a search result because we forgot a synonym. But we can do more.</p><p><a href="https://www.elastic.co/search-labs/blog/advanced-rag-techniques-part-2#table-of-contents">Back to top</a></p><h3>HyDE (Hypothetical Document Embedding)</h3><p>Let's enlist GPT-4o again, this time to implement <a href="https://arxiv.org/abs/2212.10496">HyDE</a>.</p><p>The basic premise of HyDE is to generate a hypothetical document - The kind of document that would likely contain the answer to the original query. The factuality or accuracy of the document is not a concern. With that in mind, let's write the following prompt:</p>HYDE_DOCUMENT_GENERATOR_PROMPT = '''
You are an AI assistant specialized in generating hypothetical documents based on user queries. Your task is to create a detailed, factual document that would likely contain the answer to the user's question. This hypothetical document will be used to enhance the retrieval process in a Retrieval-Augmented Generation (RAG) system.

Guidelines:
1. Carefully analyze the user's query to understand the topic and the type of information being sought.
2. Generate a hypothetical document that:
   a. Is directly relevant to the query
   b. Contains factual information that would answer the query
   c. Includes additional context and related information
   d. Uses a formal, informative tone similar to an encyclopedia or textbook entry
3. Structure the document with clear paragraphs, covering different aspects of the topic.
4. Include specific details, examples, or data points that would be relevant to the query.
5. Aim for a document length of 200-300 words.
6. Do not use citations or references, as this is a hypothetical document.
7. Avoid using phrases like "In this document" or "This text discusses" - write as if it's a real, standalone document.
8. Do not mention or refer to the original query in the generated document.
9. Ensure the content is factual and objective, avoiding opinions or speculative information.
10. Output only the generated document, without any additional explanations or meta-text.

User Question:
[The user's question will be inserted here]

Generate a hypothetical document that would likely contain the answer to this query:
'''
<p>Since vector search typically operates on cosine vector similarity, the premise of HyDE is that we can achieve better results by matching documents to documents instead of queries to documents.</p><p>What we care about is structure, flow, and terminology. Not so much factuality. GPT-4o outputs a HyDE document like this:</p>'Elastic N.V., the parent company of Elastic, the organization known for developing Elasticsearch, is subject to audits to ensure financial accuracy, 
regulatory compliance, and the integrity of its financial statements. The auditing of Elastic N.V. is typically conducted by an external, 
independent auditing firm. This is common practice for publicly traded companies to provide stakeholders with assurance regarding the company\'s 
financial position and operations.\n\nThe primary external auditor for Elastic is the audit firm Ernst &amp; Young LLP (EY). Ernst &amp; Young is one of the 
four largest professional services networks in the world, commonly referred to as the "Big Four" audit firms. These firms handle a substantial number 
of audits for major corporations around the globe, ensuring adherence to generally accepted accounting principles (GAAP) and international financial 
reporting standards (IFRS).\n\nThe audit process conducted by EY involves several steps. Initially, the auditors perform a risk assessment to identify 
areas where misstatements due to error or fraud could occur. They then design audit procedures to test the accuracy and completeness of financial statements,
 which include examining financial transactions, assessing internal controls, and reviewing compliance with relevant laws and regulations. Upon completion of 
 the audit, Ernst &amp; Young issues an audit report, which includes the auditor’s opinion on whether the financial statements are free from material misstatement 
 and are presented fairly in accordance with the applicable financial reporting framework.\n\nIn addition to external audits by firms like Ernst &amp; Young, 
 Elastic may also be subject to internal audits. Internal audits are performed by the company’s own internal auditors to evaluate the effectiveness of internal 
 controls, risk management, and governance processes.\n\nOverall, the auditing process plays a crucial role in maintaining the transparency and reliability of 
 Elastic\'s financial information, providing confidence to investors, regulators, and other stakeholders.'
<p>It looks pretty believable, like the ideal candidate for the kinds of documents we'd like to index. We're going to embed this and use it for hybrid search.</p><p><a href="https://www.elastic.co/search-labs/blog/advanced-rag-techniques-part-2#table-of-contents">Back to top</a></p><h3>Hybrid search</h3><p>This is the core of our search logic. Our lexical search component will be the generated OR clause strings. Our dense vector component will be embedded HyDE Document (aka the search vector). We use KNN to efficiently identify several candidate documents closest to our search vector. We call our lexical search component <em>Scoring with TF-IDF and BM25</em> by default. Finally, the lexical and dense vector scores will be combined using the 30/70 ratio recommended by <a href="https://arxiv.org/abs/2407.01219">Wang et al</a>.</p>def hybrid_vector_search(self, index_name: str, query_text: str, query_vector: List[float], 
                         text_fields: List[str], vector_field: str, 
                         num_candidates: int = 100, num_results: int = 10) -&gt; Dict:
    """
    Perform a hybrid search combining text-based and vector-based similarity.

    Args:
        index_name (str): The name of the Elasticsearch index to search.
        query_text (str): The text query string, which may contain 'OR' separated terms.
        query_vector (List[float]): The query vector for semantic similarity search.
        text_fields (List[str]): List of text fields to search in the index.
        vector_field (str): The name of the field containing document vectors.
        num_candidates (int): Number of candidates to consider in the initial KNN search.
        num_results (int): Number of final results to return.

    Returns:
        Dict: A tuple containing the Elasticsearch response and the search body used.
    """
    try:
        # Parse the query_text into a list of individual search terms
        # This splits terms separated by 'OR' and removes any leading/trailing whitespace
        query_terms = self.parse_or_query(query_text)

        # Construct the search body for Elasticsearch
        search_body = {
            # KNN search component for vector similarity
            "knn": {
                "field": vector_field,  # The field containing document vectors
                "query_vector": query_vector,  # The query vector to compare against
                "k": num_candidates,  # Number of nearest neighbors to retrieve
                "num_candidates": num_candidates  # Number of candidates to consider in the KNN search
            },
            "query": {
                "bool": {
                    # The 'must' clause ensures that matching documents must satisfy this condition
                    # Documents that don't match this clause are excluded from the results
                    "must": [
                        {
                            # Multi-match query to search across multiple text fields
                            "multi_match": {
                                "query": " ".join(query_terms),  # Join all query terms into a single space-separated string
                                "fields": text_fields,  # List of fields to search in
                                "type": "best_fields",  # Use the best matching field for scoring
                                "operator": "or"  # Match any of the terms (equivalent to the original OR query)
                            }
                        }
                    ],
                    # The 'should' clause boosts relevance but doesn't exclude documents
                    # It's used here to combine vector similarity with text relevance
                    "should": [
                        {
                            # Custom scoring using a script to combine vector and text scores
                            "script_score": {
                                "query": {"match_all": {}},  # Apply this scoring to all documents that matched the 'must' clause
                                "script": {
                                    # Script to combine vector similarity and text relevance
                                    "source": """
                                    # Calculate vector similarity (cosine similarity + 1)
                                    # Adding 1 ensures the score is always positive
                                    double vector_score = cosineSimilarity(params.query_vector, params.vector_field) + 1.0;
                                    # Get the text-based relevance score from the multi_match query
                                    double text_score = _score;
                                    # Combine scores: 70% vector similarity, 30% text relevance
                                    # This weighting can be adjusted based on the importance of semantic vs keyword matching
                                    return 0.7 * vector_score + 0.3 * text_score;
                                    """,
                                    # Parameters passed to the script
                                    "params": {
                                        "query_vector": query_vector,  # Query vector for similarity calculation
                                        "vector_field": vector_field  # Field containing document vectors
                                    }
                                }
                            }
                        }
                    ]
                }
            }
        }

        # Execute the search request against the Elasticsearch index
        response = self.conn.search(index=index_name, body=search_body, size=num_results)
        # Log the successful execution of the search for monitoring and debugging
        logger.info(f"Hybrid search executed on index: {index_name} with text query: {query_text}")
        # Return both the response and the search body (useful for debugging and result analysis)
        return response, search_body
    except Exception as e:
        # Log any errors that occur during the search process
        logger.error(f"Error executing hybrid search on index: {index_name}. Error: {e}")
        # Re-raise the exception for further handling in the calling code
        raise e
<p>Finally, we can piece together a RAG function. Our RAG, from query to answer, will follow this flow:</p><ol><li><p>Convert Query to OR Clauses.</p></li><li><p>Generate HyDE document and embed it.</p></li><li><p>Pass both as inputs to Hybrid Search.</p></li><li><p>Retrieve top-n results, reverse them so that the most relevant score is the "most recent" in the LLM's contextual memory (Reverse Packing) Reverse Packing Example: Query: "Elasticsearch query optimization techniques" Retrieved documents (ordered by relevance):  Reversed order for LLM context:  By reversing the order, the most relevant information (1) appears last in the context, potentially receiving more attention from the LLM during answer generation.</p><ol><li><p>"Use bool queries to combine multiple search criteria efficiently."</p></li><li><p>"Implement caching strategies to improve query response times."</p></li><li><p>"Optimize index mappings for faster search performance."</p></li><li><p>"Optimize index mappings for faster search performance."</p></li><li><p>"Implement caching strategies to improve query response times."</p></li><li><p>"Use bool queries to combine multiple search criteria efficiently."</p></li></ol></li><li><p>Pass the context to the LLM for generation.</p></li></ol>def get_context(index_name, 
                match_query, 
                text_query, 
                fields, 
                num_candidates=100, 
                num_results=20, 
                text_fields=["original_text", 'keyphrases', 'potential_questions', 'entities'], 
                embedding_field="primary_embedding"):

    embedding=embedder.get_embeddings_from_text(text_query)

    results, search_body = es_query_maker.hybrid_vector_search(
        index_name=index_name,
        query_text=match_query,
        query_vector=embedding[0][0],
        text_fields=text_fields,
        vector_field=embedding_field,
        num_candidates=num_candidates,
        num_results=num_results
    )

    # Concatenates the text in each 'field' key of the search result objects into a single block of text.
    context_docs=['\n\n'.join([field+":\n\n"+j['_source'][field] for field in fields]) for j in results['hits']['hits']]

    # Reverse Packing to ensure that the highest ranking document is seen first by the LLM.
    context_docs.reverse()
    return context_docs, search_body

def retrieval_augmented_generation(query_text):
    match_query= gpt4o.generate_query(query_text)
    fields=['original_text']

    hyde_document=gpt4o.generate_HyDE(query_text)

    context, search_body=get_context(index_name, match_query, hyde_document, fields)

    answer= gpt4o.basic_qa(query=query_text, context=context)
    return answer, match_query, hyde_document, context, search_body

<p>Let's run our query and get back our answer:</p>According to the context, Elastic N.V. is audited by an independent registered public accounting firm, PricewaterhouseCoopers (PwC). 
This information is found in the section titled "report of independent registered public accounting firm," which states:

"We have audited the accompanying consolidated balance sheets of Elastic N.V. [...] / s / pricewaterhouseco."
<p>Nice. That's correct.</p><p><a href="https://www.elastic.co/search-labs/blog/advanced-rag-techniques-part-2#table-of-contents">Back to top</a></p><h2>Experiments</h2><p>There's an important question to answer now. What did we get out of investing so much effort and additional complexity into these implementations?</p><p>Let's do a little comparison. The RAG pipeline we've implemented versus baseline hybrid search, without any of the enhancements we've made. We'll run a small series of tests and see if we notice any substantial differences. We'll refer to the RAG we have just implemented as AdvancedRAG, and the basic pipeline as SimpleRAG.</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/bltf605c8246989df32/6a1711178b73cbc61d18a11d/8da40067835ab8b4dc12fe52a51a6c26858ad32f-1440x1095.jpg" alt="Simple RAG Pipeline" /><h4>Summary of results</h4><p>This table summarizes the results of five tests of both RAG pipelines. I judged the relative superiority of each method based on answer detail and quality, but this is a totally subjective judgement. The actual answers are reproduced below this table for your consideration. With that said, let's take a look at how they did!</p><p>SimpleRAG was unable to answer questions 1 &amp; 5. AdvancedRAG also went into far greater detail on questions 2, 3, and 4. Based on the increased detail, I judged the quality of AdvancedRAG's answers better.</p><p>Test</p><p>Question</p><p>AdvancedRAG Performance</p><p>SimpleRAG Performance</p><p>AdvancedRAG Latency</p><p>SimpleRAG Latency</p><p>Winner</p><p>1</p><p>Who audits Elastic?</p><p>Correctly identified PwC as the auditor.</p><p>Failed to identify the auditor.</p><p>11.6s</p><p>4.4s</p><p>AdvancedRAG</p><p>2</p><p>What was the total revenue in 2023?</p><p>Provided the correct revenue figure. Included additional context with revenue from previous years.</p><p>Provided the correct revenue figure.</p><p>13.3s</p><p>2.8s</p><p>AdvancedRAG</p><p>3</p><p>What product does growth primarily depend on? How much?</p><p>Correctly identified Elastic Cloud as the key driver. Included overall revenue context &amp; greater detail.</p><p>Correctly identified Elastic Cloud as the key driver.</p><p>14.1s</p><p>12.8s</p><p>AdvancedRAG</p><p>4</p><p>Describe employee benefit plan</p><p>Gave a comprehensive description of retirement plans, health programs, and other benefits. Included specific contribution amounts for different years.</p><p>Provided a good overview of benefits, including compensation, retirement plans, work environment, and the Elastic Cares program.</p><p>26.6s</p><p>11.6s</p><p>AdvancedRAG</p><p>5</p><p>Which companies did Elastic acquire?</p><p>Correctly listed recent acquisitions mentioned in the report (CmdWatch, Build Security, Optimyze). Provided some acquisition dates and purchase prices.</p><p>Failed to retrieve relevant information from the provided context.</p><p>11.9s</p><p>2.7s</p><p>AdvancedRAG</p><h4>Test 1: Who audits Elastic?</h4><p>AdvancedRAG</p>According to the context, Elastic N.V. is audited by an independent 
registered public accounting firm, PricewaterhouseCoopers (PwC). 
This information is found in the section titled "report of 
independent registered public accounting firm," which states:
"We have audited the accompanying consolidated balance sheets 
of Elastic N.V. [...] / s / pricewaterhouseco."
<p>SimpleRAG</p>According to the provided context, no specific auditing firm 
or company is mentioned as the auditor for Elastic. 
The context provided talks about various features, strengths, 
deployment options, and other details related to Elastic and 
its operations but does not specify which auditing firm is 
responsible for auditing Elastic's financial statements or 
internal controls.
<p><strong>Summary</strong>: SimpleRAG did not identify PWC as the auditor</p><p>Okay that's actually quite surprising. That looks like a search failure on SimpleRAG's part. No documents related to auditing were retrieved. Let's dial down the difficulty a little with the next test.</p><h4>Test 2: total revenue 2023</h4><p>AdvancedRAG</p>### Total Revenue for 2023

According to the provided context, the total revenue for the 
year ended April 30, 2023, was **$1,068,989,000**. This 
information can be found several times in the provided text. 
Specifically, one part states:

"The following table sets forth our results of operations 
for the periods presented in dollars and as a percentage of 
our total revenue. ... year ended April 30, 2023, ... 
total revenue 1,068,989".

Additionally, it is mentioned here as well:

"For the years ended April 30, 2023, 2022, and 2021, 
representing year-over-year growth of 24% for the year 
ended April 30, 2023... total revenue $1,068,989".

Therefore, the total revenue for 2023 is $1.069 billion.
<p>SimpleRAG</p>According to the context provided, the total revenue for the fiscal year ended April 30, 2023, is:

$1,068,989,000 (or approximately $1.1 billion).
<p><strong>Summary</strong>: Both RAGs got the right answer: $1,068,989,000 total revenue in 2023</p><p>Both of them were right here. It does seem like AdvancedRAG may have acquired a broader range of documents? Certainly the answer is more detailed and incorporates information from previous years. That is to be expected given the enhancements we made, but it's far too early to call.</p><p>Let's raise the difficulty.</p><h4>Test 3: What product does growth primarily depend on? How much?</h4><p>AdvancedRAG</p>### Product Growth Primarily Depends On:
According to the context, "our revenue is derived primarily from 
subscriptions of search, observability and security built into 
the elastic stack." The document mentions various strategies and 
factors contributing to the company’s growth which include investment 
in "elastic cloud," "elastic stack," and the conversion of free 
users to paid subscribers.

### Quantitative Data:
- **Revenue**: The document states, "Our total revenue was $1.1 billion, 
$862.4 million, and $608.5 million for the years ended April 30, 2023, 
2022, and 2021, respectively."
- **Subscription Revenue**: "Subscriptions accounted for 92%, 93%, 
and 93% of our total revenue for the years ended April 30, 2023, 
2022, and 2021, respectively."
- **Elastic Cloud Contribution**: "For the years ended April 30, 2023, 
2022, and 2021, Elastic Cloud contributed 40%, 35%, and 27% of our 
total revenue, respectively."
- **Customer Growth**: The number of customers increased from 
"over 15,000 as of April 30, 2021," to "approximately 20,200 as of 
April 30, 2023."

### Summary:
Product growth for the company primarily depends on the "Elastic Stack" 
(which includes search, observability, and security solutions) and 
particularly the "Elastic Cloud." The Elastic Cloud's growing contribution 
to revenue highlights its significance, which grew from 27% in 2021 to 40% in 2023.
<p>SimpleRAG</p>According to the provided context, growth primarily depends on **Elastic Cloud**, 
which is described as the company's "family of cloud-based offerings." 
The context also emphasizes the significance of Elastic Cloud in achieving 
the company's long-term growth potential and its role in increasing subscription revenue.

From the context:

- "Increasing adoption of Elastic Cloud... is an important growth opportunity 
for our business. Organizations are increasingly looking for hosted 
deployment alternatives with reduced administrative burdens."
- "We believe that offering Elastic Cloud is important for achieving 
our long-term growth potential, and we expect Elastic Cloud’s contribution 
to our subscription revenue to continue to increase."

In terms of the contribution to total revenue, here are the specific figures provided:

- Elastic Cloud contributed 40% of the total revenue for the year ended April 30, 2023.
- Elastic Cloud contributed 35% of the total revenue for the year ended April 30, 2022.
- Elastic Cloud contributed 27% of the total revenue for the year ended April 30, 2021.

This progression shows a clear upward trend in the importance and 
contribution of Elastic Cloud to the company's overall revenue.
<p><strong>Summary</strong>: Both RAGs correctly identified Elastic Cloud as the key growth driver. However, AdvancedRAG includes more detail, factoring in subscription revenues and customer growth, and explicitly mentions other Elastic offerings.</p><h4>Test 4: Describe employee benefit plan</h4><p>AdvancedRAG</p>### Employee Benefit Plan Overview

The provided context outlines various elements of the employee benefit plan at 
Elastic, often referred to as "Elasticians." Here are the key components 
described in the texts:

1. **401(k) Plan**:
   - **Coverage**: The 401(k) plan is available to substantially all U.S. 
   employees who meet minimum age and service requirements.
   - **Contributions**: Elastic makes contributions to the 401(k) plan up to 
   6% of the participating employee’s W-2 earnings and wages.
   - **Expenses**: For the fiscal years ended April 30, Elastic recorded 
   expenses of $17.9 million (2023), $15.2 million (2022), and $11.4 million (2021) 
   related to the 401(k) plan.
   - **Defined-Contribution Plans in Other Countries**: Elastic has 
   defined-contribution plans in various other countries and recorded respective 
   expenses of $9.4 million (2023), $7.2 million (2022), and $5.1 million (2021).

2. **Stock-Based Compensation**:
   - **Types of Awards**: Stock options, restricted stock units (RSUs), 
   and shares under the Employee Stock Purchase Plan (ESPP).
   - **Fair Value Measurement**: Fair value of these stock awards is 
   measured using models like Black-Scholes.
   - **Employee Stock Purchase Plan (2022 ESPP)**: 
     - Started in 2022, it allows employees to acquire ordinary 
     shares at a discount (85% of the market value at the beginning 
     or end of the offering period).
     - Offering periods are approximately six months long.

3. **Total Rewards Compensation**:
   - **Components**: Includes cash compensation as well as equity awards, 
   reflecting a comprehensive interest in the well-being of the employees.
   - **Programs**: Market competitive medical and dental programs, 
   mental health, and holistic well-being programs.
   - **Paid Time Off**: Features 16 weeks of paid leave for new 
   parents and life-planning benefits.

4. **Retirement and Income Protection Plans**:
   - **401(k) Match**: Elastic provides a dollar-for-dollar 
   match up to 6% of eligible earnings up to a plan-limit 
   maximum for U.S.-based employees.
   - **Similar Plans Outside the U.S.**: Competitive retirement 
   plans available for employees based outside the United States.

5. **Flexible Work Environment**: 
   - **Distributed Workforce**: Elastic operates with a global 
   distributed workforce model, allowing employees to work remotely 
   and providing a broad candidate pool.
   
6. **Learning and Development**:
   - **Programs**: Access to on-demand video-based learning, 
   leadership development programs, and other specific training 
   to develop managers and leaders.

7. **Employee Engagement and Surveys**:
   - **Mechanisms**: Annual employee engagement survey and 
   mid-year pulse survey.
   - **Objectives**: To monitor morale, develop action plans, 
   and maintain high engagement scores.

8. **Onboarding Program**:
   - **X-School**: An extensive new-hire orientation program to 
   help new employees meet peers and learn about the company's 
   products and solutions.

In summary, Elastic's employee benefit plan encompasses a broad 
spectrum of financial, health, well-being, and professional development 
benefits designed to support and enhance the lives of its employees. 
The plan is both competitive and comprehensive, aiming to attract,
retain, and engage its global workforce.
<p>SimpleRAG</p>The employee benefit plan at the Company includes several key 
components aimed at enhancing the overall well-being and 
professional development of employees, referred to as "Elasticians." 

1. **Compensation and Benefits:**
   - The Company provides market-competitive compensation 
   packages which typically include cash compensation and equity awards.
   - Comprehensive medical and dental programs are offered, 
   emphasizing mental health and holistic well-being.
   - Paid time off programs feature:
     - 16 weeks of paid leave for new parents.
     - Life-planning benefits.
     - Travel reimbursements for certain healthcare services.

2. **Retirement and Income Protection Plans:**
   - In the United States, employees are eligible for a 401k 
   plan with a dollar-for-dollar match by the Company up to 6% 
   of eligible earnings, subject to a plan limit.
   - Similar competitive retirement and income protection plans 
   are available for employees outside the United States.

3. **Work Environment:**
   - The Company offers a flexible work environment, allowing most 
   employees to work from anywhere as often as they would like.
   - Physical offices around the world are available for employees 
   who prefer to work on-site or need a space for face-to-face interactions.

4. **Community Involvement:**
   - The Elastic Cares Program supports charitable organizations 
   that matter to employees on both local and global levels.
   - The program includes:
     - Donation matching.
     - Providing the Company's technology for free to certain 
     nonprofit organizations.
     - Volunteer time off initiative.
<p><strong>Summary</strong>: AdvancedRAG goes into much greater depth and detail, mentioning the 401K plan for US-based employees, as well as defining contribution plans outside of the US. It also mentions Health and Well-Being plans but misses the Elastic Cares program, which SimpleRAG mentions.</p><h4>Test 5: Which companies did Elastic acquire?</h4><p>AdvancedRAG</p>Elastic acquired several companies over the years to enhance its 
technology and market presence. According to the provided context, 
the specific acquisitions include:

1. **CmdWatch Security Inc.**: Acquired on September 17, 2021. 
The total purchase consideration was $77.8 million.
2. **Build Security Ltd.**: Acquired on September 2, 2021. The 
combined purchase price for Build Security Ltd. and another acquisition was $57.2 million.
3. **Optimyze Cloud Inc.**: Acquired on November 1, 2021. Part 
of the combined purchase price mentioned above with Build Security Ltd.
4. **Endgame, Inc.**: Although the context mentions an amended 
and restated stock incentive plan, it indicates that Endgame, Inc. was 
associated with Elastic, suggesting it could have been acquired at some point.

These acquisitions are part of Elastic's strategy to 
enhance the technology underlying its security and 
observability offerings.
<p>SimpleRAG</p>The provided context does not specify any companies that Elastic has acquired. 
Therefore, based on the context, there is no information available about the companies acquired by Elastic.
<p><strong>Summary</strong>: SimpleRAG does not retrieve any relevant info about acquisitions, leading to a failed answer. AdvancedRAG correctly lists CmdWatch, Build Security, and Optimyze, which were the key acquisitions listed in the report.</p><p><a href="https://www.elastic.co/search-labs/blog/advanced-rag-techniques-part-2#table-of-contents">Back to top</a></p><h2>Conclusion</h2><p>Based on our tests, our advanced techniques appear to increase the range and depth of the information presented, potentially enhancing quality of RAG answers.</p><p>Additionally, there may be improvements in reliability, as ambiguously worded questions such as <code>Which companies did Elastic acquire?</code> and <code>Who audits Elastic</code> were correctly answered by AdvancedRAG but not by SimpleRAG.</p><p>However, it is worth keeping in perspective that in 3 out of 5 cases, the basic RAG pipeline, incorporating Hybrid Search but no other techniques, managed to produce answers that captured most of the key information.</p><p>We should note that due to the incorporation of LLMs at the data preparation and query phases, the latency of AdvancedRAG is generally between 2-5x larger that of SimpleRAG. This is a significant cost which may make AdvancedRAG suitable only for situations where answer quality is prioritized over latency.</p><p>The significant latency costs can be alleviated using a smaller and cheaper LLM like Claude Haiku or GPT-4o-mini at the data preparation stage. Save the advanced models for answer generation.</p><p>This aligns with the findings of Wang et al. As their results show, any improvements made are relatively incremental. In short, simple baseline RAG gets you most of the way to a decent end-product, while being cheaper and faster to boot. For me, it's an interesting conclusion. For use cases where speed and efficiency are key, SimpleRAG is the sensible choice. For use cases where every last drop of performance needs squeezing out, the techniques incorporated into AdvancedRAG may offer a way forward.</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt56b7067a9d41d5a8/6a171119acf0886fb4be9c45/ea811706b6adc4731d90b925a9fefa0ac15901b4-1440x1060.jpg" alt="Wang Pipeline" /><p><a href="https://www.elastic.co/search-labs/blog/advanced-rag-techniques-part-2#table-of-contents">Back to top</a></p><h2>Appendix</h2><h3>Prompts</h3><h4>RAG question answering prompt</h4><p>Prompt for getting the LLM to generate answers based on query and context.</p>BASIC_RAG_PROMPT = '''
You are an AI assistant tasked with answering questions based primarily on the provided context, while also drawing on your own knowledge when appropriate. Your role is to accurately and comprehensively respond to queries, prioritizing the information given in the context but supplementing it with your own understanding when beneficial. Follow these guidelines:

1. Carefully read and analyze the entire context provided.
2. Primarily focus on the information present in the context to formulate your answer.
3. If the context doesn't contain sufficient information to fully answer the query, state this clearly and then supplement with your own knowledge if possible.
4. Use your own knowledge to provide additional context, explanations, or examples that enhance the answer.
5. Clearly distinguish between information from the provided context and your own knowledge. Use phrases like "According to the context..." or "The provided information states..." for context-based information, and "Based on my knowledge..." or "Drawing from my understanding..." for your own knowledge.
6. Provide comprehensive answers that address the query specifically, balancing conciseness with thoroughness.
7. When using information from the context, cite or quote relevant parts using quotation marks.
8. Maintain objectivity and clearly identify any opinions or interpretations as such.
9. If the context contains conflicting information, acknowledge this and use your knowledge to provide clarity if possible.
10. Make reasonable inferences based on the context and your knowledge, but clearly identify these as inferences.
11. If asked about the source of information, distinguish between the provided context and your own knowledge base.
12. If the query is ambiguous, ask for clarification before attempting to answer.
13. Use your judgment to determine when additional information from your knowledge base would be helpful or necessary to provide a complete and accurate answer.

Remember, your goal is to provide accurate, context-based responses, supplemented by your own knowledge when it adds value to the answer. Always prioritize the provided context, but don't hesitate to enhance it with your broader understanding when appropriate. Clearly differentiate between the two sources of information in your response.

Context:
[The concatenated documents will be inserted here]

Query:
[The user's question will be inserted here]

Please provide your answer based on the above guidelines, the given context, and your own knowledge where appropriate, clearly distinguishing between the two:
'''
<h4>Elastic query generator prompt</h4><p>Prompt for enriching queries with synonyms and converting them into the OR format.</p>ELASTIC_SEARCH_QUERY_GENERATOR_PROMPT = '''
You are an AI assistant specialized in generating Elasticsearch query strings. Your task is to create the most effective query string for the given user question. This query string will be used to search for relevant documents in an Elasticsearch index.

Guidelines:
1. Analyze the user's question carefully.
2. Generate ONLY a query string suitable for Elasticsearch's match query.
3. Focus on key terms and concepts from the question.
4. Include synonyms or related terms that might be in relevant documents.
5. Use simple Elasticsearch query string syntax if helpful (e.g., OR, AND).
6. Do not use advanced Elasticsearch features or syntax.
7. Do not include any explanations, comments, or additional text.
8. Provide only the query string, nothing else.

For the question "What is Clickthrough Data?", we would expect a response like:
clickthrough data OR click-through data OR click through rate OR CTR OR user clicks OR ad clicks OR search engine results OR web analytics

AND operator is not allowed. Use only OR.

User Question:
[The user's question will be inserted here]

Generate the Elasticsearch query string:
'''
<h4>Potential questions generator prompt</h4><p>Prompt for generating potential questions, enriching document metadata.</p>RAG_QUESTION_GENERATOR_PROMPT = '''
You are an AI assistant specialized in generating questions for Retrieval-Augmented Generation (RAG) systems. Your task is to analyze a given document and create 10 diverse questions that would effectively test a RAG system's ability to retrieve and synthesize information from this document.

Guidelines:
1. Thoroughly analyze the entire document.
2. Generate exactly 10 questions that cover various aspects and levels of complexity within the document's content.
3. Create questions that specifically target:
   a. Key facts and information
   b. Main concepts and ideas
   c. Relationships between different parts of the content
   d. Potential applications or implications of the information
   e. Comparisons or contrasts within the document
4. Ensure questions require answers of varying lengths and complexity, from simple retrieval to more complex synthesis.
5. Include questions that might require combining information from different parts of the document.
6. Frame questions to test both literal comprehension and inferential understanding.
7. Avoid yes/no questions; focus on open-ended questions that promote comprehensive answers.
8. Consider including questions that might require additional context or knowledge to fully answer, to test the RAG system's ability to combine retrieved information with broader knowledge.
9. Number the questions from 1 to 10.
10. Output only the ten questions, without any additional text, explanations, or answers.

Document:
[The document content will be inserted here]

Generate 10 questions optimized for testing a RAG system based on this document:
'''
<h4>HyDE generator prompt</h4><p>Prompt for generating hypothetical documents using HyDE</p>HYDE_DOCUMENT_GENERATOR_PROMPT = '''
You are an AI assistant specialized in generating hypothetical documents based on user queries. Your task is to create a detailed, factual document that would likely contain the answer to the user's question. This hypothetical document will be used to enhance the retrieval process in a Retrieval-Augmented Generation (RAG) system.

Guidelines:
1. Carefully analyze the user's query to understand the topic and the type of information being sought.
2. Generate a hypothetical document that:
   a. Is directly relevant to the query
   b. Contains factual information that would answer the query
   c. Includes additional context and related information
   d. Uses a formal, informative tone similar to an encyclopedia or textbook entry
3. Structure the document with clear paragraphs, covering different aspects of the topic.
4. Include specific details, examples, or data points that would be relevant to the query.
5. Aim for a document length of 200-300 words.
6. Do not use citations or references, as this is a hypothetical document.
7. Avoid using phrases like "In this document" or "This text discusses" - write as if it's a real, standalone document.
8. Do not mention or refer to the original query in the generated document.
9. Ensure the content is factual and objective, avoiding opinions or speculative information.
10. Output only the generated document, without any additional explanations or meta-text.

User Question:
[The user's question will be inserted here]

Generate a hypothetical document that would likely contain the answer to this query:
'''
<h3>Sample hybrid search query</h3>{'knn': {'field': 'primary_embedding',
  'query_vector': [0.4265527129173279,
   -0.1712949573993683,
   -0.042020395398139954,
   ...],
  'k': 100,
  'num_candidates': 100},
 'query': {'bool': {'must': [{'multi_match': {'query': 'audits Elastic Elastic auditing Elastic audit process Elastic compliance Elastic security audit Elasticsearch auditing Elasticsearch compliance Elasticsearch security audit',
      'fields': ['original_text',
       'keyphrases',
       'potential_questions',
       'entities'],
      'type': 'best_fields',
      'operator': 'or'}}],
   'should': [{'script_score': {'query': {'match_all': {}},
      'script': {'source': '\n                                        double vector_score = cosineSimilarity(params.query_vector, params.vector_field) + 1.0;\n                                        double text_score = _score;\n                                        return 0.7 * vector_score + 0.3 * text_score;\n                                        ',
       'params': {'query_vector': [0.4265527129173279,
         -0.1712949573993683,
         -0.042020395398139954,
        ...],
        'vector_field': 'primary_embedding'}}}}]}},
 'size': 10}
]]></content:encoded>
    <link>https://www.elastic.co/search-labs/blog/advanced-rag-techniques-part-2</link>
    <guid isPermaLink="true">https://www.elastic.co/search-labs/blog/advanced-rag-techniques-part-2</guid>
    <category><![CDATA[Vector Database]]></category>
    <category><![CDATA[AI]]></category>
    <dc:creator><![CDATA[Han Xiang Choong]]></dc:creator>
    <enclosure url="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/bltf605c8246989df32/6a1711178b73cbc61d18a11d/8da40067835ab8b4dc12fe52a51a6c26858ad32f-1440x1095.jpg" length="0" type="image/jpeg"/>
    <pubDate>Thu, 15 Aug 2024 00:00:00 GMT</pubDate>
  </item>
  <item>
    <title><![CDATA[Advanced RAG techniques part 1: Data processing]]></title>
    <description><![CDATA[Discussing and implementing techniques which may increase RAG performance. Part 1 of 2, focusing on the data processing and ingestion component of an advanced RAG pipeline.]]></description>
    <content:encoded><![CDATA[<p><em>This is Part 1 of our exploration into Advanced RAG Techniques. </em><a href="https://www.elastic.co/search-labs/blog/advanced-rag-techniques-part-2"><em>Click here for Part 2!</em></a></p><p>The recent paper <a href="https://arxiv.org/abs/2407.01219">Searching for Best Practices in Retrieval-Augmented Generation</a> empirically assesses the efficacy of various RAG enhancing techniques, with the goal of converging on a set of best-practices for RAG.</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt671704ff06a4011d/6a170b3ea929cf2d19ae09d8/dafa7250e7c4ead4d9b4aed7c407509131929749-1440x572.png" alt="RAG pipeline recommended by Wang" /><p>We'll implement a few of these proposed best-practices, namely the ones which aim to improve the quality of search <strong>(Sentence Chunking, HyDE, Reverse Packing)</strong>.</p><p>For brevity, we will omit those techniques focused on improving efficiency <strong>(Query Classification and Summarization)</strong>.</p><p>We will also implement a few techniques that were not covered, but which I personally find useful and interesting <strong>(Metadata Inclusion, Composite Multi-Field Embeddings, Query Enrichment)</strong>.</p><p>Finally, we'll run a short test to see if the quality of our search results and generated answers has improved versus the baseline. Let's get to it!</p><h2>RAG overview</h2><p>RAG aims to enhance LLMs by retrieving information from external knowledge bases to enrich generated answers. By providing domain-specific information, LLMs can be quickly adapted for use cases outside the scope of their training data; significantly cheaper than fine-tuning, and easier to keep up-to-date.</p><p>Measures to improve the quality of RAG typically focus on two tracks:</p><ol><li><p>Enhancing the quality and clarity of the knowledge base.</p></li><li><p>Improving the coverage and specificity of search queries.</p></li></ol><p>These two measures will achieve the goal of improving the odds that the LLM has access to relevant facts and information, and is thus less likely to hallucinate or draw upon its own knowledge - which may be outdated or irrelevant.</p><p>The diversity of methods is difficult to clarify in just a few sentences. Let's go straight to implementation to make things clearer.</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt9a4691874a19d8da/6a170b3f47d49c99f22d8a24/72b51ba2ae5e5977b56e5b915674753d6cfd0e56-1440x840.jpg" alt="Advanced RAG pipeline" /><h3>Table of contents</h3><ul><li><p><a href="https://www.elastic.co/search-labs/blog/advanced-rag-techniques-part-1#overview">Overview</a></p><ul><li><p><a href="https://www.elastic.co/search-labs/blog/advanced-rag-techniques-part-1#table-of-contents">Table of contents</a></p></li></ul></li><li><p><a href="https://www.elastic.co/search-labs/blog/advanced-rag-techniques-part-1#set-up">Set-up</a></p></li><li><p><a href="https://www.elastic.co/search-labs/blog/advanced-rag-techniques-part-1#ingesting-processing-and-embedding-documents">Ingesting, processing, and embedding documents</a>  </p><ul><li><p><a href="https://www.elastic.co/search-labs/blog/advanced-rag-techniques-part-1#data-ingestion">Data ingestion</a></p></li><li><p><a href="https://www.elastic.co/search-labs/blog/advanced-rag-techniques-part-1#sentence-level-token-wise-chunking">Sentence-level, token-wise chunking</a></p></li><li><p><a href="https://www.elastic.co/search-labs/blog/advanced-rag-techniques-part-1#metadata-inclusion-and-generation">Metadata inclusion and generation</a> </p><ul><li><p><a href="https://www.elastic.co/search-labs/blog/advanced-rag-techniques-part-1#keyphrases-extracted-by-textrank">Keyphrases extracted by TextRank</a></p></li><li><p><a href="https://www.elastic.co/search-labs/blog/advanced-rag-techniques-part-1#potential-questions-generated-by-gpt-4o">Potential questions generated by GPT-4o</a></p></li><li><p><a href="https://www.elastic.co/search-labs/blog/advanced-rag-techniques-part-1#entities-extracted-by-spacy">Entities extracted by Spacy</a></p></li></ul></li><li><p><a href="https://www.elastic.co/search-labs/blog/advanced-rag-techniques-part-1#composite-multi-field-embeddings">Composite multi-field embeddings</a></p><ul><li><p><a href="https://www.elastic.co/search-labs/blog/advanced-rag-techniques-part-1#indexing-to-elastic">Indexing to Elastic</a></p></li></ul></li></ul></li><li><p><a href="https://www.elastic.co/search-labs/blog/advanced-rag-techniques-part-1#cat-break">Cat break</a></p></li><li><p><a href="https://www.elastic.co/search-labs/blog/advanced-rag-techniques-part-1#appendix">Appendix</a></p><ul><li><p><a href="https://www.elastic.co/search-labs/blog/advanced-rag-techniques-part-1#definitions">Definitions</a></p></li></ul></li></ul><h2>Set-up</h2><p><em>All code may be found </em><a href="https://github.com/elastic/elasticsearch-labs/tree/advanced-rag-techniques/supporting-blog-content/advanced-rag-techniques"><em>in the Searchlabs repo</em></a><em>.</em></p><p>First things first. You will need the following:</p><ol><li><p>An Elastic Cloud Deployment</p></li><li><p>An LLM API - We are using a GPT-4o deployment on Azure OpenAI in this notebook</p></li><li><p>Python Version 3.12.4 or later</p></li></ol><p>We will be running all the code from <a href="https://github.com/elastic/elasticsearch-labs/blob/advanced-rag-techniques/supporting-blog-content/advanced-rag-techniques/main.ipynb">the main.ipynb notebook.</a></p><p>Go ahead and git clone the repo, navigate to supporting-blog-content/advanced-rag-techniques, then run the following commands:</p># Create a new virtual environment named 'rag_env'
python -m venv rag_env

# Activate the virtual environment (for Unix-based systems)
source rag_env/bin/activate

# (For Windows)
.\rag_env\Scripts\activate

# Install packages listed in requirements.txt
pip install -r requirements.txt
<p>Once that's done, create a <em>.env</em> file and fill out the following fields (Referenced in <a href="https://github.com/elastic/elasticsearch-labs/blob/advanced-rag-techniques/supporting-blog-content/advanced-rag-techniques/.env.example"><em>.env.example</em></a>). Credits to my co-author, Claude-3.5, for the helpful comments.</p># Elastic Cloud: Found in the 'Deployment' page of your Elastic Cloud 
# console
ELASTIC_CLOUD_ENDPOINT=""
ELASTIC_CLOUD_ID=""

# Elastic Cloud: Created during deployment setup or in 'Security' 
# settings
ELASTIC_USERNAME=""
ELASTIC_PASSWORD=""

# Elastic Cloud: The name of the index you created in Kibana or via API
ELASTIC_INDEX_NAME=""

# Azure AI Studio: Found in 'Keys and Endpoint' section of your Azure 
# OpenAI resource
AZURE_OPENAI_KEY_1=""
AZURE_OPENAI_KEY_2=""
AZURE_OPENAI_REGION=""
AZURE_OPENAI_ENDPOINT=""

# Azure AI Studio: Found in 'Deployments' section of your Azure OpenAI 
# resource
AZURE_OPENAI_DEPLOYMENT_NAME=""

# Using BAAI/bge-small-en-v1.5 because I think it is a good balance of 
# resource efficiency and performance. 
HUGGINGFACE_EMBEDDING_MODEL="BAAI/bge-small-en-v1.5"
<p>Next, we'll choose the document to ingest, and place it in the documents folder. For this article, we'll be using the <a href="https://s201.q4cdn.com/217177842/files/doc_downloads/OtherDocuments/2023/AnnualMeeting/Annual-Report-Fiscal-Year-2023.pdf">Elastic N.V. Annual Report 2023</a>. It's a pretty challenging and dense document, perfect for stress testing our RAG techniques.</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blte292dc6030d496cc/6a170b40dc55de9b03e00dfc/e513b9d67adac43da794c25a5969b893127bbbe3-1440x395.jpg" alt="Elastic Annual Report 2023" /><p>Now we're all set, let's go to ingestion. Open <em>main.ipynb</em> and execute the first two cells to import all packages and intialize all services.</p><p><a href="https://www.elastic.co/search-labs/blog/advanced-rag-techniques-part-1#table-of-contents">Back to top</a></p><h2>Ingesting, processing, and embedding documents</h2><h3>Data ingestion</h3><ul><li><p><em>Personal note: I am stunned by LlamaIndex's convenience. In the olden days before LLMs and LlamaIndex, ingesting documents of various formats was a painful process of collecting esoteric packages from all over. Now it's reduced to a single function call. Wild.</em></p></li></ul><p>The <code>SimpleDirectoryReader</code> will load every document in the <code>directory_path.</code> For <code>.pdf</code> files, it returns a list of document objects, which I convert to Python dictionaries because I find them easier to work with.</p># llamaindex_processor.py
from llama_index.core import SimpleDirectoryReader

class LlamaIndexProcessor:
   def __init__(self):
       pass 
   
   def load_documents(self, directory_path):
       ''' 
       Load all documents in directory
       '''
       reader = SimpleDirectoryReader(input_dir=directory_path)
       return reader.load_data()

# main.ipynb
llamaindex_processor=LlamaIndexProcessor()
documents=llamaindex_processor.load_documents('./documents/')
documents=[dict(doc_obj) for doc_obj in documents]
<p>Each dictionary contains the key content in the <code>text</code> field. It also contains useful metadata such as page number, filename, file size, and type.</p>{
  'id_': '5f76f0b3-22d8-49a8-9942-c2bbab14f63f',
  'metadata': {'page_label': '5',
   'file_name': 'Elastic_NV_Annual-Report-Fiscal-Year-2023.pdf',
   'file_path': '/Users/han/Desktop/Projects/truckasaurus/documents/Elastic_NV_Annual-Report-Fiscal-Year-2023.pdf',
   'file_type': 'application/pdf',
   'file_size': 3724426,
   'creation_date': '2024-07-27',
   'last_modified_date': '2024-07-27'},
   'text': 'Table of Contents\nPage\nPART I\nItem 1. Business 3\n15 Item 1A. Risk Factors\nItem 1B. Unresolved Staff Comments 48\nItem 2. Properties 48\nItem 3. Legal Proceedings 48\nItem 4. Mine Safety Disclosures 48\nPART II\nItem 5. Market for Registrant's Common Equity, Related Stockholder Matters and Issuer Purchases of \nEquity Securities49\nItem 6. [Reserved] 49\nItem 7. Management's Discussion and Analysis of Financial Condition and Results of Operations 50\nItem 7A. Quantitative and Qualitative Disclosures About Market Risk 64\nItem 8. Financial Statements and Supplementary Data 66\nItem 9. Changes in and Disagreements With Accountants on Accounting and Financial Disclosure 100\n100\n101Item 9A. Controls and Procedures\nItem 9B. Other Information\nItem 9C. Disclosure Regarding Foreign Jurisdictions That Prevent Inspections 101\nPART III\n102\n102\n102\n102Item 10. Directors, Executive Officers and Corporate Governance\nItem 11. Executive Compensation\nItem 12. Security Ownership of Certain Beneficial Owners and Management, and Related Stockholder Matters  \nItem 13. Certain Relationships and Related Transactions, and Director Independence\nItem 14. Principal Accountant Fees and Services 102\nPART IV\n103\n105Item 15. Exhibits and Financial Statement Schedules  \nItem 16. Form 10-K Summary\nSignatures 106\ni',
   ...
}
<p><a href="https://www.elastic.co/search-labs/blog/advanced-rag-techniques-part-1#table-of-contents">Back to top</a></p><h3>Sentence-level, token-wise chunking</h3><p>The first thing to do is reduce our documents to chunks of a standard length (to ensure consistency and manageability). Embedding models have unique token limits (maximum input size they can process). Tokens are the basic units of text that models process. To prevent information loss (truncation or omission of content), we should provide text that does not exceed those limits (by splitting longer texts into smaller segments).</p><p>Chunking has a significant impact on performance. Ideally, each chunk would represent a self-contained piece of information, capturing contextual information about a single topic. Chunking methods include word-level chunking, where documents are split by word count, and semantic chunking which uses an LLM to identify logical breakpoints.</p><p>Word-level chunking is cheap, fast, and easy, but runs a risk of splitting sentences and thus breaking context. Semantic chunking gets slow and expensive, especially if you're dealing with documents like the 116-page Elastic Annual Report.</p><p>Let's choose a middleground approach. Sentence level chunking is still simple, but can preserve context more effectively than word-level chunking while being significantly cheaper and faster. Additionally, we'll implement a sliding window to capture some of the surrounding context, and alleviate the impact of splitting paragraphs.</p># chunker.py 

import uuid
import re


class Chunker: 
    def __init__(self, tokenizer):
        self.tokenizer = tokenizer 
    
    def split_into_sentences(self, text):
        """Split text into sentences."""
        return re.split(r'(?&lt;=[.!?])\s+', text)
 
    def sentence_wise_tokenized_chunk_documents(self, documents, chunk_size=512, overlap=20, min_chunk_size=50):
        '''
        1. Split text into sentences.
        2. Tokenize using the provided tokenizer method.
        3. Build chunks up to the chunk_size limit.
        4. Create an overlap based on tokens - to preserve context.
        5. Only keep chunks that meet the minimum token size requirement.
        '''
        chunked_documents = []

        for doc in documents:
            sentences = self.split_into_sentences(doc['text'])
            tokens = []
            sentence_boundaries = [0]

            # Tokenize all sentences and keep track of sentence boundaries
            for sentence in sentences:
                sentence_tokens = self.tokenizer.encode(sentence, add_special_tokens=True)
                tokens.extend(sentence_tokens)
                sentence_boundaries.append(len(tokens))

            # Create chunks
            chunk_start = 0
            while chunk_start &lt; len(tokens):
                chunk_end = chunk_start + chunk_size

                # Find the last complete sentence that fits in the chunk
                sentence_end = next((i for i in sentence_boundaries if i &gt; chunk_end), len(tokens))
                chunk_end = min(chunk_end, sentence_end)

                # Create the chunk
                chunk_tokens = tokens[chunk_start:chunk_end]

                # Check if the chunk meets the minimum size requirement
                if len(chunk_tokens) &gt;= min_chunk_size:
                    # Create a new document object for this chunk
                    chunk_doc = {
                        'id_': str(uuid.uuid4()),
                        'chunk': chunk_tokens,
                        'original_text': self.tokenizer.decode(chunk_tokens),
                        'chunk_index': len(chunked_documents),
                        'parent_id': doc['id_'],
                        'chunk_token_count': len(chunk_tokens)
                    }

                    # Copy all other fields from the original document
                    for key, value in doc.items():
                        if key != 'text' and key not in chunk_doc:
                            chunk_doc[key] = value

                    chunked_documents.append(chunk_doc)

                # Move to the next chunk start, considering overlap
                chunk_start = max(chunk_start + chunk_size - overlap, chunk_end - overlap)

        return chunked_documents

# main.ipynb 
# Initialize Embedding Model
HUGGINGFACE_EMBEDDING_MODEL = os.environ.get('HUGGINGFACE_EMBEDDING_MODEL')
embedder=EmbeddingModel(model_name=HUGGINGFACE_EMBEDDING_MODEL)

# Initialize Chunker
chunker=Chunker(embedder.tokenizer)
<p>The <code>Chunker</code> class takes in the embedding model's tokenizer to encode and decode text. We'll now build chunks of 512 tokens each, with an overlap of 20 tokens. To do this, we'll split the text into sentences, tokenize those sentences, and then add the tokenized sentences to our current chunk until we cannot add more without breaching our token limit.</p><p>Finally, decode the sentences back to the original text for embedding, storing it in a field called <code>original_text</code>. Chunks are stored in a field called <code>chunk</code>. To reduce noise (aka useless documents), we will discard any documents smaller than 50 tokens in length.</p><p>Let's run it over our documents:</p>chunked_documents=chunker.sentence_wise_tokenized_chunk_documents(documents, chunk_size=512)
<p>And get back chunks of text that look like this:</p>print(chunked_documents[4]['original_text'])

[CLS] the aggregate market value of the ordinary shares held by non - affiliates of the registrant, 
based on the closing price of the shares of ordinary shares on the new york stock exchange on 
october 31, 2022 ( the last business day of the registrant 's second fiscal quarter ), was 
approximately $ 6. 1 billion. [SEP] [CLS] as of may 31, 2023, the registrant had 97, 390, 886 
ordinary shares, par value €0. 01 per share, outstanding. [SEP] [CLS] documents incorporated by 
reference portions of the registrant 's definitive proxy statement relating to the registrant 's 2
023 annual general meeting of shareholders are incorporated by reference into part iii of this annual 
...
...
<p><a href="https://www.elastic.co/search-labs/blog/advanced-rag-techniques-part-1#table-of-contents">Back to top</a></p><h3>Metadata inclusion and generation</h3><p>We've chunked our documents. Now it's time to enrich the data. I want to generate or extract additional metadata. This additional metadata can be used to influence and enhance search performance.</p><p>We'll define a <code>DocumentEnricher</code> class, whose role is to take in a list of documents (Python dictionaries), and a list of processor functions. These functions will run over the documents' <code>original_text</code> column, and store their outputs in new fields.</p><p>First, we extract keyphrases using <a href="https://github.com/elastic/elasticsearch-labs/blob/advanced-rag-techniques/supporting-blog-content/advanced-rag-techniques/nltk_processor.py">TextRank</a>. TextRank is a graph-based algorithm that extracts key phrases and sentences from text by ranking their importance based on the relationships between words.</p><p>Next, we'll <a href="https://github.com/elastic/elasticsearch-labs/blob/advanced-rag-techniques/supporting-blog-content/advanced-rag-techniques/llm.py">generate potential_questions using GPT-4o</a>.</p><p>Finally, we'll <a href="https://github.com/elastic/elasticsearch-labs/blob/advanced-rag-techniques/supporting-blog-content/advanced-rag-techniques/entity_extractor.py">extract entities</a> using <a href="https://spacy.io/">Spacy</a>.</p><p>Since the code for each of these is quite lengthy and involved, I will refrain from reproducing it here. If you are interested, the files are marked in the code samples below.</p><p>Let's run the data enrichment:</p># documentenricher.py
from tqdm import tqdm

class DocumentEnricher:

    def __init__(self):
        pass 

    def enrich_document(self, documents, processors, text_col='text'):
        for doc in tqdm(documents, desc="Enriching documents using processors: "+str(processors)): 
            for (processor, field) in processors: 
                metadata=processor(doc[text_col])
                if isinstance(metadata, list):
                    metadata='\n'.join(metadata)
                doc.update({field: metadata})
 
# main.ipynb
# Initialize processor classes 
nltkprocessor=NLTKProcessor() // nltk_processor.py
entity_extractor=EntityExtractor() // entity_extractor.py
gpt4o = LLMProcessor(model='gpt-4o') // llm.py

# Initialize LLM
documentenricher=DocumentEnricher()

# Create new fields in the documents - These are the outputs of the processor functions.
processors=[
    (nltkprocessor.textrank_phrases, "keyphrases"),
    (gpt4o.generate_questions, "potential_questions"),
    (entity_extractor.extract_entities, "entities")
    ]

# .enrich_document() will modify chunked_docs in place. 
# To view the results, we'll print chunked_docs in the next few cells!
documentenricher.enrich_document(chunked_docs, text_col='original_text', processors=processors)
<p>And take a look at the results:</p><h4>Keyphrases extracted by TextRank</h4><p>These keyphrases are a stand-in for the chunk's core topics. If a query has to do with cybersecurity, this chunk's score will be boosted.</p>print(chunked_documents[25]['keyphrases'])

'elastic agent stop', 'agent stop malware', 
'stop malware ransomware', 'malware ransomware environment', 
'ransomware environment wide', 'environment wide visibility', 
'wide visibility threat', 'visibility threat detection', 
'sep cl key', 'cl key feature'
<h4>Potential questions generated by GPT-4o</h4><p>These potential questions may directly match with user queries, offering a boost in score. We prompt GPT-4o to generate questions which can be answered using the information found in the current chunk.</p>print(chunked_documents[25]['potential_questions'])

1. What are the primary functions that Elastic Agent provides in terms of cybersecurity?
2. Describe how Logstash contributes to data management within an IT environment.
3. List and explain any key features of Logstash mentioned in the document.
4. How does Elastic Agent enhance environment-wide visibility in threat detection?
5. What capabilities does Logstash offer for handling data beyond simple collection?
6. In what ways does the document suggest that Elastic Agent stops malware and ransomware?
7. Can you identify any relationships between the functionalities of Elastic Agent and Logstash in an integrated environment?
8. What implications might the advanced threat detection capabilities of Elastic Agent have for organizational security policies?
9. Compare and contrast the roles of Elastic Agent and Logstash based on their described functions.
10. How might the centralized collection ability of Logstash support the threat detection capabilities of Elastic Agent?
<h4>Entities extracted by Spacy</h4><p>These entities serve a similar purpose to the keyphrases, but capture organizations' and individuals' names, which keyphrase extraction may miss.</p>print(chunked_documents[29]['entities'])

'appdynamics', 'apm data', 'azure sentinel', 
'microsoft', 'mcafee', 'broadcom', 'cisco', 
'dynatrace', 'coveo', 'lucidworks'
<p><a href="https://www.elastic.co/search-labs/blog/advanced-rag-techniques-part-1#table-of-contents">Back to top</a></p><h3>Composite multi-field embeddings</h3><p>Now that we have enriched our documents with additional metadata, we can leverage this information to create more robust and context-aware embeddings.</p><p>Let's review our current point in the process. We've got four fields of interest in each document.</p>{
    "chunk": "...",
    "keyphrases": "...", 
    "potential_questions": "...", 
    "entities": "..." 
}
<p>Each field represents a different perspective on the document's context, potentially highlighting a key area for the LLM to focus on.</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt84cb328fce6aae23/6a170b42964cea3e4408bbc4/aea1f513009a0c7c8545a79fad8f072a5bcae24c-1440x1067.jpg" alt="Metadata Enrichment Pipeline in RAG" /><p>The plan is to embed each of these fields, and then create a weighted sum of the embeddings, known as a Composite Embedding.</p><p>With luck, this Composite Embedding will allow the system to become more context aware, in addition to introducing another tunable hyperparameter from controlling the search behavior.</p><p>First, let's embed each field and update each document in place, using our locally defined embedding model imported at the beginning of the main.ipynb notebook.</p># EmbeddingModel defined in embedding_model.py
embedder=EmbeddingModel(model_name=HUGGINGFACE_EMBEDDING_MODEL)

cols_to_embed=['keyphrases', 'potential_questions', 'entities']

embedding_cols=[]
for col in cols_to_embed:
    # Works on text input
    embedding_col=embedder.embed_documents_text_wise(chunked_documents, text_field=col)
    embedding_cols.append(embedding_col)
# Works on token input
embedding_col=embedder.embed_documents_token_wise(chunked_documents, token_field="chunk")
embedding_cols.append(embedding_col)
<p>Each embedding function returns the embedding's field, which is just the original input field with an <code>_embedding</code> postfix.</p><p>Let's now define the weightings of our composite embedding:</p>embedding_cols=[
                'keyphrases_embedding',
                'potential_questions_embedding',
                'entities_embedding',
                'chunk_embedding']
combination_weights=[
                    0.1,
                    0.15,
                    0.05,
                    0.7
                ]
<p>The weightings allow you to assign priorities to each component, based on your usecase and the quality of your data. Intuitively, the size of these weightings is dependent on the semantic value of each component. Since the chunk text itself is by far the richest, I assign a weighting of 70%. Since the entities are the smallest, being just a list of org or person names, I assign it a weighting of 5%. The precise setting for these values has to be determined empirically, on a use-case by use-case basis.</p><p>Finally, let's write a function to apply the weightings, and create our composite embedding. We'll delete all the component embeddings as well to save space.</p>from tqdm import tqdm 
def combine_embeddings(objects, embedding_cols, combination_weights, primary_embedding='primary_embedding'):
    # Ensure the number of weights matches the number of embedding columns
    assert len(embedding_cols) == len(combination_weights), "Number of embedding columns must match number of weights"
    
    # Normalize weights to sum to 1
    weights = np.array(combination_weights) / np.sum(combination_weights)
    
    for obj in tqdm(objects, desc="Combining embeddings"):
        # Initialize the combined embedding
        combined = np.zeros_like(obj[embedding_cols[0]])
        
        # Compute the weighted sum
        for col, weight in zip(embedding_cols, weights):
            combined += weight * np.array(obj[col])
        
        # Add the new combined embedding to the object
        obj.update({primary_embedding:combined.tolist()})
        
        # Remove the original embedding columns
        for col in embedding_cols:
            obj.pop(col, None)

combine_embeddings(chunked_documents, embedding_cols, combination_weights)
<p>With this, we've completed our document processing. We now have a list of document objects which look like this:</p>{ 'id_': '7fe71686-5cd0-4831-9e79-998c6dbeae0c', 'chunk': [2312, 14613, ...], 'original_text': 'if an emerging growth company, indicate by check mark if the registrant has elected not to use the extended ...', 'chunk_index': 3, 'chunk_token_count': 399, 'metadata': {'page_label': '3', 'file_name': 'Elastic_NV_Annual-Report-Fiscal-Year-2023.pdf', ... 'keyphrases': 'sep cl unk\ncheck mark registrant\ncl unk indicate\nunk indicate check\nindicate check mark\nprincipal executive office\naccelerate filer unk\ncompany unk emerge\nunk emerge growth\nemerge growth company', 'potential_questions': '1. What are the different types of registrant statuses mentioned in the document?\n2. Under what section of the Sarbanes-Oxley Act must registrants file a report on the effectiveness of their internal ...', 'entities': 'the effe ctiveness of\nsection 13\nSEP\nUNK\nsection 21e\n1934\n1933\nu. s. c.\nsection 404\nsection 12\nal', 'primary_embedding': [-0.3946287803351879, -0.17586839850991964, ...] }
<h4>Indexing to Elastic</h4><p>Let's bulk upload our documents to Elastic Search. For this purpose, I long-ago defined a set of Elastic Helper functions in <a href="https://github.com/elastic/elasticsearch-labs/blob/advanced-rag-techniques/supporting-blog-content/advanced-rag-techniques/elastic_helpers.py"><code>elastic_helpers.py</code></a>. It is a very lengthy piece of code so let's sticking to looking at the function calls.</p><p><code>es_bulk_indexer.bulk_upload_documents</code> works with any list of dictionary objects, taking advantage of Elasticsearch's convenient dynamic mappings.</p># Initialize Elasticsearch
ELASTIC_CLOUD_ID = os.environ.get('ELASTIC_CLOUD_ID')
ELASTIC_USERNAME = os.environ.get('ELASTIC_USERNAME')
ELASTIC_PASSWORD = os.environ.get('ELASTIC_PASSWORD')
ELASTIC_CLOUD_AUTH = (ELASTIC_USERNAME, ELASTIC_PASSWORD)
es_bulk_indexer = ESBulkIndexer(cloud_id=ELASTIC_CLOUD_ID, credentials=ELASTIC_CLOUD_AUTH)
es_query_maker = ESQueryMaker(cloud_id=ELASTIC_CLOUD_ID, credentials=ELASTIC_CLOUD_AUTH)

# Define Index Name
index_name=os.environ.get('ELASTIC_INDEX_NAME')


# Create index and bulk upload 
index_exists = es_bulk_indexer.check_index_existence(index_name=index_name)
if not index_exists:
    logger.info(f"Creating new index: {index_name}")
    es_bulk_indexer.create_es_index(es_configuration=BASIC_CONFIG, index_name=index_name)

success_count = es_bulk_indexer.bulk_upload_documents(
    index_name=index_name, 
    documents=chunked_documents, 
    id_col='id_',
    batch_size=32
)
<p>Head on over to Kibana and verify that all documents have been indexed. There should be 224 of them. Not bad for such a large document!</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt8efeface6effe01d/6a170b447d8d67652870e72a/1b3b07f6b98ceb65f6594ce4be83c5b0ed7e7cf9-1440x1380.jpg" alt="Index Kibana" /><p><a href="https://www.elastic.co/search-labs/blog/advanced-rag-techniques-part-1#table-of-contents">Back to top</a></p><h2>Cat break</h2><p>Let's take a break, article's a little heavy, I know. Check out my cat:</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/bltc1db5595f71c12ff/6a170b450e2e49940241a0fe/baca4eb52b801b21ced97352cc55462f0a12d6b0-969x996.jpg" alt="Han Pipeline" /><p>Adorable. The hat went missing and I half suspect she stole and hid it somewhere :(</p><p>Congrats on making it this far :)</p><p>Join me in <a href="https://www.elastic.co/search-labs/blog/advanced-rag-techniques-part-2">Part 2</a> for testing and evaluation of our RAG pipeline!</p><h2>Appendix</h2><h3>Definitions</h3><p><strong>1. Sentence Chunking</strong></p><ul><li><p>A preprocessing technique used in RAG systems to divide text into smaller, meaningful units.</p></li><li><p><em>Process:</em> </p><ol><li><p>Input: Large block of text (e.g., document, paragraph)</p></li><li><p>Output: Smaller text segments (typically sentences or small groups of sentences)</p></li></ol></li><li><p><em>Purpose:</em> </p><ul><li><p>Creates granular, context-specific text segments</p></li><li><p>Allows for more precise indexing and retrieval</p></li><li><p>Improves the relevance of retrieved information in RAG systems</p></li></ul></li><li><p><em>Characteristics:</em> </p><ul><li><p>Segments are semantically meaningful</p></li><li><p>Can be independently indexed and retrieved</p></li><li><p>Often preserves some context to ensure standalone comprehensibility</p></li></ul></li><li><p><em>Benefits:</em> </p><ul><li><p>Enhances retrieval precision</p></li><li><p>Enables more focused augmentation in RAG pipelines</p></li></ul></li></ul><p><strong>2. HyDE (Hypothetical Document Embedding)</strong></p><ul><li><p>A technique that uses an LLM to generate a hypothetical document for query expansion in RAG systems.</p></li><li><p><em>Process:</em>  </p><ol><li><p>Input query to an LLM</p></li><li><p>LLM generates a hypothetical document answering the query</p></li><li><p>Embed the generated document</p></li><li><p>Use the embedding for vector search</p></li></ol></li><li><p><em>Key difference:</em> </p><ul><li><p>Traditional RAG: Matches query to documents</p></li><li><p>HyDE: Matches documents to documents</p></li></ul></li><li><p><em>Purpose:</em> </p><ul><li><p>Improve retrieval performance, especially for complex or ambiguous queries</p></li><li><p>Capture richer semantic context than a short query</p></li></ul></li><li><p><em>Benefits:</em> </p><ul><li><p>Leverages LLM's knowledge to expand queries</p></li><li><p>Can potentially improve relevance of retrieved documents</p></li></ul></li><li><p><em>Challenges:</em> </p><ul><li><p>Requires additional LLM inference, increasing latency and cost</p></li><li><p>Performance depends on quality of generated hypothetical document</p></li></ul></li></ul><p><strong>3. Reverse Packing</strong></p><ul><li><p>A technique used in RAG systems to reorder search results before passing them to the LLM.</p></li><li><p><em>Process:</em> </p><ol><li><p>Search engine (e.g., Elasticsearch) returns documents in descending order of relevance.</p></li><li><p>The order is reversed, placing the most relevant document last.</p></li></ol></li><li><p><em>Purpose:</em> </p><ul><li><p>Exploits the recency bias of LLMs, which tend to focus more on the latest information in their context.</p></li><li><p>Ensures the most relevant information is "freshest" in the LLM's context window.</p></li></ul></li><li><p><em>Example:</em> Original order: [Most Relevant, Second Most, Third Most, ...] Reversed order: [..., Third Most, Second Most, Most Relevant]</p></li></ul><p><strong>4. Query Classification</strong></p><ul><li><p>A technique to optimize RAG system efficiency by determining whether a query requires RAG or can be answered directly by the LLM.</p></li><li><p><em>Process:</em> </p><ol><li><p>Develop a custom dataset specific to the LLM in use</p></li><li><p>Train a specialized classification model</p></li><li><p>Use the model to categorize incoming queries</p></li></ol></li><li><p><em>Purpose:</em> </p><ul><li><p>Improve system efficiency by avoiding unnecessary RAG processing</p></li><li><p>Direct queries to the most appropriate response mechanism</p></li></ul></li><li><p><em>Requirements:</em> </p><ul><li><p>LLM-specific dataset and model</p></li><li><p>Ongoing refinement to maintain accuracy</p></li></ul></li><li><p><em>Benefits:</em> </p><ul><li><p>Reduces computational overhead for simple queries</p></li><li><p>Potentially improves response time for non-RAG queries</p></li></ul></li></ul><p><strong>5. Summarization</strong></p><ul><li><p>A technique to condense retrieved documents in RAG systems.</p></li><li><p><em>Process:</em> </p><ol><li><p>Retrieve relevant documents</p></li><li><p>Generate concise summaries of each document</p></li><li><p>Use summaries instead of full documents in the RAG pipeline</p></li></ol></li><li><p><em>Purpose:</em> </p><ul><li><p>Improve RAG performance by focusing on essential information</p></li><li><p>Reduce noise and interference from less relevant content</p></li></ul></li><li><p><em>Benefits:</em> </p><ul><li><p>Potentially improves relevance of LLM responses</p></li><li><p>Allows for inclusion of more documents within context limits</p></li></ul></li><li><p><em>Challenges:</em> </p><ul><li><p>Risk of losing important details in summarization</p></li><li><p>Additional computational overhead for summary generation</p></li></ul></li></ul><p><strong>6. Metadata Inclusion</strong></p><ul><li><p>A technique to enrich documents with additional contextual information.</p></li><li><p><em>Types of metadata:</em>  </p><ul><li><p>Keyphrases</p></li><li><p>Titles</p></li><li><p>Dates</p></li><li><p>Authorship details</p></li><li><p>Blurbs</p></li></ul></li><li><p><em>Purpose:</em> </p><ul><li><p>Increase contextual information available to the RAG system</p></li><li><p>Provide LLMs with clearer understanding of document content and relevance</p></li></ul></li><li><p><em>Benefits:</em> </p><ul><li><p>Potentially improves retrieval accuracy</p></li><li><p>Enhances LLM's ability to assess document usefulness</p></li></ul></li><li><p><em>Implementation:</em> </p><ul><li><p>Can be done during document preprocessing</p></li><li><p>May require additional data extraction or generation steps</p></li></ul></li></ul><p><strong>7. Composite Multi-Field Embeddings</strong></p><ul><li><p>An advanced embedding technique for RAG systems that creates separate embeddings for different document components.</p></li><li><p><em>Process:</em> </p><ol><li><p>Identify relevant fields (e.g., title, keyphrases, blurb, main content)</p></li><li><p>Generate separate embeddings for each field</p></li><li><p>Combine or store these embeddings for use in retrieval</p></li></ol></li><li><p><em>Difference from standard approach:</em> </p><ul><li><p>Traditional: Single embedding for entire document</p></li><li><p>Composite: Multiple embeddings for different document aspects</p></li></ul></li><li><p><em>Purpose:</em> </p><ul><li><p>Create more nuanced and context-aware document representations</p></li><li><p>Capture information from a wider variety of sources within a document</p></li></ul></li><li><p><em>Benefits:</em> </p><ul><li><p>Potentially improves performance on ambiguous or multi-faceted queries</p></li><li><p>Allows for more flexible weighting of different document aspects in retrieval</p></li></ul></li><li><p><em>Challenges:</em> </p><ul><li><p>Increased complexity in embedding storage and retrieval processes</p></li><li><p>May require more sophisticated matching algorithms</p></li></ul></li></ul><p><strong>8. Query Enrichment</strong></p><ul><li><p>A technique to expand the original query with related terms to improve search coverage.</p></li><li><p><em>Process:</em> </p><ol><li><p>Analyze the original query</p></li><li><p>Generate synonyms and semantically related phrases</p></li><li><p>Augment the query with these additional terms</p></li></ol></li><li><p><em>Purpose:</em> </p><ul><li><p>Increase the range of potential matches in the document corpus</p></li><li><p>Improve retrieval performance for queries with specific or technical language</p></li></ul></li><li><p><em>Benefits:</em> </p><ul><li><p>Potentially retrieves relevant documents that don't exactly match the original query terms</p></li><li><p>Can help overcome vocabulary mismatch between queries and documents</p></li></ul></li><li><p><em>Challenges:</em> </p><ul><li><p>Risk of query drift if not carefully implemented</p></li><li><p>May increase computational overhead in the retrieval process</p></li></ul></li></ul><p><a href="https://www.elastic.co/search-labs/blog/advanced-rag-techniques-part-1#table-of-contents">Back to top</a></p>]]></content:encoded>
    <link>https://www.elastic.co/search-labs/blog/advanced-rag-techniques-part-1</link>
    <guid isPermaLink="true">https://www.elastic.co/search-labs/blog/advanced-rag-techniques-part-1</guid>
    <category><![CDATA[Vector Database]]></category>
    <category><![CDATA[AI]]></category>
    <dc:creator><![CDATA[Han Xiang Choong]]></dc:creator>
    <enclosure url="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt9a4691874a19d8da/6a170b3f47d49c99f22d8a24/72b51ba2ae5e5977b56e5b915674753d6cfd0e56-1440x840.jpg" length="0" type="image/jpeg"/>
    <pubDate>Wed, 14 Aug 2024 00:00:00 GMT</pubDate>
  </item>
  <item>
    <title><![CDATA[Building multilingual RAG with Elastic and Mistral]]></title>
    <description><![CDATA[Building a multilingual RAG application using Elastic and Mixtral 8x22B model]]></description>
    <content:encoded><![CDATA[<p><a href="https://mistral.ai/news/mixtral-8x22b">Mixtral 8x22B</a> is the most performant open model, and one of its most powerful features is fluency in many languages; including English, Spanish, French, Italian, and German.</p><p>Imagine a multinational company with support tickets and solutions in different languages and wants to take advantage of that knowledge across divisions. Currently, knowledge is limited to the language the agent speaks. Let's fix that!</p><p>In this article, I’m going to show you how to test Mixtral’s language capabilities, by creating a multilingual RAG system.</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt4116efa3368e0387/6a17117a1949f76a59e7ab36/27ba7e0cdf3d484b5c9e697702b9a63bff49b82b-1440x868.png" alt="Building multilingual RAG with Elastic and Mistral diagram" /><p><em>You can follow the notebook to reproduce this article's example </em><a href="https://github.com/elastic/elasticsearch-labs/blob/main/supporting-blog-content/building-multilingual-rag-with-elastic-and-mistral/building_multilingual_rag_with_elastic_and_mistral.ipynb"><em>here</em></a></p><h3>Steps</h3><ol><li><p><a href="https://www.elastic.co/search-labs/blog/building-multilingual-rag-with-elastic-and-mistral#creating-endpoints">Creating embeddings endpoint</a></p></li><li><p><a href="https://www.elastic.co/search-labs/blog/building-multilingual-rag-with-elastic-and-mistral#creating-mappings">Creating mappings</a></p></li><li><p><a href="https://www.elastic.co/search-labs/blog/building-multilingual-rag-with-elastic-and-mistral#indexing-data">Indexing data</a></p></li><li><p><a href="https://www.elastic.co/search-labs/blog/building-multilingual-rag-with-elastic-and-mistral#asking-questions">Asking questions</a></p></li></ol><h2>Creating embeddings endpoint</h2><p>Our support tickets for this example will come in English, Spanish, and German. The Mistral embeddings model is not multilingual, but we can generate <a href="https://www.elastic.co/search-labs/blog/multilingual-vector-search-e5-embedding-model">multilingual embeddings</a> using the e5 model, so we can index text on different languages and manage it as a single source, giving us a much richer context.</p><p>To create e5 multilingual embeddings you can use Kibana:</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt0aadfb7eeddd9754/6a17117c6234e00fc6db1ae8/a691763d2976a23d7d82177b6a7e8ad31051b913-800x549.gif" alt="Creating a multilingual endpoint with Kibana" /><p>Or the _inference API:</p>PUT _inference/text_embedding/multilingual-embeddings
 {
    "service": "elasticsearch",
    "service_settings": {
        "model_id": ".multilingual-e5-small",
        "num_allocations": 1 ,
        "num_threads": 1
    }
}
<h2>Creating Mappings</h2><p>For the mappings we will use <a href="https://www.elastic.co/search-labs/blog/semantic-search-simplified-semantic-text">semantic_text</a> mapping type, which is one of my favorite features. It handles the process of chunking the data, generating embeddings, and querying embeddings for you!</p>PUT multilingual-mistral
{
  "mappings": {
    "properties": {
      "super_body": {
        "type": "semantic_text",
        "inference_id": "multilingual-embeddings"
      }
    }
  }
}
<p>We call the text field <code>super_body</code> because with a single mapping type it will handle chunks and embeddings.</p><h2>Indexing data</h2><p>We will index a couple of support tickets with problems and solutions in two languages, and then ask a question about problems within many documents in a third.</p><p>The following documents will be added to the index:</p><p></p><p>1. English Support Ticket: Calendar Sync Issue</p><p></p><p><em>Support Ticket #EN1234</em> <strong>Subject</strong>: Calendar sync not working with Google Calendar</p><p><strong>Description</strong>: I'm having trouble syncing my project deadlines with Google Calendar. Whenever I try to sync, I get an error message saying "Unable to connect to external calendar service."</p><p><strong>Resolution</strong>: The issue was resolved by following these steps:</p><ol><li><p>Go to Settings &gt; Integrations</p></li></ol><p></p><ol><li><p>Disconnect the Google Calendar integration</p></li></ol><p></p><ol><li><p>Clear browser cache and cookies</p></li></ol><p></p><ol><li><p>Reconnect the Google Calendar integration</p></li></ol><p></p><ol><li><p>Authorize the app again in Google's security settings</p></li></ol><p>The sync should now work correctly. If problems persist, ensure that third-party cookies are enabled in your browser settings.</p><p></p><p>2. German Support Ticket: File Upload Problem</p><p></p><p><em>Support-Ticket #DE5678</em> <strong>Betreff</strong>: Datei-Upload funktioniert nicht</p><p><strong>Beschreibung</strong>: Ich kann keine Dateien mehr in meine Projekte hochladen. Jedes Mal, wenn ich es versuche, bleibt der Ladebalken bei 99% stehen und dann erscheint eine Fehlermeldung.</p><p><strong>Lösung</strong>: Das Problem wurde durch folgende Schritte gelöst:</p><ol><li><p>Überprüfen Sie die Dateigröße. Die maximale Uploadgröße beträgt 100 MB.</p></li></ol><p></p><ol><li><p>Deaktivieren Sie vorübergehend den Virenschutz oder die Firewall.</p></li></ol><p></p><ol><li><p>Versuchen Sie, die Datei im Inkognito-Modus hochzuladen.</p></li></ol><p></p><ol><li><p>Wenn das nicht funktioniert, leeren Sie den Browser-Cache und die Cookies.</p></li></ol><p></p><ol><li><p>Als letzten Ausweg, versuchen Sie einen anderen Browser zu verwenden.</p></li></ol><p>In den meisten Fällen lag das Problem an zu großen Dateien oder an Interferenzen durch Sicherheitssoftware. Nach Anwendung dieser Schritte sollte der Upload funktionieren.</p><p></p><p>3. Marketing Campaign Ideas (noise)</p><p></p><p><em>Q3 Marketing Campaign Ideas</em></p><ol><li><p>Social media contest: "Share Your Productivity Hack"</p><ul><li><p>Users share tips using our software, best entry wins a premium subscription.</p></li></ul></li></ol><p></p><ol><li><p>Webinar series: "Mastering Project Management"</p><ul><li><p>Invite industry experts to share insights using our tool.</p></li></ul></li></ol><p></p><ol><li><p>Email campaign: "Unlock Hidden Features"</p><ul><li><p>Series of emails highlighting lesser-known but powerful features.</p></li></ul></li></ol><p></p><ol><li><p>Partner with a productivity podcast for sponsored content.</p></li></ol><p></p><ol><li><p>Create a "Project Management Memes" social media account for lighter, shareable content.</p></li></ol><p></p><p>4. Mitarbeiter des Monats (noise)</p><p></p><p><em>Mitarbeiter des Monats: Juli 2023</em></p><p>Wir freuen uns, bekannt zu geben, dass Sarah Schmidt zur Mitarbeiterin des Monats Juli gewählt wurde!</p><p>Sarah hat außergewöhnliche Leistungen in folgenden Bereichen gezeigt:</p><ul><li><p>Kundenbetreuung: Sarah hat durchschnittlich 95% positive Bewertungen erhalten.</p></li></ul><p></p><ul><li><p>Teamarbeit: Sie hat maßgeblich zur Verbesserung unseres internen Wissensmanagementsystems beigetragen.</p></li></ul><p></p><ul><li><p>Innovation: Sarah hat eine neue Methode zur Priorisierung von Support-Tickets vorgeschlagen, die unsere Reaktionszeiten um 20% verbessert hat.</p></li></ul><p>Bitte gratulieren Sie Sarah zu dieser wohlverdienten Anerkennung!</p><p>This is how a document will look like inside Elasticsearch:</p>{
    "took": 9,
    "timed_out": false,
    "_shards": {
        "total": 1,
        "successful": 1,
        "skipped": 0,
        "failed": 0
    },
    "hits": {
        "total": {
            "value": 2,
            "relation": "eq"
        },
        "max_score": 0.9155389,
        "hits": [
            {
                "_index": "multilingual-mistral",
                "_id": "1",
                "_score": 0.9155389,
                "_source": {
                    "super_body": {
                        "text": "\n        _Support Ticket #EN1234_\n        **Subject**: Calendar sync not working with Google Calendar\n\n        **Description**:\n        I'm having trouble syncing my project deadlines with Google Calendar. Whenever I try to sync, I get an error message saying \"Unable to connect to external calendar service.\"\n\n        **Resolution**:\n        The issue was resolved by following these steps:\n        1. Go to Settings &gt; Integrations\n        2. Disconnect the Google Calendar integration\n        3. Clear browser cache and cookies\n        4. Reconnect the Google Calendar integration\n        5. Authorize the app again in Google's security settings\n\n        The sync should now work correctly. If problems persist, ensure that third-party cookies are enabled in your browser settings.\n    ",
                        "inference": {
                            "inference_id": "multilingual-embeddings",
                            "model_settings": {
                                "task_type": "text_embedding",
                                "dimensions": 384,
                                "similarity": "cosine",
                                "element_type": "float"
                            },
                            "chunks": [
                                {
                                    "text": "passage: \n        _Support Ticket #EN1234_\n        **Subject**: Calendar sync not working with Google Calendar\n\n        **Description**:\n        I'm having trouble syncing my project deadlines with Google Calendar. Whenever I try to sync, I get an error message saying \"Unable to connect to external calendar service.\"\n\n        **Resolution**:\n        The issue was resolved by following these steps:\n        1. Go to Settings &gt; Integrations\n        2. Disconnect the Google Calendar integration\n        3. Clear browser cache and cookies\n        4. Reconnect the Google Calendar integration\n        5. Authorize the app again in Google's security settings\n\n        The sync should now work correctly. If problems persist, ensure that third-party cookies are enabled in your browser settings.",
                                    "embeddings": [
                                        0.0059651174,
                                        0.0016363655,
                                        -0.064753555,
                                        0.0093298275,
                                        0.05689768,
                                        -0.049640983,
                                        0.02504726,
                                        0.0048340675,
                                        0.08093895,
                                        ...
                                    ]
                                }
                            ]
                        }
                    }
                }
            }
        ]
    }
}
<h2>Asking questions</h2><p>Now, we are going to ask a question in Spanish:</p>Hola, estoy teniendo problemas para ocupar su aplicación, estoy teniendo problemas para sincronizar mi calendario, y encima al intentar subir un archivo me da error.<p>The expectation is retrieving documents #1 and #2, then sending them to the LLM as additional context, and finally, getting an answer in Spanish.</p><h4>Retrieving documents</h4><p>To retrieve the relevant documents, we can use this nice and short query that will run a search on the embeddings, and return the support tickets most relevant to the question.</p>GET multilingual-mistral/_search
{
   "size": 2,
   "_source": {
    "excludes": ["*embeddings", "*chunks"]
   },
  "query": {
    "semantic": {
      "field": "super_body",
      "query": "Hola, estoy teniendo problemas para ocupar su aplicación, estoy teniendo problemas para sincronizar mi calendario, y encima al intentar subir un archivo me da error."
    }
  }
}
<p><em>Notes about the parameters set:</em> <code>size: 2</code> Because we know we want the top 2 documents. <code>excludes</code> For clarity in the response. Documents are short so each one will be one chunk long.</p><h4>Answering the question</h4><p>Now we can call the Mistral completion API using the Python library to answer the question.</p>from mistralai.client import MistralClient
from mistralai.models.chat_completion import ChatMessage

api_key = os.environ["MISTRAL_API_KEY"]
model = "open-mixtral-8x22b"

client = MistralClient(api_key=api_key)

system_message = "You are a helpful multilingual agent that helps users with their problems. You have access to a knowledge base of different languages and you must answer in the same language the question was asked."

user_message = """
## Question:

Hola, estoy teniendo problemas para ocupar su aplicación, estoy teniendo problemas para sincronizar mi calendario, y encima al intentar subir un archivo me da error. 

## Related knowledge:

Support Ticket #EN1234 Subject: Calendar sync not working with Google Calendar...
(the rest of the content of the document)

\n

Support-Ticket #DE5678 Betreff: Datei-Upload funktioniert nicht... 
(the rest of the content of the document)

ANSWER:

"""

messages = [
    ChatMessage(role="system", content=system_message),
    ChatMessage(role="user", content=user_message)
]

chat_response = client.chat(
    model=model,
    messages=messages,
)

print(chat_response.choices[0].message.content) 
<p>The answer is in perfect Spanish and on point!</p><p></p><p>Show answer</p><p></p><p>Hola, me alegra que te hayas comunicado con nosotros. Parece que hay dos problemas distintos.</p><p>En cuanto a la sincronización del calendario, puedes seguir estos pasos para resolver el problema:</p><ol><li><p>Ve a Configuración &gt; Integraciones</p></li></ol><p></p><ol><li><p>Desconecta la integración del Calendario de Google</p></li></ol><p></p><ol><li><p>Borra la caché y las cookies del navegador</p></li></ol><p></p><ol><li><p>Vuelve a conectar la integración del Calendario de Google</p></li></ol><p></p><ol><li><p>Autoriza de nuevo la aplicación en la configuración de seguridad de Google</p></li></ol><p>Si sigues teniendo problemas, asegúrate de que las cookies de terceros están habilitadas en la configuración de tu navegador.</p><p>En cuanto al problema de subir un archivo, hay varias cosas que puedes probar:</p><ol><li><p>Comprueba el tamaño del archivo. El tamaño máximo de carga es de 100 MB.</p></li></ol><p></p><ol><li><p>Desactiva temporalmente el antivirus o el cortafuegos.</p></li></ol><p></p><ol><li><p>Intenta cargar el archivo en modo incógnito.</p></li></ol><p></p><ol><li><p>Si eso no funciona, borra la caché y las cookies del navegador.</p></li></ol><p></p><ol><li><p>Como último recurso, prueba a usar un navegador diferente.</p></li></ol><p>En la mayoría de los casos, el problema se debe a archivos demasiado grandes o a interferencias causadas por software de seguridad. Al seguir estos pasos, deberías poder cargar el archivo correctamente.</p><p>¡Espero que esto te ayude a resolver tus problemas! Si tienes alguna otra pregunta, no dudes en preguntar.</p><h2>Conclusion</h2><p>Mixtral 8x22B is a powerful model that enables us to leverage data sources in different languages, being able to answer, understand, and translate in many languages. This ability– together with multilingual embeddings– allows you to have multilingual support both in the data retrieval and the answer generation stages, removing language barriers entirely.</p><p><em>If you are interested on reproducing the examples of this article, you can find the Python Notebook with the requests </em><a href="https://github.com/elastic/elasticsearch-labs/blob/main/supporting-blog-content/building-multilingual-rag-with-elastic-and-mistral/building_multilingual_rag_with_elastic_and_mistral.ipynb"><em>here</em></a></p>]]></content:encoded>
    <link>https://www.elastic.co/search-labs/blog/building-multilingual-rag-with-elastic-and-mistral</link>
    <guid isPermaLink="true">https://www.elastic.co/search-labs/blog/building-multilingual-rag-with-elastic-and-mistral</guid>
    <category><![CDATA[AI]]></category>
    <category><![CDATA[Vector Database]]></category>
    <category><![CDATA[Python]]></category>
    <dc:creator><![CDATA[Gustavo Llermaly]]></dc:creator>
    <enclosure url="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt9cf558f36ced44dc/6a17117dd7c022520cde65a2/7dd63f367670175590e30927ef432ff93e166c84-1440x809.png" length="0" type="image/png"/>
    <pubDate>Fri, 02 Aug 2024 00:00:00 GMT</pubDate>
  </item>
  <item>
    <title><![CDATA[The sparse vector query: Searching sparse vectors with inference or precomputed query vectors]]></title>
    <description><![CDATA[Learn about the Elasticsearch sparse vector query, how it works, and how to effectively use it.]]></description>
    <content:encoded><![CDATA[<p>Sparse vector queries take advantage of Elasticsearch’s powerful <a href="https://www.elastic.co/guide/en/elasticsearch/reference/current/inference-apis.html">inference API</a>, allowing easy built-in setup for Elastic-hosted models such as <a href="https://www.elastic.co/guide/en/elasticsearch/reference/current/semantic-search-elser.html">ELSER</a> and <a href="https://www.elastic.co/guide/en/machine-learning/current/ml-nlp-e5.html">E5</a>, as well as the <a href="https://www.elastic.co/guide/en/elasticsearch/reference/current/semantic-search-inference.html">flexibility</a> to host other models.</p><h2>Background</h2><p>Vector search is evolving, and as our needs for vector search evolve so does the need for a consistent and forward thinking vector search API.</p><p>When Elastic first launched semantic search, we leveraged existing <a href="https://www.elastic.co/guide/en/elasticsearch/reference/current/rank-features.html">rank_features</a> fields using the <a href="https://www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl-text-expansion-query.html">text_expansion</a> query. We then reintroduced the <a href="https://www.elastic.co/guide/en/elasticsearch/reference/current/sparse-vector.html">sparse_vector field type</a> for semantic search use cases.</p><p>As we think about what sparse vector search is going forward, we’ve introduced a new sparse vector query. As of Elasticsearch 8.15.0, both the text_expansion query and <a href="https://www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl-weighted-tokens-query.html">weighted_tokens</a> query have been deprecated in favor of the new sparse vector query.</p><p>The sparse vector query supports two modes of querying: using an inference ID and using precomputed query vectors. Both modes of querying require data to be indexed in a sparse_vector mapped field.</p><p>These token-weight pairs are then used in a query against a sparse vector. At query time, query vectors are calculated using the same inference model that was used to create the tokens.</p><p>Let’s look at an example: let’s say we’ve indexed a document detailing when Orion is most visible in the night sky:</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/bltecb4be95e0c08699/6a17d72225daabcc8908a09a/8c7b9a7fa9db92e2e8067d5c84854940bf037062-1440x320.png" alt="Indexing sparse vectors encoding example" /><p>Now, assume we’re looking for constellations that are visible in the northern hemisphere, and we run this query through the same learned sparse encoder model. The output might look similar to this:</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt3683b4b6d0d85683/6a17d724505ac32a60ad8950/54561b412a1c1f57157290a339dfe387c2f95dad-1440x424.png" alt="Searching sparse vectors encoding example" /><p>At query time, these vectors are ORed together, and scoring is effectively a <a href="https://en.wikipedia.org/wiki/Dot_product">dot product</a> calculation between the stored dimensions and the query dimensions, which would score this example at 10.84:</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt03108313e79d6e7d/6a17d725a29299397cd02ae1/53baae570e66ebc66f0fa47b406436e2c22b3162-816x452.png" alt="Dot product scoring example" /><h2>Sparse vector queries with inference</h2><p>Sparse vector queries using inference work in a very similar way to the previous text expansion query, instead of sending in a trained model, we <a href="https://www.elastic.co/guide/en/elasticsearch/reference/current/put-inference-api.html">create an inference endpoint</a> associated with the model we want to use.</p><p>Here’s an example of how to create an inference endpoint for ELSER:</p>PUT _inference/sparse_embedding/my-elser-endpoint
{
  "service": "elser",
  "service_settings": {
    "num_allocations": 1,
    "num_threads": 1
  }
}
<p>You should use an inference endpoint to index your sparse vector data, and use the same endpoint as input to your sparse_vector query. For example:</p>POST my-index/_search
{
  "query": {
    "sparse_vector": {
      "field": "embeddings",
      "inference_id": "my-elser-endpoint",
      "query": "constellations in the northern hemisphere"
    }
  }
}
<h2>Sparse vector queries with precomputed query vectors</h2><p>You may have precomputed vectors that don’t require inference at query time. These can be sent into the sparse_vector query instead of using inference. Here is an example:</p>POST my-index/_search
{
  "query": {
    "sparse_vector": {
      "field": "embeddings",
      "query_vector": {
        "constellation": 2.5,
        "northern": 1.9,
        "hemisphere": 1.8,
        "orion": 1.5,
        "galaxy": 1.4,
        "astronomy": 0.9,
        "telescope": 0.3,
        "star": 0.01
      }
    }
  }
}
<h2>Query optimization with token pruning</h2><p>Like text expansion search, the sparse vector query is subject to performance penalties from huge boolean queries. Therefore the same <a href="https://www.elastic.co/search-labs/blog/text-expansion-pruning">token pruning strategies</a> available for text expansion strategies are available in the sparse vector query. You can see the impact of token pruning in our <a href="https://elasticsearch-benchmarks.elastic.co/index.html#tracks/msmarco-passage-ranking/nightly/default/90d">nightly MS Marco Passage Ranking benchmarks</a>.</p><p>In order to enable pruning with the default pruning configuration (which has been tuned for ELSER V2), simply add <code>prune: true</code> to your request:</p>POST my-index/_search
{
  "query": {
    "sparse_vector": {
      "field": "embeddings",
      "inference_id": "my-elser-endpoint",
      "query": "constellations in the northern hemisphere",
      "prune": true
    }
  }
}
<p>Alternately, you can adjust the pruning configuration by sending it directly in with the request:</p>GET my-index/_search
{
   "query":{
      "sparse_vector":{
         "field": "embeddings",
         "inference_id": "my-elser-endpoint",
         "query": "constellations in the northern hemisphere",
         "prune": true,
         "pruning_config": {
           "tokens_freq_ratio_threshold": 5,
           "tokens_weight_threshold": 0.4,
           "only_score_pruned_tokens": false
         }
      }
   }
}
<p>Because token pruning will incur a recall penalty, we recommend adding the pruned tokens back in a rescore:</p>GET my-index/_search
{
   "query":{
      "sparse_vector":{
         "field": "embeddings",
         "inference_id": "my-elser-endpoint",
         "query": "constellations in the northern hemisphere",
         "prune": true,
         "pruning_config": {
           "tokens_freq_ratio_threshold": 5,
           "tokens_weight_threshold": 0.4,
           "only_score_pruned_tokens": false
         }
      }
   },
   "rescore": {
      "window_size": 100,
      "query": {
         "rescore_query": {
            "sparse_vector": {
               "field": "embeddings",
               "inference_id": "my-elser-endpoint",
               "query": "constellations in the northern hemisphere",
               "prune": true,
               "pruning_config": {
                   "tokens_freq_ratio_threshold": 5,
                   "tokens_weight_threshold": 0.4,
                   "only_score_pruned_tokens": true
               }
            }
         }
      }
   }
}
<h2>What's next?</h2><p>While the <code>text_expansion</code> query is GA’d and will be supported throughout Elasticsearch 8.x, we recommend updating to the <code>sparse_vector</code> query as soon as possible in order to ensure you’re using the most up to date features as we continually improve the vector search experience in Elasticsearch.</p><p>If you are using the <code>weighted_tokens</code> query, this was never GA’d and will be replaced by the sparse_vector query very soon.</p><p>The <code>sparse_vector</code> query will be available starting with 8.15.0 and is already available in Serverless - try it out today!</p>]]></content:encoded>
    <link>https://www.elastic.co/search-labs/blog/elasticsearch-sparse-vector-query</link>
    <guid isPermaLink="true">https://www.elastic.co/search-labs/blog/elasticsearch-sparse-vector-query</guid>
    <category><![CDATA[Vector Database]]></category>
    <dc:creator><![CDATA[Kathleen DeRusso]]></dc:creator>
    <enclosure url="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt03108313e79d6e7d/6a17d725a29299397cd02ae1/53baae570e66ebc66f0fa47b406436e2c22b3162-816x452.png" length="0" type="image/png"/>
    <pubDate>Tue, 23 Jul 2024 00:00:00 GMT</pubDate>
  </item>
  <item>
    <title><![CDATA[Elasticsearch vs. OpenSearch: Vector Search Performance Comparison]]></title>
    <description><![CDATA[Elasticsearch is out-of-the-box 2x–12x faster than OpenSearch for vector search]]></description>
    <content:encoded><![CDATA[<p><a href="https://www.elastic.co/search-labs/blog/elasticsearch-opensearch-vector-search-performance-comparison#up-to-12x-faster-out-of-the-box">TLDR: Elasticsearch is up to 12x faster</a> - We at Elastic have received numerous requests from our community to clarify the performance differences between Elasticsearch and OpenSearch, particularly in the realm of Semantic Search / Vector Search, so we have undertaken this performance testing to provide a clear, data-driven comparison — no ambiguity, just straightforward facts to inform our users. The results show that <strong>Elasticsearch is up to 12x faster</strong> than OpenSearch for vector search and therefore requires fewer computational resources. This reflects Elastic's focus on consolidating Lucene as the best vector database for search and retrieval use cases.</p><p>Vector search is revolutionizing the way we conduct similarity searches, particularly in fields like AI and machine learning. With the increasing adoption of vector embedding models, the ability to efficiently search through millions of high-dimension vectors becomes critical.</p><p>When it comes to powering vector databases, Elastic and OpenSearch have taken notably different approaches. Elastic has invested heavily in optimizing Apache Lucene together with Elasticsearch to elevate them as the top-tier choice for vector search applications. In contrast, OpenSearch has broadened its focus, integrating other vector search implementations and exploring beyond Lucene's scope. Our focus on Lucene is strategic, enabling us to provide highly integrated support in our version of Elasticsearch, resulting in an enhanced feature set where each component complements and amplifies the capabilities of the other.</p><p>This blog presents a detailed comparison between Elasticsearch 8.14 and OpenSearch 2.14 accounting for different configurations and vector engines. In this performance analysis, Elasticsearch proved to be the superior platform for vector search operations, and upcoming <a href="https://www.elastic.co/search-labs/blog/vector-similarity-computations-ludicrous-speed">features</a> will widen the differences even more <a href="https://www.elastic.co/search-labs/blog/elasticsearch-lucene-vector-database-gains">significantly</a>. When pitted against OpenSearch, it excelled in every benchmark track — <strong>offering 2x to 12x faster performance on average</strong>. This was across scenarios using varying vector amounts and dimensions including <code>so_vector</code> (2M vectors, 768D), <code>openai_vector</code> (2.5M vectors, 1536D), and <code>dense_vector</code> (10M vectors, 96D), all available in <a href="https://github.com/elastic/elasticsearch-opensearch-vector-performance">this repository</a> alongside the Terraform scripts for provisioning all the required infrastructure on Google Cloud and Kubernetes manifests for running the tests.</p><p>The results detailed in this blog complement the results from a <a href="https://www.elastic.co/blog/elasticsearch-opensearch-performance-gap">previously published and third-party validated study</a> that shows Elasticsearch is 40%–140% faster than OpenSearch for the most common search analytics operations: Text Querying, Sort, Range, Date Histogram and Terms filtering. Now we can add another differentiator: Vector Search.</p><h2>Up to 12x faster out-of-the-box</h2><p>Our focused benchmarks across the four vector data sets involved both Approximate KNN and Exact KNN searches, considering different sizes, dimensions and configurations, totaling <code>40.189.820</code> uncached search requests. The results: <strong>Elasticsearch is up to 12x faster</strong> than OpenSearch for vector search and therefore requires fewer computational resources.</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt34b83c6eba3bcb6e/6a17d727dbb4ff18d6fb54fb/cdb26e91f085b90e9b12aeb8fee53b04d365ecae-1440x1156.webp" alt="p90 average" /><p>Figure 1: Grouped tasks for ANN and Exact KNN across different combinations in Elasticsearch and OpenSearch.</p><p>The groups like <code>knn-10-100</code> means KNN search with  and . In HNSW vector search,  determines the number of nearest neighbors to retrieve for a query vector. It specifies how many similar vectors to find as a result.  sets the number of candidate vectors to retrieve at each segment. More candidates can enhance accuracy but require greater computational resources.</p><p>We also tested with different quantization techniques and leveraged engine-specific optimizations, the detailed results for each track, task and vector engine are available below.</p><h2>Exact KNN and Approximate KNN</h2><p>When dealing with varying data sets and use cases, the right approach for vector search will differ. In this blog all tasks stated as <code>knn-*</code> like <code>knn-10-100</code> use <strong>Approximate KNN</strong> and <code>script-score-*</code> refer to <strong>Exact KNN</strong>, but what is the difference between them, and why are they important?</p><p>In essence, if you're handling more substantial data sets, the preferred method is the Approximate K-Nearest Neighbor (ANN) due to its superior scalability. For more modest data sets that may require a filtration process, Exact KNN method is ideal.</p><p>Exact KNN uses a brute-force method, calculating the distance between one vector and every other vector in the data set. It then ranks these distances to find the  nearest neighbors. While this method ensures an exact match, it suffers from scalability challenges for large, high-dimensional data sets. However, there are many cases in which Exact KNN is needed:</p><ul><li><p><strong>Rescoring</strong>: In scenarios involving lexical or semantic searches followed by vector-based rescoring, Exact KNN is essential. For example, in a product search engine, initial search results can be filtered based on textual queries (e.g., keywords, categories), and then vectors associated with the filtered items are used for a more accurate similarity assessment.</p></li><li><p><strong>Personalization</strong>: When dealing with a large number of users, each represented by a relatively small number (like 1 million) of distinct vectors, sorting the index by user-specific metadata (e.g., user_id) and brute-force scoring with vectors becomes efficient. This approach allows for personalized recommendations or content delivery based on precise vector comparisons tailored to individual user preferences.</p></li></ul><p>Exact KNN therefore ensures that the final ranking and recommendations based on vector similarity are precise and tailored to user preferences.</p><p>Approximate KNN (or ANN) on the other hand employs methods to make data searching faster and more efficient than Exact KNN, especially in large, high-dimensional data sets. Instead of a brute-force approach, which measures the exact nearest distance between a query and all points leading to computation and scaling challenges, ANN uses certain techniques to efficiently restructure the indexes and dimensions of searchable vectors in the data set. While this may cause a slight inaccuracy, it significantly boosts the speed of the search process, making it an effective alternative for dealing with large data sets.</p><p>In this blog all tasks stated as <code>knn-*</code> like <code>knn-10-100</code> use <strong>Approximate KNN</strong> and <code>script-score-*</code> refer to <strong>Exact KNN</strong>.</p><h2>Testing methodology</h2><p>While Elasticsearch and OpenSearch are similar in terms of API for BM25 search operations, since the latter is a fork of the former, it is not the case for Vector Search, which was introduced after the fork. OpenSearch took a different approach than Elasticsearch when it comes to algorithms, by introducing two other engines — <code>nmslib</code> and <code>faiss</code> — apart from <code>lucene</code>, each with their specific configurations and limitations (e.g., <code>nmslib</code> in OpenSearch does not allow for filters, an essential feature for many use cases).</p><p>All three engines use the Hierarchical Navigable Small World (HNSW) algorithm, which is efficient for approximate nearest neighbor search, and especially powerful when dealing with high-dimensional data. It's important to note that <code>faiss</code> also supports a second algorithm, <code>ivf</code>, but since it requires pre-training on the data set, we are going to focus solely on HNSW. The core idea of HNSW is to organize the data into multiple layers of connected graphs, with each layer representing a different granularity of the data set. The search begins at the top layer with the coarsest view and progresses down to finer and finer layers until reaching the base level.</p><p>Both search engines were tested under identical conditions in a controlled environment to ensure fair testing grounds. The method applied is similar to <a href="https://www.elastic.co/blog/elasticsearch-opensearch-performance-gap#testing-methodology">this previously published performance comparison</a>, with dedicated node pools for Elasticsearch, OpenSearch, and Rally. The <a href="https://github.com/elastic/elasticsearch-opensearch-vector-performance/blob/main/terraform/main.tf">terraform script</a> is available (alongside all sources) to provision a Kubernetes cluster with:</p><ul><li><p>1 Node pool for Elasticsearch with 3 <code>e2-standard-32</code> machines (128GB RAM and 32 CPUs)</p></li><li><p>1 Node pool for OpenSearch with 3 <code>e2-standard-32</code> machines (128GB RAM and 32 CPUs)</p></li><li><p>1 Node pool for Rally with 2 <code>t2a-standard-16</code> machines (64GB RAM and 16 CPUs)</p></li></ul><p>Each "track" (or test) ran for 10 times for each configuration, which included different engines, different configurations and different vector types. The tracks have tasks that repeat between 1000 and 10000 times, depending on the track. If one of the tasks in a track failed for instance due to a network timeout, then all tasks were discarded, so all results represent tracks that started and finished without problems. All test results are statistically validated, ensuring that improvements aren’t coincidental.</p><h2>Detailed findings</h2><p>Why compare using the 99th percentile and not the average latency? Consider a hypothetical example of average house prices in a certain neighborhood. The average price may indicate an expensive area, but on closer inspection, it may turn out that most homes are valued much lower, with only a few luxury properties inflating the average figure. This illustrates how the average price can fail to accurately represent the full spectrum of house values in the area. This is akin to examining response times, where the average can conceal critical issues.</p><h4>Tasks</h4><ul><li><p>Approximate KNN with k:10 n:50</p></li><li><p>Approximate KNN with k:10 n:100</p></li><li><p>Approximate KNN with k:100 n:1000</p></li><li><p>Approximate KNN with k:10 n:50 and keyword filters</p></li><li><p>Approximate KNN with k:10 n:100 and keyword filters</p></li><li><p>Approximate KNN with k:100 n:1000 and keyword filters</p></li><li><p>Approximate KNN with k:10 n:100 in conjunction with indexing</p></li><li><p>Exact KNN (script score)</p></li></ul><h4>Vector engines</h4><ul><li><p><code>lucene</code> in Elasticsearch and OpenSearch, both on version 9.10</p></li><li><p><code>faiss</code> in OpenSearch</p></li><li><p><code>nmslib</code> in OpenSearch</p></li></ul><h4>Vector types</h4><ul><li><p><code>hnsw</code> in Elasticsearch and OpenSearch</p></li><li><p><code>int8_hnsw</code> in Elasticsearch (HNSW with automatic 8 bit quantization: <a href="https://www.elastic.co/search-labs/blog/evaluating-scalar-quantization">link</a>)</p></li><li><p><code>sq_fp16 hnsw </code>in OpenSearch (HNSW with automatic 16 bit quantization: <a href="https://opensearch.org/docs/2.14/search-plugins/knn/knn-vector-quantization#faiss-16-bit-scalar-quantization">link</a>)</p></li></ul><h4>Out-of-the-box and Concurrent Segment Search</h4><p>As you probably know, Lucene is a highly performant text search engine library written in Java that serves as the backbone for many search platforms like Elasticsearch, OpenSearch, and Solr. At its core, Lucene organizes data into segments, which are essentially self-contained indices that allow Lucene to execute searches more efficiently. So when you issue a search to any Lucene-based search engine, your search will end up being executed in those segments, either sequentially or in parallel.</p><p>OpenSearch introduced concurrent segment search as an optional flag, and does not use it by default, you must enable it using a special index setting <code>index.search.concurrent_segment_search.enabled</code> as detailed <a href="https://opensearch.org/docs/latest/search-plugins/concurrent-segment-search/">here</a>, with some <a href="https://opensearch.org/docs/latest/search-plugins/concurrent-segment-search/#other-considerations">limitations</a>.</p><p>Elasticsearch on the other hand searches on segments concurrently <a href="https://github.com/elastic/elasticsearch/pull/101230">out-of-the-box</a>, therefore the comparisons we make in this blog will take into consideration, on top of the different vector engines and vector types, also the different configurations:</p><ul><li><p>Elasticsearch ootb: Elasticsearch out-of-the-box, with concurrent segment search;</p></li><li><p>OpenSearch ootb: without concurrent segment search enabled;</p></li><li><p>OpenSearch css: with concurrent segment search enabled</p></li></ul><p>Now, let’s dive into some detailed results for each vector data set tested:</p><h2>2.5 million vectors, 1536 dimensions (openai_vector)</h2><p>Starting with the simplest track, but also the largest in terms of dimensions, <a href="https://github.com/elastic/rally-tracks/edit/master/openai_vector">openai_vector</a> - which uses the <a href="https://huggingface.co/datasets/BeIR/nq">NQ data set</a> enriched with embeddings generated using OpenAI's <a href="https://openai.com/blog/new-and-improved-embedding-model">text-embedding-ada-002 model</a>. It is the simplest since it tests only Approximate KNN and has only 5 tasks. It tests in standalone (without indexing) as well as alongside indexing, and using a single client and 8 simultaneous clients.</p><h3>Tasks</h3><ul><li><p><strong>standalone-search-knn-10-100-multiple-clients</strong>: searching on 2.5 million vectors with 8 clients simultaneously, k: 10 and n:100</p></li><li><p><strong>standalone-search-knn-100-1000-multiple-clients</strong>: searching on 2.5 million vectors with 8 clients simultaneously, k: 100 and n:1000</p></li><li><p><strong>standalone-search-knn-10-100-single-client</strong>: searching on 2.5 million vectors with a single client, k: 10 and n:100</p></li><li><p><strong>standalone-search-knn-100-1000-single-client</strong>: searching on 2.5 million vectors with a single client, k: 100 and n:1000</p></li><li><p><strong>parallel-documents-indexing-search-knn-10-100</strong>: searching on 2.5 million vectors while also indexing additional 100000 documents, k:10 and n:100</p></li></ul><p>The averaged p99 performance is outlined below:</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/bltf5791848eb0c12fb/6a17d72925daab9cae08a09e/eea0b2b49c690baada3e09d6968e513bfffe51a9-1440x318.webp" alt="openai_vector table" /><p>Here we observed that Elasticsearch is between <strong>3x-8x faster</strong> than OpenSearch when performing vector search alongside indexing (i.e. read+write) with :10 and :100 and <strong>2x-3x faster</strong> without indexing for the same k and n. For :100 and :1000 (<em>standalone-search-knn-100-1000-single-client</em> and <em>standalone-search-knn-100-1000-multiple-clients</em> Elasticsearch is <strong>2x to 7x</strong> faster than OpenSearch, on average.</p><p>The detailed results show the exact cases and vector engines compared:</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/bltdbe41d3187ced7ec/6a17d72a445de951c44cff4c/a7a761ed631d3e6211beb83d9d93d752d10123c9-1440x1728.webp" alt="openai_vector" /><h4>Recall</h4><p></p><p>knn-recall-10-100</p><p>knn-recall-100-1000</p><p>Elasticsearch-8.14.0@lucene-hnsw</p><p>0.969485</p><p>0.995138</p><p>Elasticsearch-8.14.0@lucene-int8_hnsw</p><p>0.781445</p><p>0.784817</p><p>OpenSearch-2.14.0@lucene-hnsw</p><p>0.96519</p><p>0.995422</p><p>OpenSearch-2.14.0@faiss</p><p>0.984154</p><p>0.98049</p><p>OpenSearch-2.14.0@faiss-sq_fp16</p><p>0.980012</p><p>0.97721</p><p>OpenSearch-2.14.0@nmslib</p><p>0.982532</p><p>0.99832</p><h2>10 million vectors, 96 dimensions (dense_vector)</h2><p>In <a href="https://github.com/elastic/rally-tracks/tree/master/dense_vector">dense_vector</a> with 10M vectors and 96 dimensions. It is based on the <a href="https://big-ann-benchmarks.com/">Yandex DEEP1B</a> image data set. The data set is created from the first 10 million vectors of the "sample data" file called <code>learn.350M.fbin</code>. The search operations use vectors from the "query data" file query.<code>public.10K.fbin</code>.</p><p>Both Elasticsearch and OpenSearch perform very well on this data set, especially after a <a href="https://www.elastic.co/guide/en/elasticsearch/reference/current/indices-forcemerge.html">force merge</a> which is usually done on read-only indices and it’s similar to defragmenting the index to have a single "table" to search on.</p><h3>Tasks</h3><p>Each task warms up for 100 requests and then 1000 requests are measured</p><ul><li><p><strong>knn-search-10-100</strong>: searching on 10 million vectors, k: 10 and n:100</p></li><li><p><strong>knn-search-100-1000</strong>: searching on 10 million vectors, k: 100 and n:1000</p></li><li><p><strong>knn-search-10-100-force-merge</strong>: searching on 10 million vectors after a force merge, k: 10 and n:100</p></li><li><p><strong>knn-search-100-1000-force-merge</strong>: searching on 10 million vectors after a force merge, k: 100 and n:1000</p></li><li><p><strong>knn-search-100-1000-concurrent-with-indexing</strong>: searching on 10 million vectors while also updating <a href="https://github.com/elastic/rally-tracks/blob/master/dense_vector/challenges/default.json#L76C36-L76C37">5% of the data set</a>, k: 100 and n:1000</p></li><li><p><strong>script-score-query</strong>: Exact KNN search of <a href="https://github.com/elastic/rally-tracks/blob/master/dense_vector/queries.json">2000 specific vectors</a>.</p></li></ul><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt4629d06af85eb96c/6a17d72c6864a423e7b685dc/174995e0a2156d86359cdb7aa446dfaae6312ea4-1440x316.webp" alt="dense_vector" /><p>Both Elasticsearch and OpenSearch performed well for Approximate KNN. When the index is merged (i.e. has just a single segment) in <em>knn-search-100-1000-force-merge</em> and <em>knn-search-10-100-force-merge</em>, OpenSearch performs better than the others when using <code>nmslib</code> and <code>faiss</code>, even though they are all around 15ms and all very close.</p><p>However, when the index has multiple segments (a typical situation where an index receives updates to its documents) in <em>knn-search-10-100</em> and <em>knn-search-100-1000</em>, Elasticsearch keeps the latency in about ~7ms and ~16ms, while all other OpenSearch engines are slower.</p><p>Also when the index is being searched and written to at the same time (<em>knn-search-100-1000-concurrent-with-indexing</em>), Elasticsearch maintains the latency below 15ms (at 13.8ms), being almost <strong>4x faster</strong> than OpenSearch out-of-the-box (49.3ms) and still faster when concurrent segment search is enabled (17.9ms), but too close to be significative.</p><p>As for Exact KNN, the difference is much larger: Elasticsearch <strong>is 6x faster</strong> than OpenSearch (~260ms vs ~1600ms).</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt254f43bcaa3dbfc2/6a17d72ddbb4ffc780fb54ff/17aec6be31117440bc4d1f99984aed95df1c4f6b-1440x1728.webp" alt="dense_vector" /><h4>Recall</h4><p></p><p>knn-recall-10-100</p><p>knn-recall-100-1000</p><p>Elasticsearch-8.14.0@lucene-hnsw</p><p>0.969843</p><p>0.996577</p><p>Elasticsearch-8.14.0@lucene-int8_hnsw</p><p>0.775458</p><p>0.840254</p><p>OpenSearch-2.14.0@lucene-hnsw</p><p>0.971333</p><p>0.996747</p><p>OpenSearch-2.14.0@faiss</p><p>0.9704</p><p>0.914755</p><p>OpenSearch-2.14.0@faiss-sq_fp16</p><p>0.968025</p><p>0.913862</p><p>OpenSearch-2.14.0@nmslib</p><p>0.9674</p><p>0.910303</p><h2>2 million vectors, 768 dimensions (so_vector)</h2><p>This <a href="https://github.com/elastic/rally-tracks/tree/master/so_vector">track</a>, <code>so_vector</code>, is derived from a <a href="https://archive.org/download/stackexchange/stackoverflow.com-Posts.7z">dump of StackOverflow posts downloaded</a> on April, 21st 2022. It only contains question documents — all documents representing answers have been removed. Each question title was encoded into a vector using the sentence transformer model <a href="https://huggingface.co/sentence-transformers/multi-qa-mpnet-base-cos-v1">multi-qa-mpnet-base-cos-v1</a>. This data set contains the first 2 million questions.</p><p>Unlike the previous track, each document here contains other fields besides vectors to support testing features like Approximate KNN with filtering and hybrid search. <code>nmslib</code> for OpenSearch is notably absent in this test since <a href="https://opensearch.org/docs/latest/search-plugins/knn/filter-search-knn/#k-nn-search-with-filters">it does not support filters</a>.</p><h3>Tasks</h3><p>Each task warms up for 100 requests and then 100 requests are measured. Note the tasks were grouped for sake of simplicity, since the test contains 16 search types * 2 different k values * 3 different n values.</p><ul><li><p><strong>knn-10-50</strong>: searching on 2 million vectors without filters, k:10 and n:50</p></li><li><p><strong>knn-10-50-filtered</strong>: searching on 2 million vectors <a href="https://github.com/elastic/rally-tracks/blob/master/so_vector/operations/default.json">with filters</a>, k:10 and n:50</p></li><li><p><strong>knn-10-50-after-force-merge</strong>: searching on 2 million vectors with filters and after a force merge, k:10 and n:50</p></li><li><p><strong>knn-10-100</strong>: searching on 2 million vectors without filters, k:10 and n:100</p></li><li><p><strong>knn-10-100-filtered</strong>: searching on 2 million vectors <a href="https://github.com/elastic/rally-tracks/blob/master/so_vector/operations/default.json">with filters</a>, k:10 and n:100</p></li><li><p><strong>knn-10-100-after-force-merge</strong>: searching on 2 million vectors with filters and after a force merge, k:10 and n:100</p></li><li><p><strong>knn-100-1000</strong>: searching on 2 million vectors without filters, k:100 and n:1000</p></li><li><p><strong>knn-100-1000-filtered</strong>: searching on 2 million vectors <a href="https://github.com/elastic/rally-tracks/blob/master/so_vector/operations/default.json">with filters</a>, k:100 and n:1000</p></li><li><p><strong>knn-100-1000-after-force-merge</strong>: searching on 2 million vectors with filters and after a force merge, k:100 and n:1000</p></li><li><p><strong>exact-knn</strong>: Exact KNN search <a href="https://github.com/elastic/rally-tracks/blob/master/so_vector/operations/default.json#L56">with and without filters</a>.</p></li></ul><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt8b44e1306af35877/6a17d72f577262aca11bca3e/d4ed2982d55370cad4b2048b23ce97caa55017c0-1440x316.webp" alt="so_vector table" /><p>Elasticsearch is <strong>consistently faster</strong> than OpenSearch out-of-the-box on this test, only in two cases OpenSearch is faster, and not by much (<em>knn-10-100</em> and <em>knn-100-1000</em>). Tasks involving <em>knn-10-50</em>, <em>knn-10-100</em> and <em>knn-100-1000</em> in combination with filters show a difference of up to <strong>7x</strong> (112ms vs 803ms).</p><p>The performance of both solutions seems to even out after a "force merge", understandably, as evidenced by <em>knn-10-50-after-force-merge</em>, <em>knn-10-100-after-force-merge</em> and <em>knn-100-1000-after-force-merge.</em> On those tasks <code>faiss</code> is faster.</p><p>The performance for Exact KNN once again is very different, Elasticsearch being <strong>13 times faster</strong> than OpenSearch this time (~385ms vs ~5262ms).</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/bltf80ccc7c2df84559/6a17d7314b055de00d43203f/615cb9228eb05ddd2e9512b3a6a5bc88d4088a1a-1440x1440.webp" alt="so_vector" /><h4>Recall</h4><p></p><p>knn-recall-10-100</p><p>knn-recall-100-1000</p><p>knn-recall-10-50</p><p>Elasticsearch-8.14.0@lucene-hnsw</p><p>1</p><p>1</p><p>1</p><p>Elasticsearch-8.14.0@lucene-int8_hnsw</p><p>1</p><p>0.986667</p><p>1</p><p>OpenSearch-2.14.0@lucene-hnsw</p><p>1</p><p>1</p><p>1</p><p>OpenSearch-2.14.0@faiss</p><p>1</p><p>1</p><p>1</p><p>OpenSearch-2.14.0@faiss-sq_fp16</p><p>1</p><p>1</p><p>1</p><p>OpenSearch-2.14.0@nmslib</p><p>0.9674</p><p>0.910303</p><p>0.976394</p><h2>Elasticsearch and Lucene as clear victors</h2><p>At Elastic, we are relentlessly innovating Apache Lucene and Elasticsearch to ensure we are able to provide the premier vector database for search and retrieval use cases, including RAG (Retrieval Augmented Generation). Our recent advancements have dramatically increased performance, making vector search <a href="https://search-labs.elastic.co/search-labs/blog/elasticsearch-lucene-vector-database-gains">faster and more space efficient</a> than before, building upon the gains from Lucene 9.10. This blog presented a study that shows when comparing up-to-date versions Elasticsearch is up to 12 times faster than OpenSearch.</p><p>It's worth noting both products use the same version of Lucene (<a href="https://www.elastic.co/guide/en/elasticsearch/reference/current/release-notes-8.14.0.html">Elasticsearch 8.14 Release Notes</a> and <a href="https://github.com/opensearch-project/OpenSearch/blob/2.14/release-notes/opensearch.release-notes-2.14.0.md">OpenSearch 2.14 Release Notes</a>).</p><p>The pace of innovation at Elastic will deliver even more not only for our on-premises and Elastic Cloud customers but those using our <a href="https://www.elastic.co/search-labs/blog/stateless-your-new-state-of-find-with-elasticsearch">stateless platform</a>. Features like support for <a href="https://www.elastic.co/search-labs/blog/int4-scalar-quantization-in-lucene">scalar quantization to int4</a> will be offered with rigorous testing to ensure customers can utilize these techniques without a significant drop in recall, similar to <a href="https://www.elastic.co/search-labs/blog/evaluating-scalar-quantization">our testing for int8</a>.</p><p>Vector search efficiency is becoming a non-negotiable feature in modern search engines due to the proliferation of AI and machine learning applications. For organizations looking for a powerful search engine capable of keeping up with the demands of high-volume, high-complexity vector data, Elasticsearch is the definitive answer.</p><p>Whether expanding an established platform or initiating new projects, integrating Elasticsearch for vector search needs is a strategic move that will yield tangible, long-term benefits. With its proven performance advantage, Elasticsearch is poised to underpin the next wave of innovations in search.</p>]]></content:encoded>
    <link>https://www.elastic.co/search-labs/blog/elasticsearch-opensearch-vector-search-performance-comparison</link>
    <guid isPermaLink="true">https://www.elastic.co/search-labs/blog/elasticsearch-opensearch-vector-search-performance-comparison</guid>
    <category><![CDATA[Vector Database]]></category>
    <category><![CDATA[Lucene]]></category>
    <dc:creator><![CDATA[Ugo Sangiorgi]]></dc:creator>
    <enclosure url="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt5d70b25967c2194e/6a17d732b1e11383f879f0ca/13c3c0053e2968fb835ba2f90f34bec3a011b5c0-880x592.webp" length="0" type="image/webp"/>
    <pubDate>Wed, 26 Jun 2024 00:00:00 GMT</pubDate>
  </item>
  <item>
    <title><![CDATA[Elasticsearch new semantic_text mapping: Simplifying semantic search]]></title>
    <description><![CDATA[Learn how to use the new semantic_text field type and semantic query for simplifying semantic search in Elasticsearch.]]></description>
    <content:encoded><![CDATA[<h2>semantic_text - You know, for semantic search!</h2><p>Do you want to start using semantic search for your data, but focus on your model and results instead of on the technical details? We’ve introduced the <code>semantic_text</code> field type that will take care of the details and infrastructure that you need.</p><p><a href="https://www.elastic.co/what-is/semantic-search">Semantic search</a> is a sophisticated technique designed to enhance the relevance of search results by utilizing <a href="https://www.elastic.co/elasticsearch/machine-learning">machine learning models</a>. Unlike traditional keyword-based search, semantic search focuses on understanding the meaning of words and the context in which they are used. This is achieved through the application of machine learning models that provide a deeper semantic understanding of the text.</p><p>These models generate <a href="https://www.elastic.co/what-is/vector-embedding">vector embeddings</a>, which are numeric representations capturing the text meaning. These embeddings are stored alongside your document data, enabling <a href="https://www.elastic.co/what-is/vector-search">vector search techniques</a> that take into account the word meaning and context instead of pure lexical matches.</p><h2>How to perform semantic search</h2><p>To perform semantic search, you need to go through the following steps:</p><ul><li><p><a href="https://www.elastic.co/search-labs/blog/semantic-search-simplified-semantic-text#choosing-an-inference-model">Choose an inference mode</a>l to create embeddings, both for indexing documents and performing queries.</p></li><li><p><a href="https://www.elastic.co/search-labs/blog/semantic-search-simplified-semantic-text#creating-your-index-mapping">Create your index mapping</a> to store the inference results, so they can be efficiently searched afterwards.</p></li><li><p><a href="https://www.elastic.co/search-labs/blog/semantic-search-simplified-semantic-text#setting-up-indexing">Setting up indexing</a> so inference results are calculated for new documents added to your index.</p></li><li><p><a href="https://www.elastic.co/search-labs/blog/semantic-search-simplified-semantic-text#automatically-handling-long-text-passages">Automatically handle long text documents</a>, so search can be accurate and cover the entire document.</p></li><li><p><a href="https://www.elastic.co/search-labs/blog/semantic-search-simplified-semantic-text#querying-your-data">Querying your data</a> to retrieve results.</p></li></ul><p>Configuring semantic search from the ground up can be complex. It requires setting up mappings, ingestion pipelines, and queries tailored to your chosen inference model. Each step offers opportunities for fine-tuning and optimization, but also demands careful configuration to ensure all components work together seamlessly.</p><p>While this offers a great degree of control, it makes using semantic search a detailed and deliberate process, requiring you to configure separate pieces that are all related to each other and to the inference model.</p><p><a href="https://www.elastic.co/guide/en/elasticsearch/reference/current/semantic-text.html"><code>semantic_text</code></a> simplifies this process by focusing on what matters: the inference model. Once you have selected the inference model, <code>semantic_text</code> will make it easy to start using semantic search by providing sensible defaults, so you can focus on your search and not on how to index, generate, or query your embeddings.</p><p>Let's take a look at each of these steps, and how <code>semantic_text</code> simplifies this setup.</p><h3>Choosing an inference model</h3><p>The inference model will generate embeddings for your documents and queries. Different models have different tradeoffs in terms of:</p><ul><li><p>Accuracy and relevance of the results</p></li><li><p>Scalability and performance</p></li><li><p>Language and multilingual support</p></li><li><p>Cost</p></li></ul><p>Elasticsearch supports both internal and external inference services:</p><ul><li><p>Internal services are deployed in the Elasticsearch cluster. You can use already included models like <a href="https://www.elastic.co/guide/en/machine-learning/current/ml-nlp-elser.html">ELSER</a> and <a href="https://www.elastic.co/guide/en/machine-learning/current/ml-nlp-e5.html">E5</a>, or import <a href="https://www.elastic.co/guide/en/machine-learning/current/ml-nlp-model-ref.html#ml-nlp-model-ref-text-embedding">external models</a> into the cluster using <a href="https://www.elastic.co/guide/en/machine-learning/current/ml-nlp-import-model.html">eland</a>.</p></li><li><p>External services are deployed by model providers. Elasticsearch supports the following:   </p><ul><li><p><a href="https://www.elastic.co/search-labs/blog/elasticsearch-cohere-embeddings-support">Cohere</a></p></li><li><p><a href="https://www.elastic.co/search-labs/integrations/hugging-face">Hugging Face</a></p></li><li><p><a href="https://www.elastic.co/search-labs/integrations/mistral">Mistral</a></p></li><li><p><a href="https://www.elastic.co/search-labs/integrations/open-ai">OpenAI</a></p></li><li><p><a href="https://www.elastic.co/search-labs/blog/elasticsearch-azure-ai-studio-support">Azure AI Studio</a></p></li><li><p><a href="https://www.elastic.co/search-labs/blog/elasticsearch-azure-openai-embeddings-support">Azure OpenAI</a></p></li><li><p>Google AI Studio</p></li></ul></li></ul><p>Once you have chosen the inference mode, <a href="https://www.elastic.co/guide/en/elasticsearch/reference/current/put-inference-api.html">create an inference endpoint</a> for it. The inference endpoint identifier will be the only configuration detail that you will need to set up <code>semantic_text</code>.</p>PUT _inference/sparse_embedding/my-elser-endpoint
{
  "service": "elser",
  "service_settings": {
    "num_allocations": 1,
    "num_threads": 1
  }
}
<h3>Creating your index mapping</h3><p>Elasticsearch will need to index the embeddings generated by the model so they can be efficiently queried later.</p><p>Before semantic_text, you needed to understand about the two main field types used for storing embeddings information:</p><ul><li><p><a href="https://www.elastic.co/guide/en/elasticsearch/reference/current/sparse-vector.html"><code>sparse_vector</code></a>: It indexes sparse vector embeddings, like the ones generated by ELSER. Each embedding consists of pairs of tokens and weights. There is a small number of tokens generated per embedding.</p></li><li><p><a href="https://www.elastic.co/guide/en/elasticsearch/reference/current/dense-vector.html"><code>dense_vector</code></a>: It indexes vectors of numbers, which contains the embedding information. A model produces vectors of a fixed size, called the vector dimension.</p></li></ul><p>The field type to use is conditioned by the model you have chosen. If using dense vectors, you will need to <a href="https://www.elastic.co/guide/en/elasticsearch/reference/current/dense-vector.html#dense-vector-params">configure</a> the field to include the dimension count, the similarity function used to calculate vectors proximity, and storage customizations like quantization or the specific data type used for each element.</p><p>Now, if you're using semantic_text, you define a <a href="https://www.elastic.co/guide/en/elasticsearch/reference/current/semantic-text.html">semantic_text field mapping</a> by just specifying the inference endpoint identifier for your model:</p>PUT test-index
{
  "mappings": {
    "properties": {
      "infer_field": {
        "type": "semantic_text",
        "inference_id": "my-elser-endpoint"
      }
    }
  }
}
<p>That's it. No need for you to define other mapping options, or to understand which field type you need to use.</p><h3>Setting up indexing</h3><p>Once your index is ready to store the embeddings, it's time to generate them.</p><p>Before <code>semantic_text</code>, to generate embeddings automatically on document ingestion you needed to set up an <a href="https://www.elastic.co/guide/en/elasticsearch/reference/current/ingest.html">ingestion pipeline</a>.</p><p>Ingestion pipelines are used to automatically enrich or transform documents when ingested into an index, or when explicitly specified as part of the ingestion process.</p><p>You need to use the <a href="https://www.elastic.co/guide/en/elasticsearch/reference/current/inference-processor.html">inference processor</a> to generate embeddings for your fields. The processor needs to be configured using:</p><ul><li><p>The text fields from which to generate the embeddings</p></li><li><p>The output fields where the generated embeddings will be added</p></li><li><p>Specific inference configuration for text embeddings or sparse embeddings, depending on the model type</p></li></ul><p>With <code>semantic_text</code>, you simply add documents to your index. semantic_text fields will automatically calculate the embeddings using the specified inference endpoint.</p><p>This means there's no need to create an inference pipeline to generate the embeddings. Using bulk, index, or update APIs will do that for you automatically:</p>PUT test-index/_doc/doc1
{
  "infer_field": "These are not the droids you're looking for. He's free to go around"
}
<p>Inference requests in <code>semantic_text</code> fields are also batched. If you have 10 documents in a bulk API request, and each document contains 2 <code>semantic_text</code> fields, then that request will perform a single inference request with 20 texts to your inference service in one go, instead of making 10 separate inference requests of 2 texts each.</p><h3>Automatically handling long text passages</h3><p>Part of the challenge of selecting a model is the number of tokens that the model can generate embeddings for. Models have a limited number of tokens they can process. This is referred to as the model’s context window.</p><p>If the text you need to work with is longer than the model’s context window, you may <strong>truncate</strong> the text and use just part of it to generate embeddings. This is not ideal as you'll lose information; the resulting embeddings will not capture the full context of the input text.</p><p>Even if you have a long context window, having a long text means a lot of content will be reduced to a single embedding, making it an inaccurate representation.</p><p>Also, returning a long text will be difficult for the users to understand, as they will have to scan the text to check it's what they are looking for. Using smaller snippets would be preferable instead.</p><p>Another option is to use <strong>chunking</strong> to divide long texts into smaller fragments. These smaller chunks are added to each document to provide a better representation of the complete text. You can then use a nested query to search over all the individual fragments and retrieve the documents that contain the best-scoring chunks.</p><p>Before <code>semantic_text</code>, chunking was not done out of the box - the inference processor did not support chunking. If you needed to use chunking, you needed to do it before ingesting your documents or use the script processor to perform the chunking in Elasticsearch.</p><p>Using semantic_text means that chunking will be done on your behalf when indexing. Long documents will be split into 250-word sections with a 100-word overlap so that each section shares 100 words with the previous section. This overlap ensures continuity and prevents vital contextual information in the input text from being lost by a hard break.</p><p>If the model and inference service support batching the chunked inputs are automatically batched together into as few requests as possible, each optimally sized for the Inference Service. The resulting chunks will be stored in a nested object structure so you can check the text contained in each chunk.</p><h3>Querying your data</h3><p>Now that the documents and their embeddings are indexed in Elasticsearch, it's time to do some queries!</p><p>Before <code>semantic_text</code>, you needed to use a different query depending on the type of embeddings the model generates (dense or sparse). A <a href="https://www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl-sparse-vector-query.html">sparse vector query</a> is needed to query sparse_vector field types, and either a <a href="https://www.elastic.co/guide/en/elasticsearch/reference/current/knn-search.html">knn search</a> or a <a href="https://www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl-knn-query.html">knn query</a> can be used to search dense_vector field types.</p><p>The query process can be further customized for performance and relevance. For example, sparse vector queries can define <a href="https://www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl-sparse-vector-query.html#sparse-vector-query-with-pruning-config-and-rescore-example">token pruning</a> to avoid considering irrelevant tokens. Knn queries can specify the number of candidates to consider and the top k results to be returned from each shard.</p><p>You don't need to deal with those details when using <code>semantic_text</code>. You use a <a href="https://www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl-semantic-query.html">single query type</a> to search your documents:</p>GET test-index/_search
{
  "query": {
    "semantic": {
      "field": "infer_field",
      "query": "robots you're searching for"
    }
  }
}
<p>Just include the field and the query text. There’s no need to decide between sparse vector and knn queries, semantic text does this for you.</p><p>Compare this with using a specific <code>knn</code> search with all its configuration parameters:</p>{
  "knn": {
    "field": "infer_field",
    "k": 10,
    "num_candidates": 100,
    "query_vector_builder": {
      "text_embedding": { 
        "model_id": "my-dense-vector-embedding-model", 
        "model_text": "robots you're searching for" 
      }
    }
  }
}
<h2>Under the hood: How <code>semantic_text</code> works</h2><p>To understand how <code>semantic_text</code> works, you can create a <code>semantic_text</code> index and check what happens when you ingest a document. When the first document is ingested, the inference endpoint calculates the embeddings. When indexed, you will notice changes in the index mapping:</p>GET test-index
{
  "test-index": {
    "mappings": {
      "properties": {
        "infer_field": {
          "type": "semantic_text",
          "inference_id": "my-elser-endpoint",
          "model_settings": {
            "task_type": "sparse_embedding"
          }
        }
      }
    }
  }
}
<p>Now there is additional information about the model settings. Text embedding models will also include information like the number of dimensions or the similarity function for the model.</p><p>You can check the document already includes the embedding results:</p>GET test-index/_doc/doc1
{
  "_index": "test-sparse",
  "_id": "doc1",
  "_source": {
    "infer_field": {
      "text": "these are not the droids you're looking for. He's free to go around",
      "inference": {
        "inference_id": "my-elser-endpoint",
        "model_settings": {
          "task_type": "sparse_embedding"
        },
        "chunks": [
          {
            "text": "these are not the droids you're looking for. He's free to go around",
            "embeddings": {
              "##oid": 1.9103845,
              "##oids": 1.768872,
              "free": 1.693662,
              "dr": 1.6103356,
              "around": 1.4376559,
              "these": 1.1396849

              …
            }
          }
        ]
      }
    }
  }
}
<p>The field does not just contain the input text, but also a structure storing the original text, the model settings, and information for each chunk the input text has been divided into.</p><p>This structure consists of an object with two elements:</p><ul><li><p><em>text</em>: Contains the original input text</p></li><li><p><em>inference</em>: Inference information added by the inference endpoint, that consists of: </p><ul><li><p><em>inference_id</em> of the inference endpoint</p></li><li><p><em>model_settings</em> that contain model properties</p></li><li><p><em>chunks</em>: Nested object that contains an element for each chunk that has been created from the input text. Each chunk contains:</p><ul><li><p>The <em>text</em> for the chunk</p></li><li><p>The calculated <em>embeddings</em> for the chunk text</p></li></ul></li></ul></li></ul><h2>Customizing <code>semantic_text</code></h2><p><code>semantic_text</code> simplifies semantic search by making default decisions about indexing and querying your data:</p><ul><li><p>uses <code>sparse_vector</code> or <code>dense_vector</code> field types depending on the inference model type</p></li><li><p>Automatically defines the number of dimensions and similarity according to the inference results</p></li><li><p>Uses <code>int8_hnsw</code> index type for dense vector field types to leverage <a href="https://www.elastic.co/search-labs/blog/evaluating-scalar-quantization">scalar quantization</a>.</p></li><li><p>Uses query defaults. No token pruning is applied for <code>sparse_vector</code> queries, nor custom <code>k</code> and <code>num_candidates</code> are set for knn queries.</p></li></ul><p>Those are sensible defaults and allow you to quickly and easily start working with semantic search. Over time, you may want to customize your queries and data types to optimize search relevance, index and query performance, and index storage.</p><h3>Query customization</h3><p>There are no customization options - yet - for semantic queries. If you want to customize queries against <code>semantic_text</code> fields, you can perform <a href="https://www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl-semantic-query.html#advanced-search">advanced semantic_text search</a> using explicit knn and sparse vector queries.</p><p>We're planning to add <a href="https://www.elastic.co/guide/en/elasticsearch/reference/current/retrievers-overview.html">retrievers support</a> for <code>semantic_text</code>, and adding configuration options to the <code>semantic_text</code> field so they won't be needed at query time. Stay tuned!</p><h3>Data type customization</h3><p>If you need deeper customization for the data indexing, you can use the <code>sparse_vector</code> or <code>dense_vector</code> field types. These field types give you full control over how embeddings are generated, indexed, and queried.</p><p>You need to create an ingest pipeline with an inference processor to generate the embeddings. <a href="https://www.elastic.co/guide/en/elasticsearch/reference/current/semantic-search-inference.html">This tutorial</a> walks you through the process.</p><h2>What's next with <code>semantic_text</code>?</h2><p>We're just getting started with <code>semantic_text</code>! There are quite a few enhancements that we will keep working on, including:</p><ul><li><p>Better inference error handling</p></li><li><p>Customize the chunking strategy</p></li><li><p>Hiding embeddings in _source by default, to avoid cluttering the search responses</p></li><li><p>Inner hits support, to retrieve the relevant chunks of information for a query</p></li><li><p>Filtering and retrievers support</p></li><li><p>Kibana support</p></li></ul><h2>Try it out!</h2><p><code>semantic_text</code>is available on <a href="https://www.elastic.co/elasticsearch/serverless">Elasticsearch Serverless</a> now! It will be available soon on Elasticsearch 8.15 version for <a href="https://www.elastic.co/cloud">Elastic Cloud</a> and on <a href="https://www.elastic.co/downloads/elasticsearch">Elasticsearch downloads</a>.</p><p>If you already have an Elasticsearch serverless cluster, you can see a complete example for testing semantic search using <code>semantic_text</code> in <a href="https://www.elastic.co/search-labs/blog/elasticsearch-cohere-rerank">this tutorial</a>, or try it with <a href="https://colab.research.google.com/github/elastic/elasticsearch-labs/blob/main/notebooks/search/09-semantic-text.ipynb">this notebook</a>.</p><p>We'd love to hear about your experience with <code>semantic_text</code>! Let us know what you think in the <a href="https://www.elastic.co/community">forums</a>, or open an issue in the <a href="https://github.com/elastic/elasticsearch">GitHub repository</a>. Let's make semantic search easier together!</p>]]></content:encoded>
    <link>https://www.elastic.co/search-labs/blog/semantic-search-simplified-semantic-text</link>
    <guid isPermaLink="true">https://www.elastic.co/search-labs/blog/semantic-search-simplified-semantic-text</guid>
    <category><![CDATA[Vector Database]]></category>
    <category><![CDATA[Basics]]></category>
    <dc:creator><![CDATA[Carlos Delgado,Mike Pellegrini]]></dc:creator>
    <enclosure url="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt31515f5dc8f12092/6a170c170c48570fa101aabd/dc08f5c15b12a0e686b8922ad8d2b997ca1227d7-1024x1024.jpg" length="0" type="image/jpeg"/>
    <pubDate>Mon, 24 Jun 2024 00:00:00 GMT</pubDate>
  </item>
  <item>
    <title><![CDATA[Intelligent RAG data chunking: Fetch surrounding chunks]]></title>
    <description><![CDATA[Learn about data chunking in RAG and explore fetch surrounding chunking, a pattern in RAG that uses chunking and Elasticsearch to refine LLM responses.]]></description>
    <content:encoded><![CDATA[<p>In the realm of Retrieval-Augmented Generation (RAG), one persistent challenge is finding the optimal amount of data to feed into a Large Language Model (LLM). Too little data results in insufficient or inaccurate responses, while too much data leads to vague answers. This delicate balance inspired me to develop a <a href="https://ela.st/fetch-surrounding-chunks">notebook</a> focusing on intelligent chunking and leveraging Elasticsearch vector database.</p><p>This blog builds on that notebook and explores fetch surrounding chunking, an emerging pattern in RAG that uses intelligent chunking and Elasticsearch vector database to optimize LLM responses. The approach balances data input to enhance the accuracy and relevance of LLM-generated answers through semantic hybrid search.</p><h2>The motivation: A refined approach to RAG data chunking</h2><p>The primary motivation behind building <a href="https://ela.st/fetch-surrounding-chunks">this notebook</a> was to demonstrate a refined approach to RAG by addressing the challenge of data chunking. Traditional methods often fall short in dynamically adjusting the data size fed to LLMs, either overwhelming the model with too much context or starving it with too little. This notebook aims to strike the right balance, providing just enough information for the LLM to generate precise and contextually relevant responses. However, it must be noted that there is no one-size-fits-all solution.</p><p>This method works especially well with books and similar texts where content flows within longer sections or chapters. However, it may require adaptation for texts structured into shorter, distinct sections, such as research papers or articles, where each segment might cover a different topic. In such cases, additional strategies may be necessary to effectively chunk and retrieve related content.</p><h2>The methodology: Intelligent RAG data chunking</h2><h3>Fetch surrounding chunks</h3><p>The core idea is to partition the source text into manageable chunks, ensuring each chunk contains just the right amount of information. For this demonstration, I used text from "Harry Potter and the Sorcerer's Stone." The text was partitioned into chapters, and each chapter was further divided into smaller chunks. These chunks, along with their dense and sparse (ELSER) vector representations, were indexed in the Elasticsearch vector database.</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt6bcc905a1653ca3a/6a1711357d8d670d9970e846/23b210ce29f47f8a872d300ef01fca901d1e80ab-1163x548.png" alt="architecture" /><h3>Assigning numbers to chunks</h3><p>Each chunk within a chapter was assigned a sequential integer, allowing us to identify its position. When a matching chunk is found, the chapter number and chunk number are used to retrieve surrounding chunks, providing additional context for the LLM.</p><h3>Vector database in Elasticsearch</h3><p>These chunks and their vector representations were ingested into an Elasticsearch Cloud instance. Elasticsearch's robust vector search capabilities make it ideal for hosting these chunks, allowing for efficient retrieval of the most relevant chunks based on the semantic content or text match of a user's query.</p><h3>AI search</h3><p>To retrieve the relevant chunks, I employed a hybrid search strategy using dense vector comparisons, sparse vector comparisons, and text search in parallel. This multi-faceted approach ensures that the search results are both semantically rich and contextually accurate. A query is issued to find the matched chunk, which returns the chunk number and chapter. Surrounding chunks for that chapter are then fetched based on the matched chunk.</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blte879d45ec9558b9e/6a171137b339d50e9776a0be/62d3cb6d9cddcecda359bc1fd808b52cd0f23864-1440x778.png" alt="architecture" /><h2>The RAG pattern</h2><p>When a query is made, the search flow performs the following steps:</p><ol><li><p><strong>Query analysis:</strong> The user's query is translated into dense and sparse vectors to retrieve the most relevant chunks from the Elasticsearch index.</p></li><li><p><strong>Chunk retrieval:</strong> Using the AI search strategy, the system retrieves the top relevant chunks.</p></li><li><p><strong>Contextual expansion:</strong> Adjacent chunks (n-1 and n+1) are also retrieved to provide a more comprehensive context. If the chunk is the last in the chapter, it fetches n-1 and n-2; if it's the first, it fetches n+1 and n+2.</p></li><li><p><strong>LLM response:</strong> These intelligently selected chunks are then fed into the LLM, ensuring it receives the optimal amount of information to generate a precise and contextually relevant response.</p></li></ol><h2>Why intelligent RAG data chunking matters</h2><p>This approach addresses a critical aspect of RAG by optimizing the input data fed to LLMs. By leveraging intelligent chunking and hybrid semantic search, this method enhances the accuracy and relevance of the responses generated by LLMs. It showcases a pattern that can be widely applied in various applications within the RAG space, from customer support to content generation and beyond.</p><h2>Conclusion</h2><p><a href="https://ela.st/fetch-surrounding-chunks">This notebook</a> underscores the importance of intelligent data chunking in the RAG framework and demonstrates how Elasticsearch vector database can be leveraged to achieve optimal results. By ensuring the LLM receives just the right amount of information, this methodology paves the way for more accurate and contextually rich responses, enhancing the overall effectiveness of RAG systems.</p>]]></content:encoded>
    <link>https://www.elastic.co/search-labs/blog/advanced-chunking-fetch-surrounding-chunks</link>
    <guid isPermaLink="true">https://www.elastic.co/search-labs/blog/advanced-chunking-fetch-surrounding-chunks</guid>
    <category><![CDATA[AI]]></category>
    <category><![CDATA[Vector Database]]></category>
    <category><![CDATA[Python]]></category>
    <dc:creator><![CDATA[Sunile Manjee]]></dc:creator>
    <enclosure url="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt17ba5b693b94a883/6a171139acf0880723be9c49/4467ccd71baaae7422b9b5df9a8612eec4af1bd2-1024x1024.png" length="0" type="image/png"/>
    <pubDate>Tue, 11 Jun 2024 00:00:00 GMT</pubDate>
  </item>
  <item>
    <title><![CDATA[How to choose the best k and num_candidates for kNN search]]></title>
    <description><![CDATA[Learn strategies for selecting the optimal values for `k` and `num_candidates` parameters in kNN search, illustrated with practical examples.]]></description>
    <content:encoded><![CDATA[<h2>How to choose the best <code>k</code> and <code>num_candidates</code> for kNN search?</h2><p>Vector search has emerged as a game-changer in the current generative AI/ML world. It allows us to find similar items based on their semantic meaning rather just exact keyword matches.</p><p>Elasticsearch's k-Nearest Neighbors (kNN) algorithm is a foundational ML technique for classification and regression tasks. It found a significant place within Elasticsearch's ecosystem with the introduction of vector search capabilities. Introduced in Elasticsearch 8.5, kNN based vector search allows users to perform high-speed similarity searches on dense vector fields.</p><p>Users can find documents in the index "closest" to a given vector by leveraging the kNN algorithm using an underlying specified distance metric such as Euclidean or Cosine similarity. This feature marked a pivotal advancement as it is particularly useful in applications requiring semantic search, recommendations and other use cases such as anomaly detection.</p><p>The introduction of dense vector fields and k-nearest neighbor (kNN) search functionality in Elasticsearch has opened new horizons for implementing sophisticated search capabilities that go beyond traditional text search.</p><p>This article delves into strategies for selecting the optimal values for <code>k</code> and <code>num_candidates</code> parameters, illustrated with practical examples using Kibana.</p><h2>kNN search query</h2><p>Elasticsearch provides a kNN search option for nearest-neighbors - something like the following:</p>POST movies/_search
{
  "knn": {
    "field": "title_vector.predicted_value",
    "query_vector_builder": {
      "text_embedding": {
        "model_id": ".multilingual-e5-small",
        "model_text": "Good Ugly"
      }
    },
    "k": 3,
    "num_candidates": 100
  },
  "_source": [
    "id",
    "title"
  ]
}
<p>As the snippet shows, the <code>knn</code> query fetches the relevant results for the query in question (having a movie title as "Good Ugly") using vector search. The search is conducted in a multi-dimensional space, producing the closest vectors to the given query vector.</p><p>From the above query, notice two attributes: <code>num_candidates</code> which is the initial pool of candidates to consider and <code>k</code>, the number of nearest neighbors.</p><h2>kNN critical parameters - k and num_candidates</h2><p>To leverage the kNN feature effectively, one requires a nuanced understanding of the two critical parameters: <code>k</code> - the number of global nearest neighbors to retrieve, and <code>num_candidates</code> - the number of candidate neighbors considered for each shard during the search.</p><p>Choosing the optimal values for the <code>k</code> and <code>num_candidates</code> involves balancing precision, recall, and performance. These parameters play a crucial role to efficiently handle high-dimensional vector spaces commonly found in machine learning applications.</p><p>The optimal value for <code>k</code> largely depends on the specific use case. For example, if you're building a recommendation system, a smaller <code>k</code> (e.g., 10-20) might be sufficient to provide relevant recommendations. In contrast, for a use case where you'd want clustering or outlier detection capabilities, you might need a larger <code>k</code>.</p><p>Note that the higher <code>k</code> value can significantly increase both computation and memory usage, especially with large datasets. It's important to test different values of <code>k</code> to find a balance between result relevance and system resource usage.</p><h2>K: Unveiling the closest neighbors</h2><p>We have an option of choosing the <code>k</code> value as per our requirements. Sometimes, setting up a lower <code>k</code> value receives more or less exactly what you want with the exception that a few results might not make it to the final output. However, setting up a higher <code>k</code> value might broaden your search results in numbers, with a caveat that you may receive diversified results at times.</p><p>Imagine you're searching for a new book in the vast library of recommendations. <code>k</code>, also known as the number of nearest neighbors, determines how many books you'll be presented with. Think of it as the inner circle of your search results. Let's see how setting the lower and higher <code>k</code> values affects the number of books that the query returns.</p><h3>Setting lower K</h3><p>The lower K setting prioritizes extreme precision - meaning we will receive a handful of books that are the most similar to our query vector. This ensures a high degree of relevance to our specific interests. This might be ideal if you're searching for a book with a very specific theme or writing style.</p><h3>Setting higher K</h3><p>With a larger K value, we will be fetching a broader exploration result set. Note that the results might not be as tightly focused on your exact query. However, you'll encounter a wider range of potentially interesting books. This approach can be valuable for diversifying your reading list and discovering unexpected gems, perhaps.</p>Whenever we say higer or lower values of <code>k</code>, we mean the actual values depends on multiple factors, such as size of the data sets, available computing power and other factors. In some cases, the k=10 might be a large but in others it might be small too. So, do keep a note of the environmnet that this parateter is expected to operate.<h2>The <code>num_candidates</code> attribute: Behind the curtain</h2><p>While <code>k</code> determines the final number of books you see, <code>num_candidates</code> plays a crucial role under the hood. It essentially defines the search space per shard – the initial pool of books in a shard from which the most relevant K neighbors are identified. When we issue the query, we are expected to hint Elasticsearch to run the query amongst top "x" number of candidates on each shard.</p><p>For example, say our books index contains 5000 books evenly distributed amongst five primary shards (i.e., ~1000 books per shard). When we are performing a search, obviously choosing all 1000 documents for each shard is neither a viable nor a correct option. Instead, we will be pick up to say 25 documents (which is our <code>num_candidates</code>) from the 1000 documents. That amounts to 125 documents as our total search space (5 shards times 25 documents each).</p><p>We will let the kNN query know to choose the 25 documents from each shard and this number is the <code>num_candidates</code> parameter. When the kNN search is executed, the "coordinator" node sends the request query to all of the involved shards. The <code>num_candidates</code> documents from each shard will constitute the search space and the top <code>k</code> documents will be fetched from that space. Say, if <code>k</code> is 3, the top 3 documents out of the 25 candidate documents will be selected in each shard and returned to the coordinator node. That is, the coordinator node will receive 15 documents in total from all the involved nodes. These top 15 documents are then ranked to fetch the global top 3 (<code>k</code>==3) documents.</p><p>The process is depicted in the following figure:</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/bltb33aae4fd971e9f6/6a17d788e8fbce3ffd3a1750/41e690519fbef503742014b61b42ab193b6ff310-1440x836.jpg" alt="" /><p>Here's what <code>num_candidates</code> means for your search:</p><h3>Setting the lower num_candidates</h3><p>This approach might restrict the search space, potentially missing some relevant books that fall outside the initial exploration set. Think of it as surveying a smaller portion of the library's shelves.</p><h3>Setting the higher num_candidates</h3><p>A higher <code>num_candidates</code> value increases the likelihood of finding the true nearest neighbors within our chosen K. It expands the search space - that is - more number of candidates are considered - and hence leads to a slight increase in search time. So, a higher value generally increases accuracy (as the chance of missing relevant vectors decreases) but at the cost of performance.</p><h2>Balancing precision &amp; performance for kNN parameters</h2><p>The optimal values for <code>k</code> and <code>num_candidates</code> depend on a few factors and specific needs. If we prioritize extreme precision with a smaller set of highly relevant results, a lower <code>k</code> with a moderate <code>num_candidates</code> might be ideal. Conversely, if exploration and discovering unexpected books are your goals, a higher K with a larger <code>num_candidates</code> could be more suitable.</p><p>While there is no hard-and-fast rule to define the "lower" or "higher" number for the <code>num_candidates</code>, you need to decide this number based on your dataset, computing power and the expected precision.</p><h2>Experimentation to optimize kNN parameters</h2><p>By experimenting with different K and <code>num_candidates</code> combinations and monitoring search results and performance, you can fine-tune your searches to achieve the perfect balance between precision, exploration, and speed. Remember, there's no one-size-fits-all solution – the best approach depends on your unique goals and data characteristics.</p><h2>Practical example: Using kNN for movie recommendations</h2><p>Let's consider an example of movies to create a manual "simple" framework for understanding the effect of k and <code>num_candidates</code> attributes while searching for movies.</p><h3>Manual framework</h3><p>Let's understand how we can develop a home grown framework for tweaking the <code>k</code> and <code>num_of_candidates</code> attributes for a kNN search.</p><p>The mechanics of the framework is as follows:</p><ul><li><p>Create a movies index with a couple of <code>dense_vector</code> fields in the mapping to hold our vectorised data.</p></li><li><p>Create an embedding pipeline so each and every movie's title and synopsis fields will be embedded with a <code>multilingual-e5-small</code> model to store vectors.</p></li><li><p>Perform the indexing operation,which goes through the above embedding pipeline. The respective fields will be vectorised</p></li><li><p>Create a search query using kNN feature</p></li><li><p>Tweak the <code>k</code> and <code>num_candidates</code> options as you'd want</p></li></ul><p>Let's dig in.</p><h3>Creating an inference pipeline</h3><p>We will need to index data via Kibana - far from ideal - but it will do for this manual framework understanding. However, every movie that gets indexed must have the title and synopsis field vectorised to enable semantic search on our data. We can do this by elegantly creating a inference pipeline processor and attaching it to our batch indexing operation.</p><p>Let's create an inference pipeline:</p># Creating an inference pipeline processor
# The title and synopsis fields gets vectorised and stored in respective fields

PUT _ingest/pipeline/movie_embedding_pipeline
{
  "processors": [
    {
      "inference": {
        "model_id": ".multilingual-e5-small",
        "target_field": "title_vector",
        "field_map": { "title": "text_field" }
      }
    },
    {
      "inference": {
        "model_id": ".multilingual-e5-small",
        "target_field": "synopsis_vector",
        "field_map": { "synopsis": "text_field" }
      }
    }
  ]
}
<p>The inference pipeline <code>movie_embedding_pipeline</code>, as shown above, creates vector fields text embedding for title and synopsis fields. It uses the inbuilt <code>multilingual-e5-small</code> model to create the text embeddings.</p><h3>Creating index mappings</h3><p>We will need to create a mapping with couple of properties as <code>dense_vector</code> fields. The following code snippet does the job:</p># Creating a movies index
# Note the vector fields
PUT movies
{
  "mappings": { 
    "properties": { 
      "title": {
        "type": "text",
        "fields": { 
          "original": {
            "type": "keyword"
          }
        }
      },
      "title_vector.predicted_value": {
        "type": "dense_vector",
        "dims": 384,
        "index": true
      },
      "synopsis": {
        "type": "text"
      },
      "synopsis_vector.predicted_value": {
        "type": "dense_vector",
        "dims": 384,
        "index": true
      },
      "actors": {
        "type": "text"
      },
      "director": {
        "type": "text"
      },
      "rating": {
        "type": "half_float"
      },
      "release_date": {
        "type": "date",
        "format": "dd-MM-yyyy"
      },
      "certificate": {
        "type": "keyword"
      },
      "genre": {
        "type": "text"
      }
    }
  }
}

<p>Once the above command gets executed, we have a new movies index with the appropriate dense vector fields, including <code>title_vector.predicted_value</code> and <code>synopsis_vector.predicted_value</code> fields that hold respective vectors.</p>The <code>index</code> mapping parameter was set to false by default up to release 8.10. This has been changed in release 8.11, where the parameter is set to true by default, which makes it unnecessary to specify it.<p>Next step is to ingest the data.</p><h3>Indexing movies</h3><p>We can use <code>_bulk</code> operation to index a set of movies - I'm reusing a dataset that I had created for my Elasticsearch in Action 2nd edition book - which is available <a href="https://github.com/madhusudhankonda/elasticsearch-in-action/blob/main/datasets/movie_bulk_data.json">here</a>:</p><p>For completeness, a snippet of the ingestion using the <code>_bulk</code> operation is provided here:</p>POST _bulk?pipeline=movie_embedding_pipeline
{"index":{"_index":"movies","_id":"1"}}
{"title": "The Shawshank Redemption","synopsis": "Two imprisoned men bond over a number of years, finding solace and eventual redemption through acts of common decency.","actors": ["Tim Robbins", "Morgan Freeman", "Bob Gunton", "William Sadler"] ,"director":" Frank Darabont ","rating":"9.3","certificate":"R","genre": "Drama "}
{"index":{"_index":"movies","_id":"2"}}
{"title": "The Godfather","synopsis": "An organized crime dynasty's aging patriarch transfers control of his clandestine empire to his reluctant son.","actors": ["Marlon Brando", "Al Pacino", "James Caan", "Diane Keaton"] ,"director":" Francis Ford Coppola ","rating":"9.2","certificate":"R","genre": ["Crime", "Drama"] }
{"index":{"_index":"movies","_id":"3"}}
{"title": "The Dark Knight","synopsis": "When the menace known as the Joker wreaks havoc and chaos on the people of Gotham, Batman must accept one of the greatest psychological and physical tests of his ability to fight injustice.","actors": ["Christian Bale", "Heath Ledger", "Aaron Eckhart", "Michael Caine"] ,"director":" Christopher Nolan ","rating":"9.0","certificate":"PG-13","genre": ["Action", "Crime", "Drama"] }

<p>Make sure you replace the script with the full dataset.</p>Note that the <code>_bulk</code> operation is suffixed with the pipeline (<code>?pipeline=movie_embedding_pipeline</code>) so the every movie gets passed through this pipeline, thus producing the vectors.<p>As we primed our <code>movies</code> indexed with vector embeddings, it's time to start our experiments on fine tuning <code>k</code> and <code>num_candidates</code> attributes.</p><h3>kNN search</h3><p>As we have vector data in our movies index, we will be using approximate k-nearest neighbor (kNN) search. For example, to recommend movies similar that has father-son sentiment ("Father and son" as search query), we'll use a kNN search to find the nearest neighbors:</p>POST movies/_search
{
  "_source": ["title"], 
  "knn": {
    "field": "title_vector.predicted_value",
    "query_vector_builder": {
      "text_embedding": {
        "model_id": ".multilingual-e5-small",
        "model_text": "Father and son"
      }
    },
    "k": 5,
    "num_candidates": 10
  }
}
<p>In the given example, the query leverages the top-level kNN search option parameter that directly focuses on finding documents closest to a given query vector. One key difference between this search with knn query at the top level as opposed to query at the top level is that in the former case, the query vector will be generated on-the-fly by a machine learning model.</p><p>The part in bold is not technically correct. On-the-fly vector generation is only achieved by using <code>query_vector_builder</code> instead of <code>query_vector</code> where you pass in the vector (computed outside of ES) but both the top-level knn search option and the knn search query provide this capability.</p><p>The script fetches the relevant results based on our search query (which is built using the <code>query_vector_builder</code> block). We are using a random <code>k</code> and <code>num_candidates</code> values set to 5 and 10 respectively.</p><h3>kNN query attributes</h3><p>The above query has a set of attributes that would make up the kNN query. The following information about these attributes will help you understand the query better:</p><p>The <code>field</code> attribute specifies the field in the index that contains the vector representations of our documents. In this case, <code>title_vector.predicted_value</code> is the field storing the document vectors.</p><p>The <code>query_vector_builder</code> attribute is where the example significantly diverges from simpler kNN queries. Instead of providing a static query vector, this configuration dynamically generates a query vector using a text embedding model. The model transforms a piece of text ("Father and son" in the example) into a vector that represents its semantic meaning.</p><p>The <code>text_embedding</code> indicates that a text embedding model will be used to generate the query vector.</p><p>The <code>model_id</code> is the identifier for the pre-trained machine learning model to use, It is the <code>.multilingual-e5-small</code> model in this example.</p><p>The <code>model_text</code> attribute is the text input that will be converted into a vector by the specified model. Here, it's the words "Father and son", which the model will interpret semantically to find similar movie titles.</p><p>The <code>k</code> is the number of nearest neighbors to retrieve - that is, it determines how many of the most similar documents to return based on the query vector.</p><p>The <code>num_candidates</code> attribute is the broader set of candidate documents per shard as potential matches to ensure the final results are as accurate as possible.</p><h3>kNN results</h3><p>Executing the kNN basic search script should get us top 5 results - for brevity, I'm providing just the list of the movies.</p># The results should get you a set of 5 movies as shown in the list below:

"title": "The Godfather"
"title": "The Godfather: Part II"
"title": "Pulp Fiction"
"title": "12 Angry Men"
"title": "Life Is Beautiful"
<p>As you can expect, Godfather (both parts) are part of the father-and-son bonding while Pulp Fiction shouldn't have been part of the results (though the query is asking about "bonding" - Pulp Fiction is all about the bonding between few people).</p><p>Now that we have a basic framework setup, we can tweak the parameters appropriate and deduce the approximate settings. Before we tweak the settings, let's understand the optimal setting of <code>k</code> attribute.</p><h3>Choosing optimal K value</h3><p>Choosing the optimal value of k in k-Nearest Neighbors (kNN) algorithms is crucial for attaining the best possible performance on our dataset with minimal errors. However, there isn't a one-size-fits-all answer, as the best <code>k</code> value can depend on a few factors such as specifics of our data and what we are trying to predict.</p><p>To choose an optimal <code>k</code> value, one must create a custom framework with several strategies and considerations.</p><ul><li><p>k = 1: Try running the search query with k=1 as a first step. Make sure you change the input query for each run. The query should give you unreliable results as changing the input query will return incorrect results over time. This leads to a ML pattern called "overfitting" where the model becomes overly reliant on the specific data points in the immediate neighborhood. Model, thus, struggles to generalize to unseen examples.</p></li><li><p>k = 5: Run the search query with k=5 and check the predictions. The stability of the search query should ideally improved and you should be getting adequate reliable predictions.</p></li></ul><p>You can either incrementally increase the value of <code>k</code> - may be increase in the steps of 5 or x - until you find that sweet spot where you'd find the results for the input queries are pretty much spot on with less number of errors.</p><p>You can go to extreme values of <code>k</code> too, for example, pick a higher value of <code>k=50</code>, as discussed below:</p><ul><li><p>k = 50: Increase the <code>k</code> value to 50 and check the search results. The errored results most likely outshine the actual/expected predictions. This is when you know that you are hitting the hard boundary of the <code>k</code> value. Larger <code>k</code> values leads to a ML feature called "underfitting" - a underfitting in KNN happens when the model is too simplistic and fails to capture the underlying patterns in the data.</p></li></ul><h3>Choosing the optimal <code>num_candidates</code> value</h3><p>The <code>num_candidates</code> parameter plays a crucial role in finding the optimal balance between search accuracy and performance. Unlike k, which directly influences the number of search results returned, <code>num_candidates</code> determines the size of the initial candidate set from which the final k nearest neighbors are selected. As discussed earlier, the <code>num_candidates</code> parameter defines how many nearest neighbors will be selected on each shard.</p><p>Adjusting this parameter is essential for ensuring that the search process is both efficient and yields high-quality results.</p><ul><li><p><code>num_candidates</code> = Small Value (e.g., 10): Start with a low value ("low-value-exploration") for <code>num_candidates</code> as a preliminary step. The aim is to establish a baseline for performance at this stage. As the candidate bunch is just a handful of candidates, the search will be fast but might miss relevant results - which leads to poor accuracy. This scenario helps us to understand the minimum threshold where the search quality is noticeably compromised.</p></li><li><p><code>num_candidates</code> = Moderate Value (e.g., 25?): Increase the <code>num_candidates</code> to a moderate value ("moderate-value-exploration") and observe the changes in search quality and execution time. A moderate number of candidates is likely to improve the accuracy of the results by considering a wider pool of potential neighbors. As the number of candidates increased, there's going to be cost of resources, be mindful of that. So, keep monitoring the performance metrics closely. However, as the search accuracy increases, perhaps the increase in computational cost could be justifiable.</p></li><li><p><code>num_candidates</code> = Step Increase: Continue to incrementally increase <code>num_candidates</code> (incremental-increase-exploration), possibly in steps of 20 or 50 (depending on the size of your dataset). Evaluate whether the additional candidates contribute to a meaningful improvement in search accuracy with each of the increments. There will be a a point of diminishing returns where increasing <code>num_candidates</code> further yields little to no improvement in result quality. At the same time you may have noticed, this will strain our resources and significantly impacts performance.</p></li><li><p><code>num_candidates</code> = High Value (say, 1000, 5000): Experiment with a high value for <code>num_candidates</code> to understand the upper bounds of the impact of choosing the higher settings. There's a possibility of your search accuracy stabilizing or degrading slightly due to the inclusion of less relevant candidates. This may lead to dilute the precision of the final k results. Do note that, as we've been talking about it, the high values of <code>num_candidates</code> will always increase the computational load - thus longer query times and potential resource constraints.</p></li></ul><h3>Finding the optimal balance</h3><p>We now know how to adjust the <code>k</code> and <code>num_candidates</code> attributes and how our experiments to different settings would change the outcome of search accuracy.</p><p>The goal is to find a sweet spot where the search results are consistently accurate with lower performance overhead from processing a large candidate set is manageable.</p><p>Of course, the optimal value will vary depending on the specifics of our data, the dimensionality of the vectors, and other performance requirements.</p><h2>Wrap up</h2><p>The optimal K value lies in finding the sweet spot by experiment and trials. You want to use enough neighbors (K being lower side) to capture the essential patterns but not so many (<code>k</code> being on the higher side) that the model becomes overly influenced by noise or irrelevant details. You also want to tweak the candidates so that the search results are accurate at a given <code>k</code> value.</p>]]></content:encoded>
    <link>https://www.elastic.co/search-labs/blog/elasticsearch-knn-and-num-candidates-strategies</link>
    <guid isPermaLink="true">https://www.elastic.co/search-labs/blog/elasticsearch-knn-and-num-candidates-strategies</guid>
    <category><![CDATA[Vector Database]]></category>
    <dc:creator><![CDATA[Madhusudhan Konda]]></dc:creator>
    <enclosure url="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/bltb33aae4fd971e9f6/6a17d788e8fbce3ffd3a1750/41e690519fbef503742014b61b42ab193b6ff310-1440x836.jpg" length="0" type="image/jpeg"/>
    <pubDate>Fri, 24 May 2024 00:00:00 GMT</pubDate>
  </item>
  <item>
    <title><![CDATA[Elasticsearch open inference API adds support for Azure OpenAI embeddings]]></title>
    <description><![CDATA[Elasticsearch open inference API adds support for Azure OpenAI embeddings to be stored in the world's most downloaded vector database.]]></description>
    <content:encoded><![CDATA[<p>We're happy to announce that Elasticsearch now supports <a href="https://learn.microsoft.com/en-us/azure/ai-services/openai/overview">Azure OpenAI embeddings</a> in our <a href="https://www.elastic.co/guide/en/elasticsearch/reference/current/inference-apis.html">open inference API</a>, enabling developers to store generated embeddings into our highly scalable and performant <a href="https://www.elastic.co/search-labs/blog/elasticsearch-lucene-vector-database-gains">vector database</a>.</p><p>This new functionality further solidifies our commitment to not only working with Microsoft and the Azure platform, but also toward our commitment to offering our customers more flexibility with their AI solutions.</p><h2>Ongoing Investment in AI at Elastic</h2><p>This is the latest in a series of additional features and integrations on AI enablement for Elasticsearch following on from:</p><ul><li><p><a href="https://www.elastic.co/search-labs/blog/elasticsearch-azure-ai-studio-support">Elasticsearch open inference API adds Azure AI Studio support</a></p></li><li><p><a href="https://www.elastic.co/search-labs/blog/elasticsearch-azure-openai-completion-support">Elasticsearch open inference API adds support for Azure OpenAI chat completions</a></p></li><li><p><a href="https://www.elastic.co/search-labs/blog/elasticsearch-openai-completion-support">Elasticsearch open inference API adds support for OpenAI chat completions</a></p></li><li><p><a href="https://www.elastic.co/search-labs/blog/elasticsearch-cohere-embeddings-support">Elasticsearch open inference API adds support for Cohere Embeddings</a></p></li><li><p><a href="https://www.elastic.co/search-labs/blog/azure-openai-on-your-data-elasticsearch-vector-database">Introducing Elasticsearch vector database to Azure OpenAI Service On Your Data (preview)</a></p></li></ul><p>The new <a href="https://www.elastic.co/guide/en/elasticsearch/reference/current/inference-apis.html">inference</a> embeddings service provider for Azure OpenAI is already available in our <a href="https://www.elastic.co/search-labs/blog/building-elastic-cloud-serverless">stateless offering</a> on Elastic Cloud, and will be soon available to everyone in an upcoming Elastic release.</p><h2>Using Azure OpenAI Embeddings with the Elasticsearch Inference API</h2><h3>Deploying an Azure OpenAI Embeddings Model</h3><p>To get started, you will need a <a href="https://azure.microsoft.com/">Microsoft Azure Subscription</a> as well as access to <a href="https://aka.ms/oai/access">Azure OpenAI service</a>. Once you have registered and have access, you will need to create a resource in your <a href="https://azure.microsoft.com/en-us/get-started/azure-portal">Azure Portal</a>, and then deploy an embedding model to <a href="https://oai.azure.com/">Azure OpenAI Studio</a>. To do this, if you do not already have an Azure OpenAI resource in your Azure Portal, create a new one from the “Azure OpenAI” type which can be found in the Azure Marketplace, and take note of your resource name as you will need this later. When you create your resource, the region you choose may impact what models you have access to. See the Azure OpenAI deployment <a href="https://learn.microsoft.com/en-us/azure/ai-services/openai/concepts/models#standard-deployment-model-availability">model availability table</a> for additional details.</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt35aedf6095d45d06/6a17d706abe0f20405dfe854/53dfbc95b12448541a816f78f8d996c2a3c24399-416x220.png" alt="Azure OpenAI on Marketplace" /><p>Once you have your resource, you will also need one of your API keys which can be found in the “Keys and Endpoint” information from the Azure Portal's left side navigation:</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt2d58e5f2c964df0a/6a17d707e31791350e2d5678/c45e937ec65b16818168264b30e0ec419f0f7eac-724x404.png" alt="Keys and Endpoint" /><p>Now, to deploy your Azure OpenAI Embedding model, go into your <a href="https://oai.azure.com/">Azure OpenAI Studio's</a> console and create your deployment using an <a href="https://platform.openai.com/docs/guides/embeddings/embedding-models">OpenAI Embeddings model</a> such as <code>text-embedding-ada-002</code>. Once your deployment is created, you should see the deployment overview. Also take note of the deployment name, in the example below it is “example-embeddings-model”.</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt9541eda646e4cfb3/6a17d709be608642de004594/ab8c1d64aaa3a41436f5c6be5a2ea867c648ae11-938x311.png" alt="Example Deployment" /><h3>Using your deployed Azure OpenAI embeddings model with the Elasticsearch Inference API</h3><p>With an Azure OpenAI embeddings model deployed, we can now configure your Elasticsearch deployment's <code>_inference</code> API and create a pipeline to index embeddings vectors in your documents. Please refer to the <a href="https://github.com/elastic/elasticsearch-labs/">Elastic Search Labs GitHub repository</a> for more in-depth guides and interactive notebooks.</p><p>To perform these tasks, you can use the Kibana Dev Console, or any REST console of your choice.</p><p>First, configure your inference endpoint using the <a href="https://www.elastic.co/guide/en/elasticsearch/reference/8.14/put-inference-api.html">create inference model endpoint</a> - we'll call this “example_model”:</p>PUT _inference/text_embedding/example_model
{
    "service": "azureopenai",
    "service_settings": {
        "api_key": "&lt;api-key&gt;",
        "resource_name": "&lt;resource-name&gt;",
        "deployment_id": "&lt;deployment-id&gt;",
        "api_version": "2024-02-01"
    },
    "task_settings": {
        "user": "&lt;optional-username&gt;"
    }
}
<p>For your inference endpoint, you will need your API key, your resource name, and the deployment id that you created above. For the “api_version”, you will want to use an available API version from the <a href="https://learn.microsoft.com/en-us/azure/ai-services/openai/reference#embeddings">Azure OpenAI embeddings documentation</a> - we suggest always using the latest version which is “2024-02-01” as of this writing. You can also optionally add a username in the task setting's “user” field which should be a unique identifier representing your end-user to help Azure OpenAI to monitor and detect abuse. If you do not want to do this, omit the entire “task_settings” object.</p><p>After running this command you should receive a <code>200 OK</code> status indicating that the model is properly set up.</p><p>Using the <a href="https://www.elastic.co/guide/en/elasticsearch/reference/8.14/post-inference-api.html">perform inference endpoint</a>, we can see an example of your inference endpoint at work:</p>POST _inference/text_embedding/example_model
{
  "input": "What is Elastic?"
}
<p>The output from the above command should provide the embeddings vector for the input text:</p>{
    "text_embedding": [
        {
            "embedding": [
                -0.0038039694,
                0.0054465225,
                -0.0018359756,
                -0.02274399,
                -0.01969836,
                ...
            ]
        }
    ]
}
<p>Now that we know our inference endpoint works, we can create a pipeline that uses it:</p>PUT _ingest/pipeline/azureopenai_embeddings
{
  "processors": [
    {
      "inference": {
        "model_id": "example_model", 
        "input_output": { 
          "input_field": "name",
          "output_field": "name_embedding"
        }
      }
    }
  ]
}
<p>This will create an ingestion pipeline named “azureopenai_embeddings” that will read the contents of the “name” field upon ingestion and apply the embeddings inference from our model to the “name_embedding” output field. You can then use this ingestion pipeline when documents are ingested (e.g. via the _bulk ingest endpoint), or when reindexing an index that is already populated.</p><p>This is currently available through the open inference API in our <a href="https://www.elastic.co/search-labs/blog/building-elastic-cloud-serverless">stateless offering</a> on Elastic Cloud. It'll also be soon available to everyone in an upcoming versioned Elasticsearch release, with additional semantic text capabilites that will make this step even simpler to integrate into your existing workflows.</p><p>For an additional use case, you can walk through the <a href="https://www.elastic.co/guide/en/elasticsearch/reference/8.14/semantic-search-inference.html">semantic search with inference tutorial</a> for how to perform ingestion and semantic search on a larger scale with Azure OpenAI and other services such as reranking or chat completions.</p><h2>Plenty more on the horizon</h2><p>This new extensibility is only one of many new features we are bringing to the AI table from Elastic. Bookmark <a href="https://www.elastic.co/search-labs">Elastic Search Labs</a> now to stay up to date! Ready to build RAG into your apps? Want to try different LLMs with a vector database? Check out our sample notebooks for LangChain, Cohere and more <a href="https://github.com/elastic/elasticsearch-labs">on Github</a>, and join the Elasticsearch <a href="https://www.elastic.co/training/elasticsearch-engineer">Engineer training</a> starting soon!</p>]]></content:encoded>
    <link>https://www.elastic.co/search-labs/blog/elasticsearch-azure-openai-embeddings-support</link>
    <guid isPermaLink="true">https://www.elastic.co/search-labs/blog/elasticsearch-azure-openai-embeddings-support</guid>
    <category><![CDATA[Vector Database]]></category>
    <category><![CDATA[AI]]></category>
    <dc:creator><![CDATA[Mark Hoy]]></dc:creator>
    <enclosure url="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/bltdf867d9327b3b843/6a17d70b3e9e450670ba12da/1ea2acd6fcfea41d4f57ce576c0aebd416724129-1440x660.png" length="0" type="image/png"/>
    <pubDate>Wed, 22 May 2024 00:00:00 GMT</pubDate>
  </item>
  <item>
    <title><![CDATA[Elasticsearch open inference API adds Azure AI Studio support]]></title>
    <description><![CDATA[Elasticsearch open inference API now supports Azure AI Studio. Learn how to use Azure AI Studio capabilities with Elasticsearch in this blog.]]></description>
    <content:encoded><![CDATA[<p>As part of our ongoing commitment to serve the Microsoft Azure developers with the tools of their choice, we are happy to announce that Elasticsearch now provides integration of the <a href="https://learn.microsoft.com/en-us/azure/ai-studio/how-to/model-catalog-overview">hosted model catalog</a> on Microsoft Azure AI Studio into our open inference API. This complements the ability for developers to bring their<a href="https://techcommunity.microsoft.com/t5/ai-azure-ai-services-blog/azure-openai-service-expands-quot-on-your-data-quot-with/ba-p/4097023"> Elasticsearch vector database to be used in Azure OpenAI</a>.</p><p>Developers can use the capabilities of the world's most downloaded vector database to store and utilize embeddings generated from OpenAI models from Azure AI studio or access the wide array of chat completion model deployments for quick access to conversational models like <code>mistral-small</code>.</p><p>Just recently we've added support for Azure OpenAI <a href="https://www.elastic.co/search-labs/blog/elasticsearch-azure-openai-embeddings-support">text embeddings</a> and <a href="https://www.elastic.co/search-labs/blog/elasticsearch-azure-openai-completion-support">completion</a>, and now we've added support for utilizing Azure AI Studio. Microsoft Azure developers have complete access to Azure OpenAI &amp; Microsoft Azure AI Studio service capabilities and can <a href="https://www.elastic.co/search-labs/blog/azure-openai-on-your-data-elasticsearch-vector-database">bring their Elasticsearch</a> data to <a href="https://techcommunity.microsoft.com/t5/ai-azure-ai-services-blog/azure-openai-service-expands-quot-on-your-data-quot-with/ba-p/4097023">revolutionize conversational search</a>.</p><p>Let's walk you through just how easily you can use these capabilities with Elasticsearch.</p><h2>Deploying a model in Azure AI Studio</h2><p>To get started, you'll need a <a href="https://azure.microsoft.com/">Microsoft Azure</a> subscription as well as access to <a href="https://ai.azure.com/">Azure AI Studio</a>. Once you are set up, you'll need to deploy either a text embedding model or a chat completion model from the <a href="https://ai.azure.com/explore/models">Azure AI Studio model catalog</a>. Once your model is deployed, on the deployment overview page take note of the target URL and your deployment's API key - you'll need these later to create your inference endpoint in Elasticsearch.</p><p>Furthermore, when you deploy your model, Azure offers two different types of deployment options - a “pay as you go” model (where you pay by the token), and a “realtime” deployment which is a dedicated VM that is billed by the hour. Not all models will have both deployment types available, so be sure to take note as well as which deployment type is used.</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt51abed9b55573072/6a17d7016864a46975b685d7/fb111aa1bb5cf45f78b7eabb1c2eb0498c441773-763x319.png" alt="Azure AI Studio Deployment" /><h2>Creating an Inference API Endpoint in Elasticsearch</h2><p>Once your model is deployed, we can now create an endpoint for your inference task in Elasticsearch. For the examples below we are using the <a href="https://ai.azure.com/explore/models/Cohere-command-r/version/3/registry/azureml-cohere">Cohere Command R model</a> to perform chat completion.</p><p>In Elasticsearch, create your endpoint by providing the service as “azureaistudio”, and the service settings including your API key and target from your deployed model. You'll also need to provide the model provider, as well as the endpoint type from before (either “token” or “realtime”). In our example, we've deployed a Cohere model with a token type endpoint.</p>PUT _inference/completion/test_cohere_chat_completion
{
  "service": "azureaistudio",
  "service_settings": {
    "api_key": "&lt;&lt;API_KEY&gt;&gt;",
    "target": "&lt;&lt;TARGET_URL&gt;&gt;",
    "provider": "cohere",
    "endpoint_type": "token"
  }
}
<p>When you send Elasticsearch the command, it should return back the created model to confirm that it was successful. Note that the API key will never be returned and is stored in Elasticsearch's secure settings.</p>{
    "model_id": "test_cohere_chat_completion",
    "task_type": "completion",
    "service": "azureaistudio",
    "service_settings": {
        "target": "&lt;&lt;TARGET_URL&gt;&gt;",
        "provider": "cohere",
        "endpoint_type": "token"
    },
    "task_settings": {}
}
<p>Adding a model for using text embeddings is just as easy. For reference, if we had deployed the <a href="https://ai.azure.com/explore/models/Cohere-embed-v3-english/version/3/registry/azureml-cohere">Cohere-embed-v3-english model</a>, we can create our inference model in Elasticsearch with the “text_embeddings” task type by providing the appropriate API key and target URL from that deployment's overview page:</p>PUT _inference/text_embeddings/test_cohere_embeddings
{
  "service": "azureaistudio",
  "service_settings": {
    "api_key": "&lt;&lt;API_KEY&gt;&gt;",
    "target": "&lt;&lt;TARGET_URL&gt;&gt;",
    "provider": "cohere",
    "endpoint_type": "token"
  }
}
<h2>Let's perform some inference</h2><p>That's all there is to setting up your model. Now that that's out of the way, we can use the model. First, let's test the model out by asking it to provide some text given a simple prompt. To do this, we'll call the _inference API with our input text:</p>POST _inference/completion/test_cohere_chat_completion
{
  "input": "The answer to the universe is"
}
<p>And we should see Elasticsearch provide a response. Behind the scenes, Elasticsearch is calling out to Azure AI Studio with the input text and processes the results from the inference. In this case, we received the response:</p>{
    "completion": [
        {
            "result": "42. \n\nIn Douglas Adams' *The Hitchhiker's Guide to the Galaxy*, a super-computer named Deep Thought is asked what the answer to the ultimate question of life, the universe, and everything is. After calculating for 7.5-million years, Deep Thought announces that the answer is 42. \n\nThe number 42 has since become a reference to the novel, and many fans of the book series speculate as to what the actual question might be."
        }
    ]
}
<p>We've tried to make it easy for the end user to not have to deal with all the technical details behind the scenes, but we can also control our inference a bit more by providing additional parameters to control the processing such as sampling temperature and requesting the maximum number of tokens to be generated:</p>POST _inference/completion/test_cohere_chat_completion
{
  "input": "The answer to the universe is",
  "task_settings": {
    "temperature": 1.0,
    "do_sample": true,
    "max_new_tokens": 50
  }
}
<h2>That was easy. What else can we do?</h2><p>This becomes even more powerful when we are able to use our new model in other ways such as adding additional text to a document when it's used in an Elasticsearch ingestion pipeline. For example, the following pipeline definition will use our model and anytime a document using this pipeline is ingested, any text in the field “question_field” will be sent through the inference API and the response will be written to the “completed_text_answer” field in the document. This allows large batches of documents to be augmented.</p>PUT _ingest/pipeline/azure_ai_studio_cohere_completions
{
  "processors": [
    {
      "inference": {
        "model_id": "test_cohere_chat_completion", 
        "input_output": { 
          "input_field": "question_field",
          "output_field": "completed_text_answer"
        }
      }
    }
  ]
}
<h2>Limitless possibilities</h2><p>By harnessing the power of Azure AI Studio deployed models in your Elasticsearch inference pipelines, you can enhance your search experience's natural language processing and predictive analytics capabilities.</p><p>In upcoming versions of Elasticsearch, users can take advantage of new field mapping types that simplify the process even further where designing an ingest pipeline would no longer be necessary. Also, as alluded to in our <a href="https://www.elastic.co/search-labs/blog/elasticsearch-cohere-rerank#elasticsearchs-accelerated-roadmap-to-semantic-reranking-and-retrievers">accelerated roadmap for semantic search</a> the future will provide dramatically simplified support for inference tasks with Elasticsearch retrievers at query time.</p><p>These capabilities are available through the open inference API in our <a href="https://www.elastic.co/search-labs/blog/building-elastic-cloud-serverless">stateless offering</a> on Elastic Cloud. It'll also be soon available to everyone in an upcoming versioned Elasticsearch release.</p>]]></content:encoded>
    <link>https://www.elastic.co/search-labs/blog/elasticsearch-azure-ai-studio-support</link>
    <guid isPermaLink="true">https://www.elastic.co/search-labs/blog/elasticsearch-azure-ai-studio-support</guid>
    <category><![CDATA[AI]]></category>
    <category><![CDATA[Vector Database]]></category>
    <dc:creator><![CDATA[Mark Hoy]]></dc:creator>
    <enclosure url="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt857508512ce282f5/6a17d703e9ea87a89fa9c415/d5cfda5d59f5812a9819831938219a34c11a0bd9-1440x962.jpg" length="0" type="image/jpeg"/>
    <pubDate>Wed, 22 May 2024 00:00:00 GMT</pubDate>
  </item>
  <item>
    <title><![CDATA[Red Hat & Elastic: Red Hat OpenShift AI integration with Elasticsearch]]></title>
    <description><![CDATA[Red Hat OpenShift users can now implement Elasticsearch for vector search &amp; RAG applications via the Red Hat Ecosystem Catalog. Explore this integration here.]]></description>
    <content:encoded><![CDATA[<p>Red Hat and Elastic have <a href="https://www.redhat.com/en/about/press-releases/red-hat-and-elastic-fuel-retrieval-augmented-generation-genai-use-cases">collaborated</a> to enable integration for the Elasticsearch vector database on <a href="https://www.redhat.com/en/technologies/cloud-computing/openshift/openshift-ai">Red Hat OpenShift AI</a>. Red Hat OpenShift users can implement Elasticsearch for vector search and Retrieval-Augmented Generation (RAG) applications via the <a href="https://catalog.redhat.com/software/container-stacks/detail/5f32f067651c4c0bcecf1bfe">Red Hat Ecosystem Catalog</a>.</p><p>Elastic Cloud on Kubernetes (ECK) is a certified offering on Red Hat OpenShift. Elastic is an IBM <a href="https://cloud.ibm.com/docs/databases-for-elasticsearch">partner</a>, and IBM Watsonx Assistant and Watsonx Discovery use Elastic <a href="https://www.ibm.com/docs/en/announcements/watsonx-discovery-10">vector search</a> for question-answering and retrieval augmentation use cases.</p><p>With this collaboration, Elasticsearch users can benefit from Red Hat OpenShift AI, a flexible, scalable MLOps platform for building, training, testing, and serving models for AI-enabled applications.</p><h2>Elasticsearch vector database for generative AI and RAG apps</h2><p>Elasticsearch Relevance Engine (ESRE) is a comprehensive suite of developer tools for building generative AI and RAG applications. ESRE incorporates a <a href="https://www.elastic.co/search-labs/blog/elasticsearch-lucene-vector-database-gains">vector database</a> that stores embeddings for text, image, and video data. ESRE’s native hybrid search can effectively combine results containing text, vectors, and geospatial data, with filtering, aggregations, and document-level security.</p><p>With ESRE, developers can implement vector search and semantic search, including k-nearest neighbors (<a href="https://www.elastic.co/search-labs/blog/simplifying-knn-search?trk=feed-detail_main-feed-card_feed-article-content">kNN</a>) and approximate nearest neighbor (ANN) search, along with support for both built-in and third-party natural language processing (<a href="https://www.elastic.co/search-labs/blog/how-to-deploy-nlp-text-embeddings-and-vector-search">NLP</a>) models. ESRE also seamlessly integrates with key third-party ecosystem products from providers such as <a href="https://www.elastic.co/search-labs/blog/elasticsearch-cohere-rerank">Cohere</a>, LangChain, and LlamaIndex. Elasticsearch can be self-managed or deployed with <a href="https://cloud.elastic.co/registration?onboarding_token=vectorsearch&amp;cta=cloud-registration&amp;tech=trial&amp;plcmt=article%20content&amp;pg=search-labs">Elastic Cloud</a>.</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/bltc88e752ba4c25d75/6a17d774445de9105c4cff50/4387b921978cde8ce8cdcf9dcb435d4fdaec6229-1440x663.png" alt="Elasticsearch as the preferred vector database solution on Red Hat OpenShift AI" /><p>As part of this collaboration, users are now able to leverage ESRE capabilities by downloading Elasticsearch directly from the <a href="https://catalog.redhat.com/software/container-stacks/detail/5f32f067651c4c0bcecf1bfe">Red Hat Ecosystem Catalog</a>.</p><h2>What is Red Hat OpenShift AI for generative AI apps</h2><p>Red Hat OpenShift AI is a hybrid MLOps platform that brings IT, data science, and app dev teams together. Designed to simplify Generative AI application development and deployment, it provides a comprehensive infrastructure stack tailored for distributed workloads. This includes training, optimizing, fine-tuning, and deploying foundational and predictive AI models. Collaborating with model builders helps provide access to a variety of pre-built models. Developers and data scientists can work together on the same platform, greatly enhancing collaboration. The platform facilitates end-to-end AI lifecycle management—from model development and training to deployment, serving, and continuous monitoring.</p><ul><li><p><strong>Model development</strong>: Conduct exploratory data science in JupyterLab with access to core AI / ML libraries and frameworks, including TensorFlow and PyTorch using our notebook images or your own.</p></li><li><p><strong>Model serving &amp; monitoring</strong>: Deploy models across on-premise or any cloud, either in a fully managed or self-managed Red Hat OpenShift footprint and centrally monitor their performance.</p></li><li><p><strong>Lifecycle Management</strong>: Create repeatable data science pipelines for model training and validation and integrate them with DevOps pipelines for the delivery of models across your enterprise.</p></li><li><p><strong>Increased capabilities and collaboration</strong>: Create projects and share them across teams. Combine Red Hat components, open-source software, and ISV-certified software.</p></li></ul><h2>Get started with Red Hat and Elasticsearch</h2><p>To get started, just follow the installation instructions provided in the <a href="https://catalog.redhat.com/software/container-stacks/detail/5f32f067651c4c0bcecf1bfe">Red Hat Ecosystem Catalog</a>, and start building your next generative AI application with RAG!</p><p>Visit <a href="https://www.elastic.co/search-labs">Elasticsearch Labs</a> for articles and sample notebooks on vector search, RAG, and more.</p>]]></content:encoded>
    <link>https://www.elastic.co/search-labs/blog/elasticsearch-redhat-openshift-ai-vector-database</link>
    <guid isPermaLink="true">https://www.elastic.co/search-labs/blog/elasticsearch-redhat-openshift-ai-vector-database</guid>
    <category><![CDATA[Vector Database]]></category>
    <category><![CDATA[AI]]></category>
    <dc:creator><![CDATA[Aditya Tripathi]]></dc:creator>
    <enclosure url="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/bltd5dd849e60ee215c/6a17d776faa913959493c6d3/56eeb9068e892907fa03ccda7556f9c0eae66f0b-1401x841.png" length="0" type="image/png"/>
    <pubDate>Tue, 07 May 2024 00:00:00 GMT</pubDate>
  </item>
  <item>
    <title><![CDATA[Making Elasticsearch and Lucene the best vector database: up to 8x faster and 32x efficient]]></title>
    <description><![CDATA[Discover the recent enhancements and optimizations that notably improve vector search performance in Elasticsearch &amp; Lucene vector database.]]></description>
    <content:encoded><![CDATA[<h2>Elasticsearch and Lucene report card: noteworthy speed and efficiency investments</h2><p>Our mission at Elastic is to make Apache Lucene the best vector database out there, and to continue to make Elasticsearch the best retrieval platform out there for search and RAG. Our investments into Lucene are key to ensure that every release of Elasticsearch brings increasing faster performance and scale.</p><p>Customers are already building the next generation of AI enabled search applications with Elastic’s vector database and vector search technology. <a href="https://roboflow.com/">Roboflow</a> is used by over 500,000 engineers to create datasets, train models, and deploy computer vision models to production. Roboflow uses Elastic vector database to store and search billions of vector embeddings.</p><p>In this blog we summarize recent enhancements and optimisations that significantly improve vector search performance in Elasticsearch and Apache Lucene, over and above performance gains <a href="https://www.elastic.co/search-labs/blog/apache-lucene-9.9-search-speedups">delivered with Lucene 9.9</a> and Elasticsearch 8.12.x.</p><p>The integration of vector search into Elasticsearch relies on Apache Lucene, the layer that orchestrates data storage and retrieval. <a href="https://www.elastic.co/search-labs/blog/vector-search-elasticsearch-rationale">Lucene's architecture</a> organizes data into segments, immutable units that undergo periodic merging. This structure allows for efficient management of inverted indices, essential for text search. With vector search, Lucene extends its capabilities to handle multi-dimensional points, employing the hierarchical navigable small world (HNSW) algorithm to index vectors.</p><p>This approach facilitates scalability, enabling data sets to exceed available RAM size while maintaining performance. Additionally, Lucene's segment-based approach offers lock-free search operations, supporting incremental changes and ensuring visibility consistency across various data structures. The integration however comes with its own engineering challenges. Merging segments requires recomputing HNSW graphs, incurring index-time overhead. Searches must cover multiple segments, leading to possible latency overhead. Moreover, optimal performance requires scaling RAM as data grows, which may raise resource management concerns.</p><p>Lucene's integration into Elasticsearch comes with the benefit of robust vector search capabilities. This includes aggregations, document level security, geo-spatial queries, pre-filtering, to full compatibility with various Elasticsearch features. Imagine running vector searches using a geo bounding box, this is an example usecase enabled by Elasticsearch and Lucene.</p><p>Lucene's architecture lays a solid foundation for efficient and versatile vector search within Elasticsearch. Let’s explore optimization strategies and enhancements we have implemented to integrate vector search into Lucene, which delivers a high performance and comprehensive feature-set for developers.</p><h2>Harnessing Lucene's architecture for multi-threaded search</h2><p>Lucene's segmented architecture enables the implementation of multi-threaded search capabilities. Elasticsearch’s performance gains come from efficiently searching multiple segments simultaneously. Latency of individual searches is significantly reduced by using the processing power of all available CPU cores. While it may not directly improve overall throughput, this enhancement prioritizes minimizing response times, ensuring that users receive their search results as swiftly as possible.</p><p>Furthermore, this optimization is particularly beneficial for Hierarchical Navigable Small World (HNSW) searches, as each graph is independent of the others and can be searched in parallel, maximizing efficiency and speeding up retrieval times even further.</p><p>The advantage of having multiple independent segments extends to the architectural level, especially in serverless environments. In this <a href="https://www.elastic.co/blog/elastic-serverless-architecture">new architecture,</a> the indexing tier is responsible for creating new segments, each containing its own HSNW graph. The search tier can simply replicate these segments without incurring the CPU cost of indexation. This separation allows a significant portion of compute resources to be dedicated to searches, optimizing overall system performance and responsiveness.</p><h2>Accelerating multi-graph vector search</h2><p>In spite of gains achieved with parallelization, each segment's searches would remain independent, unaware of progress made by other segment searches. So our focus shifted towards optimizing the efficiency of concurrent searches across multiple segments.</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt35f29a49abcd2e22/6a17d78ae31791cd572d5682/103e9a7a97e9c219edb028e0fc675346920002cc-974x407.png" alt="" /><p>The graph shows that the number queries per second increased from 104 queries/sec to 219 queries/sec.</p><p>Recognizing the potential for further speedups, we leveraged our insights from optimizing lexical search, to enable information exchange among segment searches allowing for better coordination and efficiency in vector search.</p><p><a href="https://www.elastic.co/search-labs/blog/multi-graph-vector-search">Our strategy for accelerating multi-graph vector search</a> revolves around balancing exploration and exploitation within the proximity graph. By adjusting the size of the expanded match set, we control the trade-off between runtime and recall, crucial for achieving optimal search performance across multiple graphs.</p><p>In multi-graph search scenarios, the challenge lies in efficiently navigating individual graphs, while ensuring comprehensive exploration to avoid local minima. While searching multiple graphs independently yields higher recall, it incurs increased runtime due to redundant exploration efforts. To mitigate this, we devised a strategy to intelligently share state between searches, enabling informed traversal decisions based on global and local competitive thresholds.</p><p>This approach involves maintaining shared global and local queues of distances to closest vectors, dynamically adapting search parameters based on the competitiveness of each graph's local search. By synchronizing information exchange and adjusting search strategies accordingly, we achieve significant improvements in search latency while preserving recall rates comparable to single-graph searches.</p><p>The impact of these optimizations is evident in our benchmark results. In concurrent search and indexing scenarios, we notice up to 60% reduction in query latencies! Even for queries conducted outside of indexing operations, we observed notable speedups and a dramatic decrease in the number of vector operations required. These enhancements, integrated into Lucene 9.10 and subsequently Elasticsearch 8.13, mark significant strides towards enhancing vector database performance for search while maintaining excellent recall rates.</p><h2>Harnessing Java's latest advancements for ludicrous speed</h2><p>In the area of Java development, automatic vectorization has been a boon, optimizing scalar operations into SIMD (Single Instruction Multiple Data) instructions through the HotSpot C2 compiler. While this automatic optimization has been beneficial, it has its limitations, particularly in scenarios where explicit control over code shape yields superior performance. Enter Project Panama Vector API, a recent addition to the JDK offering an API for expressing computations reliably compiled to SIMD instructions at runtime.</p><p>Lucene's vector search implementation relies on fundamental operations like dot product, square, and cosine distance, both in floating point and binary variants. Traditionally, these operations were backed by scalar implementations, leaving performance enhancements to the JIT compiler. However, recent advancements introduce a paradigm shift, enabling developers to express these operations explicitly for optimal performance.</p><p>Consider the dot product operation, a fundamental vector computation. Traditionally implemented in Java with scalar arithmetic, recent innovations leverage the Panama Vector API to express dot product computations in a manner conducive to SIMD instructions. This revised implementation iterates over input arrays, multiplying and accumulating elements in batches, aligning with the underlying hardware capabilities.</p><p><a href="https://www.elastic.co/blog/accelerating-vector-search-simd-instructions">By harnessing Panama Vector API,</a> Java code now interfaces seamlessly with SIMD instructions, unlocking the potential for significant performance gains. The compiled code, when executed on compatible CPUs, leverages advanced vector instructions like AVX2 or AVX 512, resulting in accelerated computations. Disassembling the compiled code reveals optimized instructions tailored to the underlying hardware architecture.</p><p>Microbenchmarks comparing traditional Java implementations to those leveraging Panama Vector API illustrate dramatic performance improvements. Across various vector operations and dimension sizes, the optimized implementations outperform their predecessors by significant margins, offering a glimpse into the transformative power of SIMD instructions.</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt8efb30efc7e6c157/6a17d78bfbc5f8285d49190c/d2a5f15bb0d16608b67753a82312d2f254370622-1204x120.png" alt="" /><p>Micro-benchmark comparing dot product with the new Panama API (dotProductNew) and the scalar implementation (dotProductOld).</p><p>Beyond microbenchmarks, the real-world impact of these optimizations is quite exciting to think about. Vector search benchmarks, such as <a href="https://elasticsearch-benchmarks.elastic.co/#tracks/so_vector/nightly/default/90d">SO Vector,</a> demonstrate notable enhancements in indexing throughput, merge times, and query latencies. Elasticsearch, embracing these advancements, incorporates the faster implementations by default, ensuring users reap the performance benefits seamlessly.</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/bltdd3a97bf5f0cd24a/6a17d78d3e9e452c84ba12e6/e82a7ee152fdebfc856106bdc1f68c6eab9b5798-1349x882.png" alt="" /><p>The graph shows indexing throughput increased from about 900 documents/sec to about 1300 documents/sec.</p><p>Despite the incubating status of Panama Vector API, its quality and potential benefits are undeniable. Lucene's pragmatic approach allows for selective adoption of non-final JDK APIs, balancing the promise of performance improvements with maintenance considerations. With Lucene and Elasticsearch, users can leverage these advancements effortlessly, with performance gains translating directly to real-world workloads.</p><p>The integration of Panama Vector API into Java development yields a new era of performance optimization, particularly in vector search scenarios. By embracing hardware-accelerated SIMD instructions, developers can unlock efficiency gains, visible both in microbenchmarks and macro-level benchmarks. As Java continues to evolve, leveraging its latest features promises to propel performance to new heights, enriching user experiences across diverse applications.</p><h2>Maximizing memory efficiency with scalar quantization</h2><p>Memory consumption has long been a concern for efficient vector database operations, particularly for searching large datasets. Lucene introduces a breakthrough optimization technique - scalar quantization - aimed at significantly reducing memory requirements without sacrificing search performance.</p><p>Consider a scenario where querying millions of float32 vectors of high dimensions demands substantial memory, leading to significant costs. By embracing byte quantization, Lucene slashes memory usage by approximately 75%, offering a viable solution to the memory-intensive nature of vector search operations.</p><p>For quantizing floats to bytes, Lucene implements <a href="https://www.elastic.co/search-labs/blog/scalar-quantization-in-lucene">Scalar quantization</a> a lossy compression technique that transforms raw data into a compressed form, sacrificing some information for space efficiency. Lucene's implementation of scalar quantization achieves remarkable space savings with minimal impact on recall, making it an ideal solution for memory-constrained environments.</p><p>Lucene's architecture, consisting of nodes, shards, and segments, which facilitates efficient distribution and management of documents for search. Each segment stores raw vectors, quantized vectors, and metadata, ensuring optimized storage and retrieval mechanisms.</p><p>Lucene's vector quantization adapts dynamically over time, adjusting quantiles during segment merge operations to maintain optimal recall. By intelligently handling quantization updates and re-quantization when necessary, Lucene ensures consistent performance while accommodating changes in data distribution.</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt4680278043366ea4/6a17d78e1d1b83ebd893e2d5/73fb017cce8096a108a7a7297c86cfb26866922c-1440x447.png" alt="" /><p>Example of merged quantiles where segments A and B have 1000 documents and C only has 100.</p><p>Experimental results demonstrate the efficacy of scalar quantization in reducing memory footprint while maintaining search performance. Despite minor differences in recall compared to raw vectors, Lucene's quantized vectors offer significant speed improvements and recall recovery with minimal additional vectors.</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt62d403efee1b4ebb/6a17d790e317918e322d5686/aacf329d8eb54a9b73a1e4722e14f27379dd80d7-576x455.png" alt="" /><p>Recall@10 for quantized vectors vs raw vectors. The search performance of quantized vectors is significantly faster than raw, and recall is quickly recoverable by gathering just 5 more vectors; visible by quantized@15.</p><p>Lucene's scalar quantization presents a revolutionary approach to memory optimization in vector search operations. With no need for training or optimization steps, Lucene seamlessly integrates quantization into its indexing process, automatically adapting to changes in data distribution over time. As Lucene and Elasticsearch continue to evolve, widespread adoption of scalar quantization will revolutionize memory efficiency for vector database applications, paving the way for enhanced search performance at scale.</p><h2>Achieving seamless compression with minimal impact on recall</h2><p>To make compression even better, we aimed to reduce each dimension from 7 bits to just 4 bits. Our main goal was to compress data further while still keeping search results accurate. By making some improvements, we managed to compress data by a factor of 8 without making search results worse. Here's how we did it.</p><p>We focused on keeping search results accurate while making data smaller. By making sure we didn't lose important information during compression, we could still find things well even with less detailed data. To make sure we didn't lose any important information, we added a smart error correction system.</p><p>We checked our compression improvements by testing them with different types of data and real search situations. This helped us see how well our searches worked with different compression levels and what we might lose in accuracy by compressing more.</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blta71070ad46fd4573/6a17d791be608665ca00459d/7c32834cfb733a6ad3deb64eb9813d4855539823-972x602.png" alt="" /><p>Comparison of int4 dot product values to the corresponding float values for a random sample of 100 documents and their 10 nearest neighbors.</p><p>These compression features were created to easily work with existing vector search systems. They help organizations and users save space without needing to change much in their setup. With this simple compression, organizations can expand their search systems without wasting resources.</p><p>In short, moving to 4 bits per dimension for scalar quantization was a big step in making compression more efficient. It lets users compress their original vectors by 8 times. By optimizing carefully, adding error correction, testing with real data, and offering scalable deployment, organizations could save a lot of storage space without making search results worse. This opens up new chances for efficient and scalable search applications.</p><h2>Paving the way for binary quantization</h2><p>The optimization to reduce each dimension to 4 bits not only delivers significant compression gains but also lays the groundwork for further advancements in compression efficiency. Specifically, future advancements like binary quantization into Lucene, a development that has the potential to revolutionize vector storage and retrieval.</p><p>In an ongoing effort to push the boundaries of compression in vector search, we are actively working on integrating binary quantization into Lucene using the same techniques and principles that underpin our existing optimization strategies. The goal is to achieve binary quantization of vector dimensions, thereby reducing the size of the vector representation by a factor of 32 compared to the original floating-point format.</p><p>Through our iterations and experiments, we want to deliver the full potential of vector search while maximizing resource utilization and scalability. Stay tuned for further updates on our progress towards integrating binary quantization into Lucene and Elasticsearch, and the transformative impact it will have on vector database storage and retrieval.</p><h2>Multi-vector integration in Lucene and Elasticsearch</h2><p>Several real world applications rely on text embedding models and large text inputs. Most embedding models have token limits, which necessitate chunking of longer text into passages. Therefore, instead of a single document, multiple passages and embeddings must be managed, potentially complicating metadata preservation.</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blte7cf9100604bef40/6a17d793033c8d5c696baff7/b8a6073b44c078ef8ee5294e559cf8092bf40e38-1440x903.png" alt="" /><p>Now instead of having a single piece of metadata indicating, for example the first chapter of the book “Little Women”, you have to index that information data for every sentence.</p><p>Lucene's "join" functionality, integral to Elasticsearch's nested field type, offers a solution. This feature enables multiple nested documents within a top-level document, allowing searches across nested documents and subsequent joins with their parent documents. So, how do we deliver support for vectors in nested fields with Elasticsearch?</p><p>The key lies in how Lucene joins back to parent documents when searching child vector passages. The parallel concept here is the debate around pre-filtering versus post-filtering in kNN methods, as the timing of joining significantly impacts result quality and quantity. To address this, <a href="https://www.elastic.co/search-labs/blog/adding-passage-vector-search-to-lucene">recent enhancements to Lucene</a> enable pre-joining against parent documents while searching the HNSW graph.</p><p>Practically, pre-joining ensures that when retrieving the k nearest neighbors of a query vector, the algorithm returns the k nearest documents instead of passages. This approach diversifies results without complicating the HNSW algorithm, requiring only a minimal additional memory overhead per stored vector.</p><p>Efficiency is improved by leveraging certain restrictions, such as disjoint sets of parent and child documents and the monotonicity of document IDs. These restrictions allow for optimizations using bit sets, providing rapid identification of parent document IDs.</p><p>Searching through a vast number of documents efficiently required investing in nested fields and joins in Lucene. This work helps storage and search for dense vectors that represent passages within long texts, making document searches in Lucene more effective. Overall, these advancements represent an exciting step forward in the area of vector database retrieval within Lucene.</p><h2>Wrapping up (for now)</h2><p>We're dedicated to making Elasticsearch and Lucene the best vector database with every release. Our goal is to make it easier for people to search for things. With some of the investments we discuss in this blog, there is significant progress, but we're not done!</p><p>To say that the gen AI ecosystem is rapidly evolving is an understatement. At Elastic, we want to give developers the most flexible and open tools to keep up with all the innovation—with features available across recent releases until 8.13 and <a href="https://www.elastic.co/blog/elastic-serverless-architecture">serverless</a></p>]]></content:encoded>
    <link>https://www.elastic.co/search-labs/blog/elasticsearch-lucene-vector-database-gains</link>
    <guid isPermaLink="true">https://www.elastic.co/search-labs/blog/elasticsearch-lucene-vector-database-gains</guid>
    <category><![CDATA[Vector Database]]></category>
    <category><![CDATA[AI]]></category>
    <dc:creator><![CDATA[Mayya Sharipova,Benjamin Trent,Jim Ferenczi]]></dc:creator>
    <enclosure url="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt35f29a49abcd2e22/6a17d78ae31791cd572d5682/103e9a7a97e9c219edb028e0fc675346920002cc-974x407.png" length="0" type="image/png"/>
    <pubDate>Fri, 26 Apr 2024 00:00:00 GMT</pubDate>
  </item>
  <item>
    <title><![CDATA[Elastic Cloud adds Elasticsearch Vector Database optimized instance to Google Cloud]]></title>
    <description><![CDATA[Elasticsearch's vector search optimized profile for GCP is available. Learn more about it and how to use it in this blog.]]></description>
    <content:encoded><![CDATA[<p>Elastic Cloud Vector Search optimized hardware profile is available for Google Elastic Cloud users. This hardware profile is optimized for applications that require the storage of dense or sparse embeddings for search and Generative AI use cases powered by RAG (retrieval augmented generation). This release follows the previous release of a Vector Search optimized hardware profile for AWS Elastic Cloud users in Nov 2023.</p><h2>GCP Vector Search optimized instances: what you need to know</h2><p>Elastic Cloud users benefit from having Elastic managed infrastructure across all major cloud providers (GCP, AWS and Azure) along with <a href="https://www.elastic.co/guide/en/cloud/current/ec-regions-templates-instances.html">wide region support</a> for GCP users. For more specific details on the instance configuration for this hardware profile, refer to our documentation for instance type: <a href="https://www.elastic.co/guide/en/cloud/current/ec-default-gcp-configurations.html">gcp.es.datahot.n2d.64x8x11</a></p><h2>Vector Search, HNSW, and memory</h2><p>Elasticsearch uses the <a href="https://www.elastic.co/search-labs/blog/vector-search-elasticsearch-rationale">Hierarchical Navigable Small World</a> graph (HNSW) data structure to implement its Approximate Nearest Neighbor search (ANN). Because of its layered approach, HNSW's hierarchical aspect offers excellent query latency. To be most performant, HNSW requires the vectors to be cached in the node's memory. This caching is done automatically and uses the available RAM not taken up by the Elasticsearch JVM. Because of this, memory optimizations are important steps for scalability.</p><p>Consult our vector search <a href="https://www.elastic.co/guide/en/elasticsearch/reference/current/tune-knn-search.html#_ensure_data_nodes_have_enough_memory">tuning guide</a> to determine the right setup for your vector search embeddings and whether you have adequate memory for your deployment.</p><p>With this in mind, the Vector Search optimized hardware profile is configured with a smaller than standard Elasticsearch JVM heap setting. This provides more RAM for caching vectors on a node, allowing users to provision fewer nodes for their vector search use cases.</p><p>If you’re using compression techniques like <a href="https://www.elastic.co/search-labs/blog/scalar-quantization-in-lucene">scalar quantization</a>, the memory requirement is lowered by a factor of 4. To store quantized embeddings (available in versions Elasticsearch 8.12 and later) simply ensure that you’re storing in the correct <code>element_type: byte</code>. To utilize our <a href="https://www.elastic.co/guide/en/elasticsearch/reference/current/dense-vector.html#dense-vector-quantization">automatic quantization</a> of <code>float</code> vectors update your embeddings to use index type: <code>int8_hnsw</code> like in the following mapping example.</p>PUT my-byte-quantized-index
{
  "mappings": {
    "properties": {
      "my_vector": {
        "type": "dense_vector",
        "dims": 512,
        "index_options": {
          "type": "int8_hnsw"
        }
      }
    }
  }
}
<p>In upcoming versions, Elasticsearch will provide this as the default mapping, removing the need for users to adjust their mapping.</p><p>Combining this optimized hardware profile with Elasticsearch’s <a href="https://www.elastic.co/guide/en/elasticsearch/reference/current/dense-vector.html#dense-vector-quantization">automatic quantization</a> are two examples where Elastic is focused on vector search to be cost-effective while still being extremely performant.</p><h2>Getting Started with Elastic Cloud vector search optimized profile for GCP</h2><p>Start a <a href="https://cloud.elastic.co/registration?onboarding_token=vectorsearch&amp;cta=cloud-registration&amp;tech=trial&amp;plcmt=article%20content&amp;pg=search-labs">free trial</a> on Elastic Cloud and simply select the new Vector Search optimized profile to get started.</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt4a33219437748674/6a17d7823e9e458302ba12de/c0434f399ee75c99b290060d7b0e613cbcd0829b-1440x1390.png" alt="cloud UI view for new deployments" /><h2>Migrating existing Elastic Cloud deployments</h2><p>Migrating to this new Vector Search optimized hardware profile is a few clicks away. Simply navigate to your Elastic Cloud management UI, click to manage the specific deployment, and edit the hardware profile. In this example, we are migrating from a ‘Storage optimized’ profile to the new ‘Vector Search’ optimized profile. When choosing to do so, while there is a reduction to available storage and vCPU, what is gained is the ability to store more vectors per memory with vector search.</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt18139bf4f13d62da/6a17d7843e9e4537e0ba12e2/f13962f914d5d9a3be765bde2ac95a9e2d797d3f-1440x561.png" alt="cloud UI view for migrating deployments" /><p>Migrating to a new hardware profile uses the grow and shrink approach for deployment changes. This approach adds new instances, migrates data from old instances to the new ones, and then shrinks the deployment by removing the old instances. This approach allows for high availability during configuration changes even for single availability zones.</p><p>The following image shows a typical architecture for a deployment running in Elastic Cloud, where vector search will be the primary use case.</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/bltbaac029d716db095/6a17d785e9ea874ffca9c421/58e00f32bef1411dbc11849a78b5ecd3c334528a-1440x570.png" alt="deployment view" /><p>This example deployment uses our new Vector Search optimized hardware profile, now available in GCP. This setup includes:</p><ul><li><p>Two data nodes in our hot tier with our vector search profile</p></li><li><p>One Kibana node</p></li><li><p>One Machine Learning node</p></li><li><p>One integration server</p></li><li><p>One master tiebreaker</p></li></ul><p>By deploying these two “full-sized” data nodes with the Vector Search optimized hardware profile and while taking advantage of Elastic’s automatic dense vector <a href="https://www.elastic.co/guide/en/elasticsearch/reference/current/dense-vector.html#dense-vector-quantization">scalar quantization</a>, you can index roughly 60 million vectors, including one replica (with 768 dimensions).</p><h2>Conclusion</h2><p>Vector search is a powerful tool when building modern search applications, be it for semantic document retrieval on its own or integrating with an LLM service provider in a <a href="https://www.elastic.co/search-labs/blog/retrieval-augmented-generation-rag">RAG setup</a>. Elasticsearch provides a full-featured vector database natively integrated with a full-featured search platform. Along with improving vector search feature set and usability, Elastic continues to improve scalability. The vector search node type is the latest example, allowing users to scale their search application.</p><p>Elastic is committed to providing scalable, price effective infrastructure to support enterprise grade search experiences. Customers can depend on us for reliable and easy to maintain infrastructure and cost levers like vector compression, so you benefit from the lowest possible total cost of ownership for building search experiences powered by AI.</p>]]></content:encoded>
    <link>https://www.elastic.co/search-labs/blog/elasticsearch-vector-profile-gcp</link>
    <guid isPermaLink="true">https://www.elastic.co/search-labs/blog/elasticsearch-vector-profile-gcp</guid>
    <category><![CDATA[Vector Database]]></category>
    <category><![CDATA[AI]]></category>
    <category><![CDATA[Elastic Cloud Hosted]]></category>
    <dc:creator><![CDATA[Serena Chou,Jeff Vestal,Yuvraj Gupta]]></dc:creator>
    <enclosure url="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/bltbaac029d716db095/6a17d785e9ea874ffca9c421/58e00f32bef1411dbc11849a78b5ecd3c334528a-1440x570.png" length="0" type="image/png"/>
    <pubDate>Thu, 25 Apr 2024 00:00:00 GMT</pubDate>
  </item>
  <item>
    <title><![CDATA[Elasticsearch open Inference API adds support for Cohere’s Rerank 3 model]]></title>
    <description><![CDATA[“Learn about Cohere reranking, how to use Cohere's Rerank 3 model with the Elasticsearch open inference API and Elastic's roadmap for semantic reranking.”]]></description>
    <content:encoded><![CDATA[<p>Cohere's <a href="https://txt.cohere.com/rerank-3/">Rerank 3 model</a> <code>rerank-english-v3.0</code> is now available in their Rerank <a href="https://docs.cohere.com/reference/rerank-1">endpoint</a>. As the only vector database included in Cohere’s Rerank 3 launch, Elasticsearch has integrated seamless support for this new model into our open Inference API.</p><p>So briefly, what is reranking? Rerankers take the ‘top n’ search results from existing vector search and keyword search systems, and provide a semantic boost to those results. With good reranking in place, you have better ‘top n’ results without requiring you to change your model or your data indexes – ultimately providing better search results you can send to large language models (LLMs) as context.</p><p>Recently, we collaborated with the Cohere team to make it easy for Elasticsearch developers to use Cohere’s <a href="https://www.elastic.co/search-labs/blog/elasticsearch-cohere-embeddings-support">embeddings</a> (available in <a href="https://www.elastic.co/blog/whats-new-elastic-search-8-13-0">Elasticsearch 8.13</a> and Serverless!). It is a natural evolution to include Cohere’s incredible reranking capabilities to unlock all of the tools necessary for true refinement of results past the first-stage of retrieval.</p><p>Cohere’s Rerank 3 model can be added to <em>any</em> existing Elasticsearch retrieval flow without requiring any significant code changes. Given Elastic’s vector database and hybrid search capabilities, users can also bring embeddings from any 3rd party model to Elastic, to use with Rerank 3.</p><h2>Elastic’s approach to hybrid search</h2><p>When looking to implement RAG (Retrieval Augmented Generation), the strategy for retrieval and reranking is a key optimization for customers to ground LLMs and achieve accurate results. Customers have trusted Elastic for years with their private data, and are able to leverage several first-stage retrieval algorithms (e.g. for BM25/keyword, dense, and sparse vector retrieval). More importantly, most real-world search use cases benefit from <a href="https://www.elastic.co/blog/improving-information-retrieval-elastic-stack-hybrid">hybrid search</a> which we have supported since Elasticsearch <a href="https://www.elastic.co/blog/whats-new-elastic-enterprise-search-8-9-0">8.9</a>.</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt1eee7a702c738a0a/6a171220d7c022784fde65d8/855663e958a2100d87f534883507bdd6cca46686-1440x897.png" alt="reranking" /><p>For mid-stage reranking, we also offer native support for <a href="https://www.elastic.co/guide/en/elasticsearch/reference/current/learning-to-rank.html">Learning To Rank </a>and <a href="https://www.elastic.co/guide/en/elasticsearch/reference/7.17/filter-search-results.html#rescore">query rescore</a>. In this walkthrough, we will focus on Cohere’s last stage reranking capabilities, and will cover Elastic’s mid stage reranking capabilities in a subsequent blog post!</p><h2>Cohere’s approach to reranking</h2><p>Cohere has seen phenomenal results with their new Rerank model. In the testing, Cohere is reporting that reranking models in particular benefit from long context. Chunking for model token limits is a necessary constraint when preparing your document for dense vector retrieval. But with Cohere’s approach for reranking, a considerable benefit to reranking can be seen based on context contained in the full document, rather than a specific chunk within the document. Rerank has a 4k token limit to enable the input of more context to unlock the full relevance benefits of incorporating this model into your Elasticsearch based search system.</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt8acce02c4315035c/6a171221acf08840f7be9c79/485637d45e3b3aa0d6f7d7ad53434144ac590361-1440x883.png" alt="cohere results" /><p>(i) General retrieval based on BEIR benchmark; accuracy measured as nDCG@10</p><p>(ii) Code retrieval based on 6 common code benchmarks; accuracy measured as nDCG@10</p><p>(iii) Long context retrieval based on 7 common benchmarks; accuracy measured as nDCG@10</p><p>(iv) Semi-structured (JSON) retrieval based on 4 common benchmarks; accuracy measured as nDCG@10</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt22ff716baf6d1636/6a1712231949f7ead1e7ab62/8b5f5438bcb5dbc103c2a2e34088dedf59697025-571x326.png" alt="rag" /><p>If you’re interested in how to chunk with <a href="https://www.elastic.co/search-labs/integrations/langchain">LangChain</a> and <a href="https://www.elastic.co/search-labs/integrations/llama-index">LlamaIndex</a>, we provide chat application reference code, integrations and more in <a href="https://www.elastic.co/search-labs">Search Labs</a> and our open source <a href="https://github.com/elastic/elasticsearch-labs">repository</a>. Alternatively, you can leverage Elastic’s <a href="https://www.elastic.co/search-labs/blog/adding-passage-vector-search-to-lucene">passage retrieval</a> capabilities and chunk with <a href="https://www.elastic.co/search-labs/blog/chunking-via-ingest-pipelines">ingest pipelines</a>.</p><h2>Building a RAG implementation with Elasticsearch and Cohere</h2><p>Now that you have a general understanding of how these capabilities can be leveraged, let’s jump into an example on building a RAG implementation with Elasticsearch and Cohere.</p><p>You'll need a <code>Cohere</code> account and some working knowledge of the Cohere <a href="https://docs.cohere.com/reference/rerank-1">Rerank endpoint</a>. If you’re intending to use Cohere’s newest generative model <code>Command R+</code> familiarize yourself with the <a href="https://docs.cohere.com/reference/chat">Chat endpoint</a>.</p><p>In <a href="https://www.elastic.co/kibana">Kibana</a>, you'll have access to a console for you to input these next steps in Elasticsearch even without an IDE set up. If you prefer to use a language client - you can revisit these steps in the <a href="https://docs.cohere.com/docs/elasticsearch-and-cohere">provided guide</a>.</p><h2>Elasticsearch vector database</h2><p>In an earlier announcement, we had some steps to get you started with the Elasticsearch vector database. You can review the steps to cover ingesting a sample <code>books</code> catalog, and generate embeddings using Cohere’s Embed capabilities by reading the <a href="https://www.elastic.co/search-labs/blog/elasticsearch-cohere-embeddings-support">announcement</a>. Alternatively, if you prefer we also provide a <a href="https://www.elastic.co/guide/en/elasticsearch/reference/current/semantic-search-inference.html">tutorial</a> and <a href="https://github.com/elastic/elasticsearch-labs/blob/main/notebooks/integrations/cohere/inference-cohere.ipynb">Jupyter notebook</a> to get you started on this process.</p><h2>Cohere reranking</h2><p>The following section assumes that you’ve ingested data and have issued your first search. This will give you a baseline as to how the search results are ranked with your first dense vector retrieval.</p><p>The previous announcement concluded with a query issued against the sample <code>books</code> catalog, and, and generated the following results in response to the query string “Snow”. These results are returned in descending order of relevance.</p>    {
      "took": 201,
      "timed_out": false,
      "_shards": {
        "total": 3,
        "successful": 3,
        "skipped": 0,
        "failed": 0
      },
      "hits": {
        "total": {
          "value": 6,
          "relation": "eq"
        },
        "max_score": 0.80008936,
        "hits": [
          {
            "_index": "cohere-embeddings",
            "_id": "3VAixI4Bi8x57NL3O03c",
            "_score": 0.80008936,
            "_source": {
              "name": "Snow Crash",
              "author": "Neal Stephenson"
            }
          },
          {
            "_index": "cohere-embeddings",
            "_id": "4FAixI4Bi8x57NL3O03c",
            "_score": 0.6495671,
            "_source": {
              "name": "Fahrenheit 451",
              "author": "Ray Bradbury"
            }
          },
          {
            "_index": "cohere-embeddings",
            "_id": "31AixI4Bi8x57NL3O03c",
            "_score": 0.62768984,
            "_source": {
              "name": "1984",
              "author": "George Orwell"
            }
          },
          {
            "_index": "cohere-embeddings",
            "_id": "4VAixI4Bi8x57NL3O03c",
            "_score": 0.6197722,
            "_source": {
              "name": "Brave New World",
              "author": "Aldous Huxley"
            }
          },
          {
            "_index": "cohere-embeddings",
            "_id": "3lAixI4Bi8x57NL3O03c",
            "_score": 0.61449933,
            "_source": {
              "name": "Revelation Space",
              "author": "Alastair Reynolds"
            }
          },
          {
            "_index": "cohere-embeddings",
            "_id": "4lAixI4Bi8x57NL3O03c",
            "_score": 0.59593034,
            "_source": {
              "name": "The Handmaid's Tale",
              "author": "Margaret Atwood"
            }
          }
        ]
      }
    }
<p>You’ll next want to configure an inference endpoint for Cohere Rerank by specifying the Rerank 3 model and API key.</p>    PUT _inference/rerank/cohere_rerank 
    {
        "service": "cohere",
        "service_settings": {
            "api_key": &lt;API-KEY&gt;, 
            "model_id": "rerank-english-v3.0"
        },
        "task_settings": {
            "top_n": 10,
            "return_documents": true
        }
    }
<p>Once this inference endpoint is specified, you’ll now be able to rerank your results by passing in the original query used for retrieval, “Snow” along with the documents we just retrieved with the kNN search. Remember, you can repeat this with any <a href="https://www.elastic.co/blog/improving-information-retrieval-elastic-stack-hybrid">hybrid</a> <a href="https://github.com/elastic/elasticsearch-labs/blob/main/notebooks/search/02-hybrid-search.ipynb">search</a> query as well!</p><p>To demonstrate this while still using the dev console, we’ll do a little cleanup on the JSON response above.</p><p>Take the <code>hits</code> from the JSON response and form the following JSON for the <code>input</code>, and then POST to the cohere_rerank endpoint we just configured.</p>    POST _inference/rerank/cohere_rerank
    {
      "input": ["Snow Crash", "Fahrenheit 451", "1984", "Brave New World","Revelation Space", "The Handmaid's Tale"], 
      "query": "Snow" 
    }
<p>And there you have it, your results have been reranked using Cohere's Rerank 3 model.</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/bltd73a44a4694ab658/6a171224964cea5e0108bcef/7b1f1c0c012efa2d029051bda0c72e4a75834793-1440x874.png" alt="Kibana rerank" /><p>The <code>books</code> corpus that we used to illustrate these capabilities does not contain large passages, and is a relatively simple example. When instrumenting this for your own search experience, we recommend that you follow Cohere’s approach to populate your <code>input</code> with the context from the full documents returned from the first retrieved result set, not just a retrieved chunk within the documents.</p><h2>Elasticsearch’s accelerated roadmap to semantic reranking and retrievers</h2><p>In <strong>upcoming</strong> versions of Elasticsearch we will continue to build seamless support for mid and final stage rerankers. Our end goal is to enable developers to have the ability to use semantic reranking to improve the results from any search whether it is BM25, dense or sparse vector retrieval, or a combination with hybrid retrieval. To provide this experience, we are building a concept called <code>retrievers</code> into the query DSL. Retrievers will provide an intuitive way to execute semantic reranking, and will also enable direct execution of what you’ve configured in the open inference API in the Elasticsearch stack without relying on you to execute this in your application logic.</p><p>When incorporating the use of retrievers in the earlier dense vector example, this is how different the reranking experience can be:</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt436f1768726bf40d/6a171226e8fbcedab039fd7d/f80a4ef6f5793f1c3fb8f84706ca9a87336acdbd-611x223.png" alt="rag roadmap" /><p>(i) <strong>Elastic’s roadmap:</strong> The indexing step is simplified with the addition of Elastic’s future capabilities to automatically chunk indexed data</p><p>(ii) <strong>Elastic’s roadmap:</strong> The kNN retriever specifies the model (in this case Cohere’s Rerank 3) that was configured as an inference endpoint</p><p>(iii) <strong>Cohere’s roadmap:</strong> The step between sending the resulting data to Cohere’s Command R+ will benefit from a planned feature named <code>extractive snippets</code> which will enable the user to return a relevant chunk of the reranked document to the Command R+ model</p><p>This was our original kNN dense vector search executed on the <code>books</code> corpus to return the first set of results for “Snow”.</p>    GET cohere-embeddings/_search
    {
      "knn": {
        "field": "name_embedding",
        "query_vector_builder": {
          "text_embedding": {
            "model_id": "cohere_embeddings",
            "model_text": "Snow"
          }
        },
        "k": 10,
        "num_candidates": 100
      },
      "_source": [
        "name",
        "author"
      ]
    }
<p>As explained in this blog, there are a few steps to retrieve the documents and pass on the correct response to the inference endpoint. At the time of this publication, this logic should be handled in your application code.</p><p>In the future, retrievers can be configured to use the Cohere rerank inference endpoint directly within a single API call.</p>    {
      "retriever": {
        "text_similarity_rank": {
          "retriever": {
            "knn": {
              "field": "name_embedding",
              "query_vector_builder": {
                "text_embedding": {
                  "model_id": "cohere_embeddings",
                  "model_text": "Snow"
                }
              },
              "k": 10,
              "num_candidates": 100
            }
          },
          "field": "name",
          "window_size": 10,
          "inference_id": "cohere_rerank",
          "inference_text": "Snow"
        }
      },
      "_source": [
        "name",
        "author"
      ]
    }
<p>In this case, the kNN query is exactly the same as my original, but the cleansing of the response before input to the rerank endpoint will no longer be a necessary step. A retriever will know that a kNN query has been executed and seamlessly rerank using the Cohere rerank inference endpoint specified in the configuration. This same principle can be applied to <strong>any</strong> search, BM25, dense, sparse and hybrid.</p><p>Retrievers as an enabler of great semantic reranking is on our active and near term roadmap.</p><h2>Cohere’s generative model capabilities</h2><p>Now you’re ready with a semantically reranked set of documents that can be used to ground the responses for the large language model of your choice! We recommend Cohere’s newest generative model <code>Command R+</code>. When building the full RAG pipeline, in your application code you can easily issue a command to Cohere’s Chat API with the user query and the reranked documents.</p><p>An example of how this might be achieved in your <a href="https://elasticsearch-py.readthedocs.io/en/v8.13.0/">Python</a> application code can be seen below:</p>    response = co.chat(message=query, documents=documents, model='command-r-plus')

    source_documents = []
    for citation in response.citations:
        for document_id in citation.document_ids:
            if document_id not in source_documents:
                source_documents.append(document_id)

    print(f"Query: {query}")
    print(f"Response: {response.text}")
    print("Sources:")
    for document in response.documents:
        if document['id'] in source_documents:
            print(f"{document['title']}: {document['text']}")
<p>This integration with Cohere is offered in <a href="https://www.elastic.co/blog/elastic-serverless-architecture">Serverless</a> and soon will be available to try in a versioned Elasticsearch release either on Elastic Cloud or on your laptop or self-managed environment. We recommend you use our <a href="https://github.com/elastic/elasticsearch-serverless-python/releases/tag/v0.2.0.20231031">Elastic Python client v0.2.0</a> against your Serverless project to get started!</p><p>Happy reranking!</p>]]></content:encoded>
    <link>https://www.elastic.co/search-labs/blog/elasticsearch-cohere-rerank</link>
    <guid isPermaLink="true">https://www.elastic.co/search-labs/blog/elasticsearch-cohere-rerank</guid>
    <category><![CDATA[Vector Database]]></category>
    <category><![CDATA[AI]]></category>
    <dc:creator><![CDATA[Serena Chou,Max Hniebergall]]></dc:creator>
    <enclosure url="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blte1f1748575c25298/6a1712272b835f8974f4b33b/808a666fc35b91149ce28e0a37769cff2554b6f5-1440x863.png" length="0" type="image/png"/>
    <pubDate>Thu, 11 Apr 2024 00:00:00 GMT</pubDate>
  </item>
  <item>
    <title><![CDATA[Avatar assisted & dialogue driven voice to RAG search]]></title>
    <description><![CDATA[Create avatar-assisted voice search experience by integrating speech-to-text, semantic search, RAG and a synthesized avatar for responses.]]></description>
    <content:encoded><![CDATA[<h2>The evolution of search</h2><p>Search has evolved from simple text queries yielding straightforward results to a complex system accommodating various formats like text, images, videos, and questions.</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt24559d31d5b7b8f6/6a170b206f7f04db68914857/128bd2fe93461638e6ac28ca994a07c5a21e9c70-1440x659.png" alt="Legacy Search" /><p>Search not too long ago comprised of a text query and relevant results. Today's search results are enhanced with generative AI, machine learning, and interactive chat features, offering a richer, more dynamic, and contextually relevant user experience. Additionally, voice search and speech avatars have transformed traditional search, offering a more interactive and convenient user experience.</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/bltd19b4894abd43db2/6a170b22cf4f25868db2d18b/1ebe15a22c031bc439cb2500c21d1c60f79b008c-1440x781.png" alt="Search today" /><h3>The desire for dialogue in search</h3><p>In a realm where dialogue underpins every interaction, whether with fellow humans or bots, shouldn't our search experiences reflect this fundamental aspect? Envision the vast array of document corpora residing within an enterprise. Naturally, this environment sparks curiosity and a multitude of questions, leading to subsequent inquiries. This innate human trait drives us to seek answers, delve deeper following initial responses, and continuously explore. Yet, traditional question-and-answer mechanisms fall short, as they often disregard the context of preceding exchanges, leading to a disjointed and laborious process that feels unnatural and prompts users to disengage prematurely.</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt1ec8ee317d02777e/6a170b238b73cb33a818a046/f3feb0843430a550151182aba95648ce5b45820b-1440x766.png" alt="you have questions" /><h3>Beyond question and answer search</h3><p>Consider the act of using a television to search for content, such as seeking action movies featuring Nicolas Cage. While most current systems adeptly provide relevant results, the inquiry rarely ends there. Subsequent questions, such as inquiring about the runtime or release dates of these movies, are a natural progression in our quest for information. However, standard search applications are not designed to facilitate a continuous dialogue; they are structured around isolated question-and-answer formats, which limits the depth of interaction and exploration.</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt8ae209de746281fb/6a170b2566c4f994d9f8c043/60e914ec7dc1809b0f8a18eb2ec186fa3f2e890b-1006x430.png" alt="results" /><h2>Avatar assisted voice search experience</h2><p>This is where the concept of an avatar-assisted search experience comes into play, especially in scenarios where users, myself included, prefer direct answers without the need to sift through information. Occasionally, we desire the convenience of having answers delivered to us, bypassing the effort of reading through content. The development of an avatar to generate responses could further modernize this interaction, providing a more engaging, efficient, and natural user experience.</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt3a6145eda1e0487f/6a170b2714b2704159e3c625/4e77349fb83be542c9817a65cb6a905e18879f6c-1116x1174.png" alt="results" /><h2>Live demo: creating an avatar assisted voice search experience</h2><p>This demo showcases a seamless integration of speech-to-text, Elasticsearch's semantic search capabilities, Azure OpenAI's RAG, and a synthesized avatar for responses.</p><h2>Integration details</h2><h4>Speech to search</h4><p>The advanced search experience begins with user voice interactions, which are converted into text by Azure Speech to Text, forming the basis of the search query. This query is then processed through Elasticsearch, using the ELSER, to retrieve relevant documents, such as TV guides listing “action movies featuring Nicolas Cage.” This ensures precision and relevance in the search results.</p><h4>RAG &amp; cache</h4><p>In the enhanced search framework, merely fetching documents isn't enough. Azure OpenAI's GPT-4 refines raw data into understandable responses, ensuring smooth conversation flow. Additionally, Elasticsearch boosts efficiency as a GenAI caching layer, recycling answers for related queries, thus conserving resources. For example, if there's a cached response for "action movies featuring Nicolas Cage," the caching API will swiftly use this for similar questions like “Nicolas Cage high-intensity movies,” accelerating the search experience.</p><h4>Avatar response generation</h4><p>The experience is further enriched with an avatar response feature, powered by Azure Synthesizer, adding a visual and auditory dimension that surpasses traditional text-based interfaces. This creates a more engaging and interactive user experience, integrating various advanced technologies to deliver a dynamic, intuitive, and compelling search experience.</p><h2>Summary</h2><p>The shift from traditional Google searches to platforms like ChatGPT for answering queries illustrates a broader trend: our preference for dialogue over static information retrieval. This predilection underscores the importance for enterprises to adopt a more intuitive and conversational approach in their search functionalities. By embracing this paradigm, businesses can better align with the natural human inclination towards dialogue, thereby enhancing the overall search and discovery process within their data ecosystems.</p><h2>Demo assets</h2><p>Still curious, here is the <a href="https://github.com/sunileman/voice-movie-search">link to the source code</a>.</p>]]></content:encoded>
    <link>https://www.elastic.co/search-labs/blog/avatar-assisted-dialogue-driven-voice-to-rag-search</link>
    <guid isPermaLink="true">https://www.elastic.co/search-labs/blog/avatar-assisted-dialogue-driven-voice-to-rag-search</guid>
    <category><![CDATA[AI]]></category>
    <category><![CDATA[Vector Database]]></category>
    <dc:creator><![CDATA[Sunile Manjee]]></dc:creator>
    <enclosure url="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/bltd3572b96ad92d331/6a170b29b339d560d0769fd3/9b274d1191d203babb55dc7693897fd278df1a09-1024x1024.png" length="0" type="image/png"/>
    <pubDate>Fri, 08 Mar 2024 00:00:00 GMT</pubDate>
  </item>
  <item>
    <title><![CDATA[AI plagiarism: Plagiarism detection with Elasticsearch]]></title>
    <description><![CDATA[Here's how to check for AI plagiarism using Elasticsearch, focusing on use cases with NLP models and Vector Search.]]></description>
    <content:encoded><![CDATA[<p>Plagiarism can be <strong>direct</strong>, involving the copying of parts or the entire content, or <strong>paraphrased</strong>, where the author's work is rephrased by changing some words or phrases.</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/bltb6e139760e56ec98/6a171147dc55de0ad2e00edf/5d7073187fda829438aeec8d3a1194a5bea2ba57-1440x347.png" alt="" /><p>There is a distinction between inspiration and paraphrasing. It is possible to read a content, get inspired, and then explore the idea with your own words, even if you come to a similar conclusion.</p><p>While plagiarism has been a topic of discussion for a long time, the accelerated production and publication of content have kept it relevant and posed an ongoing challenge.</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/bltc2af1678cb04506b/6a171149cf4f251c2ab2d257/a0b9a98d729db09dae0a79315c001e6763c12704-1400x1016.png" alt="" /><p>This challenge isn't limited to books, academic research, or judicial documents, where plagiarism checks are frequently conducted. It can also extend to newspapers and even social media.</p><p>With the abundance of information and easy access to publishing, how can plagiarism be effectively checked on a scalable level?</p><p>Universities, government entities, and companies employ diverse tools, but while a straightforward <a href="https://www.elastic.co/search-labs/lexical-and-semantic-search-with-elasticsearch">lexical search</a> can effectively detect direct plagiarism, the primary challenge lies in identifying <strong>paraphrased content.</strong></p><h2>Plagiarism detection with Generative AI</h2><p>A new challenge emerges with Generative AI. Is content generated by AI considered plagiarism when copied?</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt8a1ed1b6fa3f1a56/6a17114b4a531bc40736aa69/9345b28d6d27c37469bc38e823c41780b4eabfe5-1440x875.png" alt="" /><p>The <a href="https://openai.com/">OpenAI</a> <a href="https://openai.com/policies/terms-of-use">terms of use</a>, for example, specify that OpenAI will not claim copyright over content generated by the API for users. In this case, individuals using their Generative AI can use the generated content as they prefer without citation.</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/bltbc2e08ab5b808e38/6a17114dab7f086cbadb9f93/e1f415f69a247666f02ddc81468944920c874cd7-968x814.png" alt="" /><p>However, the acceptance of using Generative AI to improve efficiency remains a topic of discussion.</p><p>In an effort to contribute to plagiarism detection, OpenAI developed a <a href="https://huggingface.co/roberta-base-openai-detector">detection model</a> but later acknowledged that its accuracy is not sufficiently high.</p><p><em>"We believe this is not high enough accuracy for standalone detection and needs to be paired with metadata-based approaches, human judgment, and public education to be more effective."</em></p><p>The challenge persists; however, with the availability of more tools, there are now increased options for detecting plagiarism, even in cases of paraphrased and AI content.</p><h2>Detecting plagiarism with Elasticsearch</h2><p>Recognizing this, in this blog we are exploring one more use case with Natural Language Processing (NLP) models and Vector Search, plagiarism detection, beyond metadata searches.</p><p>This is demonstrated with <a href="https://github.com/elastic/elasticsearch-labs/blob/main/supporting-blog-content/plagiarism-detection-with-elasticsearch/plagiarism_detection_es.ipynb">Python examples</a>, where we utilize a <a href="https://sbert.net/datasets/emnlp2016-2018.json">dataset</a> from <a href="https://www.sbert.net/">SentenceTransformers</a> containing NLP-related articles. We check the abstracts for plagiarism by performing 'semantic textual similarity' considering 'abstract' embeddings generated with a <a href="https://huggingface.co/sentence-transformers/all-mpnet-base-v2">text embedding model</a> previously imported into Elasticsearch. Additionally, to identify AI-generated content — AI plagiarism, an <a href="https://huggingface.co/roberta-base-openai-detector">NLP model</a> developed by OpenAI was also imported into Elasticsearch.</p><p>The following image illustrates the data flow:</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt0464f88d3ed12070/6a17114fab7f084905db9f97/1ad89c98a2f42a497548ca3947749bad54ec1172-1440x880.png" alt="" /><p>During the <a href="https://www.elastic.co/guide/en/elasticsearch/reference/current/ingest.html">ingest pipeline</a> with an <a href="https://www.elastic.co/guide/en/elasticsearch/reference/current/inference-processor.html">inference processor</a>, the 'abstract' paragraph is mapped to a 768-dimensional vector, the 'abstract_vector.predicted_value'.</p><p>Mapping:</p>"abstract_vector.predicted_value": { # Inference results field
"type": "dense_vector", 
"dims": 768, # model embedding_size
"index": "true", 
"similarity": "dot_product" # When indexing vectors for approximate kNN search, you need to specify the similarity function for comparing the vectors.
<p>The similarity between vector representations is measured using a vector similarity metric, defined using the 'similarity' <a href="https://www.elastic.co/guide/en/elasticsearch/reference/current/dense-vector.html#dense-vector-params">parameter</a>.</p><p><a href="https://en.wikipedia.org/wiki/Cosine_similarity">Cosine</a> is the default similarity metric, computed as '(1 + cosine(query, vector)) / 2'. Unless you need to preserve the original vectors and cannot normalize them in advance, the most efficient way to perform cosine similarity is to normalize all vectors to unit length. This helps avoid performing extra vector length computations during the search, instead use 'dot_product'.</p><p>In this same pipeline, another inference processor containing the <a href="https://huggingface.co/roberta-base-openai-detector">text classification model</a> detects whether the content is 'Real' probably written by humans, or 'Fake' probably written by AI, adding the 'openai-detector.predicted_value' to each document.</p><p>Ingest Pipeline:</p>client.ingest.put_pipeline( 
    id="plagiarism-checker-pipeline",
    processors = [
    {
      "inference": { #for ml models - to infer against the data that is being ingested in the pipeline
        "model_id": "roberta-base-openai-detector", #text classification model id
        "target_field": "openai-detector", # Target field for the inference results
        "field_map": { #Maps the document field names to the known field names of the model.
        "abstract": "text_field" # Field matching our configured trained model input. 
        }
      }
    },
    {
      "inference": {
        "model_id": "sentence-transformers__all-mpnet-base-v2", #text embedding model id
        "target_field": "abstract_vector", # Target field for the inference results
        "field_map": {
        "abstract": "text_field" # Field matching our configured trained model input. Typically for NLP models, the field name is text_field.
        }
      }
    }
    
  ]
)
<p>At query time, the same text embedding model is also employed to generate the vector representation of the query 'model_text' in a 'query_vector_builder' object.</p><p>A k-nearest neighbor (kNN) search finds the k nearest vector to the query vector measured by the similarity metric.</p><p>The _score of each document is derived from the similarity, ensuring that a larger score corresponds to a higher ranking. This means that the document is more similar semantically. As a result, we are printing three possibilities: if score &gt; 0.9, we are considering 'high similarity'; if &lt; 0.7, 'low similarity’, otherwise, 'moderate similarity’. You have the flexibility to set different threshold values to determine what level of _score qualifies as plagiarism or not, based on your use case.</p><p>Additionally, text classification is performed to also check for AI-generated elements in the text query.</p><p>Query:</p>from elasticsearch import Elasticsearch
from elasticsearch.client import MlClient

#duplicated text - direct plagiarism test

model_text = 'Understanding and reasoning about cooking recipes is a fruitful research direction towards enabling machines to interpret procedural text. In this work, we introduce RecipeQA, a dataset for multimodal comprehension of cooking recipes. It comprises of approximately 20K instructional recipes with multiple modalities such as titles, descriptions and aligned set of images. With over 36K automatically generated question-answer pairs, we design a set of comprehension and reasoning tasks that require joint understanding of images and text, capturing the temporal flow of events and making sense of procedural knowledge. Our preliminary results indicate that RecipeQA will serve as a challenging test bed and an ideal benchmark for evaluating machine comprehension systems. The data and leaderboard are available at http://hucvl.github.io/recipeqa.'

response = client.search(index='plagiarism-checker', size=1,
    knn={
        "field": "abstract_vector.predicted_value",
        "k": 9,
        "num_candidates": 974,
        "query_vector_builder": { #The 'all-mpnet-base-v2' model is also employed to generate the vector representation of the query in a 'query_vector_builder' object.
            "text_embedding": {
                "model_id": "sentence-transformers__all-mpnet-base-v2",
                "model_text": model_text
            }
        }
    }
)

for hit in response['hits']['hits']:
    score = hit['_score']
    title = hit['_source']['title']
    abstract = hit['_source']['abstract']
    openai = hit['_source']['openai-detector']['predicted_value']
    url = hit['_source']['url']

    if score &gt; 0.9:
        print(f"\nHigh similarity detected! This might be plagiarism.")
        print(f"\nMost similar document: '{title}'\n\nAbstract: {abstract}\n\nurl: {url}\n\nScore:{score}\n\n")

        if openai == 'Fake':
            print("This document may have been created by AI.\n")

    elif score &lt; 0.7:
        print(f"\nLow similarity detected. This might not be plagiarism.")

        if openai == 'Fake':
            print("This document may have been created by AI.\n")

    else:
        print(f"\nModerate similarity detected.")
        print(f"\nMost similar document: '{title}'\n\nAbstract: {abstract}\n\nurl: {url}\n\nScore:{score}\n\n")

        if openai == 'Fake':
            print("This document may have been created by AI.\n")

ml_client = MlClient(client)

model_id = 'roberta-base-openai-detector' #open ai text classification model

document = [
    {
        "text_field": model_text
    }
]

ml_response = ml_client.infer_trained_model(model_id=model_id, docs=document)

predicted_value = ml_response['inference_results'][0]['predicted_value']

if predicted_value == 'Fake':
    print("\nNote: The text query you entered may have been generated by AI.\n")
<p>Output:</p>High similarity detected! This might be plagiarism.

Most similar document: 'RecipeQA: A Challenge Dataset for Multimodal Comprehension of Cooking Recipes'

Abstract: Understanding and reasoning about cooking recipes is a fruitful research direction towards enabling machines to interpret procedural text. In this work, we introduce RecipeQA, a dataset for multimodal comprehension of cooking recipes. It comprises of approximately 20K instructional recipes with multiple modalities such as titles, descriptions and aligned set of images. With over 36K automatically generated question-answer pairs, we design a set of comprehension and reasoning tasks that require joint understanding of images and text, capturing the temporal flow of events and making sense of procedural knowledge. Our preliminary results indicate that RecipeQA will serve as a challenging test bed and an ideal benchmark for evaluating machine comprehension systems. The data and leaderboard are available at[ http://hucvl.github.io/recipeqa](http://hucvl.github.io/recipeqa).

url:[http://aclweb.org/anthology/D18-1166](http://aclweb.org/anthology/D18-1166)

Score:1.0
<p>In this example, after utilizing one of the 'abstract' values from our dataset as the text query 'model_text', plagiarism was identified. The similarity score is 1.0, indicating a high level of similarity — <strong>direct plagiarism</strong>. The vectorized query and document were not recognized as AI-generated content, which was expected.</p><p>Query:</p>#similar text - paraphrase plagiarism test 

model_text = 'Comprehending and deducing information from culinary instructions represents a promising avenue for research aimed at empowering artificial intelligence to decipher step-by-step text. In this study, we present CuisineInquiry, a database for the multifaceted understanding of cooking guidelines. It encompasses a substantial number of informative recipes featuring various elements such as headings, explanations, and a matched assortment of visuals. Utilizing an extensive set of automatically crafted question-answer pairings, we formulate a series of tasks focusing on understanding and logic that necessitate a combined interpretation of visuals and written content. This involves capturing the sequential progression of events and extracting meaning from procedural expertise. Our initial findings suggest that CuisineInquiry is poised to function as a demanding experimental platform.'
<p>Output:</p>High similarity detected! This might be plagiarism.

Most similar document: 'RecipeQA: A Challenge Dataset for Multimodal Comprehension of Cooking Recipes'

Abstract: Understanding and reasoning about cooking recipes is a fruitful research direction towards enabling machines to interpret procedural text. In this work, we introduce RecipeQA, a dataset for multimodal comprehension of cooking recipes. It comprises of approximately 20K instructional recipes with multiple modalities such as titles, descriptions and aligned set of images. With over 36K automatically generated question-answer pairs, we design a set of comprehension and reasoning tasks that require joint understanding of images and text, capturing the temporal flow of events and making sense of procedural knowledge. Our preliminary results indicate that RecipeQA will serve as a challenging test bed and an ideal benchmark for evaluating machine comprehension systems. The data and leaderboard are available at[ http://hucvl.github.io/recipeqa](http://hucvl.github.io/recipeqa).

url:[http://aclweb.org/anthology/D18-1166](http://aclweb.org/anthology/D18-1166)

Score:0.9302529

Note: The text query you entered may have been generated by AI.
<p>By updating the text query 'model_text' with an AI-generated text that conveys the same message while minimizing the repetition of similar words, the detected similarity was still high, but the score was 0.9302529 instead of 1.0 — <strong>paraphrase plagiarism</strong>. It was also expected that this query, which was generated by AI, would be detected.</p><p>Lastly, considering the text query 'model_text' as a text about Elasticsearch, which is not an abstract of one of these documents, the detected similarity was 0.68991005, indicating low similarity according to the considered threshold values.</p><p>Query:</p>#different text - not a plagiarism

model_text = 'Elasticsearch provides near real-time search and analytics for all types of data.'
<p>Output:</p>Low similarity detected. This might not be plagiarism.
<p>Although plagiarism was accurately identified in the text query generated by AI, as well as in cases of paraphrasing and direct copied content, navigating the landscape of plagiarism detection involves acknowledging various aspects.</p><p>In the context of AI-generated content detection, we explored a model that makes a valuable contribution. However, it is crucial to recognize the inherent limitations in standalone detection, necessitating the incorporation of other methods to boost the accuracy.</p><p>The variability introduced by the choice of text embedding models is another consideration. Different models, trained with distinct datasets, result in varying levels of similarity, highlighting the importance of the text embeddings generated.</p><p>Lastly, in these examples, we used the document's abstract. However, plagiarism detection often involves large documents, making it essential to address the challenge of text length. It is common for the text to exceed a model's token limit, requiring segmentation into chunks before building embeddings. A <a href="https://www.elastic.co/guide/en/elasticsearch/reference/8.11/knn-search.html#nested-knn-search">practical approach</a> to handling this involves utilizing nested structures with dense_vector.</p><h2>Conclusion</h2><p>In this blog, we discussed the challenges of detecting plagiarism, particularly in paraphrased and AI-generated content, and how semantic textual similarity and text classification can be used for this purpose.</p><p>By combining these methods, we provided an example of plagiarism detection where we successfully identified AI-generated content, direct and paraphrased plagiarism.</p><p>The primary goal was to establish a filtering system that simplifies detection but human assessment remains essential for validation.</p><p>If you are interested in learning more about semantic textual similarity and NLP, we encourage you to also check out these links:</p><ul><li><p><a href="https://www.elastic.co/what-is/semantic-search">What is semantic search?</a></p></li><li><p><a href="https://www.elastic.co/what-is/natural-language-processing">What is natural language processing (NLP)?</a></p></li><li><p><a href="https://www.elastic.co/search-labs/blog/lexical-and-semantic-search-with-elasticsearch">Lexical and Semantic Search with Elasticsearch</a></p></li><li><p><a href="https://www.elastic.co/search-labs/blog/chunking-via-ingest-pipelines">Chunking Large Documents via Ingest pipelines plus nested vectors equals easy passage search</a></p></li></ul>]]></content:encoded>
    <link>https://www.elastic.co/search-labs/blog/ai-plagiarism-checker-with-elasticsearch</link>
    <guid isPermaLink="true">https://www.elastic.co/search-labs/blog/ai-plagiarism-checker-with-elasticsearch</guid>
    <category><![CDATA[Vector Database]]></category>
    <category><![CDATA[Python]]></category>
    <dc:creator><![CDATA[Priscilla Parodi]]></dc:creator>
    <enclosure url="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt68a5bc2434a9b03b/6a1711510e2e49a09641a22a/83e05cd4f81799fbb7b7950ed87600e825ec81e9-1024x1024.png" length="0" type="image/png"/>
    <pubDate>Tue, 19 Dec 2023 00:00:00 GMT</pubDate>
  </item>
  <item>
    <title><![CDATA[Introducing kNN Query: An expert way to do kNN search]]></title>
    <description><![CDATA[Explore how the kNN query in Elasticsearch can be used and how it differs from top-level kNN search, including examples.]]></description>
    <content:encoded><![CDATA[<h3>kNN search as a top-level section</h3><p><a href="https://www.elastic.co/guide/en/elasticsearch/reference/current/knn-search.html">kNN search</a> in Elasticsearch is organized as a top level section of a search request. We have designed it this way so that:</p><ul><li><p>It can always return global k nearest neighbors regardless of a number of shards</p></li><li><p>These global k results are combined with a results from other queries to form a hybrid search</p></li><li><p>The global k results are passed to aggregations to form facets.</p></li></ul><p>Here is a simplified diagram how kNN search is executed internally (some phases are omitted) :</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/bltf9df3a17c59cedde/6a170b2aab7f08ec09db9e96/24e227a8482aead389c0ad6298779fd8610006f7-2849x1020.gif" alt="Execution for top level kNN search" /><p>Figure 1: The steps for the top level kNN search are:</p><ol><li><p>A user submits a search request</p></li><li><p>The coordinator node sends a kNN search part of the request to data nodes in the DFS phase</p></li><li><p>Each data node runs kNN search and sends back the local top-k results to the coordinator</p></li><li><p>The coordinator merges all local results to form the global top k nearest neighbors.</p></li><li><p>The coordinator sends back the global k nearest neighbors to the data nodes with any additional queries provided</p></li><li><p>Each data node runs additional queries and sends back the local <code>size</code> results to the coordinator</p></li><li><p>The coordinator merges all local results and sends a response to the user</p></li></ol><p>We first run kNN search in the DFS phase to obtain the global top k results. These global k results are then passed to other parts of the search request, such as other queries or aggregations. Even the execution looks complex, from a user’s perspective this model of running kNN search is simple, as the user can always be sure that kNN search returns the global k results.</p><h3>Introducing kNN query in Elasticsearch</h3><p>With time we realized there is also a need to represent kNN search as a query. Query is a core component of a search request in Elasticsearch, and representing kNN search as a query allows for flexibility to combine it with other queries to address more complex requests.</p><p>kNN query, unlike the top level kNN search, doesn’t have a <code>k</code> parameter. The number of results (nearest neighbors) returned is defined by the <code>size</code> parameter, as in other queries. Similar to kNN search, the <code>num_candidates</code> parameter defines how many candidates to consider on each shard while executing a kNN search.</p>GET products/_search
{
 "size" : 3,
 "query": {
   "knn": {
     "field": "embedding",
     "query_vector": [2,2,2,0],
     "num_candidates": 10
   }
 }
}

<p>kNN query is executed differently from the top level kNN search. Here is a simplified diagram that describes how a kNN query is executed internally (some phases are omitted):</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/bltf2768a24d52acbbd/6a170b2c7d8d6762fd70e71a/fe505bf7e95ae94c0f7605df1348ef15536134b9-2849x1020.gif" alt="Execution for kNN query" /><p>Figure 2: The steps for query based kNN search are:</p><ol><li><p>A user submits a search request</p></li><li><p>The coordinator sends to the data nodes a kNN search query with additional queries provided</p></li><li><p>Each data node runs the query and sends back the local size results to the coordinator node</p></li><li><p>The coordinator node merges all local results and sends a response to the user</p></li></ol><p>We run kNN search on a shard to get <code>num_candidates</code> results; these results are passed to other queries and aggregations on a shard to get size results from the shard. As we don’t collect the global k nearest neighbors first, in this model the number of nearest neighbors collected and visible for other queries and aggregations depend on the number of shards.</p><h3>kNN query API examples</h3><p>Let’s look at API examples that demonstrate differences between the top level kNN search and kNN query.</p><p>We create an index of products and index some documents:</p>PUT products
{
 "mappings": {
   "dynamic": "strict",
   "properties": {
     "department": {
       "type": "keyword"
     },
     "brand": {
       "type": "keyword"
     },
     "description": {
       "type": "text"
     },
     "embedding": {
       "type": "dense_vector",
       "index": true,
       "similarity": "l2_norm"
     },
     "price": {
       "type": "float"
     }
   }
 }
}
POST products/_bulk?refresh=true
{"index":{"_id":1}}
{"department":"women","brand": "Levi's", "description":"high-rise red jeans","embedding":[1,1,1,1],"price":100}
{"index":{"_id":2}}
{"department":"women","brand": "Calvin Klein","description":"high-rise beautiful jeans","embedding":[1,1,1,1],"price":250}
{"index":{"_id":3}}
{"department":"women","brand": "Gap","description":"every day jeans","embedding":[1,1,1,1],"price":50}
{"index":{"_id":4}}
{"department":"women","brand": "Levi's","description":"jeans","embedding":[2,2,2,0],"price":75}
{"index":{"_id":5}}
{"department":"women","brand": "Levi's","description":"luxury jeans","embedding":[2,2,2,0],"price":150}
{"index":{"_id":6}}
{"department":"men","brand": "Levi's", "description":"jeans","embedding":[2,2,2,0],"price":50}
{"index":{"_id":7}}
{"department":"women","brand": "Levi's", "description":"jeans 2023","embedding":[2,2,2,0],"price":150}
<p>kNN query similar to the top level kNN search, has <code>num_candidates</code> and an internal <code>filter</code> parameter that acts as a pre-filter.</p>GET products/_search
{
 "size" : 3,
 "query": {
   "knn": {
     "field": "embedding",
     "query_vector": [2,2,2,0],
     "num_candidates": 10,
     "filter" : {
       "term" : {
         "department" : "women"
       }
     }
   }
 }
} 
<p>kNN query can get more diverse results than kNN search for collapsing and aggregations. For the kNN query below, on each shard we execute kNN search to obtain 10 nearest neighbors which are then passed to collapse to get 3 top results. Thus, we will get 3 diverse hits in a response.</p>GET products/_search
{
 "size" : 3,
 "query": {
   "knn": {
     "field": "embedding",
     "query_vector": [2,2,2,0],
     "num_candidates": 10,
     "filter" : {
       "term" : {
         "department" : "women"
       }
     }
   }
 },
 "collapse": {
   "field": "brand"        
 }
}
<p>The top level kNN search first gets the global top 3 results in the DFS phase, and then passes them to collapse in the query phase. We will get only 1 hit in a response, as all the global 3 nearest neighbors happened to be from the same brand.</p>GET products/_search?size=3
{
 "knn" : {
   "field": "embedding",
     "query_vector": [2,2,2,0],
     "k" : 3,
     "num_candidates": 10,
     "filter" : {
       "term" : {
         "department" : "women"
       }
     }
 },
 "collapse": {
   "field": "brand"        
 }
}
<p>Similarly for aggregations, a kNN query allows us to get 3 distinct buckets, while kNN search only allows 1.</p>GET products/_search
{
"size": 0,
"query": {
   "knn": {
     "field": "embedding",
     "query_vector": [2,2,2,0],
     "num_candidates": 10,
     "filter" : {
       "term" : {
         "department" : "women"
       }
     }
   }
 },
 "aggs": {
   "brands": {
     "terms": {
       "field": "brand"
     }
   }
 }
}
​
GET products/_search
{
"size": 0,
"knn" : {
 "field": "embedding",
   "query_vector": [2,2,2,0],
   "k" : 3,
   "num_candidates": 10,
   "filter" : {
     "term" : {
       "department" : "women"
     }
   }
 },
 "aggs": {
   "brands": {
     "terms": {
       "field": "brand"
     }
   }
 }
}
<p>Now, let’s look at other examples that show the flexibility of the kNN query. Specifically, how it can be flexibly combined with other queries.</p><p>kNN can be a part of a <a href="https://www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl-bool-query.html">boolean</a> query (with a caveat that all external query filters are applied as post-filters for kNN search). We can use a <a href="https://www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl-bool-query.html#named-queries">_name</a> parameter for kNN query to enhance results with extra information that tells if the kNN query was a match and its score contribution.</p>GET products/_search?include_named_queries_score
{
 "size": 3,
 "query": {
   "bool": {
     "should": [
       {
         "knn": {
           "field": "embedding",
           "query_vector": [2,2,2,0],
           "num_candidates": 10,
           "_name": "knn_query"
         }
       },
       {
         "match": {
           "description": {
             "query": "luxury",
             "_name": "bm25query"
           }
         }
       }
     ]
   }
 }
}
<p>kNN can also be a part of complex queries, such as a <a href="https://www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl-pinned-query.html">pinned</a> query. This is useful when we want to display the top nearest results, but also want to promote a selected number of other results.</p>GET products/_search
{
 "size": 3,
 "query": {
   "pinned": {
     "ids": [ "1", "2" ],
     "organic": {
       "knn": {
           "field": "embedding",
           "query_vector": [2,2,2,0],
           "num_candidates": 10,
           "_name": "knn_query"
         }
     }
   }
 }
}
<p>We can even make the kNN query a part of our <a href="https://www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl-function-score-query.html">function_score</a> query. This is useful when we need to define custom scores for results returned by kNN query: ​</p>GET products/_search
{
 "size": 3,
 "query": {
   "function_score": {
     "query": {
       "knn": {
           "field": "embedding",
           "query_vector": [2,2,2,0],
           "num_candidates": 10,
           "_name": "knn_query"
         }
     },
     "functions": [
       {
         "filter": { "match": { "department": "men" } },
         "weight": 100
       },
       {
         "filter": { "match": { "department": "women" } },
         "weight": 50
       }
     ]
   }
 }
}
<p>kNN query being a part of <a href="https://www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl-dis-max-query.html">dis_max</a> query is useful when we want to combine results from kNN search and other queries, so that a document’s score comes from the highest ranked clause with a tie breaking increment for any additional clause. ​</p>GET products/_search
{
 "size": 5,
 "query": {
   "dis_max": {
     "queries": [
       {
         "knn": {
           "field": "embedding",
           "query_vector": [2,2, 2,0],
           "num_candidates": 3,
           "_name": "knn_query"
         }
       },
       {
         "match": {
           "description": "high-rise jeans"
         }
       }
     ],
     "tie_breaker": 0.8
   }
 }
}
<p>kNN search as a query has been introduced with the 8.12 release. Please try it out, and we would appreciate any feedback.</p>]]></content:encoded>
    <link>https://www.elastic.co/search-labs/blog/knn-query-elasticsearch</link>
    <guid isPermaLink="true">https://www.elastic.co/search-labs/blog/knn-query-elasticsearch</guid>
    <category><![CDATA[Vector Database]]></category>
    <dc:creator><![CDATA[Mayya Sharipova,Benjamin Trent]]></dc:creator>
    <enclosure url="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/bltd528ca7843f1946f/6a170b2e2b835ff0b7f4b21b/d2c2a3cddc393d80b11e4ed93672e345d0addd7d-1024x1024.png" length="0" type="image/png"/>
    <pubDate>Thu, 07 Dec 2023 00:00:00 GMT</pubDate>
  </item>
  <item>
    <title><![CDATA[Enhancing chatbot capabilities with NLP and vector search in Elasticsearch]]></title>
    <description><![CDATA[Explore how vector search and NLP work to enhance chatbot capabilities and see how Elasticsearch facilitates the process.]]></description>
    <content:encoded><![CDATA[<p>Conversational interfaces have been around for a while and are becoming increasingly popular as a means of assisting with various tasks, such as customer service, information retrieval, and task automation. Typically accessed through voice assistants or messaging apps, these interfaces simulate human conversation in order to help users resolve their queries more efficiently.</p><p>As technology advances, chatbots are used to handle more complex tasks — and quickly — while still providing a personalized experience for users. Natural language processing (NLP) enables chatbots to process the user's language, identifies the intent behind their message, and extracts relevant information from it. For example, Named Entity Recognition extracts key information in a text by classifying them into a set of categories. Sentiment Analysis identifies the emotional tone, and Question Answering the “answer” to a query. The goal of NLP is to enable algorithms to process human language and perform tasks that historically only humans were capable of, such as finding relevant passages among large amounts of text, summarizing text, and generating new, original content.</p><p>These advanced NLP capabilities are built upon a technology known as <a href="https://www.elastic.co/what-is/vector-search">vector search</a>. Elastic has native support for vector search, performing exact and approximate <a href="https://www.elastic.co/guide/en/elasticsearch/reference/current/knn-search.html#knn-search">k-nearest neighbor (kNN) search</a>, and for NLP, enabling the use of custom or <a href="https://www.elastic.co/guide/en/machine-learning/current/ml-nlp-model-ref.html#ml-nlp-model-ref">third-party models</a> directly in Elasticsearch.</p><p>In this blog post, we will explore how vector search and NLP work to enhance chatbot capabilities and demonstrate how Elasticsearch facilitates the process. Let's begin with a brief overview of vector search.</p><h2>Vector search</h2><p>Although humans can comprehend the meaning and context of written language, machines cannot do the same. This is where vectors come in. By converting text into vector representations (numerical representations of the meaning of the text), machines can overcome this limitation. Compared to a traditional search, instead of relying on keywords and lexical search based on frequencies, vectors enable the process of text data using operations defined for numerical values.</p><p>This allows vector search to locate data that shares similar concepts or contexts by using distances in the "embedding space" to represent similarity given a query vector. When the data is similar, the corresponding vectors will be alike.</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt53a615a8ba0ac931/6a17d795fbc5f8b257491910/08542abf8108aace288745b1aca8579b476ddc1b-1440x618.png" alt="" /><p>Vector search is not only utilized in NLP applications, but it’s also used in various other domains where unstructured data is involved, including image and video processing.</p><p>In a chatbot flow, there can be several approaches to users' queries, and as a result, there are different ways to improve information retrieval for a better user experience. Since each alternative has its own set of advantages and possible disadvantages, it is essential to take into account the available data and resources, as well as the training time (when applicable) and expected accuracy. In the following section, we will cover these aspects for question-answering NLP models.</p><h2>Question-answering</h2><p>A question-answering (QA) model is a type of NLP model that is designed to answer questions asked in natural language. When users have questions that require inferring answers from multiple resources, without a pre-existing target answer available in the documents, generative QA models can be useful. However, these models can be computationally expensive and require large amounts of data for domain related training, which may make them less practical in some situations, even though this method can be particularly valuable to handle out-of-domain questions.</p><p>On the other hand, when users have questions on a specific topic, and the actual answer is present in the document, extractive QA models can be used. These models directly extract the answer from the source document, providing transparent and verifiable results, making them a more practical option for businesses or organizations that want to provide a simple and efficient way of answering questions.</p><p>The example below demonstrates the use of a pre-trained extractive QA model, <a href="https://huggingface.co/deepset/minilm-uncased-squad2">available on Hugging Face</a> and deployed into Elasticsearch, to extract answers from a given context:</p>POST _ml/trained_models/deepset__minilm-uncased-squad2/deployment/_infer
{
    "docs": [{"text_field": "Canvas is a data visualization and presentation application within Kibana. With Canvas, live data can be pulled directly from Elasticsearch and combined with colors, images, text, and other customized options to create dynamic, multi-page displays."}],
    "inference_config": {"question_answering": {"question": "What is Kibana Canvas?"}}
}


{
  "predicted_value": "a data visualization and presentation application",
  "start_offset": 10,
  "end_offset": 59,
  "prediction_probability": 0.28304219431376443
}
<p><a href="https://www.elastic.co/guide/en/machine-learning/current/ml-nlp-deploy-models.html">Deploy trained models.</a></p><p><a href="https://www.elastic.co/guide/en/machine-learning/current/ml-nlp-ner-example.html#ex-ner-ingest">Add a model to an inference ingest pipeline.</a></p><p>There are various ways to handle user queries and retrieve information, and using multiple language models and data sources can be an effective alternative when dealing with unstructured data. To illustrate this, we have an example of the data processing of a chatbot employed to respond to queries with answers considering data extracted from selected documents.</p><h2>Chatbot data processing: NLP and vector search</h2><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blta418a9c54bb16cf9/6a17d7975772624b371bca43/c2d1a2f110e937b1d3e5df0d5caac3c906c98fb0-1440x748.png" alt="" /><p>As shown above, the data processing for our chatbot can be divided into three parts:</p><ul><li><p><strong>Vector processing:</strong> This part converts documents into vector representations.</p></li><li><p><strong>User input processing:</strong> This part extracts relevant information from the user query and performs semantic search and hybrid retrieval.</p></li><li><p><strong>Optimization:</strong> This part includes monitoring and is crucial for ensuring the chatbot's reliability, optimal performance, and great user experience.</p></li></ul><h2>Vector processing</h2><p>For the <strong>processing</strong> part, the first step is to determine component parts of each document to then convert each element to a vector representation; these representations can be created for a wide range of data formats.</p><p>There are various methods that can be used to compute embeddings, including pre-trained models and libraries.</p><p>It's important to note that the effectiveness of search and retrieval on these representations depends on the existing data and the quality and relevance of the method used.</p><p>As the vectors are computed, they are stored in Elasticsearch with a <a href="https://www.elastic.co/guide/en/elasticsearch/reference/current/dense-vector.html">dense_vector</a> field type.</p>PUT &lt;target&gt;
{
  "mappings": {
    "properties": {
      "doc_part_vector": {
        "type": "dense_vector",
        "dims": 3
      },
      "doc_part" : {
        "type" : "keyword"
      }
    }
  }
}
<h2>Chatbot user input processing</h2><p>For the <strong>user</strong> part, after receiving a question, it's useful to extract all possible information from it before proceeding. This helps to understand the user's intention, and in this case, we are using a <a href="https://huggingface.co/dslim/bert-base-NER">Named Entity Recognition model (NER)</a> to assist with that. NER is the process of identifying and classifying named entities into predefined entity categories.</p>POST _ml/trained_models/dslim__bert-base-ner/deployment/_infer
{
  "docs": { "text_field": "How many people work for Elastic?"}
}


{
  "predicted_value": "How many people work for [Elastic](ORG&amp;Elastic)?",
  "entities": [
    {
      "entity": "Elastic",
      "class_name": "ORG",
      "class_probability": 0.4993975435876747,
      "start_pos": 25,
      "end_pos": 32
    }
  ]
}
<p>Although not a necessary step, by using structured data or the above or another NLP model result to categorize the user's query, we can restrict the kNN search using a <a href="https://www.elastic.co/guide/en/elasticsearch/reference/current/knn-search.html#knn-search-filter-example">filter</a>. This helps to improve performance and accuracy by reducing the amount of data that needs to be processed.</p>    "filter": {
      "term": {
        "org": "Elastic"
      }
    }
<h2>Semantic search and hybrid retrieval</h2><p>Since the prompt originates from user queries and the chatbot needs to process human language with its variability and ambiguity, <a href="https://www.elastic.co/guide/en/elasticsearch/reference/current/knn-search.html#semantic-search">semantic search</a> is a great fit. In Elasticsearch, you can perform semantic search in a single step by passing the query string and the ID of the <a href="https://huggingface.co/sentence-transformers/msmarco-MiniLM-L-12-v3">embedding model</a> into a query_vector_builder object. This will vectorize the query and perform kNN <a href="https://www.elastic.co/guide/en/elasticsearch/reference/current/search-search.html">search</a> to retrieve top k matches that are closest in meaning to the query:</p>POST /&lt;target&gt;/_search
{
  "knn": {
    "field": "doc_part_vector",
    "k": 5,
    "num_candidates": 20,
    "query_vector_builder": {
      "text_embedding": {
        "model_id": "&lt;text-embedding-model-id&gt;",
        "model_text": "&lt;query_string&gt;"
      }
    }
  }
 }
<p><a href="https://www.elastic.co/guide/en/machine-learning/8.7/ml-nlp-text-emb-vector-search-example.html">End-to-end example: How to deploy a text embedding model and use it for semantic search.</a> Elasticsearch uses the Lucene implementation of the Okapi BM25, a <strong>sparse model</strong> , to rank text queries for relevance, while <strong>dense models</strong> are used for <strong>semantic search</strong>. To <strong>combine</strong> the <a href="https://www.elastic.co/guide/en/elasticsearch/reference/current/knn-search.html#_combine_approximate_knn_with_other_features"><strong>strengths of both</strong></a> <strong>,</strong> vector matches and matches obtained from the text query, you can perform a <strong>hybrid retrieval</strong> :</p>POST &lt;target&gt;/_search
{
  "query": {
          "match": {
            "content": {
              "query": "&lt;query_string&gt;"
            }
        }
  },
  "knn": {
    "field": "doc_part_vector",
    "query_vector_builder": {
      "text_embedding": {
    "model_id": "&lt;text-embedding-model-id&gt;",
     "model_text": "&lt;query_string&gt;"
      }
    },
    "filter": {
      "term": {
        "org": "Elastic"
      }
    }
  }
}
<h3>Combining both sparse and dense models often yields the best results</h3><p>Sparse models generally perform better on short queries and specific terminologies, while dense models leverage context and associations. If you want to learn more about how these methods compare and complement each other, <a href="https://www.elastic.co/blog/improving-information-retrieval-elastic-stack-benchmarking-passage-retrieval">here</a> we benchmark BM25 against two dense models that have been specifically trained for retrieval.</p><p>The most relevant result can usually be the first answer given to the user, the<a href="https://www.elastic.co/guide/en/elasticsearch/reference/current/search-search.html#search-api-response-body-score">_score</a> is a number used to determine the <strong>relevance</strong> of the returned document.</p><h2>Chatbot optimization</h2><p>To help improve the user experience, performance, and reliability of your chatbot, in addition to applying hybrid scoring, you can incorporate the following approaches: <strong>Sentiment Analysis:</strong> To provide awareness of user comments and reactions as the dialog unfolds, you can incorporate a <a href="https://huggingface.co/distilbert-base-uncased-finetuned-sst-2-english">sentiment analysis model</a>:</p>POST _ml/trained_models/distilbert-base-uncased-finetuned-sst-2-english/deployment/_infer
{
  "docs": { "text_field": "That was not my question!"}
}


{
  "predicted_value": "NEGATIVE",
  "prediction_probability": 0.980080439016437
}
<p><a href="https://www.elastic.co/blog/chatgpt-elasticsearch-openai-meets-private-data"><strong>GPT's capabilities</strong></a> <strong>:</strong> As an alternative to enhance the overall experience, you can combine Elasticsearch's search relevance with OpenAI's GPT question-answering capabilities, utilizing the <a href="https://platform.openai.com/docs/guides/chat">Chat Completion API</a> to return to the user model-generated responses considering these top k documents as a context. <em>Prompt: "answer this question &lt;user_question&gt; using only this document &lt;top_search_result&gt;"</em></p><p><strong>Observability:</strong> Ensuring the performance of any chatbot is crucial, and monitoring is an essential component in achieving this. In addition to logs that capture chatbot interactions, it's important to track response time, latency, and other relevant chatbot metrics. By doing so, you can identify patterns, trends, and even detect anomalies.<a href="https://www.elastic.co/blog/monitor-openai-api-gpt-models-opentelemetry-elastic">Elastic Observability</a> tools enable you to collect and analyze this information.</p><h2>Summary</h2><p>This blog post covers what NLP and vector search are and delves into an example of a chatbot employed to respond to user queries by considering data extracted from the vector representation of documents.</p><p>As demonstrated, using NLP and vector search, chatbots are capable of performing complex tasks that go beyond structured, targeted data. This includes making recommendations and answering specific product or business-related queries using multiple data sources and formats as context, while also providing a personalized user experience.</p><p>Use cases range from providing customer service by assisting customers with their inquiries to helping developers with their queries, by providing step-by-step guidance, suggesting recommendations, or even automating tasks. Depending on the goal and existing data, other models and methods can also be utilized to achieve even better results and improve the overall user experience.</p><p>Here are some links on the topic that may be useful:</p><ol><li><p><a href="https://www.elastic.co/blog/how-to-deploy-natural-language-processing-nlp-getting-started">How to deploy natural language processing (NLP): Getting started</a></p></li><li><p><a href="https://www.elastic.co/blog/overview-image-similarity-search-in-elastic">Overview of image similarity search in Elasticsearch</a></p></li><li><p><a href="https://www.elastic.co/blog/chatgpt-elasticsearch-openai-meets-private-data">ChatGPT and Elasticsearch: OpenAI meets private data</a></p></li><li><p><a href="https://www.elastic.co/blog/monitor-openai-api-gpt-models-opentelemetry-elastic">Monitor OpenAI API and GPT models with OpenTelemetry and Elastic</a></p></li><li><p><a href="https://www.elastic.co/blog/why-technology-leaders-need-vector-search">5 reasons IT leaders need vector search to improve search experiences</a></p></li></ol><p>By incorporating NLP and native vector search in Elasticsearch, you can take advantage of its speed, scalability, and search capabilities to create highly efficient and effective chatbots capable of handling large amounts of data, whether structured or unstructured.</p><p>Ready to get started? Begin a <a href="https://cloud.elastic.co/registration?onboarding_token=vectorsearch&amp;cta=cloud-registration&amp;tech=trial&amp;plcmt=article%20content&amp;pg=search-labs">free trial of Elastic Cloud</a>.</p><p><em>In this blog post, we may have used or we may refer to third party generative AI tools, which are owned and operated by their respective owners. Elastic does not have any control over the third party tools and we have no responsibility or liability for their content, operation or use, nor for any loss or damage that may arise from your use of such tools. Please exercise caution when using AI tools with personal, sensitive or confidential information. Any data you submit may be used for AI training or other purposes. There is no guarantee that information you provide will be kept secure or confidential. You should familiarize yourself with the privacy practices and terms of use of any generative AI tools prior to use.</em></p><p><em>Elastic, Elasticsearch and associated marks are trademarks, logos or registered trademarks of Elasticsearch N.V. in the United States and other countries. All other company and product names are trademarks, logos or registered trademarks of their respective owners.</em></p>]]></content:encoded>
    <link>https://www.elastic.co/search-labs/blog/enhancing-chatbot-capabilities-with-nlp-and-vector-search-in-elasticsearch</link>
    <guid isPermaLink="true">https://www.elastic.co/search-labs/blog/enhancing-chatbot-capabilities-with-nlp-and-vector-search-in-elasticsearch</guid>
    <category><![CDATA[Vector Database]]></category>
    <dc:creator><![CDATA[Priscilla Parodi]]></dc:creator>
    <enclosure url="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt5c545fc80b6d79d6/6a170214839dfad776dcfd6f/d968e646240cd3ef7c79b5124d562a5f951d812b-1440x840.png" length="0" type="image/png"/>
    <pubDate>Wed, 21 Jun 2023 00:00:00 GMT</pubDate>
  </item>
  </channel>
</rss>