<?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[AI Tools  - 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[AI Tools  - 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/ai-tools</link>
    </image>
    <link>https://www.elastic.co/search-labs/blog/category/ai-tools</link>
    <atom:link href="https://www.elastic.co/search-labs/rss/category/ai-tools.xml" rel="self" type="application/rss+xml"/>
    <language><![CDATA[en]]></language>
    <lastBuildDate>Thu, 24 Sep 2026 01:58:10 GMT</lastBuildDate>
  <item>
    <title><![CDATA[ jina-ocr-v1: One OCR model for layout, tables, math and 100+ languages]]></title>
    <description><![CDATA[jina-ocr-v1 scores 83.4 on olmOCR-bench with 570 million active parameters, the highest of any OCR model under 600 million, and it outscores GPT-5.2 on OmniDocBench.]]></description>
    <content:encoded><![CDATA[<p><code>jina-ocr-v1</code> is Jina AI by Elastic’s new preprocessing model for text scans, text-rich image data, fully rendered print pages, and other printed materials in digitization-resistant formats. It’s an <em>end-to-end document parser</em>, understanding document structures from their visual layout and using AI to process raw image data into useful, structured text for indexing and further processing. It turns scanned pages, photographed documents, slides, and handwritten notes in over 100 languages into structured, machine-readable text, with a single call to an AI model.</p><p>Optical character recognition (OCR) has been a goal of AI for a long time, and in some simple cases (such as straightforward layouts, crisp images, standard fonts, and highly resourced standardized languages), reliability can be very high using inexpensive conventional software. But real data is messy. Scans and photos of documents are often blurry, and images are compressed and quality varies. Plus, page layouts can be complex. And these examples don’t even take into consideration handwriting and global languages.</p><h2>A vision-language model trained for OCR</h2><p><code>jina-ocr-v1</code> is a <a href="https://en.wikipedia.org/wiki/Vision-language_model">vision-language model</a> (VLM) with specialized training. Unlike all-purpose large language models (LLMs), it doesn’t answer questions. It’s trained to do one thing: output the text in an image in a coherent order matching how humans would read the image, preserving what’s important about the content’s visual structure.</p><p>This integrates into one model a feature set that otherwise requires a pipeline of multiple models and specialized programs to implement, including:</p><ul><li><p>Layout-aware document processing that produces <a href="https://www.markdownguide.org/">Markdown format</a> output, retaining as much of the structure of the original document in its text output as possible.</p></li><li><p>Support for over 100 languages, with all major international languages and scripts represented.  </p></li><li><p>Handwriting recognition, including block text in a wide array of languages and English cursive.</p></li><li><p>Table extraction into basic HTML format, suitable for further processing and importing into spreadsheets or other applications.</p></li><li><p>Math recognition, turning printed images of formulas into LaTeX math code that you can insert directly into any document engine or scientific software that supports LaTeX.</p></li></ul><p><code>jina-ocr-v1</code> has a total of 3.4 billion parameters, but because it uses a <a href="https://en.wikipedia.org/wiki/Mixture_of_experts">mixture-of-experts architecture</a>, it only uses a part of the model at any one time. Only 570 million parameters, about a sixth of the total, get used when responding to user input, but exactly which 570 million is determined dynamically at inference time. This means <code>jina-ocr-v1</code> takes up as much memory as any other 3.4 billion parameter model, but runs as quickly as a 570 million parameter one.</p><h3>jina-ocr-v1 specifications</h3><p></p><p><strong>Total size</strong></p><p>3.4 billion (10⁹) parameters</p><p><strong>Active parameters</strong></p><p>570 million</p><p><strong>Backbone</strong></p><p><a href="https://huggingface.co/deepseek-ai/DeepSeek-OCR">DeepSeek-OCR</a></p><p><strong>Input resolution</strong></p><p>1024x1024 but with additional higher resolution areas. Features not visible at 2048x2048 are unlikely to be read.</p><p><strong>Output</strong></p><p>UTF-8 text with Markdown formatting for sections and lists, HTML for tables, and LaTeX for math.</p><h3>How the jina-ocr-v1 architecture builds on DeepSeek-OCR</h3><p>The full <code>jina-ocr-v1</code> model is a 3.4 billion (3.4×109) parameter <a href="https://en.wikipedia.org/wiki/Mixture_of_experts">mixture-of-experts</a> model with 570 million active parameters. It’s based on the encoder-decoder architecture of <a href="https://arxiv.org/abs/2510.18234">DeepSeek-OCR</a> with the addition of <a href="https://arxiv.org/abs/2509.18362">FastMTP</a>, which reduces computational load at inference time by predicting multiple output tokens at once. This approach results in more efficient processing, with lower latency and less resource consumption at inference time.</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/bltcd2cffc73e8af351/6aaba3cd66ee2e0e88377c27/Untitled_design.png" alt="jina-ocr-v1 architecture diagram: vision encoder to text decoder with FastMTP, outputting Markdown, HTML and LaTeX" /><p><em><strong>Figure 1:</strong></em><em> The architecture of </em><em><code>jina-ocr-v1</code></em><em>.</em></p><p>For more information about the model’s architecture, see the <a href="https://arxiv.org/abs/2609.03181">technical report</a> and <code>jina-ocr-v1</code>’s <a href="https://huggingface.co/jinaai/jina-ocr-v1">model page on Hugging Face</a>. </p><h3>Input formats and resolution limits</h3><p>Following the DeepSeek-OCR architecture, images are resized to 1024x1024 and processed into 256 input tokens for the text decoder. To incorporate additional detail, the image is also processed into up to nine additional cropped higher-detail image tiles, depending on the resolution and geometry of the image, with each reprocessed into up to 100 additional tokens each. Very large images with features too small to make out at 2048x2048 resolution will probably not be processed correctly.</p><p><code>jina-ocr-v1</code> uses the <a href="https://pillow.readthedocs.io/en/stable/index.html">PIL image processing library</a>, automatically supporting the <a href="https://pillow.readthedocs.io/en/stable/handbook/image-file-formats.html">vast majority of image formats</a>, including PNG, JPG, and GIF. However, it doesn’t have native support for PDFs. You’ll need to convert your PDF pages into an image format (we recommend PNG with minimal compression) for use with <code>jina-ocr-v1</code>. A variety of open source and commercial utilities can perform this task.</p><p>For more details on input processing, see the <a href="https://arxiv.org/abs/2609.03181">technical reports</a> for <code>jina-ocr-v1</code> and for <a href="https://arxiv.org/abs/2510.18234">DeepSeek-OCR</a>.</p><h3>Output formats: Markdown, HTML tables, and LaTeX math</h3><p><code>jina-ocr-v1</code>generates text with Markdown formatting, supporting only the <a href="https://www.markdownguide.org/basic-syntax/">basic syntactic elements</a> most compatible with different Markdown implementations. Markdown supports common text structure information, like sections and lists, in a simple, human-readable format. It’s readily converted into HTML and other rich text formats.</p><p>The model departs from basic Markdown to encode tables and math formulas. Specifically:</p><ul><li><p>Tables are encoded in basic HTML format, using <code>&lt;table&gt;</code>, <code>&lt;th&gt;</code>, <code>&lt;tr&gt;</code>, and <code>&lt;td&gt;</code> tags, without CSS or other stylesheet information.</p></li><li><p>Formulas are encoded in <a href="https://www.overleaf.com/learn/latex/Mathematical_expressions">LaTeX math mode</a> syntax, with the commands available in the <a href="https://ctan.org/pkg/amsmath?lang=en"><code>amsmath</code> package</a>.</p></li></ul><h2>Pushing the boundary of performance and cost</h2><p><code>jina-ocr-v1</code> scores among the top models on <a href="https://huggingface.co/datasets/allenai/olmOCR-bench"><code>olmOCR-bench</code></a> and is the best model overall with less than 600 million active parameters. It sits on the <a href="https://www.sciencedirect.com/topics/engineering/pareto-frontier">Pareto frontier</a> for AI OCR models, meaning that all the models that perform better on this benchmark have more active parameters, taking more computing power and resources than <code>jina-ocr-v1</code>.</p><p><strong>Model</strong></p><p><strong>Total parameters</strong></p><p><strong>Active parameters</strong></p><p><strong>olmOCR-bench</strong></p><p><strong>overall score</strong></p><p><a href="https://arxiv.org/abs/2607.07836"><code>Infinity-Parser2-Pro</code></a></p><p>35 billion</p><p>2.95 billion</p><p>87.6</p><p><a href="https://github.com/datalab-to/chandra/"><code>Chandra 2</code></a></p><p>5.3 billion</p><p>4.96 billion</p><p>85.8</p><p><a href="https://arxiv.org/abs/2603.13032"><code>dots.mocr</code></a></p><p>3 billion</p><p>1.54 billion</p><p>83.9</p><p><code>jina-ocr-v1</code></p><p>3.4 billion</p><p>574 million</p><p>83.4</p><p><a href="https://huggingface.co/datalab-to/surya-ocr-2"><code>Surya OCR 2</code></a></p><p>650 million</p><p>585 million</p><p>83.3</p><p><a href="https://arxiv.org/abs/2601.14251"><code>LightOnOCR-2-1B</code></a></p><p>1 billion</p><p>596 million</p><p>83.2</p><p><a href="https://github.com/datalab-to/chandra/"><code>Chandra 1</code></a></p><p>9 billion</p><p>7.57 billion</p><p>83.1</p><p><a href="https://huggingface.co/infly/Infinity-Parser-7B"><code>Infinity-Parser-7B</code></a></p><p>8 billion</p><p>7.07 billion</p><p>82.5</p><p><a href="https://arxiv.org/abs/2606.03264"><code>PaddleOCR-VL-1.6</code></a></p><p>0.9 billion</p><p>0.9 billion</p><p>81.0</p><p><a href="https://huggingface.co/tiiuae/Falcon-OCR"><code>Falcon-OCR</code></a></p><p>300 million</p><p>270 million</p><p>80.3</p><p><a href="https://arxiv.org/abs/2603.13398"><code>Qianfan-OCR</code></a></p><p>5 billion</p><p>4.02 billion</p><p>79.8</p><p><a href="https://arxiv.org/abs/2512.02498"><code>dots.ocr</code></a></p><p>3 billion</p><p>1.54 billion</p><p>79.1</p><p><a href="https://arxiv.org/abs/2601.20552"><code>DeepSeek-OCR 2</code></a></p><p>3 billion</p><p>570 million</p><p>76.3</p><p><strong>Table 1:</strong> <code>jina-ocr-v1</code> among <code>olmOCR-bench</code> top performers.</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt166a96fa5efa95df/6aaba42f9936f56a388b9e8f/image27.png" alt="Chart plotting olmOCR-bench score against active parameters, showing jina-ocr-v1 on the OCR performance frontier" /><p><em><strong>Figure 2:</strong></em><em><code>jina-ocr-v1</code></em><em>’s place at the Pareto (performance vs size) frontier on the olmOCR-bench test suite. Only much more compute-intensive models have significantly higher published scores.</em></p><p><a href="https://github.com/opendatalab/OmniDocBench">OmniDocBench</a> (Table 2) offers a more mixed picture, but <code>jina-ocr-v1</code> scores highly among specialized OCR models adapted from VLMs. However, a comparison of Table 1 and Table 2 reveals that there are no models of comparable size that beat <code>jina-ocr-v1</code> on both benchmarks, showing that its high performance is robust across different task types.</p><p><strong>Model</strong></p><p><strong>Total parameters</strong></p><p><strong>Active parameters</strong></p><p><strong>OmniDocBench</strong></p><p><strong>overall score</strong></p><p><a href="https://arxiv.org/abs/2606.03264"><code>PaddleOCR-VL-1.6</code></a></p><p>0.9 billion</p><p>0.9 billion</p><p>96.34</p><p><a href="https://arxiv.org/abs/2511.19575"><code>HunyuanOCR</code></a></p><p>1 billion</p><p>1 billion</p><p>94.74</p><p><code>jina-ocr-v1</code></p><p>3.4 billion</p><p>574 million</p><p>91.14 </p><p><a href="https://arxiv.org/abs/2512.02498"><code>dots.ocr</code></a></p><p>3 billion</p><p>3 billion</p><p>90.77</p><p><a href="https://arxiv.org/abs/2601.20552"><code>DeepSeek-OCR 2</code></a></p><p>3 billion</p><p>575 million</p><p>90.25</p><p></p><p><strong>Table 2: </strong><code>jina-ocr-v1</code> score on OmniDocBench compared to selected top specialized VLM models.</p><p>Table 3 compares <code>jina-ocr-v1</code> to vision-enabled, nonspecialized LLMs. It significantly outperforms GPT-5.2 and the largest Qwen3 VLM on OmniDocBench, while coming close to Gemini 3 in overall score.</p><p></p><p><strong>Model</strong></p><p><strong>Total parameters</strong></p><p><strong>Active parameters</strong></p><p><strong>OmniDocBench</strong></p><p><strong>Overall score</strong></p><p><strong>TextEdit score*</strong></p><p><strong>Read-</strong></p><p><strong>OrderEdit score*</strong></p><p><a href="https://huggingface.co/ATH-MaaS/Ovis2.6-30B-A3B"><code>Ovis2.6-30B-A3B</code></a></p><p>30 billion</p><p>3 billion</p><p>93.7</p><p>0.035</p><p>0.135</p><p><code>Gemini 3 Pro</code></p><p><em>Closed model, not disclosed</em></p><p>92.91</p><p>0.064</p><p>0.165</p><p><code>Gemini 3 Flash</code></p><p><em>Closed model, not disclosed</em></p><p>92.62</p><p>0.066</p><p>0.172</p><p><code>jina-ocr-v1</code></p><p>3.4 billion</p><p>574 million</p><p>91.14</p><p>0.046</p><p>0.142</p><p><a href="https://arxiv.org/abs/2511.21631"><code>Qwen3-VL-235B</code></a></p><p>235 billion</p><p>22 billion</p><p>89.78</p><p>0.063</p><p>0.166</p><p><code>GPT-5.2</code></p><p><em>Closed model, not disclosed</em></p><p>86.59</p><p>0.114</p><p>0.193</p><p><em>* TextEdit and Read OrderEdit benchmarks are scored so that a smaller value is better than a higher one.</em></p><p><strong>Table 3: </strong><code>jina-ocr-v1</code> scores compared to top general-purpose LLMs pressed into OCR tasks. </p><p>As shown in Table 3, on OmniDocBench tests focused on correct reading order and overall character-level accuracy (TextEdit and ReadOrderEdit),  <code>jina-ocr-v1</code> beats all three frontier LLMs handily. Only <a href="https://huggingface.co/ATH-MaaS/Ovis2.6-30B-A3B"><code>Ovis2.6-30B-A3B</code></a> scores higher in all areas in OmniDocBench, at almost nine times as many total parameters and roughly six times as many active parameters.</p><p>To test  on your data, you can<a href="https://jina.ai/api-dashboard/document-ocr-test/"> try it out on the Jina AI website</a>, or read the section <a href="https://docs.google.com/document/d/1_jwLhD-PDbf-S1-fbyWYj_z2xjyU6KC1AOLrHLWMMLQ/edit?tab=t.0#heading=h.tjwd1r7emi8">Five ways to access the jina-ocr-v1 OCR API</a> to see how to integrate it in your document processing and search pipeline.</p><h2>Layout-aware end-to-end document parsing</h2><p>OCR has been around for a long time, long enough that many people have experienced struggling with its shortcomings. It’s not that it’s so hard to recognize letters on the page, especially if clearly imaged and using a modern digital font, it’s that recognizing letters in a left-to-right sequence is only the first step to making sense of what’s printed on a page. </p><p>Consider the image below, extracted from the print edition of the June/July 2024 UK edition of <a href="https://www.cosmopolitan.com/"><em>Cosmopolitan</em> magazine</a>:</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/bltbaab6a4943c43432/6aaba49e9233693187b1ca4f/image17.png" alt="Cosmopolitan magazine page with multi-column layout used to test OCR reading order" /><p><em><strong>Figure 3:</strong></em><em> Image extracted from page 17 of UK Cosmopolitan, June/July 2024, from the </em><a href="https://archive.org/details/cosmopolitan-uk-june-july-2024/page/17/mode/1up"><em>Internet Archive</em></a><em>.</em></p><p>Simply reading from left to right will produce a chaotic, unreadable, useless text, even if every word and letter is recognized correctly. A good OCR model has to <em>parse </em>the image, recognizing, like a human does, the individual elements in relation to each other, and reading the text as intended, attending to fonts and spatial organization.</p><p>It should also recognize elements that <em>don’t </em>belong in its output. A page may contain images that incidentally have text in them, like the book covers in Figure 3. There’s also often header and footer information that isn’t part of content, like page numbers and boilerplate. It’s difficult to filter those elements out after OCR, so recognizing and removing them has to be a part of preprocessing or the OCR process itself. </p><p>Figure 4 shows how <code>jina-ocr-v1</code> parses the image in Figure 3, including elements to ignore. Each colored block indicates text elements that belong together, containing text and possibly other blocks, while the images of books are marked out because, while they do contain text, that text doesn’t belong in the OCR output. </p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt70cee772057eddde/6aaba4c6d1556f34f572825a/image15.png" alt="Cosmopolitan page annotated to show text elements OCR should ignore: book covers, header, page number" /><p><em><strong>Figure 4:</strong></em><em> The same print page but parsed into elements that an OCR process has to take into account and the ones it should ignore.</em></p><p>These problems mean that OCR for materials with complex layouts has traditionally been a multistage process, using different algorithms to:</p><ol><li><p>Break up the page.</p></li><li><p>Identify the role of each part of the image.</p></li><li><p>Recognize the text with the help of a statistical language model.</p></li><li><p>Reassemble the result for output.</p></li></ol><p>Each stage is fragile, and errors accumulate through the processing pipeline. </p><p>Instead, end-to-end document parsing does all that in a robust, single-pass generative language process.</p><p><code>jina-ocr-v1</code> handles this kind of visually complex document with ease, as shown in Figure 5, not only capturing the words, but ordering them correctly, preserving structural information about titles and sections, and ignoring the text in the pictures of books as well as the non-content header and footer.</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt778cf6dfc235f5a6/6aabbaf4bf4107a7092fc08b/Screenshot_2026-09-17_at_1.03.22_p.m..png" alt="" /><p><em><strong>Figure 5:</strong></em><em><code>jina-ocr-v1</code></em><em> output in Markdown format, from processing the image in Figure 3.</em></p><p><code>jina-ocr-v1</code> does more than just identify the letters on a page. It manages complex structural information and supports over 100 languages. It also returns information in a Markdown-style format that’s both human-readable and widely supported out of the box by downstream applications.</p><h2>Table extraction to HTML</h2><p>Identifying and extracting tables sounds simple, but it’s very challenging to do well without an AI model. </p><p>As an example, consider this table from a recent <a href="https://arxiv.org/abs/2512.04032">Jina AI conference paper</a>:</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt843771b972c37380/6aaba557dcc13fc50f528093/image20.png" alt="Benchmark table rendered from LaTeX in a conference paper, used to test jina-ocr-v1 table extraction" /><p><em><strong>Figure 6:</strong></em><em> A rendered table, originally composed in LaTeX, from a formal conference paper. </em></p><p>OCR software that lacks table awareness would likely produce a result like this:</p>Model AI2D Chart Text Doc Info OCR SEED CharXiv Avg
QA VQA VQA VQA Bench 2+ (RQ/DQ) 
jina-vlm 82.0 81.9 83.2 90.6 71.6 778 67.2 32.3/63.5 72.3
Qwen3-VL-2B 76.9 77.2 79.5 92.3 71.9 858 67.3 28.8/62.3 71.6
IVL3.5-2B 78.8 80.7 76.5 88.5 69.3 836 68.0 31.6/65.0 71.6
IVL3-2B 78.6 80.2 77.0 87.4 67.1 835 64.6 28.3/54.7 69.2
Qwen2-VL-2B 74.7 73.5 79.7 89.2 64.0 809 62.4 23.3/55.0 66.4<p>This is useless as a table without further processing. But, when presented with the same table, <code>jina-ocr-v1</code> produces clean, minimalist HTML:</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt93589337fe1db4f4/6aabbb228e00eb53835ca45c/Screenshot_2026-09-17_at_1.04.06_p.m..png" alt="" /><p><em><strong>Figure 7:</strong></em><em> HTML generated by </em><em>jina-ocr-v1</em><em> for the table image in Figure 6. Some whitespace has been removed for compact display.</em></p><p>This HTML yields a functionally identical table that’s suitable for display, reading into spreadsheets or other applications that support tables, or other further processing.</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt5619d0beeb94611f/6aaba5784d4dfe0a003554e1/image11.png" alt="HTML table generated by jina-ocr-v1 rendered in a browser with borders added via CSS" /><p><em><strong>Figure 8:</strong></em><em> The HTML in Figure 7, rendered in a browser. Visible borders were added via a stylesheet for clarity. </em></p><p>When tables appear inside a page with other text, the HTML tables are inserted into the Markdown output, as in Figure 9:</p><p></p><p><em><strong>Figure 9:</strong></em><em> An example document page containing a table (left), and the rendered Markdown output, including HTML table, with visible borders added via CSS for clarity (right).</em></p><h2>Math recognition: Printed formulas to LaTeX</h2><p>Mathematical formulas are visually complex and nonlinear, full of shapes that look like ordinary printed language, but cannot be processed like ordinary language. <code>jina-ocr-v1</code> has special training to recognize and transform printed math into LaTeX math code. </p><p>Figure 10 is an image of a block of formula-rich text extracted from a <a href="https://arxiv.org/abs/2605.08384">recent Jina AI conference paper</a>: </p><p></p><p><em><strong>Figure 10:</strong></em><em> An extract from a scientific paper (left), and </em><em><code>jina-ocr-v1</code></em><em>’s raw output from processing that image (right). The parts between “$” characters are LaTeX math.</em></p><p>After pasting the generated output from Figure 10 into a fresh LaTeX document (with <code>\usepackage{amsmath}</code>) and compiling it to printable form, the result is functionally identical, missing only the block-level equation alignment present in the original:</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt4f3b9633ad0d985b/6aaba6a1f08d25594899349a/image43.png" alt="LaTeX math output from jina-ocr-v1 recompiled, showing InfoNCE training formulas intact" /><p><em><strong>Figure 11:</strong></em><em><code>jina-ocr-v1</code></em><em> LaTeX formula output, recompiled as LaTeX. Some block alignment information is lost, but the math formulas are all intact and correct.</em></p><h2>Handwriting recognition: Print and cursive</h2><p><code>jina-ocr-v1</code> handles both print and cursive handwriting:</p><p></p><p><em><strong>Figure 12:</strong></em><em> A 1941 handwritten letter to US President Franklin D. Roosevelt from a child, preserved in the </em><a href="https://www.fdrlibrary.org/"><em>FDR Presidential Library</em></a><em> (left), and </em><em><code>jina-ocr-v1</code></em><em>'s output for it (right). Note that the cursive archivist’s mark in the upper right-hand corner, written in a completely different hand, proves too hard to manage with so little context.</em></p><p>Cursive handwriting is highly varied, and even human readers may struggle with it. While <code>jina-ocr-v1</code> performs well on neat cursive English, messy writing is as hard for it to read as for humans. When writing is sloppy, poorly captured by OCR, or otherwise difficult to decipher, <code>jina-ocr-v1</code> can perform quite poorly.</p><p>For example, the handwriting below is from the famous children’s books illustrator <a href="https://en.wikipedia.org/wiki/Beatrix_Potter">Beatrix Potter</a>:</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/bltf70dcc382ac0e101/6aabbb9792336965b1b1caf6/Screenshot_2026-09-17_at_1.05.53_p.m..png" alt="" /><p><em><strong>Figure 13:</strong></em><em> A letter from Beatrix Potter to a child acquaintance (top), and the extracted text (bottom).</em></p><p><code>jina-ocr-v1</code> is able to accurately capture the flowing cursive in Figure 13, even when it wraps around Potter’s drawings of mice. However, the place-name at the top is much less cleanly written and lacks disambiguating context, resulting in errors. Even the human reader likely finds it difficult to read.</p><h2>OCR for slides, reports, labels, and business cards</h2><p><code>jina-ocr-v1</code>’s expanded training for complex layouts makes it work well with unconventional materials. The examples below show some of the scope of the model’s abilities.</p><h3>Presentation slides</h3><p></p><p><em><strong>Figure 14:</strong></em><em> A slide from Elastic NV’s most recent quarterly financial presentation (left), and </em><em><code>jina-ocr-v1</code></em><em>’s output, rendered via HTML (right). Note that the model correctly extracts the hierarchy of titles and headings and associates each text with the proper heading. It also ignores the page number and boilerplate Elastic logo.</em></p><h3>Business reports</h3><p><code>jina-ocr-v1</code> processes the visually dense pages of corporate business reports, such as in Figure 15:</p><p></p><p></p><p><em><strong>Figure 15:</strong></em><em> Page 2 of the </em><a href="https://ir.spacex.com/financials/default.aspx"><em>SpaceX quarterly earnings</em></a><em> report for Q2 2026 (August 4, 2026) (left), and the rendered </em><em><code>jina-ocr-v1</code></em><em> output for it (right). </em><em><code>jina-ocr-v1</code></em><em> is aware that “Countries with Starlink Coverage” goes with “167,” even though a simple column-based reading wouldn’t have made that association. </em></p><p>Glossy business reports and presentations often have complex, visually interesting layouts that make them hard for traditional OCR to correctly process. `jina-ocr-v1` excels at these kinds of materials, like the example in Figure 16 below:</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/bltadada7b071fd37f1/6aaba8fa11ab88364a5b94cd/image4.png" alt="Meiji Group Integrated Report 2025 page in Japanese with complex visual hierarchy for OCR" /><p></p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/bltf3015a6f6f2ad7cf/6aaba92d5ef72d2063c9512c/image40.png" alt=" Japanese Meiji Integrated Report text extracted by jina-ocr-v1, preserving the heading hierarchy" /><p><em><strong>Figure 16:</strong></em><em> Page 2 of the </em><a href="https://www.meiji.com/investor/library/integratedreports/"><em>2025 Integrated Report</em></a><em> from the Japanese </em><a href="https://www.meiji.co.jp/"><em>Meiji Group</em></a><em> (top), and an HTML rendering of </em><em><code>jina-ocr-v1</code></em><em>’s output (bottom). Note how the visual hierarchy of elements in the original is preserved in the Markdown output.</em></p><h3>Labels and packaging</h3><p><code>jina-ocr-v1</code> can read print from labels and graphic design materials. This is especially helpful with high-information content materials, like medical packaging:</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/bltc6b9e125f488ce3d/6aaba9a292a5874460ee0f90/image10.png" alt="Dapagliflozin and metformin drug packaging label from DailyMed used to test OCR on medical text" /><p></p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt8a5548e01ae8a0d0/6aaba9bb11ab8871d95b94d1/image18.png" alt="Text extracted by jina-ocr-v1 from a drug packaging label, rendered from Markdown to HTML" /><p><em><strong>Figure 17:</strong></em><em> A drug packaging image from the US government’s </em><a href="https://dailymed.nlm.nih.gov/dailymed/drugInfo.cfm?setid=e6957674-86c1-4536-8e23-54ad7f2cacc4"><em>DailyMed</em></a><em> website (top), and the extracted text rendered from Markdown to HTML (bottom).</em></p><p>It works just as well with more conventional consumer goods labeling and photos, if the image quality is high, as in Figure 18:</p><p><em><strong>Figure 18:</strong></em><em> A common household product with text on the label (left), and extracted output rendered as HTML (right). </em></p><h3>Business cards</h3><p><code>jina-ocr-v1</code> has no difficulty with visually compact documents, like business cards:</p><p></p><p><em><strong>Figure 19:</strong></em><em> A photo of a business card </em><a href="https://commons.wikimedia.org/wiki/File:Dandelion_business_card_(53439119976).jpg"><em>sourced from Wikimedia Commons</em></a><em> (left), and the HTML rendered text result (right). Note how the three-level header hierarchy is preserved and how </em><em><code>jina-ocr-v1</code></em><em> correctly understands the alphanumeric structure of Canadian postal codes, correctly inserting “1” and “0” (instead of “I” and “O”) into “K0K 1Z0.”</em></p><h2>Multilingual OCR across 100+ languages</h2><p><code>jina-ocr-v1</code> has been trained in over 100 global natural languages. It performs high-quality OCR for a diverse range of global media, without additional modules to support different language or writing systems.</p><h3>Diacritics and modified Latin alphabets</h3><p>Single-language OCR software (especially for English) often struggles to render diacritical marks on letters. Most languages use them, and failing to accurately render diacritics can make indexing, information retrieval, and other downstream applications fail.</p><p>Even in Western European languages, like French and German, modified letters like the c-cedilla (Ç) and Eszett (ß), pose well-known problems for many OCR suites:</p><p><em><strong>Figure 20:</strong></em><em><code>jina-ocr-v1</code></em><em> handling a French c-cedilla (Ç) and a German Eszett (ß) together in a single image (left), with the plain text Markdown result (right).</em></p><p>Figures 21 and 22 show <code>jina-ocr-v1</code> handling Czech and Turkish, which are languages written with heavily modified Latin alphabets that use diacritics pervasively:</p><p></p><p><em><strong>Figure 21:</strong></em><em> COVID-19 information from the Czech government, in the original PDF (left), and </em><em><code>jina-ocr-v1</code></em><em>’s Markdown output rendered to HTML (right).</em></p><p><em><strong>Figure 22:</strong></em><em> A Turkish-language presentation slide from </em><a href="https://commons.wikimedia.org/wiki/File:AKILCI_VITAMIN_ve_MINERAL_KULLANIMI-Dr.Fatos_Tanzer.pdf"><em>Wikimedia Commons</em></a><em> (left), and plain text output (right). Note that </em><em><code>jina-ocr-v1</code></em><em> correctly recognizes the distinctly Turkish yumuşak g (ğ) and highly problematic “dotless i” (ı).</em></p><h3>Greek and Cyrillic</h3><p>In addition to Latin alphabet writing, <code>jina-ocr-v1</code> supports Greek and Cyrillic writing:</p><p></p><p><strong>Figure 23: </strong>Greek press release from the construction company <a href="https://www.gekterna.com/el/press_release/e65-se-kykloforia-1821-chlm-sygchronou-asfalous-aftokinitodromou/">GEK TERNA</a> (ΓΕΚ ΤΕΡΝΑ) (left), and extracted text (right).</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt1fd863b282ffd5f8/6aababce9936f54af68b9eb7/image45.png" alt="Ukrainian slide on the Ukraine Investment Framework used to test jina-ocr-v1 on Cyrillic script" /><p></p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt0e37954b0590c484/6aababe99936f54b688b9ebb/image30.png" alt="Ukrainian text extracted by jina-ocr-v1 from a Ukraine Investment Framework slide, rendered as Markdown" /><p><strong>Figure 24:</strong> Ukrainian <a href="https://www.ukrainefacility.me.gov.ua/wp-content/uploads/2024/06/ukraine-investment-framework.pdf">presentation slide</a> (top), and Markdown output from <code>jina-ocr-v1</code> rendered via HTML (bottom).</p><h3>Asian and Middle Eastern languages</h3><p><code>jina-ocr-v1</code> goes beyond Latin scripts. You can see its Japanese support in Figure 16, but it also handles other major Asian languages. </p><p>Chinese:</p><p></p><p><em><strong>Figure 25:</strong></em><em> Chinese. Public promotional materials for a new food traceability reporting system at Lǎoxiāng Jī Restaurants (老乡鸡) downloaded from </em><a href="https://www.sina.cn/news/detail/5296026617381099.html"><em>Sina.cn</em></a><em> (left), and rendered Markdown (including HTML table layout) from </em><em><code>jina-ocr-v1</code></em><em> (right).</em></p><p>Korean:</p><p><em><strong>Figure 26:</strong></em><em> Korean. Page 3 of Hyundai Group’s </em><a href="https://www.hyundai.com/worldwide/ko/company/ir"><em>Q2 2026 earnings announcement</em></a><em> in the original PDF (left), and rendered Markdown (right).</em></p><p>Thai:</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt308b201ab664c1d0/6aabacaab15ff9a5c3cc6343/image38.png" alt="Thai Rath newspaper home page screenshot used to test jina-ocr-v1 on Thai script" /><p></p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt8fb08b9e267b9940/6aabacd29936f540398b9ec3/image12.png" alt="Thai text extracted by jina-ocr-v1 from the Thai Rath newspaper home page" /><p><em><strong>Figure 27: </strong></em><em>Thai. A screenshot of the home page of the daily newspaper Thai Rath (ไทยรัฐ) on August 26, 2026 (top), and the text extracted text from it (bottom).</em></p><p>Hindi:</p><p><em><strong>Figure 28:</strong></em><em> Hindi. A screenshotted headline from the </em><a href="https://www.bbc.com/hindi/articles/ce8l8904yp2o"><em>BBC Hindi</em></a><em> website (left), and </em><em><code>jina-ocr-v1</code></em><em>'s extracted text (right).</em></p><p>Arabic:</p><p><em><strong>Figure 29: </strong></em><em>Arabic. US government public information pamphlet about COVID-19 (left), and </em><em><code>jina-ocr-v1</code></em><em>’s extracted text (right).</em></p><h3>Mixed-language documents</h3><p><code>jina-ocr-v1</code> excels at materials with mixtures of languages:</p><p><em><strong>Figure 30:</strong></em><em> US Department of Agriculture notice in English, Spanish, Vietnamese, Chinese, and Arabic (left), all correctly handled by </em><em><code>jina-ocr-v1</code></em><em> (right).</em></p><h2>Five ways to access the jina-ocr-v1 OCR API</h2><p><code>jina-ocr-v1</code> belongs at or near the front of your data pipeline, preprocessing rendered text images into clean Unicode text and tractable structured formats. This adds value to every downstream part of your processes. Your data ingestion pipeline makes fewer errors, and your retrieval has greater accuracy. Plus, the data retrieved is more immediately usable, whether to human readers or agentic AI.</p><p>There are five ways to access and use <code>jina-ocr-v1</code>:</p><p><strong>Access method</strong></p><p><strong>Interface</strong></p><p><strong>Native PDF support</strong></p><p><strong>Licensing</strong></p><p><strong>Best for</strong></p><p>Elastic inference API / EIS</p><p><code>chat_completions</code>  inference endpoint</p><p>Not yet; convert pages to images first</p><p>Included with Elastic Cloud</p><p>Teams already ingesting into Elasticsearch</p><p>Jina API</p><p>HTTP service, prepaid tokens</p><p>Not yet; convert pages to images first</p><p>Pay per token</p><p>Use outside Elasticsearch, no Elastic account needed</p><p>Local installation</p><p>Jina On-Prem or download from Hugging Face</p><p>Not yet; convert pages to images first</p><p>CC BY-NC 4.0 for academic and noncommercial; contact Elastic Sales for commercial</p><p>Air-gapped, regulated, or high-volume workloads</p><p>Jina AI Reader API</p><p>HTTP header <code>X-Respond-With: jina-ocr-v1</code></p><p>Yes; converts PDF and HTML to images automatically</p><p>Part of the Reader API service</p><p>Fastest route if your inputs are PDFs or web pages</p><p></p><h3>Elastic inference API and Elastic Inference Service</h3><p><code>jina-ocr-v1</code> is available via the <a href="https://www.elastic.co/docs/api/doc/elasticsearch/operation/operation-inference-embedding">Elastic inference API</a> and <a href="https://www.elastic.co/docs/explore-analyze/elastic-inference/eis">EIS</a> as an inference endpoint.</p><p>Because <code>jina-ocr-v1</code> returns streaming text, like a chat-style LLM, access to it is via the <a href="https://www.elastic.co/docs/api/doc/elasticsearch/v8/operation/operation-inference-chat-completion-unified"><code>chat_completions</code></a> interface. We’re integrating native support for PDFs into the Elastic service and will make it available in the near future. Until then, you’ll need to process PDF documents into images first.</p><h3>Jina API</h3><p>For users who want to try out <code>jina-ocr-v1</code> or don’t want to access it via Elasticsearch’s service infrastructure, access via the <a href="https://jina.ai/api-dashboard">Jina API</a> is available using a standard HTTP service. This service uses prepaid tokens, with no fixed subscriptions, and comes with 10 million free tokens to try it out at no charge. </p><p>For more information, visit the <a href="http://jina.ai/models/jina-ocr-v1">model’s page at </a><a href="http://jina.ai">jina.ai</a> and the<a href="https://jina.ai/api-dashboard/document-ocr-test/"> <code>jina-ocr-v1</code> sandbox</a>.</p><h3>Local installation and on-prem licensing</h3><p>Jina AI models are available for download and licensed commercial use via <a href="https://github.com/jina-ai/jina-on-prem">Jina On-Prem</a> and from <a href="https://huggingface.co/jinaai/jina-ocr-v1">the model’s page</a> at Hugging Face. Jina AI’s latest models are free for academic research and noncommercial use under a <a href="https://creativecommons.org/licenses/by-nc/4.0/deed.en">CC BY-NC 4.0</a> license. To commercially license local installations of , please contact <a href="https://www.elastic.co/contact">Elastic Sales</a>.</p><h3>Jina AI Reader API</h3><p>You can configure the  <a href="https://jina.ai/reader/">Jina AI Reader API service</a> to use <code>jina-ocr-v1</code> in conjunction with a variety of additional features, including automatic PDF- and HTML-to-image conversion. This is not a default setup. To configure Jina AI Reader API to use  <code>jina-ocr-v1</code>, add the following to the header parameters in your request:</p>X-Respond-With: jina-ocr-v1<p>To use this service, see the <a href="https://jina.ai/reader/">Reader API documentation page at </a><a href="http://jina.ai">jina.ai</a>.</p><h2>jina-ocr-v1 technical report and model card</h2><p>For more information about , see the model’s <a href="https://arxiv.org/abs/2609.03181">technical report</a> and <a href="https://huggingface.co/jinaai/jina-ocr-v1">page on Hugging Face</a>.</p>]]></content:encoded>
    <link>https://www.elastic.co/search-labs/blog/ocr-model-jina-ocr-v1-document-parsing</link>
    <guid isPermaLink="true">https://www.elastic.co/search-labs/blog/ocr-model-jina-ocr-v1-document-parsing</guid>
    <category><![CDATA[Jina AI]]></category>
    <category><![CDATA[AI Tools ]]></category>
    <dc:creator><![CDATA[Scott Martens]]></dc:creator>
    <enclosure url="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt4fdf97c03d008f70/6aaba324e96fa620177b67f8/image7.png" length="0" type="image/png"/>
    <pubDate>Thu, 17 Sep 2026 00:00:00 GMT</pubDate>
  </item>
  <item>
    <title><![CDATA[You and your AI agent shouldn't be using curl: Introducing the Elastic CLI and Agent Skills]]></title>
    <description><![CDATA[Elastic CLI reaches every Elasticsearch, Kibana and Cloud API from one command, and it's what Elastic Agent Skills run on. Input is validated against a JSON Schema before anything leaves your machine, and API keys stay in your OS keychain.]]></description>
    <content:encoded><![CDATA[<p><a href="https://github.com/elastic/cli">The Elastic CLI</a> gives you one command for every public Elastic API: Elasticsearch, Kibana, and Elastic Cloud's control plane, including Serverless projects. Learn <code>elastic es search</code>, and you already know how <code>elastic kb data-views list</code> behaves. It's built to be driven by an AI coding agent as easily as by you, so every command takes JSON in and out and validates input against a JSON Schema before sending anything. Plus, it exits with a code that an agent can branch on. Administrators control which commands run at all, and API keys go to your OS keychain, never into a large language model (LLM) transcript. Our Agent Skills now run on it. The command line interface (CLI) is in technical preview today.</p><p><a href="https://cloud.elastic.co/serverless-registration">Start a free Elastic Cloud Serverless trial</a> or <a href="https://cloud.elastic.co/login">log in to Elastic Cloud</a> to follow along, and install the CLI via <a href="https://www.npmjs.com/">npm</a>: </p>npm install -g @elastic/cli<h2>Designing the Elastic CLI for people and AI agents</h2><p>A useful side effect of building flexible tools for developers is that they’re more useful to AI agents, too. It’s also the tool that our<a href="https://github.com/elastic/agent-skills"> Agent Skills</a> now use to get things done, closing <a href="https://www.elastic.co/search-labs/blog/agent-skills-elastic">the loop that we opened in March 2026</a>, when we said that a CLI for agent workflows was coming.</p><p>The CLI gives every public API across Elasticsearch, Kibana, and Elastic Cloud one shape: the same flags, input and output conventions, authentication method, and failure mode. Consistency is the ergonomic feature; everything else is built on it.</p><p>Agents need the same thing, only stricter. An agent won’t know how to craft a valid CLI command or notice if a tool “feels” wrong; it needs output it can parse and input it can validate before sending, along with failures it can branch on. Agents are now a first-class interface to Elastic, alongside people, whether they live on the platform or in your editor and terminal.<a href="https://github.com/elastic/agent-skills"> Agent Skills</a>, and now the CLI, are how we serve the second kind, so those needs are built into the core of the CLI rather than tacked on.</p><h3>JSON input and output for every command</h3><p>Agents love structured text, and almost every Elastic API already speaks JSON, so first-class JSON support was a hard requirement. Developers who are quick with a <u><code>jq</code></u> query will be equally satisfied.</p><ul><li><p><strong>JSON output:</strong> Any command, like <code>elastic version</code> and <code>elastic es indices delete ...</code>, supports <code>--json</code>, which prints JSON-parseable output to stdout and nothing else. Failed commands print <code>{"error": {"code": "...", "message": "..."}}</code> to stderr.</p></li><li><p><strong>JSON input:</strong> Every command that takes input accepts JSON on stdin or via <code>--input-file</code>. Every top-level key in that JSON also works as a CLI argument, and inline arguments take precedence, so you can keep a big request body in a file and tweak a value or two per invocation.</p></li><li><p><strong>JSON Schema as </strong><strong><code>-</code></strong><strong><code>-help</code></strong><strong> output:</strong> Pass <code>--help --json</code> to any command, and it prints a valid JSON Schema for its input, which also feeds nicely into codegen tools. <code>elastic cli-schema</code> prints the whole command tree.</p></li></ul><h3>Exit codes that an AI agent can branch on</h3><p>Agents loop on exit codes as much as they do over stdout. All failure modes are distinguishable from success, even if stdout and stderr are never read.</p><h3>Safety rails: Keychain storage, allow lists, and validation</h3><p>No model uses tools perfectly 100% of the time, so an agent-friendly CLI should provide safety rails wherever possible.</p><ul><li><p><strong>Contexts and secret storage:</strong> Connection details live in named contexts in <code>~/.elasticrc.yml</code>, <code>kubectl</code>-style; switch with <code>--use-context</code>. API commands never take credentials as flags. <code>elastic config context add</code> writes API keys to your OS keychain (macOS, Linux, Windows) and leaves a <code>$(keychain:...)</code> reference in the YAML; <code>$(env:...)</code>, <code>$(cmd:...)</code>, and <code>$(file:...)</code> work, too. Creating a Serverless project with <code>--save-as</code> writes its credentials straight to the keychain and never prints them, so nothing leaks into logs or LLM transcripts.</p></li><li><p><strong>Allowlists/blocklists:</strong> A <code>commands.allowed</code> (or <code>commands.blocked</code>) list in the config file, globally or per context, ensures that only the commands an administrator wants are runnable.</p></li></ul>commands:
   allowed:
     - version
     - stack.es.search
     - stack.es.esql.*<ul><li><p><strong>Validation:</strong> Every command has a JSON Schema, so inputs are validated before any request is sent. Add <code>--dry-run</code> to any command that takes input, and it validates and exits without sending anything.</p></li><li><p><strong>Confirmation:</strong> Destructive commands prompt in a terminal. In a noninteractive session, where agents live, they refuse to run without <code>--yes</code> and say so in a structured error.</p></li><li><p><strong>Sanitization:</strong> Index, field, and pipeline names have length limits and forbidden characters. <code>elastic sanitize index-name '&lt;value&gt;'</code> (and <code>field-name</code>, <code>pipeline-name</code>, …) prints a version stripped of anything invalid.</p></li></ul><h3>Keeping API responses inside an agent's context window</h3><p>Elastic APIs return a lot of data, and an agent’s context window is finite. Three controls help keep unnecessary text out of the context window:</p><ul><li><p><strong>Field masks:</strong> <code>--output-fields</code> takes a comma-separated list, with dot notation for nested fields.</p></li></ul>elastic es info --output-fields 'name,version.number'
 # {
 #   "name": "serverless",
 #   "version": { "number": "9.5.0" }
 # }<ul><li><p><strong>String templates:</strong> For total control, <code>--output-template</code> takes a <a href="https://mustache.github.io/">mustache</a>-style template.</p></li></ul>elastic es info --output-template 'ES version: {{ version.number }}'
# ES version: 9.5.0<ul><li><strong>Command profiles:</strong> <code>--command-profile</code> serverless (or <code>default_profile: serverless</code> in your config) hides Elastic Cloud Hosted commands and the Elasticsearch namespaces that don’t exist on Serverless. That means less to scroll past and less for an agent to guess wrong. It’s the profile we recommend for agents.</li></ul><p><strong>Control</strong></p><p><strong>What it does</strong></p><p><strong>Syntax</strong></p><p><strong>When to use</strong></p><p>Field mask</p><p>Returns only the fields you name, using dot notation for nested fields</p><p><code>--output-fields 'name,version.number'</code></p><p>You want valid JSON back, just less of it. This is the default choice for agents parsing structured output.</p><p>String template</p><p>Renders the response through a mustache-style template</p><p><code>--output-template 'ES version: {{ version.number }}'</code></p><p>You need one value in a specific shape, for a shell variable, a log line, or a prompt.</p><p>Command profile</p><p>Hides commands and namespaces that don't apply to your deployment</p><p><code>--command-profile serverless</code>or <code>default_profile: serverless</code></p><p>You want a smaller command surface so an agent has less to scroll past and less to guess wrong. This is recommended for agents.</p><p></p><h2>Helpers for bulk ingest, scroll search, and msearch</h2><p>Some of Elasticsearch’s most popular APIs have a learning curve, so elastic es helpers wraps them:</p><ul><li><p><code>scroll-search</code>: Stream a large result set as NDJSON with paging handled for you.</p></li><li><p><code>bulk-ingest</code>: Ingest from a file, a directory, or stdin (NDJSON, JSON arrays, or CSV) with streaming, batching, concurrency, and retries.</p></li><li><p><code>msearch</code>: Send multiple searches in one request.</p></li><li><p><code>watch</code>: Print new documents from an index to stdout as they’re indexed. This is great for piping into logging tools.</p></li></ul><p><code>elastic es</code> and <code>elastic kb</code> are aliases for <code>elastic stack elasticsearch</code> and <code>elastic stack kibana</code>. If we don’t ship a command you need, <code>elastic extension create</code> scaffolds one for you.</p><h2>Searching Elastic docs from the terminal</h2><p>If you or your agents don’t know which API to use, elastic docs search (plus docs read and docs ask) searches Elastic’s documentation from the terminal, returning Markdown or <code>--json</code>. These are experimental. You’ll see a warning until you pass <code>--accept-experimental</code>, so explore, but don’t script against them yet.</p><h2>Shell completion for Bash, Zsh, and Fish</h2><p>Autocomplete hooks are available for Bash, Zsh, and Fish, and they always respect your <code>commands.allowed</code> or <code>commands.blocked</code> policy.</p><h2>How Elastic Agent Skills use the CLI</h2><p><a href="https://github.com/elastic/agent-skills">Agent Skills</a> teach an AI coding agent how an Elastic expert approaches a job; for example, which cluster health field is the verdict or how to stage a reindex so it doesn’t fall over. They capture process and judgment but not transport. A skill that embeds <a href="https://curl.se/">curl</a> with an auth header has hard-coded a hostname, key, and runtime, and it breaks when any of those change.</p><p>So our skills now use a <em>universal</em> format that runs unchanged in any runtime that can execute the <code>elastic</code> CLI, including Claude Code, Codex, Cursor, and GitHub Copilot. The body refers to operations in HTTP shorthand (<code>GET /_cluster/health</code>, <code>POST /_query</code>), and an operations table at the end binds each to a CLI command. That table is the only place transport appears:</p><p>HTTP API (shorthand)</p><p><code>elastic</code> CLI command</p><p><code>GET /{index}/_mapping</code></p><p><code>elastic es indices get-mapping --index '&lt;index&gt;'</code></p><p><code>POST /_query</code></p><p><code>elastic es esql query --format tsv --query "&lt;esql&gt;"</code></p><p><code>POST cloud:/api/v1/serverless/projects/elasticsearch</code></p><p><code>elastic cloud serverless projects search create --input-file &lt;json&gt; --wait --save-as &lt;ctx&gt;</code></p><p>Every universal skill also inherits a blunt preamble; that is, use the CLI, don’t guess credentials, don’t call the HTTP API directly, and never ask the user to paste an API key into the chat.</p><p>The two halves need each other. The skill supplies the expertise that the model doesn’t have, and the CLI supplies a way to act on it that’s validated, credential-safe, and scoped by your allowlist. Tell your agent to <em>spin up a Serverless project and load products.csv into it</em>, and the provisioning skill creates it with <code>--save-as</code>. The ingest skill dry-runs a mapping and loads with <code>elastic es bulk</code>, and the Elasticsearch Query Language (ES|QL) skill writes a query that parses on the first try. Every step returns JSON, exits non-zero on failure, and can only do what your policy allows.</p><p>Skills for Elastic Cloud onboarding and provisioning, Elastic Workflows, and Kubernetes investigation are available today. Skills for Elasticsearch query, ingest, reindex, and index design, plus Kibana dashboards and alerting, are close behind.</p><h2>What’s in the Elastic CLI technical preview, and what’s next</h2><p>The preview covers all public Elasticsearch Serverless, Kibana Serverless, and Elastic Cloud APIs. Hosted-only 9.x Elasticsearch API coverage is nearly 100%, and hosted-only 9.x Kibana APIs will be added soon.</p><p>We’re actively planning more developer experience work, including broader coverage for all supported stack releases, more helpers for common workflows, more skills in the public catalog, and loading the same skills into agents that run on the Elastic platform itself. What shapes that list is hearing how you and your agents use the CLI. Tell us what’s awkward and what’s missing, along with what you’d automate next.</p><h2>Install the Elastic CLI and Agent Skills</h2><p>The Elastic CLI is available now on npm (Node.js 22+). Install it and the skills together:</p>npm install -g @elastic/cli # or: npx -y @elastic/cli --help
npx skills add elastic/agent-skills<p>Then, add a context and check it:</p>elastic config context add prod --es-url https://&lt;project&gt;.es.us-east-1.aws.elastic.cloud --es-api-key &lt;KEY&gt;
 elastic status<p>Even without a project, you can <a href="https://cloud.elastic.co/serverless-registration">start a free Serverless trial</a> in about a minute, with no credit card. If you already have a project, <a href="https://cloud.elastic.co/login">log in</a> and create API keys for Elastic Cloud and your Elasticsearch clusters. Before pointing an agent at anything real, start with a trial project, a read-only key, and a scoped commands.allowed list. Be sure to take five minutes to read the <a href="https://github.com/elastic/agent-skills#security-considerations">security notes</a> in the skills repo.</p><p>Replace all those curl commands in your Bash scripts, and add some usage instructions to your AGENTS.md. Then let your agent’s skills work efficiently and accurately with our APIs. Let us know what you think, and don’t hesitate to<a href="https://github.com/elastic/cli/issues"> open an issue</a> if you find a bug or if your use case isn’t well supported. Your feedback directly shapes what we build next.</p><h2>Elastic CLI and Agent Skills resources</h2><ul><li><p><a href="https://github.com/elastic/cli">Elastic CLI on GitHub</a> and<a href="https://github.com/elastic/cli/tree/main/docs/cli"> CLI documentation</a></p></li><li><p><a href="https://github.com/elastic/agent-skills">Elastic Agent Skills on GitHub</a></p></li><li><p><a href="https://agentskills.io">agentskills.io specification</a></p></li><li><p><a href="https://www.elastic.co/docs/deploy-manage/deploy/elastic-cloud/serverless">Elastic Cloud Serverless documentation</a></p></li><li><p><a href="https://github.com/elastic/cli/issues">Report a CLI issue</a> ·<a href="https://github.com/elastic/agent-skills/issues"> Report a skills issue</a> ·<a href="https://discuss.elastic.co/"> Discuss</a></p></li></ul>]]></content:encoded>
    <link>https://www.elastic.co/search-labs/blog/elastic-cli-ai-agents</link>
    <guid isPermaLink="true">https://www.elastic.co/search-labs/blog/elastic-cli-ai-agents</guid>
    <category><![CDATA[Developer Experience]]></category>
    <category><![CDATA[AI Tools ]]></category>
    <category><![CDATA[Agentic AI]]></category>
    <dc:creator><![CDATA[Josh Mock,Matt Ryan]]></dc:creator>
    <enclosure url="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt111f4783cff3ef01/6aa7bad035eddc3a1a11d192/image1.png" length="0" type="image/png"/>
    <pubDate>Mon, 14 Sep 2026 00:00:00 GMT</pubDate>
  </item>
  <item>
    <title><![CDATA[Know your facts: How Elasticsearch AI Indices let agents skip the reading and keep the answer]]></title>
    <description><![CDATA[A technical walkthrough of precomputing facts into an Elasticsearch AI Index, so agents answer from a single ES|QL query instead of reading whole documents, with fewer tokens and lower latency.]]></description>
    <content:encoded><![CDATA[<p>Pulling whole documents into an agent's context to answer one question is expensive, and the cost compounds with every miss. In this walkthrough, we precompute the facts instead. A Kibana workflow distills each document into a fact-level Knowledge Indicator (KI), stored in an Elasticsearch AI Index and retrieved with a single Elasticsearch Query Language (ES|QL) query. On the same question, an agent answering from KIs reached the same grounded answer using fewer tokens and lower latency than reading raw documents, without loading a single full document into context. These facts are precomputed once and then stored for use by future agents when they encounter similar queries. This is Part 2 of our series on building context with AI indices; <a href="https://www.elastic.co/search-labs/blog/ai-index-building-context-agents">Part 1</a> covered routing agents to the right index.</p><p>Managing context depends on good retrieval. Rather than have agents rediscover the same content for every question, burning tokens by retracing similar steps over and over again, Elastic’s agentic AI capabilities enable us to precompute these details and store them in a structured, searchable form, and they let agents load that context directly. We call this precomputed unit of context a Knowledge Indicator.</p><p>The default agentic retrieval augmented generation (RAG) pattern does the opposite. It retrieves whole documents and dumps them into the model's context at query time, paying for that retrieval in tokens and latency on every single question. Precomputing the answer as a KI moves that cost out of the hot path and does it once.</p><h2>How it works: AI Index, Kibana Workflows, and the query-ki skill</h2><p>Building context through AI indices has three main parts: the AI Index (a special Elasticsearch index where KIs live), Kibana Workflows to create your KIs, and a <code>query-ki</code> skill to help agents directly query KIs using ES|QL: </p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt201b0bf84c5f5002/6a8fef16ecdaa77015050aa9/unnamed.png" alt="AI Index architecture: Kibana Workflows write Knowledge Indicators, agents read them via the query-ki ES|QL skill" /><p>This blog post is similar to Part 1 in that we’re using the same core building blocks. But in this post, we’re demonstrating a very different use case. Instead of precomputing index metadata, we’re distilling specific <em>facts</em> from our indexed documents that may be used to directly answer agents’ questions without subsequent searches. We've also provided a <a href="https://github.com/elastic/elasticsearch-labs/blob/main/supporting-blog-content/precomputed-context-technical-walkthrough-part-2/index-facts-kis.ipynb">notebook</a>, if you'd like to create the same KIs yourself, end to end, as you go through these examples. </p><h3>Prerequisites: Elasticsearch Serverless and an LLM API key</h3><p>This tutorial assumes you have:</p><ol><li><p>An Elasticsearch Serverless project. You can <a href="https://cloud.elastic.co/registration?onboarding_token=search&amp;cta=cloud-registration&amp;tech=trial&amp;plcmt=article%20content&amp;pg=search-labs">sign up for a trial</a> if you don't have one.</p></li><li><p>An API key to access your Elasticsearch project.</p></li><li><p>An OpenAI-compatible large language model (LLM) API key, to access AI indices via Deep Agents scripts.</p></li></ol><h2>Load the BrowseComp-Plus sample corpus into Elasticsearch</h2><p>First, we’ll need some sources. Sources can be data that already exists in your Elasticsearch indices or external data accessed via connectors or ES|QL data sources. 
For this blog, we’ll create an index, <code>browsecomp-plus</code>, to hold our example data, with the following mappings:</p>{
  "browsecomp-plus": {
    "mappings": {
      "_meta": {
        "description": "BrowseComp-Plus corpus: ~100k human-verified web documents (news articles, Wikipedia entries, institutional pages) used as a reasoning-intensive browsing/QA retrieval benchmark. BM25-only index."
      },
      "properties": {
        "docid": {
          "type": "keyword",
          "meta": {
            "description": "Stable corpus document id."
          }
        },
        "text": {
          "type": "text",
          "meta": {
            "description": "Full document text: title, date, and body content."
          }
        },
        "title": {
          "type": "text",
          "meta": {
            "description": "Document title (from the document's front matter)."
          }
        },
        "url": {
          "type": "keyword",
          "meta": {
            "description": "Source URL the document was crawled from."
          }
        }
      }
    }
  }
}<p>and populate it with a small sample of BrowseComp-Plus data via the <a href="https://www.elastic.co/docs/api/doc/elasticsearch/operation/operation-bulk"><code>_bulk</code> API</a>. You can use the supporting <a href="https://github.com/elastic/elasticsearch-labs/blob/main/supporting-blog-content/precomputed-context-technical-walkthrough-part-2/index-facts-kis.ipynb">notebook</a> to load a sample of this data in your project. </p><h2>Create the AI Index that stores your KIs</h2><p>Just like in Part 1, the first step is to create an AI Index:</p>PUT ai-index-idx-my-corpus<p>This is preconfigured with the same required mappings as we listed out in Part 1. We perform hybrid search here using <a href="https://www.elastic.co/docs/reference/elasticsearch/mapping-reference/semantic-text"><code>semantic_text</code></a> out of the box.</p><h2>How agents retrieve KIs using ES|QL</h2><p>A KI is a document in the AI Index. What makes KIs useful is <em>retrieval</em>, or querying the AI Index to find the right content. This query is packaged within a small, portable skill that’s harness-agnostic and can be run in any agent harness. </p><p>Here’s a sample <code>query-ki</code> skill:</p> ---
name: query-ki
description: &gt;-
  Retrieve Knowledge Indicators (precomputed context) from the Elasticsearch AI
  Index before answering. Use it to find which index to search (routing profiles)
  or to look up precomputed facts without reading source documents. Trigger on any question that depends on specific facts, names, dates, or on choosing a data source.
allowed-tools: esql_query
---

# Retrieving Knowledge Indicators

Knowledge Indicators (KIs) live in Elasticsearch indices named <code>ai-index-*</code>.
Retrieve them by calling the <code>esql_query</code> tool with the query below. Substitute
the user's question for <code>&lt;query&gt;</code>, and <code>corpus_entry</code> as the <code>&lt;ki_type&gt;</code> for facts.

```esql
FROM ai-index-idx-* METADATA _id, _index, _score
| WHERE type == "&lt;ki_type&gt;"
| FORK
    (WHERE MATCH(content, "&lt;query&gt;") OR MATCH(description, "&lt;query&gt;")
     | SORT _score DESC | LIMIT 20)
    (WHERE MATCH(content.semantic, "&lt;query&gt;") OR MATCH(description.semantic, "&lt;query&gt;")
     | SORT _score DESC | LIMIT 20)
| FUSE
| SORT _score DESC
| KEEP title, content, description, tags
| LIMIT 5
```

Ground your answer in what the query returns, and cite the KI titles you used. If
nothing relevant comes back, say so rather than guessing.<p>Save this as<code>skills/query-ki/SKILL.md</code>.</p><p>Here’s what this skill is doing: </p><ul><li><p>We’re defining <code>corpus_entry</code> as our KI use case.</p></li><li><p>We’re performing a hybrid ES|QL search on our AI indices, filtering by the appropriate <code>type</code>, using reciprocal rank fusion (RRF) as the default method to fuse results.</p></li><li><p>The KI results will directly ground the agent’s answer when determining what facts are relevant to the users’ query.</p></li></ul><p>When we say that AI indices and KIs are <em>harness-agnostic</em>, it’s because the skill is just instructions plus a query. It will work in Elastic Agent Builder, a Kibana workflow agent, Claude Code, or any other harness. We’ll be using Deep Agents for examples of how to query it outside the Kibana ecosystem. Since an AI Index is, at its core, an Elasticsearch index, you can also explore your data directly. </p><h2>Precompute facts as KIs for agentic RAG</h2><p>In this example, we extract actual facts so agents can retrieve an answer without consuming a full document. We generate one fact-based KI per selected document, though the actual number and structure of KIs you generate are completely customizable.</p><p>We'll use a sample of the <a href="https://github.com/texttron/BrowseComp-Plus">BrowseComp-Plus</a> corpus, indexed into a <code>browsecomp-plus</code> index, with <code>docid</code>, <code>url</code>, <code>title</code>, and <code>text</code> fields.</p><h3>Baseline: Retrieving whole documents with RRF</h3><p>As a baseline, here's a simple <a href="https://www.elastic.co/docs/reference/elasticsearch/rest-apis/reciprocal-rank-fusion">RRF</a> query:</p>POST /_query?format=txt
{
  "query": """
    FROM browsecomp-plus METADATA _score, _id, _index
    | FORK
        (WHERE match(title, "What was the actress who played Torvi from Vikings also known for?") | SORT _score DESC | LIMIT 100)
        (WHERE match(text,  "What was the actress who played Torvi from Vikings also known for?") | SORT _score DESC | LIMIT 100)
    | FUSE // uses RRF by default
    | SORT _score DESC
    | KEEP _id, title, text
    | LIMIT 10
  """
}<p>This drops several hundred words of raw body text into the model's context. It may work, but it's expensive, and the cost compounds with every miss.</p><h3>Build the Kibana workflow</h3><p>The workflow below reads a batch of documents with a single ES|QL query and writes one fact-level KI per document into the AI Index. Each iteration runs two steps: <code>generate_ki</code> distills a raw document into a structured KI, and <code>sink_ki</code> writes it to the AI Index keyed on <code>docid</code> so reruns are idempotent.</p><p>Copy and paste the following YAML into the <a href="https://www.elastic.co/docs/explore-analyze/workflows">Elastic Workflows</a> editor:</p>version: '1'
name: browsecomp-plus-doc-ki
description: Query the BrowseComp-Plus corpus with ES|QL, generate a KI per doc with an AI agent, and bulk-write each into the AI Index as a corpus_entry.
enabled: true
tags:
  - precomputed-context
  - browsecomp-plus
triggers:
  - type: manual
steps:
  - name: query_corpus
    type: elasticsearch.esql.query
    with:
      # WHERE drops empty bodies and restricts to the curated KI_DOCIDS -- the
      # specific documents this example's question depends on -- so the workflow
      # generates only a handful of KIs instead of one per corpus document.
      # SUBSTRING keeps the prompt bounded (a full body would blow the context window).
      # Column order drives the foreach.item[N] indices:
      #   item[0]=docid  item[1]=title  item[2]=url  item[3]=text
      query: &gt;
        FROM browsecomp-plus
        | WHERE text IS NOT NULL AND docid IN ("11589", "50639", "64501", "41758", "57766", "84983", "82008")
        | KEEP docid, title, url, text
        | EVAL text = SUBSTRING(text, 1, 12000)

  - name: loop_corpus_docs
    type: foreach
    foreach: '{{ steps.query_corpus.output.values }}'
    steps:
      # Turn the raw doc into a retrieval-optimized Knowledge Indicator.
      - name: generate_ki
        type: ai.agent
        timeout: 300s
        with:
          message: &gt;
            You are a knowledge engineer building a Knowledge Indicator (KI)
            for an enterprise document-retrieval corpus. A KI is a compact,
            high-signal record that a hybrid (BM25 + semantic) search engine
            and an AI agent use to FIND and JUDGE the source document without
            reading it in full.

            Read the document below and extract a faithful, richly structured KI.
            Follow these rules strictly:
            - Be 100% grounded: never state anything not supported by the text.
            - Prefer concrete, named specifics (people, organizations, products,
              dates, places, figures) over vague phrasing.
            - Write for retrieval, not prose flourish. No marketing language.
            - If a field cannot be determined from the text, return an empty
              string or empty array rather than guessing.

            Document ID: {{ foreach.item[0] }}
            Original Title: {{ foreach.item[1] }}
            Source URL: {{ foreach.item[2] }}
            Document Body:
            {{ foreach.item[3] }}
          schema:
            type: object
            properties:
              title:
                type: string
                description: A concise, specific, human-readable title (&lt;= 12 words).
              summary:
                type: string
                description: A dense 3-5 sentence factual summary capturing the document's main claims, named entities, and conclusions. PRIMARY semantic search surface.
              answers_questions:
                type: array
                items:
                  type: string
                description: 2-5 natural-language questions this document can authoritatively answer.
              key_entities:
                type: array
                items:
                  type: string
                description: 3-10 salient named entities (people, organizations, products, places, dates) explicitly mentioned in the text.
              topics:
                type: array
                items:
                  type: string
                description: 3-8 short topic/category labels.
              tagline:
                type: string
                description: A single ultra-short phrase (&lt;= 6 words) as a quick-reference label.
            required:
              - title
              - summary
              - answers_questions
              - key_entities
              - topics

      # Direct bulk write to the AI Index. The explicit <code>index</code> action row sets
      # _id = docid so re-runs upsert in place (idempotent). <code>index:</code> in <code>with</code>
      # supplies the default target index for the bulk request.
      - name: sink_ki
        type: elasticsearch.bulk
        with:
          index: ai-index-idx-my-corpus
          operations:
            - index:
                _id: '{{ foreach.item[0] }}'
            - '@timestamp': '{{ execution.startedAt | date: "%Y-%m-%dT%H:%M:%S.%LZ" }}'
              type: corpus_entry
              title: '{{ foreach.item[1] | default: steps.generate_ki.output.structured_output.title }}'
              tags:
                - browsecomp-plus
              references:
                uri: '{{ foreach.item[2] }}'
              attributes:
                docid: '{{ foreach.item[0] }}'
                url: '{{ foreach.item[2] }}'
                source_index: browsecomp-plus
                tagline: '{{ steps.generate_ki.output.structured_output.tagline }}'
                topics: '{{ steps.generate_ki.output.structured_output.topics | json }}'
                answers_questions: '{{ steps.generate_ki.output.structured_output.answers_questions | json }}'
                key_entities: '{{ steps.generate_ki.output.structured_output.key_entities | json }}'
              content: &gt;
                === SOURCE / PROVENANCE ===
                Backing Elasticsearch index: browsecomp-plus
                Document ID (docid): {{ foreach.item[0] }}
                Source URL: {{ foreach.item[2] }}
                Retrieve the full original document with ES|QL:
                FROM browsecomp-plus | WHERE docid == "{{ foreach.item[0] }}"
                === KNOWLEDGE INDICATOR ===
                {{ steps.generate_ki.output.structured_output.summary }}
                Questions this document answers: {{ steps.generate_ki.output.structured_output.answers_questions | join: " | " }}
                Key entities: {{ steps.generate_ki.output.structured_output.key_entities | join: ", " }}
              description: &gt;
                {{ steps.generate_ki.output.structured_output.tagline }}.
                Topics: {{ steps.generate_ki.output.structured_output.topics | join: ", " }}.
                Entities: {{ steps.generate_ki.output.structured_output.key_entities | join: ", " }}.<p>Here’s what this workflow is doing: </p><ul><li><p><code>query_corpus</code> runs an ES|QL query against the <code>browsecomp-plus</code> index, applying some rules, like dropping documents with empty bodies and trimming each body to 12,000 chars so the agent prompt stays inside the context window.</p></li><ul><li><p>Note: In this example, we’re cherry-picking some concrete KI IDs, because generating KIs for every document in the index would take a long time, and we want this exercise to be short for those following along.</p></li></ul><li><p><code>loop_corpus_docs</code> iterates over every returned document, running the following two steps per document: </p></li><ul><li><p><code>generate_ki</code> reads the document and calls an LLM to emit a strictly grounded, structured KI.</p></li><li><p><code>sink_ki</code> bulk-writes each KI into the AI Index (<code>ai-index-idx-my-corpus</code>) as a KI of type <code>corpus_entry</code>. It forces <code>_id</code> to be the same as the document’s <code>docid</code> so rerunning the workflow is idempotent.</p></li></ul></ul><p>To summarize, this workflow turns each raw corpus document into a compact, searchable metadata record that agents can find and judge without reading the full source into the context window.</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt5a37c99585270959/6a8ff23da1b20b401c8728c7/unnamed.png" alt="Kibana Workflow browsecomp-plus-doc-ki: query_corpus, generate_ki and sink_ki write a corpus_entry KI to the AI Index" /><p>This workflow is used for example purposes, and the same <code>foreach</code> caveat as in Part 1 applies. For scale, use <a href="https://www.elastic.co/docs/explore-analyze/workflows/steps/composition"><code>workflow.executeAsync</code></a> or native parallel support. The <a href="https://www.elastic.co/docs/explore-analyze/workflows/reference/cheat-sheet">cheat sheet</a> is useful for optimizing Workflows. There could also be cost and efficiency gains in production by using <a href="https://www.elastic.co/docs/explore-analyze/workflows/steps/ai-steps#ai-prompt"><code>ai.prompt</code></a> or by choosing different models with which to create KIs. </p><h3>Inspect the KIs in your AI Index</h3><p>Once the workflow runs, you can query the AI Index to browse what was written:</p><p></p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt626b3ba4dc3a52c0/6a8ff268971ef9107f537cb5/unnamed.png" alt="ES|QL query in Kibana Discover returning five corpus_entry Knowledge Indicators from an Elasticsearch AI Index" /><p>Here’s an example of what one of the KI documents looks like: </p>{
  "_index": "ai-index-idx-my-corpus",
  "_id": "57766",
  "_version": 1,
  "_seq_no": 0,
  "_primary_term": 1,
  "found": true,
  "_source": {
    "@timestamp": "2026-08-05T20:35:39.034Z",
    "type": "corpus_entry",
    "title": "Vikings (TV series) - Wikipedia",
    "tags": [
      "browsecomp-plus"
    ],
    "references": {
      "uri": "https://en.wikipedia.org/wiki/Vikings_%28TV_series%29"
    },
    "attributes": {
      "docid": "57766",
      "url": "https://en.wikipedia.org/wiki/Vikings_%28TV_series%29",
      "source_index": "browsecomp-plus",
      "tagline": "Ragnar Lothbrok's rise and legacy",
      "topics": """["Historical drama television","Viking Age","Norse mythology and sagas","Canadian-Irish co-production","Television cast and production","Medieval Scandinavia"]""",
      "answers_questions": """["When did the Vikings TV series premiere and on which network?","Who created and wrote the Vikings TV series?","Where was the Vikings TV series filmed?","Who are the main cast members of Vikings?","What historical and literary sources inspired the Vikings TV series?"]""",
      "key_entities": """["Michael Hirst","Travis Fimmel","Katheryn Winnick","History Channel","Amazon Prime Video","Ashford Studios","County Wicklow, Ireland","Vikings: Valhalla","Ragnar Lodbrok","Wardruna"]"""
    },
    "content": """=== SOURCE / PROVENANCE === Backing Elasticsearch index: browsecomp-plus Document ID (docid): 57766 Source URL: https://en.wikipedia.org/wiki/Vikings_%28TV_series%29 Retrieve the full original document with ES|QL: FROM browsecomp-plus | WHERE docid == "57766" === KNOWLEDGE INDICATOR === Vikings is a historical drama television series created and written by Michael Hirst, co-produced between Canada and Ireland, that premiered on the History Channel on March 3, 2013, and concluded on March 3, 2021, after 6 seasons and 89 episodes. The series is inspired by the sagas of legendary Norse hero Ragnar Lodbrok — drawing on 13th-century texts Ragnars saga Loðbrókar and Ragnarssona þáttr, as well as Saxo Grammaticus' Gesta Danorum — and follows Ragnar's rise from farmer to Scandinavian king, then the exploits of his sons across England, Scandinavia, Kievan Rus', the Mediterranean, and North America. Principal cast includes Travis Fimmel as Ragnar Lothbrok, Katheryn Winnick as Lagertha, Gustaf Skarsgård as Floki, and Alexander Ludwig as Bjorn Ironside, among many others. The series was filmed entirely in Ireland at Ashford Studios and County Wicklow, with additional location shoots in Iceland, Morocco, Norway, and Canada; the first season budget was US$40 million. A sequel series, Vikings: Valhalla, premiered on Netflix on February 25, 2022. Questions this document answers: When did the Vikings TV series premiere and on which network? | Who created and wrote the Vikings TV series? | Where was the Vikings TV series filmed? | Who are the main cast members of Vikings? | What historical and literary sources inspired the Vikings TV series? Key entities: Michael Hirst, Travis Fimmel, Katheryn Winnick, History Channel, Amazon Prime Video, Ashford Studios, County Wicklow, Ireland, Vikings: Valhalla, Ragnar Lodbrok, Wardruna
""",
    "description": """Ragnar Lothbrok's rise and legacy. Topics: Historical drama television, Viking Age, Norse mythology and sagas, Canadian-Irish co-production, Television cast and production, Medieval Scandinavia. Entities: Michael Hirst, Travis Fimmel, Katheryn Winnick, History Channel, Amazon Prime Video, Ashford Studios, County Wicklow, Ireland, Vikings: Valhalla, Ragnar Lodbrok, Wardruna.
"""
  }
}<h3>Query KIs from LangChain Deep Agents</h3><p>We’ll use <a href="https://docs.langchain.com/oss/python/deepagents/overview">LangChain Deep Agents</a> with an OpenAI-compatible key to show that AI indices and KIs will work with any agent harness, inside and outside of Kibana’s Agent Builder ecosystem. </p><p>First, let’s create <code>facts_baseline_agent.py</code> to measure our baseline before applying KIs: </p># Example question: What was the actress who played Torvi from Vikings also known for?
import os
import sys
import time
from elasticsearch import Elasticsearch

from langchain_core.messages import AIMessage
from langchain_core.tools import tool
from langchain_openai import ChatOpenAI
from deepagents import create_deep_agent

if len(sys.argv) &lt; 2:
    sys.exit(f'Usage: python {sys.argv[0]} "your question"')

es = Elasticsearch(os.environ["ES_URL"], api_key=os.environ["ES_API_KEY"])


@tool
def esql_query(query: str) -&gt; list[dict] | str:
    """Execute an ES|QL query against Elasticsearch and return the matching rows.

    Args:
        query: A complete ES|QL query string, e.g. 'FROM browsecomp-plus | LIMIT 5'.
               Full-text search syntax: WHERE MATCH(field, "value") — not field MATCH "value".
    """
    try:
        resp = es.esql.query(query=query, format="json")
        cols = [c["name"] for c in resp["columns"]]
        return [dict(zip(cols, row)) for row in resp["values"]]
    except Exception as e:
        return f"ES|QL error: {e}"


@tool
def get_mapping(index: str) -&gt; dict:
    """Return the field mapping for an Elasticsearch index or pattern."""
    return es.indices.get_mapping(index=index).body


baseline_agent = create_deep_agent(
    model=ChatOpenAI(  # any OpenAI-compatible endpoint; configure via LLM_* env vars
        base_url=os.environ.get("LLM_BASE_URL", "https://openrouter.ai/api/v1"),
        model=os.environ.get("LLM_MODEL", "anthropic/claude-sonnet-4.5"),
        api_key=os.environ["LLM_API_KEY"],
    ),
    tools=[esql_query, get_mapping],  # no query-ki skill
    system_prompt=(
        "You are a research assistant answering questions about a document corpus "
        "stored in the Elasticsearch index <code>browsecomp-plus</code> (fields: docid, url, "
        "title, text). You have NOT memorized the corpus. Answer by querying the raw "
        "index directly with ES|QL via the esql_query tool. "
        "Full-text search syntax: WHERE MATCH(field, \"value\") — never use field MATCH \"value\". "
        "Use get_mapping if you are unsure of field names. Ground your answer strictly "
        "in the rows returned, and cite the docid or url you used."
    ),
)

start = time.perf_counter()
result = baseline_agent.invoke(
    {
        "messages": [
            {
                "role": "user",
                "content": sys.argv[1],
            }
        ]
    }
)
latency = time.perf_counter() - start

print("\n--- Tool calls ---")
for m in result["messages"]:
    if isinstance(m, AIMessage) and m.tool_calls:
        for tc in m.tool_calls:
            print(f"  [{tc['name']}] {str(tc['args'])[:120]}")
total = sum(
    len(m.tool_calls)
    for m in result["messages"]
    if isinstance(m, AIMessage) and m.tool_calls
)
print(f"Total: {total}\n")

print("--- Usage ---")
input_tokens = sum(
    (m.usage_metadata or {}).get("input_tokens", 0)
    for m in result["messages"]
    if isinstance(m, AIMessage) and m.usage_metadata
)
output_tokens = sum(
    (m.usage_metadata or {}).get("output_tokens", 0)
    for m in result["messages"]
    if isinstance(m, AIMessage) and m.usage_metadata
)
print(f"Tokens: {input_tokens + output_tokens} (input {input_tokens}, output {output_tokens})")
print(f"Latency: {latency:.2f}s\n")

print("--- Answer ---")
print(result["messages"][-1].content)<p>When I ran this baseline query, <em>What was the actress who played Torvi from Vikings also known for?</em>, it output the following: </p>% python facts_baseline_agent.py "What was the actress who played Torvi from Vikings also known for?"

--- Tool calls ---
  [esql_query] {'query': 'FROM browsecomp-plus | WHERE MATCH(text, "Torvi Vikings actress") | LIMIT 10'}
  [esql_query] {'query': 'FROM browsecomp-plus | WHERE MATCH(text, "Georgia Hirst") | LIMIT 10'}
  [read_file] {'file_path': '/large_tool_results/toolu_bdrk_01SVEwHdtc9uE1zdfgCwchb5', 'offset': 0, 'limit': 50}
  [esql_query] {'query': 'FROM browsecomp-plus | WHERE MATCH(text, "Torvi Vikings TV") | LIMIT 10'}
  [esql_query] {'query': 'FROM browsecomp-plus | WHERE MATCH(text, "Vikings series cast actress") | LIMIT 10'}
  [esql_query] {'query': 'FROM browsecomp-plus | WHERE MATCH(text, "Michael Hirst Vikings") | LIMIT 10'}
  [esql_query] {'query': 'FROM browsecomp-plus | WHERE MATCH(text, "Torvi") | LIMIT 20'}
  [read_file] {'file_path': '/large_tool_results/toolu_bdrk_01PS3ryzML5BstqMXnV4c65N', 'offset': 0, 'limit': 100}
Total: 8

--- Usage ---
Tokens: 386187 (input 384940, output 1247)
Latency: 44.86s

--- Answer ---
Perfect! I found the information. Based on the document from docid 11589 (Georgia Hirst's Wikipedia page), I now have the answer to the question.

**Georgia Hirst**, the actress who played Torvi in the History Channel series Vikings (2014-2020), is also known for:

1. **Her role as Becky in the zombie horror movie "Ravers" (2018)** - This film had its world premiere at the 2018 London FrightFest Film Festival.

2. **Being the daughter of Michael Hirst**, the creator and writer of Vikings. This is a notable connection that's frequently mentioned about her.

3. **Her role in the short film "Dungeness" (2018)**, directed by Stephen Gates, where she was the sole actor.

4. **Her role as Grace in the interactive rom-com "Five Dates" (2020)**.

5. **Her advocacy work** - In 2017, she made an appearance on Sky News urging young women to get smear tests after being diagnosed with precancerous cells at age 22.

6. **Her philanthropy** - She joined The Celebs in 2021 to record a cover of The Beatles' "Let It Be" in support of the Mind charity.

So while she's primarily known for her role as Torvi in Vikings (where she appeared in 63 episodes across seasons 2-6), she's also recognized for her work in horror films like "Ravers," her health advocacy, and being part of the Hirst family that created the show.<p>(Note: Deep Agents automatically adds the <code>read_file</code> tool to handle paginated tool results, which is why it shows up in the output.) </p><p>Next, let’s create an agent that knows how to use our <code>query-ki</code> skill, <code>facts_ki_agent.py</code>: </p># Example question: What was the actress who played Torvi from Vikings also known for?
import os
import sys
import time
from elasticsearch import Elasticsearch
from langchain_core.messages import AIMessage
from langchain_core.tools import tool
from langchain_openai import ChatOpenAI
from deepagents import create_deep_agent
from deepagents.backends.filesystem import FilesystemBackend

if len(sys.argv) &lt; 2:
    sys.exit(f'Usage: python {sys.argv[0]} "your question"')

es = Elasticsearch(os.environ["ES_URL"], api_key=os.environ["ES_API_KEY"])


@tool
def esql_query(query: str) -&gt; list[dict] | str:
    """Execute an ES|QL query against Elasticsearch and return the matching rows.

    Args:
        query: A complete ES|QL query string, e.g. 'FROM ai-index-idx-* | LIMIT 5'.
    """
    try:
        resp = es.esql.query(query=query, format="json")
        cols = [c["name"] for c in resp["columns"]]
        return [dict(zip(cols, row)) for row in resp["values"]]
    except Exception as e:
        return f"ES|QL error: {e}"


# FilesystemBackend loads skills from disk, relative to root_dir.
backend = FilesystemBackend(root_dir=".", virtual_mode=False)

agent = create_deep_agent(
    model=ChatOpenAI(  # any OpenAI-compatible endpoint; configure via LLM_* env vars
        base_url=os.environ.get("LLM_BASE_URL", "https://openrouter.ai/api/v1"),
        model=os.environ.get("LLM_MODEL", "anthropic/claude-sonnet-4.5"),
        api_key=os.environ["LLM_API_KEY"],
    ),
    tools=[esql_query],
    skills=["skills"],
    backend=backend,
    system_prompt=(
        "You are a research assistant answering questions about a document corpus. "
        "You have NOT memorized the corpus. When a question depends on specific facts, "
        "names, dates, or events, use the query-ki skill to retrieve Knowledge "
        "Indicators before answering. Ground your answer strictly in what it returns, "
        "and cite the KI titles you used."
    ),
)

start = time.perf_counter()
result = agent.invoke(
    {
        "messages": [
            {
                "role": "user",
                "content": sys.argv[1],
            }
        ]
    }
)
latency = time.perf_counter() - start

print("\n--- Tool calls ---")
for m in result["messages"]:
    if isinstance(m, AIMessage) and m.tool_calls:
        for tc in m.tool_calls:
            print(f"  [{tc['name']}] {str(tc['args'])[:120]}")
total = sum(
    len(m.tool_calls)
    for m in result["messages"]
    if isinstance(m, AIMessage) and m.tool_calls
)
print(f"Total: {total}\n")

print("--- Usage ---")
input_tokens = sum(
    (m.usage_metadata or {}).get("input_tokens", 0)
    for m in result["messages"]
    if isinstance(m, AIMessage) and m.usage_metadata
)
output_tokens = sum(
    (m.usage_metadata or {}).get("output_tokens", 0)
    for m in result["messages"]
    if isinstance(m, AIMessage) and m.usage_metadata
)
print(f"Tokens: {input_tokens + output_tokens} (input {input_tokens}, output {output_tokens})")
print(f"Latency: {latency:.2f}s\n")

print("--- Answer ---")
print(result["messages"][-1].content)<p>This agent will query the KI indices to generate the answer, rather than pulling every document into context. </p><p>When I ran these results against the same query, here was the output: </p>% python facts_ki_agent.py "What was the actress who played Torvi from Vikings also known for?"

--- Tool calls ---
  [read_file] {'file_path': './query-ki/SKILL.md', 'limit': 1000}
  [esql_query] {'query': 'FROM ai-index-idx-* METADATA _id, _index, _score\n| WHERE type == "corpus_entry"\n| FORK\n    (WHERE MATCH(co
Total: 2

--- Usage ---
Tokens: 27625 (input 27037, output 588)
Latency: 15.22s

--- Answer ---
Based on the retrieved information, **Georgia Hirst** played Torvi in Vikings. She is also known for:

1. **Ravers (2018)** - A zombie horror film where she played the character Becky. This was her debut film and premiered at the London FrightFest Film Festival.

2. **Being the daughter of Michael Hirst** - The creator and writer of Vikings itself, making her connection to the show a notable family affair. Her older half-sister Maude Hirst also appeared in Vikings as Helga.

3. **Cervical cancer awareness advocacy** - She has publicly advocated for cervical cancer screening after being diagnosed with precancerous cells at age 22 and successfully recovering through treatment.

4. **Charity work** - In 2021, she participated in a celebrity cover of The Beatles' "Let It Be" in support of the Mind charity (a mental health organization), alongside Anne Hegerty, Ivan Kaye, Eunice Olumide, and Shona McGarty.

**Sources cited:** "Georgia Hirst" and "Georgia Hirst - Wikipedia" Knowledge Indicators from the AI Index.<h2>How much can precomputing facts reduce agent token usage?</h2><p>Both agents had similar conclusions, but they took far different paths to get there: </p><p>The same question and the same grounded answer result in 93% fewer tokens and two tool calls instead of eight, when answering from KIs.</p><p>
</p><p>Baseline (No AI Index)</p><p>With AI Index</p><p>Total tool calls</p><p>8</p><p>2</p><p><code>read_file</code> calls</p><p>2</p><p>1</p><p><code>esql_query</code> calls</p><p>6, all against the <code>browsecomp-plus</code> index</p><p>1, from <code>ai-index-idx-*</code></p><p>Tokens consumed</p><p>386,187</p><p>27,625</p><p>Latency</p><p>44.86s</p><p>15.22s</p><p>Answer</p><p>Grounded, correct</p><p>Grounded, correct</p><p>Exact tool call counts, latency, and answers will vary between runs and using different agents. </p><p>Both agents produced solid, grounded answers. The difference is cost. Querying KIs from the AI Index cut token use by 93% and cut latency by roughly two thirds. Here’s how both paths went, side by side:</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt52fac74907997882/6a8ff2a049d4293b02a4fd64/unnamed.png" alt="Agentic RAG tool calls: 8 calls and 386,187 tokens without Knowledge Indicators, 2 calls and 27,625 tokens with them" /><p>That was in-depth, but it shows what AI indices and Workflows do together: the same answer, at a fraction of the tokens.</p><h2>Build precomputed context in Elasticsearch Serverless</h2><p>This walkthrough shows how to generate more sophisticated KIs based on documented facts and query them for knowledge retrieval use cases using Elasticsearch primitives. </p><p>Managing context is critical in agentic search systems. And at its core, context is a retrieval problem. AI indices help you manage context within the Elastic Stack. Try it out in Serverless, and let us know what you think in our <a href="https://discuss.elastic.co/top?period=monthly">Discuss forums</a> or the <code>#stack-kibana</code> channel in our <a href="https://elasticstack.slack.com/signup#/domain-signup">Community Slack</a>.</p><p>We’d also love to hear from you about what use cases you’d like to solve using AI indices.</p>]]></content:encoded>
    <link>https://www.elastic.co/search-labs/blog/agentic-rag-precomputed-facts-ai-index</link>
    <guid isPermaLink="true">https://www.elastic.co/search-labs/blog/agentic-rag-precomputed-facts-ai-index</guid>
    <category><![CDATA[AI Tools ]]></category>
    <category><![CDATA[Agentic AI]]></category>
    <category><![CDATA[ES|QL]]></category>
    <dc:creator><![CDATA[Kathleen DeRusso,Matt Nowzari ,Apostolos Matsagkas,Peter Pišljar]]></dc:creator>
    <enclosure url="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt3e1939e01169bb08/6a8fedbec8ced9f736055f59/1.jpg" length="0" type="image/jpeg"/>
    <pubDate>Thu, 27 Aug 2026 00:00:00 GMT</pubDate>
  </item>
  <item>
    <title><![CDATA[Let the big model think, let the small model work: Splitting LLM costs in Elastic Workflows]]></title>
    <description><![CDATA[Build an Elastic workflow that sends a data sample to a large model to propose classification labels. A human signs off, then a smaller model applies them across the full corpus.]]></description>
    <content:encoded><![CDATA[<p>Split the expensive part of large language model (LLM) classification from the cheap part. This article builds an <a href="https://www.elastic.co/docs/explore-analyze/workflows">Elastic workflow</a> where Claude Sonnet reads a stratified sample of NASA pilot incident reports and proposes classification labels based on what it finds. A human reviews the schema and signs off, and then <a href="https://mistral.ai/news/mistral-small-3-1/">Mistral Small 3.1</a> applies the labels across the full corpus. The routing is YAML, the results land in Elasticsearch as structured data, and the pattern works wherever you have free text that needs labeling.</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt0ceb64be1f3ad4e5/6a87ef8373f743fa6848688d/image2.png" alt="Example NASA ASRS pilot incident report showing a free-text narrative describing a near-miss at an uncontrolled airfield, the type of document classified by the LLM pipeline" /><p><a href="https://asrs.arc.nasa.gov/">NASA Aviation Safety Reporting System (ASRS)</a> reports describe unusual events during flights, such as missed altitudes, confusing clearances, runway issues, or mechanical problems. Each report already has an official category, like altitude deviation, course deviation, or ground encounter. In this article, we ask a different question: <em>What does this report reveal about the pilot who wrote it?</em> The idea is to ask a model to infer a schema grounded on the data to classify the report based on criteria that help us figure out information about the report writers. Then ask a second model to apply the labels.</p><p><em><strong>You can find the full workflow definitions and helper scripts </strong></em><a href="https://github.com/elastic/elasticsearch-labs/tree/main/supporting-blog-content/larger-llms-task-planning-smaller-llms-execution"><em><strong>here</strong></em></a><em><strong>.</strong></em></p><h2>What you need to run this LLM pipeline</h2><ul><li><p>Elastic Stack 9.4+ or Elastic Cloud Serverless. Elastic Workflows has been generally available (GA) since 9.4.</p></li><li><p>Elastic Agent Builder enabled in your deployment.</p></li><li><p>A <a href="https://www.elastic.co/docs/reference/kibana/connectors-kibana/ai-connector">Kibana generative AI (GenAI) connector</a> pointing at Claude Sonnet (or an equivalent reasoning model). This is the planner.</p></li><li><p>A <a href="https://console.mistral.ai/api-keys">Mistral API key</a>. We’ll use it to register an Elasticsearch inference endpoint.</p></li><li><p>Python 3.10+ with <code>elasticsearch&gt;=9.0</code> and <code>pandas</code>. Used by the dataset loader.</p></li></ul><h2>How two-tier LLM orchestration works</h2><p>The workflow has two jobs: Decide what labels should exist, and then apply those labels to every report.</p><p><strong>The first job is open-ended.</strong> A large model reads a varied sample of reports and proposes a small schema of categorical fields. A field is one way to describe the writer, such as <code>attribution_style</code> or <code>procedure_orientation</code>. Each field has a few allowed values, such as <code>self_critical</code>, <code>system_attributing</code>, or <code>balanced</code>.</p><p><strong>The second job is repeatable.</strong> After a human approves the schema, a smaller model reads each report and chooses one value for each field.</p><p>We use Elastic Workflows because the steps are known ahead of time: sample reports, propose labels, wait for approval, classify every document, and store the results. Writing those steps in YAML makes the process reproducible, observable, and cheaper to rerun.</p><h3><strong>Why split LLM work across two model tiers?</strong></h3><p>A small model could handle classification, but schema discovery is a different shape of problem. It requires reading a diverse sample, spotting latent patterns, and proposing complex structures. In practice, smaller models over-anchor on surface keywords and produce redundant or nonexclusive fields.</p><p>Classification is simpler, the schema exists, the values are enumerated, and the task is to pick one per field. A smaller model handles this reliably and at a fraction of the cost, since it runs once per document across the entire corpus.</p><p><em>Large</em> and <em>small</em> here mean reasoning capability. In this article, Claude Sonnet plays the planner and Mistral Small 3.1 plays the executor.</p><h2>Classifying NASA pilot reports with a two-tier LLM pipeline</h2><p>We’ll use the NASA ASRS database, which collects voluntary, anonymous incident reports from pilots, controllers, and mechanics. The dataset is public, and the reports are written as free-text narratives.</p><p>What we want to ask is:</p><p><em>What does this report reveal about the pilot who wrote it?</em></p><p>The planner reads a varied sample of reports and decides which distinctions are meaningful based on how the reports are actually written.</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt6b7f0b9d35ef59e3/6a87efa5b6895193cca26e8e/image4.png" alt="Elastic Workflow pipeline diagram showing schema discovery by a large language model, human approval via waitForInput, and classification by a small language model storing results in Elasticsearch" /><p><strong>Step</strong></p><p><strong>Role</strong></p><p><strong>Model tier</strong></p><p><code>sample</code></p><p>Pull a diverse subset of reports from the corpus.</p><p>(no LLM)</p><p><code>discover</code></p><p>Read the question and the sample, propose a schema of fields with enum values.</p><p><strong>Large</strong></p><p><code>approve</code></p><p>Human reviews the proposed schema and approves or edits it.</p><p>(Human via <a href="https://www.elastic.co/docs/explore-analyze/workflows/steps/wait-for-input"><code>waitForInput</code></a>)</p><p><code>apply</code></p><p>Iterate over the corpus, assign one value per field to each report.</p><p><strong>Small</strong></p><p><code>store</code></p><p>Write the schema and the per-document field values to Elasticsearch.</p><p>(No LLM)</p><h2>Registering Mistral and Claude as Elasticsearch inference endpoints</h2><p>The small model will be registered as an Elasticsearch <a href="https://www.elastic.co/guide/en/elasticsearch/reference/current/infer-service-mistral.html">inference endpoint</a> using the native <code>mistral</code> service integration. </p>INFERENCE_ID = "mistral-small-extractor"

es.inference.put(
    task_type="chat_completion",
    inference_id=INFERENCE_ID,
    inference_config={
        "service": "mistral",
        "service_settings": {
            "api_key": MISTRAL_API_KEY,
            "model": "mistral-small-latest",
            # 6 RPM is conservative for the Mistral free tier to avoid 429s.
            "rate_limit": {"requests_per_minute": 6},
        },
    },
)<p>The alias <code>mistral-small-latest</code> resolves to <a href="https://mistral.ai/news/mistral-small-3-1">Mistral Small 3.1</a>. It has a 128k context window and supports JSON-mode output.</p><p>The large model will be an <a href="https://www.elastic.co/docs/reference/kibana/connectors-kibana/ai-connector">AI connector</a> pointing at Claude Sonnet. The Agent Builder UI walks you through creating the connector. Take a note of the connector ID since we’ll reference it from the workflow.</p><h2>Indexing NASA ASRS incident reports into Elasticsearch</h2><p>The ASRS dataset is indexed with keyword mappings for aggregation fields and text mappings for the narratives the models will read.</p><p>Download the ASRS CSV (the database publishes quarterly extracts at the <a href="https://asrs.arc.nasa.gov/search/database.html">ASRS Database Online</a> page), and index it. The mappings are:</p>{
  "properties": {
    "acn":          { "type": "keyword" },
    "flight_phase": { "type": "keyword" },
    "anomaly":      { "type": "keyword" },
    "synopsis":     { "type": "text" },
    "narrative":    { "type": "text" }
  }
}<p>The mapping types follow how each field is used. <code>flight_phase</code> and <code>anomaly</code> are mapped as <code>keyword</code> because we’ll run terms aggregations on them to build the sample, and aggregations need exact, non-analyzed values. <code>narrative</code> and <code>synopsis</code> are mapped as <code>text</code> because they hold free-form prose that the models will read. The companion notebook has the full loader script that reads the CSV and bulk-indexes the documents.</p><h2>Building a stratified sample for the planning LLM</h2><p>The YAML snippets in this and the following sections are steps of the workflow definition that the notebook registers via the Workflows API. The first two steps generate a representative sample: They aggregate by flight phase and by anomaly and pull a few documents per bucket with <code>top_hits</code>.</p>- name: by_phase
  type: elasticsearch.request
  with:
    method: POST
    path: "/incident_reports/_search"
    body:
      size: 0
      aggs:
        per_phase:
          terms:
            field: flight_phase
            size: 8
          aggs:
            sampled_docs:
              top_hits:
                size: 5
                _source: ["acn", "synopsis", "narrative"]

- name: by_anomaly
  type: elasticsearch.request
  with:
    method: POST
    path: "/incident_reports/_search"
    body:
      size: 0
      aggs:
        per_anomaly:
          terms:
            field: anomaly
            size: 8
          aggs:
            sampled_docs:
              top_hits:
                size: 3
                _source: ["acn", "synopsis", "narrative"]<h2>How the large LLM discovers a classification schema from the data</h2><p>The prompt needs both the question and the sample. A question alone may produce generic labels disconnected from the corpus, and a sample alone produces descriptive clusters that ignore the angle of the question. </p><p>When both are present and the output is structured, the model produces labels that are grounded in the data and oriented to the task: a schema of categorical fields, each with two to four mutually exclusive value options backed by evidence from the sample.</p><p>Here’s the planner step from the workflow:</p>- name: discover
  type: ai.prompt
  connector-id: "claude-sonnet"
  with:
    systemPrompt: |
      You design categorical schemas for use by downstream classifiers. A
      schema is a small set of fields, each with a few mutually exclusive
      values. Every value you propose must be grounded in evidence from the
      provided sample and must serve the stated question. You do not invent
      values that are not supported by at least two documents in the sample.
      You do not propose fields that a reasonable analyst could have written
      without reading the documents.
    prompt: |
      Question:
      ${{ inputs.goal }}

      Sample documents stratified by flight phase:
      ${{ steps.by_phase.output.aggregations.per_phase.buckets | json }}

      Sample documents stratified by anomaly type:
      ${{ steps.by_anomaly.output.aggregations.per_anomaly.buckets | json }}

      Propose between 2 and 4 categorical fields that:
      - serve the question (you can explain how)
      - depend on patterns visible in the sample (you can cite document IDs)
      - would not be obvious to someone who has not read the sample

      For each field, return: name (snake_case), definition, why_useful,
      and values (2 to 4 mutually exclusive options).

      For each value, return: value (snake_case) and definition.
    schema:
      type: object
      properties:
        fields:
          type: array
          minItems: 2
          maxItems: 4
          items:
            type: object
            required: [name, definition, why_useful, values]
            properties:
              name: { type: string }
              definition: { type: string }
              why_useful: { type: string }
              values:
                type: array
                minItems: 2
                maxItems: 4
                items:
                  type: object
                  required: [value, definition]
                  properties:
                    value: { type: string }
                    definition: { type: string }
    temperature: 0.3<p>The structured output schema enforces the shape of the response:</p><p> </p><ul><li><p><code>name</code>: Identifier for the categorical field.</p></li><li><p><code>definition</code>: What this field measures, in one sentence.</p></li><li><p><code>why_useful</code>: How this field serves the question; this also helps the downstream classifier understand the intent.</p></li><li><p><code>values</code>: Two to four mutually exclusive options. Each has a <code>value</code> and a <code>definition</code>.</p></li></ul><p>Here’s an example of the produced schema. We can see how the writer is being classified and the reasons why the model decided to create the category. <code>definition</code>and <code>why_useful</code> fields are used by the second model to classify the documents.</p>{
  "fields": [
    {
      "name": "attribution_style",
      "definition": "How the reporter frames responsibility for what happened.",
      "why_useful": "Surfaces reporting culture independent of the technical event. Useful for training and safety-management programmes that want to distinguish reporter style from incident type.",
      "values": [
        {
          "value": "self_critical",
          "definition": "Assigns the cause primarily to their own action, even when external factors clearly contributed."
        },
        {
          "value": "system_attributing",
          "definition": "Frames the cause as external: ATC, equipment, weather, or organisational factors."
        },
        {
          "value": "balanced",
          "definition": "Distributes responsibility across self and system without emphasising either."
        }
       ]
    },
    {
      "name": "procedure_orientation",
      "definition": "How the reporter relates to written procedure.",
      "why_useful": "Distinguishes pilots who frame events through SOPs from those who frame them through personal judgment.",
      "values": [
        // procedure_first, experience_first (same structure as above)
      ]
    }
  ]
}<h2>Human-in-the-loop schema approval with waitForInput</h2><p>The proposed schema is now passed to a person for approval. Elastic Workflows has a <a href="https://www.elastic.co/docs/explore-analyze/workflows/steps/wait-for-input"><code>waitForInput</code></a> step that pauses the workflow with a schema, exposes a form, and resumes when the input is submitted.</p><p><code>waitForInput</code> has no timeout of its own, so if nobody responds, the execution <a href="https://www.elastic.co/docs/explore-analyze/workflows/authoring-techniques/human-in-the-loop#what-happens-while-the-workflow-is-paused">waits indefinitely</a>. To put a limit on that, set a workflow-level <code>settings.timeout</code>; if it elapses before the reviewer submits the form, the execution is canceled.</p>- name: human_gate
  type: waitForInput
  with:
    message: "Review and edit the proposed schema. The approved fields will be applied across the full corpus."
    schema:
      type: object
      required: [approved_fields]
      properties:
        approved_fields:
          type: array
          items:
            type: object
            properties:
              name: { type: string }
              definition: { type: string }
              values:
                type: array
                items:
                  type: object
                  properties:
                    value: { type: string }
        notes:
          type: string<p>When the workflow reaches this step, the execution pauses and the Kibana UI shows an "Action is required" badge. Clicking <strong>Provide action</strong> opens a form where the reviewer can paste or edit the schema JSON. Since <code>waitForInput</code> cannot be prepopulated from a previous step, the code polls the <em>discover</em> step output and prints a paste-ready JSON block that can be copied directly into this form.</p>discover = step_output(execution_id, "discover")  # polls until the step completes

# Strip <code>why_useful</code> (not part of the human_gate form) and wrap in the shape
# expected by the waitForInput form so this is paste-ready.
approved_fields = [
    {
        "name": field["name"],
        "definition": field["definition"],
        "values": [
            {"value": v["value"], "definition": v["definition"]}
            for v in field["values"]
        ],
    }
    for field in discover["content"]["fields"]
]

print(json.dumps({"approved_fields": approved_fields, "notes": ""}, indent=2))<p>The <code>step_output</code> helper (in the notebook) polls the execution via <code>GET /api/workflows/executions/{id}</code> until the <em>discover</em> step completes and then returns its output.</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/bltf3771a69b51089e7/6a87efeca8b3236eb5cc01f5/image1.png" alt="Kibana execution view showing an Elastic Workflow paused at the waitForInput step with the Provide action button highlighted for human-in-the-loop schema approval" /><p>Code JSON output pasted on Kibana:</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt85d91c93043ad0d1/6a87f00d6ea6da5cfe0083e0/image5.png" alt="Kibana Provide action modal displaying the approved classification schema JSON with pilot experience level fields, where a reviewer edits the schema before the workflow resumes" /><p>The reviewer can keep useful fields, rewrite unclear ones, merge overlapping values, and add notes. After approval, the workflow resumes and sends the final schema to the executor step.</p><p>For a new corpus, keep this human gate in place. Once the schema is stable, you can auto-approve and only fall back to review when it’s worth it: Route just the low-confidence extractions to a person, or compare a new discovery run against the schema stored in the <code>schemas</code> index and trigger review only when fields or values change beyond a threshold.</p><h2>Classifying the full corpus with a smaller LLM</h2><p>By the time the workflow reaches this step, the open-ended part of the job is over. From here, the small model takes over and classifies each report against the approved schema.</p>- name: fetch_corpus
  type: elasticsearch.request
  with:
    method: POST
    path: "/incident_reports/_search"
    body:
      size: 100
      _source: ["acn", "narrative"]
      query:
        match_all: {}

- name: classify_all
  type: foreach
  foreach: "${{ steps.fetch_corpus.output.hits.hits }}"
  iteration-on-failure:
    retry:
      max-attempts: 5
      delay: "3s"
    fallback:
      - name: notify_failure
        type: slack_api.postMessage
        connector-id: "team-alerts"
        with:
          channelNames:
            - "#pipeline-alerts"
          text: "Classification failed for ACN ${{ foreach.item._source.acn }} after all retries."
    continue: true
  steps:
    - name: classify
      type: ai.agent
      inference-id: "mistral-small-extractor"
      timeout: "120s"
      with:
        message: |
          You will classify the following report against a fixed schema.
          For each field in the schema, assign exactly one of its value
          options, or null if none of the values clearly applies. Include
          the short quote that supports the assignment and a confidence
          score between 0 and 1. Set review_required to true if any field
          returned null or any confidence is below 0.5.

          Schema:
          ${{ steps.human_gate.output.approved_fields | json }}

          Report:
          ${{ foreach.item._source.narrative }}
        schema:
          type: object
          properties:
            field_values:
              type: object
              additionalProperties: true
            review_required: { type: boolean }
    - name: write_extraction
      type: elasticsearch.index
      with:
        index: extractions
        document:
          acn: "${{ foreach.item._source.acn }}"
          field_values: "${{ steps.classify.output.structured_output.field_values }}"
          review_required: "${{ steps.classify.output.structured_output.review_required }}"<p><em>Note: The classification step uses </em><em><code>ai.agent</code></em><em> instead of </em><em><code>ai.prompt</code></em><em> because </em><a href="https://www.elastic.co/docs/explore-analyze/workflows/steps/ai-steps#step-types"><em><code>ai.agent</code></em><em> accepts an </em><em><code>inference-id</code></em></a><em>, which lets it call the Elasticsearch </em><em><code>_inference</code></em><em> endpoint directly, while </em><em><code>ai.prompt</code></em><em> only accepts a </em><em><code>connector-id</code></em><em>.</em></p><p>The <code>fetch_corpus</code> step is the third <code>elasticsearch.request</code> in the workflow, so it’s worth saying why we read from the index again. The first two (<code>by_phase</code> and <code>by_anomaly</code>) only pulled a small stratified sample for the planner to reason over, not the data to label. Now that the schema is approved, <code>fetch_corpus</code> pulls the documents we actually want to classify. We cap it at 100 with <code>match_all</code> to keep the demo fast; this is where you would page through the full corpus.</p><p>For every field, it returns a value (or null), a confidence, and a short quote. Setting <code>additionalProperties: true</code> in the JSON schema lets the step return one entry per field without the workflow having to know the field names ahead of time. A stored extraction looks like this:</p>{
  "acn": "2238341",
  "field_values": {
    "attribution_style": {
      "value": "self_critical",
      "confidence": 0.82,
      "quote": "I should have caught the altitude bust earlier"
    },
    "procedure_orientation": {
      "value": "procedure_first",
      "confidence": 0.44,
      "quote": "we ran the QRH before doing anything else"
    }
  },
  "review_required": true
}<p>Here, <code>review_required</code> is <code>true</code> because <code>procedure_orientation</code> came back at <code>0.44</code> confidence, below our <code>0.5</code> threshold, which is the signal a confidence-based quality gate would act on.</p><p>The <code>fetch_corpus</code> step pulls the documents to classify. The <a href="https://www.elastic.co/docs/explore-analyze/workflows/steps/foreach"><code>foreach</code></a>step iterates over them sequentially, and <code>iteration-on-failure</code> handles the errors: <code>retry</code> covers transient API errors from the inference endpoint, and, if all attempts fail, the <code>fallback</code> step posts to <a href="https://www.elastic.co/docs/reference/kibana/connectors-kibana/slack-action-type#slack-workflow-examples">Slack</a> so the failure doesn’t pass silently. (An <a href="https://www.elastic.co/docs/reference/kibana/connectors-kibana/email-action-type">email</a> connector works the same way.) <code>continue: true</code> then lets the loop move on to the next document instead of failing the whole run. </p><p><em>For production-scale corpora, consider using </em><a href="https://www.elastic.co/docs/explore-analyze/workflows/steps/composition#workflow-executeasync"><em><code>executeAsync</code></em></a><em>, which is the fan-out version of execute.</em></p><h2>Writing schemas and extractions back to Elasticsearch</h2><p>The workflow produces two things: the approved schema and the per-document field values. The <code>store_schema</code> step runs right after the human gate, before the classification step fans out:</p>- name: store_schema
  type: elasticsearch.index
  with:
    index: schemas
    document:
      question: "${{ inputs.goal }}"
      approved_fields: "${{ steps.human_gate.output.approved_fields }}"
      reviewer_notes: "${{ steps.human_gate.output.notes }}"<p>Each extraction is written inside the <code>foreach</code> loop, so results are persisted as they’re produced rather than batched at the end.</p><p>The <code>schemas</code> index holds one document per discovery run (question, approved fields, reviewer notes). The <code>extractions</code> index holds one document per report per schema version. </p><h2>What this two-tier LLM orchestration pattern gives you</h2><p>We built one Elastic workflow that pulls a stratified sample from an incident report index, sends it with a question to a large reasoning model to generate a classification schema based on the data and a user-defined angle, pauses for human approval, and then iterates over the full corpus with a small Mistral model that assigns one value per field. </p><p>The approved schema and per-document field values are written back to Elasticsearch as structured data.</p><p>The point of the exercise is that two different shapes of work, schema discovery, and schema application can use two different model tiers and that a workflow lets you write the routing decision down.</p><h2>Next steps for your own LLM pipeline</h2><ul><li><p>Try it on a corpus of your own. The pattern doesn’t care whether the input is incident reports, customer feedback, weekly status updates, or property listings.</p></li><li><p>Promote the <code>foreach</code> step to <code>workflow.executeAsync</code> once you’re comfortable for parallel fan-out at scale.</p></li><li><p>Schedule the rediscovery workflow on a cron trigger so you can discover different schema variations based on the data that comes in.</p></li><li><p>Read the <a href="https://www.elastic.co/docs/explore-analyze/workflows">Elastic Workflows documentation</a> for the full step catalog.</p></li></ul><h3><strong>Related reading</strong></h3><ul><li><p><a href="https://www.elastic.co/search-labs/blog/build-ai-agents-elastic-inference-service">Build AI agents with Elastic Inference Service</a> (EIS) covers the broader multi-model wiring pattern via EIS, complementary to the Workflows-orchestrated split shown here.</p></li><li><p><a href="https://www.elastic.co/search-labs/blog/ai-agentic-workflows-elastic-ai-agent-builder">How to build AI agentic workflows with Elasticsearch</a> is a higher-level survey of how Agent Builder and Workflows fit together.</p></li><li><p><a href="https://www.elastic.co/search-labs/blog/langextract-elasticsearch-tutorial-usage-example">LangExtract and Elasticsearch tutorial</a> explores a different extraction pattern using a hand-authored schema; useful for contrast with the discover-then-apply approach above.</p></li></ul>]]></content:encoded>
    <link>https://www.elastic.co/search-labs/blog/llm-orchestration-elastic-workflows</link>
    <guid isPermaLink="true">https://www.elastic.co/search-labs/blog/llm-orchestration-elastic-workflows</guid>
    <category><![CDATA[Agentic AI]]></category>
    <category><![CDATA[Integrations]]></category>
    <category><![CDATA[AI Tools ]]></category>
    <dc:creator><![CDATA[Jeffrey Rengifo]]></dc:creator>
    <enclosure url="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/bltdf42ed268fb6f953/6a87ef5c386ac3fab0adf4e2/image3.png" length="0" type="image/png"/>
    <pubDate>Fri, 21 Aug 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[Implementing an agentic reference architecture with Elastic Agent Builder and MCP]]></title>
    <description><![CDATA[Explore an agentic reference architecture with Elastic Agent Builder, MCP, and semantic search to build a security agent for automated threat analysis.]]></description>
    <content:encoded><![CDATA[<p>In this article, we will present a reference architecture for using Elasticsearch with AI capabilities through the <a href="https://www.elastic.co/docs/solutions/search/elastic-agent-builder">Elastic Agent Builder</a>, exposing an <a href="https://modelcontextprotocol.io/docs/getting-started/intro">MCP server</a> to access Agent Builder tools and Elasticsearch data.</p><p>Model Context Protocol (<a href="https://modelcontextprotocol.io/docs/getting-started/intro">MCP</a>) is an open-source standard that enables applications and LLMs to communicate with external systems via <a href="https://modelcontextprotocol.io/specification/2025-06-18/server/tools">MCP tools</a> (programmatic capabilities), and <a href="https://docs.langchain.com/oss/python/langgraph/overview">LangGraph</a> (an extension of <a href="https://docs.langchain.com/oss/javascript/langchain/overview">LangChain</a>) provides the orchestration framework for these agentic workflows.</p><p>We’ll implement an application that can search both internal knowledge (Elasticsearch stored data) and external sources (on the internet) to identify potential and known vulnerabilities related to a specific tool. The application will gather the information and generate a detailed summary of the findings.</p><h2>Requirements</h2><ul><li><p>Elasticsearch 9.2</p></li><li><p>Python 3.1x</p></li><li><p><a href="https://platform.openai.com/api-keys">OpenAI API Key</a></p></li><li><p><a href="https://www.elastic.co/docs/deploy-manage/api-keys/elasticsearch-api-keys">Elasticsearch API Key</a></p></li><li><p><a href="https://serpapi.com/users/sign_up?plan=free">Serper API Key</a></p></li></ul><h2>Elastic Agent Builder</h2><p><a href="https://www.elastic.co/docs/solutions/search/elastic-agent-builder">Elastic Agent Builder</a> is a set of AI-powered capabilities for developing and integrating agents that can interact with your Elasticsearch data. It provides a built-in agent that can be used for natural language conversations with your data or instance, and it also supports tool creation, Elastic APIs, A2A, and MCP. In this article, we will focus on using the <a href="https://www.elastic.co/docs/solutions/search/agent-builder/mcp-server">MCP server</a> for external access to the Elastic Agent Builder tools.</p><p>To know more about Agent Builder features, you can read <a href="https://www.elastic.co/search-labs/blog/elastic-ai-agent-builder-context-engineering-introduction">this article</a>.</p><h3>Agent Builder MCP feature</h3><p>The <a href="https://www.elastic.co/docs/solutions/search/agent-builder/mcp-server">MCP server</a> is available in the Agent Builder and can be accessed at:</p>{KIBANA_URL}/api/agent_builder/mcp
# Or if you are using a custom Kibana space:
{KIBANA_URL}/s/{SPACE_NAME}/api/agent_builder/mcp<p>The Agent Builder offers <a href="https://www.elastic.co/docs/solutions/search/agent-builder/tools#built-in-tools">Built-in tools</a>, and you can also create your <a href="https://www.elastic.co/docs/solutions/search/agent-builder/tools#custom-tools">custom tools</a>.</p><h2>Reference architecture</h2><p>To get a complete overview of the elements used by an agentic application in an end-to-end workflow, let’s look at the following diagram:</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt7a1c664318e2c848/6a170bde964cea3c6908bbe8/c5bbba345340bfe5571b17d53b5896d4a3235eac-4720x2560.png" alt="Agent Builder MCP feature reference architecture." /><p>Elasticsearch is at the center of this architecture, functioning as a vector store, providing the embeddings generation model, and also serving the MCP server to access the data via tools. To better explain the workflow, let’s look at the ingestion and the Agent Builder layer separately.</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/bltb616cae7400ea5f0/6a170be0dc55debba1e00e27/97a0075ae637d64140ec7ff0d167297723675632-3000x1176.png" alt="Elasticsearch at the center of the architecture, functioning as a vector store, providing the embeddings generation model, and also serving the MCP server to access the data via tools." /><p>Here, the first element is the data that will be stored in Elasticsearch. The data passes through an ingest pipeline, where it is processed by the Elasticsearch ELSER model to generate embeddings and then stored in Elasticsearch.</p><h3>Elastic Agent Builder layer</h3><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt33175b496b636661/6a170be2dc55de7daae00e2b/9bb396bbd4c3baa3be26f9d9e386f4d5405132ab-2180x2560.png" alt="The agent builder layer where the Agent Builder plays a central role by exposing the tools needed to interact with the Elasticsearch data." /><p>On this layer, the Agent Builder plays a central role by exposing the tools needed to interact with the Elasticsearch data. It manages the tools that operate over Elasticsearch indices and makes them available for consumption. Then <a href="https://docs.langchain.com/oss/python/langchain/overview">LangChain</a> handles the orchestration via the MCP client.</p><p>This architecture allows Agent Builder to work as one of many MCP servers available to the client so that the Elasticsearch agent builder can combine with other MCPs. This way, the MCP client can ask cross-source questions and then combine the answers.</p><h2>Use case: Security vulnerability agent</h2><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt9d7291c0c39e3dbe/6a170be4ab7f08b2bedb9ec4/1b46b29a8cde4645ebaec1f747be4f6888dd8d39-1600x906.png" alt="Agent builder and MCP use case. Building a security vulnerability agent." /><p>The security vulnerability agent identifies potential risks based on a user’s question by combining three complementary layers:</p><p><strong>First</strong>, it performs a <a href="https://www.elastic.co/docs/solutions/search/semantic-search">semantic search</a> with embeddings over an internal knowledge base of past incidents, configurations, and known vulnerabilities to retrieve relevant historical evidence.</p><p><strong>Second</strong>, it searches the internet for newly published recommendations or threat intelligence that may not yet exist internally.</p><p><strong>Finally</strong>, an LLM correlates and prioritizes both internal and external findings, evaluates their relevance to the user’s specific environment, and produces a clear explanation along with potential mitigation steps.</p><h2>Developing the application</h2><p>The application’s code can be found in the attached <a href="https://github.com/elastic/elasticsearch-labs/blob/main/supporting-blog-content/elasticsearch-reference-architecture-for-agentic-applications/notebook.ipynb">notebook</a>.</p><p>You can see the setup for the Python application below:</p># load environment variables
load_dotenv()

ELASTICSEARCH_ENDPOINT = os.getenv("ELASTICSEARCH_ENDPOINT")
ELASTICSEARCH_API_KEY = os.getenv("ELASTICSEARCH_API_KEY")
OPENAI_API_KEY = os.getenv("OPENAI_API_KEY")
SERPER_API_KEY = os.getenv("SERPER_API_KEY")
KIBANA_URL = os.getenv("KIBANA_URL")

INDEX_NAME = "security-vulnerabilities"
KIBANA_HEADERS = {
    "kbn-xsrf": "true",
    "Content-Type": "application/json",
    "Authorization": f"ApiKey {ELASTICSEARCH_API_KEY}",
} # Useful for Agent Builder API calls


es_client = Elasticsearch(ELASTICSEARCH_ENDPOINT, api_key=ELASTICSEARCH_API_KEY) # Elasticsearch client<p>We need to access Agent Builder and create one agent specialized in security queries and one tool to perform semantic search. You need to have the<a href="https://www.elastic.co/docs/solutions/search/agent-builder/get-started"> Agent Builder </a><a href="https://www.elastic.co/docs/solutions/search/agent-builder/get-started"><strong>enabled</strong></a> for the next step. Once it’s on, we’ll use the <a href="https://www.elastic.co/docs/solutions/search/agent-builder/kibana-api#tools">tools API</a> to create a tool that will perform a semantic search.</p>security_search_tool = {
    "id": "security-semantic-search",
    "type": "index_search",
    "description": "Search internal security documents including incident reports, pentests, internal CVEs, security guidelines, and architecture decisions. Uses semantic search powered by ELSER to find relevant security information even without exact keyword matches. Returns documents with severity assessment and affected systems.",
    "tags": ["security", "semantic", "vulnerabilities"],
    "configuration": {
        "pattern": INDEX_NAME,
    },
}

try:
    response = requests.post(
        f"{KIBANA_URL}/api/agent_builder/tools",
        headers=KIBANA_HEADERS,
        json=security_search_tool,
    )

    if response.status_code == 200:
        print("✅ Security semantic search tool created successfully")    
    else:
        print(f"Response: {response.text}")
except Exception as e:
    print(f"❌ Error creating tool: {e}")<p>Configure your tools following the <a href="https://www.elastic.co/docs/solutions/search/agent-builder/tools#best-practices">best practices</a> defined by Elastic for developing Tools. Once created, this tool will be ready to use in the Kibana UI.</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt57d9fb62f55979e7/6a170be6509168a2a9e1bb0d/5e5b3282dea07987613d8e8d35c372ca68820e44-1600x381.png" alt="Configuring tools following the best practices defined by Elastic for developing Tools." /><p>With the tool created, we can start writing the code for the ingestion workflow:</p><h3>Ingest pipeline</h3><p>To define the data structure, we need to have a <a href="https://github.com/elastic/elasticsearch-labs/blob/main/supporting-blog-content/elasticsearch-reference-architecture-for-agentic-applications/dataset.json">dataset</a> prepared for ingestion. Below is a sample document for this example:</p>{
    "title": "Incident Report: Node.js Express 4.17 Prototype Pollution RCE",
    "content": "In March 2024, our production Node.js Express 4.17 API gateway experienced a critical prototype pollution vulnerability leading to remote code execution. The attack vector involved manipulating object prototypes through JSON payloads in POST requests. This affected all Express middleware processing user input. Immediate mitigation: upgrade to Express 4.18.2+, implement input validation, use Object.freeze() for critical objects. Related to CVE-2022-24999.",
    "doc_type": "incident_report",
    "severity": "critical",
    "affected_systems": [
      "api-gateway-prod",
      "api-gateway-staging"
    ],
    "date": "2024-03-15"
}<p>For this type of document, we will use the following index mappings:</p>index_mapping = {
    "mappings": {
        "properties": {
            "title": {"type": "text", "copy_to": "semantic_field"},
            "content": {"type": "text", "copy_to": "semantic_field"},
            "doc_type": {"type": "keyword", "copy_to": "semantic_field"},
            "severity": {"type": "keyword", "copy_to": "semantic_field"},
            "affected_systems": {"type": "keyword", "copy_to": "semantic_field"},
            "date": {"type": "date"},
            "semantic_field": {"type": "semantic_text"},
        }
    }
}

if es_client.indices.exists(index=INDEX_NAME) is False:
    es_client.indices.create(index=INDEX_NAME, body=index_mapping)
    print(f"✅ Index '{INDEX_NAME}' created with semantic_text field for ELSER")
else:
    print(f"ℹ️  Index '{INDEX_NAME}' already exists, skipping creation")<p>We are creating a <a href="https://www.elastic.co/docs/reference/elasticsearch/mapping-reference/semantic-text">semantic_text</a> field to perform semantic search using the information from the fields marked with the <a href="https://www.elastic.co/docs/reference/elasticsearch/mapping-reference/copy-to">copy_to</a> property.</p><p>With that mapping definition, we can ingest the data using the <a href="https://www.elastic.co/docs/api/doc/elasticsearch/operation/operation-bulk">bulk API</a>.</p>def build_bulk_actions(documents, index_name):
    for doc in documents:
        yield {"_index": index_name, "_source": doc}


try:
    with open("dataset.json", "r") as f:
        security_documents = json.load(f)

    success, failed = helpers.bulk(
        es_client,
        build_bulk_actions(security_documents, INDEX_NAME),
        refresh=True,
    )
    print(f"📥 {success} documents indexed successfully")

except Exception as e:
    print(f"❌ Error during bulk indexing: {str(e)}")<h3>LangChain MCP client</h3><p>Here we’re going to create an MCP client using LangChain to consume the Agent Builder tools and build a workflow with LangGraph to orchestrate the client execution. The first step is to <a href="https://www.elastic.co/docs/solutions/search/agent-builder/mcp-server#configuring-mcp-clients">connect to the MCP server</a>:</p>client = MultiServerMCPClient(
    {
        "agent-builder": {
            "transport": "streamable_http",
            "url": MCP_ENDPOINT,
            "headers": {"Authorization": f"ApiKey {ELASTICSEARCH_API_KEY}"},
        }
    }
)

tools = await client.get_tools()

print(f"📋 MCP Tools available: {[t.name for t in tools]}") # ['platform_core_search',  ... 'security-semantic-search']<p>Next, we create an agent that selects the appropriate tool based on the user input:</p>reasoning = {"effort": "low"}

llm = ChatOpenAI(
    model="gpt-5.2-2025-12-11", reasoning=reasoning, openai_api_key=OPENAI_API_KEY
) # LLM client 

agent = create_agent(
    llm,
    tools=tools,
    system_prompt="""You are a cybersecurity expert specializing in infrastructure security.

        Your role is to:
        1. Analyze security queries from users
        2. Search internal security documents (incidents, pentests, CVEs, guidelines)
        3. Provide actionable security recommendations
        4. Assess vulnerability severity and impact

        When responding:
        - Always search internal documents first using the agent builder tools
        - Provide specific, technical, and actionable advice
        - Cite relevant internal incidents and documentation
        - Assess severity (critical, high, medium, low)
        - Recommend immediate mitigation steps

        Be concise but comprehensive. Focus on practical security guidance.""",
)<p>We’ll use the GPT-5.2 model, which represents OpenAI’s state-of-the-art for agent management tasks. We configure it with low reasoning effort to achieve faster responses compared to the medium or high settings, while still delivering high-quality results by leveraging the full capabilities of the GPT-5 family. You can read more about the GPT 5.2 <a href="https://openai.com/index/introducing-gpt-5-2/">here</a>.</p><p>Now that the initial setup is done, the next step is to define a workflow capable of making decisions, running tool calls, and summarizing results.</p><p>For this, we use LangGraph. We won’t cover LangGraph in depth here; <a href="https://www.elastic.co/search-labs/blog/ai-agent-workflow-finance-langgraph-elasticsearch">this article</a> provides a detailed overview of its functionality.</p><p>The following image shows a high-level view of the LangGraph application.</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blte293b7cf62f54f8e/6a170be7964cea816908bbec/729295115427ec981a594e873245fa541dd977aa-332x531.png" alt="High-level view of the LangGraph application." /><p>We need to define the application state:</p>class AgentState(TypedDict):
    query: str
    agent_builder_response: dict
    internet_results: list
    final_response: str
    needs_internet_search: bool<p>To better understand how the workflow operates, here is a brief description of each function. For full implementation details, refer to the accompanying <a href="https://github.com/elastic/elasticsearch-labs/blob/main/supporting-blog-content/elasticsearch-reference-architecture-for-agentic-applications/notebook.ipynb">notebook</a>.</p><ul><li><p><strong>call_agent_builder_semantic_search:</strong> Queries internal documentation using the Agent Builder MCP server and also stores the retrieved messages in the state.</p></li><li><p><strong>decide_internet_search:</strong> Analyzes the internal results and determines whether an external search is required.</p></li><li><p><strong>perform_internet_search: </strong>Runs an external search using the <a href="https://serper.dev/">Serper</a> API when needed.</p></li><li><p><strong>generate_response:</strong> Correlates internal and external findings and produces a final, actionable cybersecurity analysis for the user.</p></li></ul><p>With the workflow defined, we can now send a query:</p>query = "We are using Node.js with Express 4.17 for our API gateway. Are there known prototype pollution or remote code execution vulnerabilities?"<p>In this example, we want to evaluate whether this specific version of Express is affected by known vulnerabilities.</p><h4>Research results</h4><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/bltac164b2086d23589/6a170be9a29299162cd01057/b18a31e42bcd8f4d86bb605f85d4ff77135b0855-1084x517.png" alt="Elastic agent builder and MCP security agent research results." /><p>See the complete response in <a href="https://github.com/elastic/elasticsearch-labs/blob/main/supporting-blog-content/elasticsearch-reference-architecture-for-agentic-applications/notebook.ipynb">this file</a>.</p><p>This response clearly correlates internal and internet findings and provides actionable mitigation steps. It successfully highlights the severity of the vulnerability and offers a structured, security-oriented summary.</p><h3>Extensions and future enhancements</h3><p>This architecture is modular and allows us to extend its capabilities by replacing, improving, or adding components to the existing list. We could add another agent, consumed by the same MCP client. We can also use an automated ingestion workflow with tools such as Logstash, Kafka, or <a href="https://www.elastic.co/docs/reference/search-connectors/self-managed-connectors">Elastic self-managed connectors.</a> Feel free to change the LLM, the MCP client framework, or the embeddings model or add more tools depending on your needs.</p><h2>Conclusion</h2><p>This reference architecture shows a practical way to combine Elasticsearch, the Agent Builder, and MCP to build an AI-driven application. Its structure keeps each part independent, which makes the system easy to implement, maintain, and extend.</p><p>You can start with a simple setup (like the security use case in this article) and scale it by adding new tools, data sources, or agents as your needs grow. Overall, it provides a straightforward path for building flexible and reliable agentic workflows on top of Elasticsearch.</p>]]></content:encoded>
    <link>https://www.elastic.co/search-labs/blog/agent-builder-mcp-reference-architecture-elasticsearch</link>
    <guid isPermaLink="true">https://www.elastic.co/search-labs/blog/agent-builder-mcp-reference-architecture-elasticsearch</guid>
    <category><![CDATA[Agentic AI]]></category>
    <category><![CDATA[AI Tools ]]></category>
    <dc:creator><![CDATA[Jeffrey Rengifo]]></dc:creator>
    <enclosure url="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt22bfbe4b04ea2e92/6a170beb60084b717d3c4597/33a57e3f61f9095c99b6d1499175a6edb0d5dfc5-4720x2560.png" length="0" type="image/png"/>
    <pubDate>Wed, 07 Jan 2026 00:00:00 GMT</pubDate>
  </item>
  </channel>
</rss>