<?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[Carlos Delgado - 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[Carlos Delgado - Elasticsearch Labs]]></title>
      <url>https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt1121c0bf0e8a6e65/6a88da6340a1841030ef456f/search-labs-thumbnail.png</url>
      <link>https://www.elastic.co/search-labs/author/carlos-delgado</link>
    </image>
    <link>https://www.elastic.co/search-labs/author/carlos-delgado</link>
    <atom:link href="https://www.elastic.co/search-labs/rss/author/carlos-delgado.xml" rel="self" type="application/rss+xml"/>
    <language><![CDATA[en]]></language>
    <lastBuildDate>Fri, 25 Sep 2026 22:45:05 GMT</lastBuildDate>
  <item>
    <title><![CDATA[Hybrid search and multistage retrieval in ES|QL]]></title>
    <description><![CDATA[Explore the multistage retrieval capabilities of ES|QL, using FORK and FUSE commands to integrate hybrid search with semantic reranking and native LLM completions.]]></description>
    <content:encoded><![CDATA[<p>In Elasticsearch 9.2, we’ve introduced the ability to do dense vector search and hybrid search in Elasticsearch Query Language (ES|QL). This continues our investment in making ES|QL the best search language to solve modern search use cases.</p><h2>Multistage retrieval: The challenge of modern search</h2><p>Modern search has evolved beyond simple keyword matching. Today's search applications need to understand intent, handle natural language, and combine multiple ranking signals to deliver the best results.</p><p>Retrieval of the most relevant results happens in multiple stages, with each stage gradually refining the result set. This wasn’t the case in the past, where most use cases would require one or two stages of retrieval: an initial query to get results and a potential rescoring phase.	</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt3382265814939417/6a170df3a929cf1246ae0a61/fceada10b0c09d6a4a372f137bb3040e1ff41fbf-1600x895.png" alt="" /><p>We start with an initial retrieval, where we cast a wide net to gather results that are relevant to our query. Since we need to sieve through all the data, we should use techniques that return results fast, even when we index billions of documents.</p><p>We therefore employ trusted techniques, such as lexical search that Elasticsearch has supported and optimized since the beginning, or vector search, where Elasticsearch excels in speed and accuracy.</p><p>Lexical search using BM25 is quite fast and best at exact term matching or phrase matching, and <a href="https://www.elastic.co/docs/solutions/search/vector">vector</a> or <a href="https://www.elastic.co/docs/solutions/search/semantic-search">semantic search</a> is better suited for handling natural language queries. <a href="https://www.elastic.co/what-is/hybrid-search">Hybrid search</a> combines lexical and <a href="https://www.elastic.co/docs/solutions/search/vector">vector search</a> results to bring the best from both. The challenge that hybrid search solves is that vector and lexical search have completely different and incompatible scoring functions which produce values in different intervals, following different distributions. A vector search score close to 1 can mean a very close match, but it doesn’t mean the same for lexical search. Hybrid search methods, such as <a href="https://www.elastic.co/docs/reference/elasticsearch/rest-apis/reciprocal-rank-fusion">reciprocal rank fusion</a> (RRF) and linear combination of scores, assign new scores that blend the original scores from lexical and vector search.</p><p>After hybrid search, we can employ techniques such as <a href="https://www.elastic.co/docs/solutions/search/ranking/semantic-reranking">semantic reranking</a> and <a href="https://www.elastic.co/docs/solutions/search/ranking/learning-to-rank-ltr">Learning To Rank</a> (LTR), which use specialized machine learning models to rerank the result.</p><p>With our most relevant results, we can use large language models (LLMs) to further enrich our response or pass the most relevant results as context to LLMs in agentic workflows in tools such as <a href="https://www.elastic.co/search-labs/blog/elastic-ai-agent-builder-context-engineering-introduction">Elastic Agent Builder</a>.</p><p>ES|QL is able to handle all these stages of retrieval. By design, ES|QL is a piped language, where each command transforms the input and sends the output to the next command. Each stage of retrieval is represented by one or more consecutive ES|QL commands. In this article, we show how each stage is supported in ES|QL.</p><h2>Vector search</h2><p>In Elasticsearch 9.2, we introduced tech preview support for dense vector search in ES|QL. This is as simple as calling the <code>knn</code> function, which only requires a <a href="https://www.elastic.co/docs/reference/elasticsearch/mapping-reference/dense-vector"><code>dense_vector</code></a> field and a query vector:</p>FROM books METADATA _score
| WHERE KNN(description_vector, ?query_vector)
| SORT _score DESC
| LIMIT 100<p>This query executes an approximate nearest neighbor search, retrieving 100 documents that are the most similar to the <code>query_vector</code>.</p><h2>Hybrid search: Reciprocal rank fusion</h2><p>In Elasticsearch 9.2, we introduced support for hybrid search using RRF and linear combination of results in ES|QL.</p><p>This allows combining vector search and lexical search results into a single result set.</p><p>To achieve this in ES|QL, we need to use the <a href="https://www.elastic.co/docs/reference/query-languages/esql/commands/fork"><code>FORK</code></a> and <a href="https://www.elastic.co/docs/reference/query-languages/esql/commands/fuse"><code>FUSE</code></a> commands. <code>FORK</code> runs multiple branches of execution, and <code>FUSE</code> merges the results and assigns new relevance scores using RRF or linear combination.</p><p>In the following example, we use <code>FORK</code> to run two separate branches, where one is doing a lexical search using the <code>match</code> function, while the other is doing a vector search using the <code>knn</code> function. We then merge the results together using <code>FUSE</code>:</p>FROM books METADATA _score, _id, _index
| FORK (WHERE KNN(description_vector, ?query_vector) | SORT _score DESC | LIMIT 100)
       (WHERE MATCH(description, ?query) | SORT _score DESC | LIMIT 100)
| FUSE // uses RRF by default
| SORT _score DESC<p>Let's decompose the query to better understand the execution model and first look at the output of the <code>FORK</code> command:</p>FROM books METADATA _score, _id, _index
| FORK (WHERE KNN(description_vector, ?query_vector) | SORT _score DESC | LIMIT 100)
       (WHERE MATCH(description, ?query) | SORT _score DESC | LIMIT 100)<p>The<code> FORK</code> commands outputs the results from both branches and adds a <code>_fork</code> discriminator column:</p><p>_id</p><p>title</p><p>_score</p><p>_fork</p><p>4001</p><p>The Hobbit</p><p>0.88</p><p>fork1</p><p>3999</p><p>The Fellowship of the Ring</p><p>0.88</p><p>fork1</p><p>4005</p><p>The Two Towers</p><p>0.86</p><p>fork1</p><p>4006</p><p>The Return of the King</p><p>0.84</p><p>fork1</p><p>4123</p><p>The Silmarillion</p><p>0.78</p><p>fork1</p><p>4144</p><p>The Children of Húrin</p><p>0.79</p><p>fork1</p><p>4001</p><p>The Hobbit</p><p>4.55</p><p>fork2</p><p>3999</p><p>The Fellowship of the Ring</p><p>4.25</p><p>fork2</p><p>4123</p><p>The Silmarillion</p><p>4.11</p><p>fork2</p><p>4005</p><p>The Two Towers</p><p>3.8</p><p>fork2</p><p>4006</p><p>The Return of the King</p><p>4.1</p><p>fork2</p><p>As you’ll notice, certain documents appear twice, which is why we then use <code>FUSE</code> to merge rows that represent the same documents and assign new relevance scores. <code>FUSE</code> is executed in two stages:</p><ul><li><p>For each row, <code>FUSE</code> assigns a new relevance score, depending on the hybrid search algorithm that is being used.</p></li><li><p>Rows that represent the same document are merged together, and a new score is computed.</p></li></ul><p>In our example, we’re using RRF. As a first step, <code>FUSE</code> assigns a new score to each row using the RRF formula:</p>score(doc) = 1 / (rank_constant + rank(doc))<p>Where the <code>rank_constant</code> takes a default value of 60 and <code>rank(doc)</code>represents the position of the document in the result set.</p><p>In the first phase, our results become:</p><p>_id</p><p>title</p><p>_score</p><p>_fork</p><p>4001</p><p>The Hobbit</p><p>1 / (60 + 1) = 0.01639</p><p>fork1</p><p>3999</p><p>The Fellowship of the Ring</p><p>1 / (60 + 2) = 0.01613</p><p>fork1</p><p>4005</p><p>The Two Towers</p><p>1 / (60 + 3) = 0.01587</p><p>fork1</p><p>4006</p><p>The Return of the King</p><p>1 / (60 + 4) = 0.01563</p><p>fork1</p><p>4123</p><p> The Silmarillion</p><p>1 / (60 + 5) = 0.01538</p><p>fork1</p><p>4144</p><p>The Children of Húrin</p><p>1 / (60 + 6) = 0.01515</p><p>fork1</p><p>4001</p><p>The Hobbit</p><p>1 / (60 + 1) = 0.01639</p><p>fork2</p><p>3999</p><p>The Fellowship of the Ring</p><p>1 / (60 + 2) = 0.01613</p><p>fork2</p><p>4123</p><p>The Silmarillion</p><p>1 / (60 + 3) = 0.01587</p><p>fork2</p><p>4005</p><p>The Two Towers</p><p>1 / (60 + 4) = 0.01563</p><p>fork2</p><p>4006</p><p>The Return of the King</p><p>1 / (60 + 5) = 0.01538</p><p>fork2</p><p>Then the rows are merged together and a new score is assigned. Since a <code>SORT _score DESC</code> follows the <code>FUSE</code> command, the final results are:</p><p>_id</p><p>title</p><p>_score</p><p>4001</p><p>The Hobbit</p><p>0.01639 + 0.01639 = 0.03279</p><p>3999</p><p>The Fellowship of the Ring</p><p>0.01613 + 0.01613 = 0.03226</p><p>4005</p><p>The Two Towers</p><p>0.01587 + 0.01563 = 0.0315</p><p>4123</p><p>The Silmarillion</p><p>0.01538 + 0.01587 = 0.03125</p><p>4006</p><p>The Return of the King</p><p>0.01563 + 0.01538 = 0.03101</p><p>4144</p><p>The Children of Húrin</p><p>0.01515</p><h2>Hybrid search: Linear combination of scores</h2><p><a href="https://www.elastic.co/docs/reference/elasticsearch/rest-apis/reciprocal-rank-fusion">Reciprocal rank fusion</a> is the simplest way to do hybrid search, but it isn’t the only hybrid search method that we support in ES|QL.</p><p>In the following example, we use <code>FUSE</code> to combine lexical and <a href="https://www.elastic.co/docs/solutions/search/semantic-search/semantic-search-semantic-text">semantic search</a> results using linear combination of scores:</p>FROM books METADATA _score, _id, _index
| FORK (WHERE MATCH(semantic_description, ?query) | SORT _score DESC | LIMIT 100)
       (WHERE MATCH(description, ?query) | SORT _score DESC | LIMIT 100)
| FUSE LINEAR WITH { "weights": { "fork1": 0.7, "fork2": 0.3 } }
| SORT _score DESC<p>Let's first decompose the query and take a look at the input of the <code>FUSE</code> command when we only run the <code>FORK</code> command.</p><p>Notice that we use the <code>match</code> function, which is able to not only query lexical fields, such as <code>text</code> or <code>keyword</code>, but also <a href="https://www.elastic.co/docs/reference/elasticsearch/mapping-reference/semantic-text"><code>semantic_text</code></a> fields.</p><p>The first <code>FORK</code> branch executes a semantic query by querying a <code>semantic_text</code> field, while the second one executes a lexical query:</p>FROM books METADATA _score, _id, _index
| FORK (WHERE MATCH(semantic_description, ?query) | SORT _score DESC | LIMIT 100)
       (WHERE MATCH(description, ?query) | SORT _score DESC | LIMIT 100)<p>The output of the <code>FORK</code> command can contain rows with the same <code>_id</code> and <code>_index</code> values representing the same Elasticsearch document:</p><p>_id</p><p>title</p><p>_score</p><p>_fork</p><p>4001</p><p>The Hobbit</p><p>0.88</p><p>fork1</p><p>3999</p><p>The Fellowship of the Ring</p><p>0.88</p><p>fork1</p><p>4005</p><p>The Two Towers</p><p>0.86</p><p>fork1</p><p>4006</p><p>The Return of the King</p><p>0.84</p><p>fork1</p><p>4123</p><p>The Silmarillion</p><p>0.78</p><p>fork1</p><p>4144</p><p>The Children of Húrin</p><p>0.79</p><p>fork1</p><p>4001</p><p>The Hobbit</p><p>4.55</p><p>fork2</p><p>3999</p><p>The Fellowship of the Ring</p><p>4.25</p><p>fork2</p><p>4123</p><p>The Silmarillion</p><p>4.11</p><p>fork2</p><p>4005</p><p>The Two Towers</p><p>3.8</p><p>fork2</p><p>4006</p><p>The Return of the King</p><p>4.1</p><p>fork2</p><p>In the next step, we use <code>FUSE</code> to merge rows that have the same <code>_id</code> and <code>_index</code> values, and assign new relevance scores.</p><p>The new score is a linear combination of the scores the row had in each <code>FORK</code> branch:</p>_score = 0.7 *_score1 + 0.3 * _score2<p>Here, <code>_score1</code> and <code>_score2</code> represent the score a document has in the first <code>FORK</code> branch and the second <code>FORK</code> branch, respectively.</p><p>Notice that we also apply custom weights, giving more weight to the semantic score over the lexical one, resulting in this set of documents:</p><p>_id</p><p>title</p><p>_score</p><p>4001</p><p>The Hobbit</p><p>0.7 * 0.88 + 0.3 * 4.55 = 1.981</p><p>3999</p><p>The Fellowship of the Ring</p><p>0.7 * 0.88 + 0.3 * 4.25 = 1.891</p><p>4006</p><p>The Return of the King</p><p>0.7 * 0.84 + 0.3 * 4.1 = 1.818</p><p>4123</p><p>The Silmarillion</p><p>0.7 * 0.78 + 0.3 * 4.11 = 1.779</p><p>4005</p><p>The Two Towers</p><p>0.7 * 0.86 + 0.3 * 3.8 = 1.742</p><p>4144</p><p>The Children of Húrin</p><p>0.7 * 0.79 + 0.3 * 0 = 0.553</p><p>One challenge is that the semantic and lexical scores can be incompatible to apply the linear combination, since they can follow completely different distributions. To mitigate this, we first need to normalize the scores, employing score normalization methods, such as <code>minmax</code>. This ensures that the scores from each <code>FORK</code> branch are first normalized to take values between 0 and 1, before applying the linear combination formula.</p><p>To achieve this with <code>FUSE</code>, we need to specify the <code>normalizer</code> option:</p>FROM books METADATA _score, _id, _index
| FORK (WHERE MATCH(semantic_description, ?query) | SORT _score DESC | LIMIT 100)
       (WHERE MATCH(description, ?query) | SORT _score DESC | LIMIT 100)
| FUSE LINEAR WITH { "weights": { "fork1": 0.7, "fork2": 0.3 }, "normalizer": "minmax" }
| SORT _score DESC<h2>Semantic reranking</h2><p>At this stage, after hybrid search, we should be left with the most relevant documents. We can now use semantic reranking to reorder the results using the <code>RERANK</code> command. By default, <code>RERANK</code> uses the latest Elastic <a href="https://www.elastic.co/docs/solutions/search/ranking/semantic-reranking">semantic reranking</a> machine learning model, so no additional configuration is needed:</p>FROM books METADATA _score, _id, _index
| FORK (WHERE KNN(description_vector, ?query_vector) | SORT _score DESC | LIMIT 100)
       (WHERE MATCH(description, ?query) | SORT _score DESC | LIMIT 100)
| FUSE
| SORT _score DESC
| LIMIT 100
| RERANK ?query ON description
| SORT _score DESC<p>We now have our best results, sorted by relevance.</p><p>One key feature that sets the <code>RERANK</code> command apart from other products that offer semantic reranking integrations is that it doesn’t require the input to represent a mapped field from an index. <code>RERANK</code> only expects an expression that evaluates to a string value, making it possible to do semantic reranking using multiple fields:</p>FROM books METADATA _score, _id, _index
| FORK (WHERE KNN(description_vector, ?query_vector) | SORT _score DESC | LIMIT 100)
       (WHERE MATCH(description, ?query) | SORT _score DESC | LIMIT 100)
| FUSE
| SORT _score DESC
| LIMIT 100
| RERANK ?query ON CONCAT(title, "\n", description) 
| SORT _score DESC<h2>LLM completions</h2><p>Now we have a set of highly relevant, reranked results.</p><p>At this stage, you might simply decide to return the results back to your application or you might want to further enhance your results using LLM completions.</p><p>If you’re using ES|QL as part of a retrieval-augmented generation (RAG) workflow, you can choose to call your favorite LLM directly from ES|QL.
To achieve this, we’ve added a new <code>COMPLETION</code> command that takes in a prompt, a completion inference ID which designates which LLM to call, and a column identifier to specify where to output the LLM response.</p><p>In the following example, we’re using <code>COMPLETION</code> to add a new <code>_completion</code> column that contains the summary of the <code>content</code> column:</p>FROM books METADATA _score, _id, _index
| FORK (WHERE KNN(description_vector, ?query_vector) | SORT _score DESC | LIMIT 100)
       (WHERE MATCH(description, ?query) | SORT _score DESC | LIMIT 100)
| FUSE
| SORT _score DESC
| LIMIT 100
| RERANK ?query ON description
| SORT _score DESC
| LIMIT 10
| COMPLETION CONCAT("Summarize the following:\n", description) WITH { "inference_id" : "my_inference_endpoint" } <p>Each row now contains a summary:</p><p>_id</p><p>title</p><p>_score</p><p>summary</p><p>4001</p><p>The Hobbit</p><p>0.03279</p><p>Bilbo helps dwarves reclaim Erebor from the dragon Smaug.</p><p>3999</p><p>The Fellowship of the Ring</p><p>0.03226</p><p>Frodo begins the quest to destroy the One Ring.</p><p>4005</p><p>The Two Towers</p><p>0.0315</p><p>The Fellowship splits; war comes to Rohan; Frodo nears Mordor.</p><p>4123</p><p>The Silmarillion</p><p>0.03125</p><p>Ancient myths and history of Middle-earth's First Age.</p><p>4006</p><p>The Return of the King</p><p>0.3101</p><p>Sauron is defeated and Aragorn is crowned King.</p><p>4144</p><p>The Children of Húrin</p><p>0.01515</p><p>The tragic tale of Túrin Turambar's cursed life.</p><p>In another use case, you may simply want to answer a question using the proprietary data that you have indexed in Elasticsearch. In this case, the best search results that we’ve computed in the previous stage can be used as context for the prompt:</p>FROM books METADATA _score, _id, _index
| FORK (WHERE KNN(description_vector, ?query_vector) | SORT _score DESC | LIMIT 100)
       (WHERE MATCH(description, ?query) | SORT _score DESC | LIMIT 100)
| FUSE
| SORT _score DESC
| LIMIT 100
| RERANK ?query ON description
| SORT _score DESC
| LIMIT 10
| STATS context = VALUES(CONCAT(title, "\n", description)
| COMPLETION CONCAT("Answer the following question ", ?query, "based on:\n", context) WITH { "inference_id" : "my_inference_endpoint" }<p>Since the <code>COMPLETION</code> command unlocks the ability to send any prompt to an LLM, the possibilities are endless. Although we’re only showing a few examples, the <code>COMPLETION</code> command can be used in a wide range of scenarios, from security analysts using it to assign scores depending on whether a log event can represent a malicious action or data scientists using it to analyze data, to cases where you just need to<a href="https://www.elastic.co/search-labs/blog/esql-completion-command-llm-fact-generator"> generate Chuck Norris facts based on your data</a>.</p><h2>This is only the beginning</h2><p>In the future, we’ll be expanding ES|QL to improve semantic reranking for long documents, better conditional execution of the ES|QL queries using multiple <code>FORK</code> commands, support sparse vector queries, removing close duplicate results to enhance result diversity, allowing full text search on runtime generated columns, and many other scenarios.</p><p>Additional tutorials and guides:</p><ul><li><p><a href="https://www.elastic.co/docs/solutions/search/esql-for-search">ES|QL for search</a></p></li><li><p><a href="https://www.elastic.co/docs/reference/query-languages/esql/esql-search-tutorial">ES|QL for search tutorial</a></p></li><li><p><a href="https://www.elastic.co/docs/reference/elasticsearch/mapping-reference/semantic-text">Semantic_text field type</a></p></li><li><p><a href="https://www.elastic.co/docs/reference/query-languages/esql/commands/fork"><code>FORK</code></a> and <a href="https://www.elastic.co/docs/reference/query-languages/esql/commands/fuse"><code>FUSE</code></a> documentation</p></li><li><p>ES|QL search functions</p></li></ul>]]></content:encoded>
    <link>https://www.elastic.co/search-labs/blog/hybrid-search-multi-stage-retrieval-esql</link>
    <guid isPermaLink="true">https://www.elastic.co/search-labs/blog/hybrid-search-multi-stage-retrieval-esql</guid>
    <category><![CDATA[ES|QL]]></category>
    <category><![CDATA[Hybrid Search]]></category>
    <category><![CDATA[Relevance]]></category>
    <dc:creator><![CDATA[Ioana Tagirta,Aurélien Foucret,Carlos Delgado]]></dc:creator>
    <enclosure url="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt3382265814939417/6a170df3a929cf1246ae0a61/fceada10b0c09d6a4a372f137bb3040e1ff41fbf-1600x895.png" length="0" type="image/png"/>
    <pubDate>Thu, 08 Jan 2026 00:00:00 GMT</pubDate>
  </item>
  <item>
    <title><![CDATA[Elasticsearch new semantic_text mapping: Simplifying semantic search]]></title>
    <description><![CDATA[Learn how to use the new semantic_text field type and semantic query for simplifying semantic search in Elasticsearch.]]></description>
    <content:encoded><![CDATA[<h2>semantic_text - You know, for semantic search!</h2><p>Do you want to start using semantic search for your data, but focus on your model and results instead of on the technical details? We’ve introduced the <code>semantic_text</code> field type that will take care of the details and infrastructure that you need.</p><p><a href="https://www.elastic.co/what-is/semantic-search">Semantic search</a> is a sophisticated technique designed to enhance the relevance of search results by utilizing <a href="https://www.elastic.co/elasticsearch/machine-learning">machine learning models</a>. Unlike traditional keyword-based search, semantic search focuses on understanding the meaning of words and the context in which they are used. This is achieved through the application of machine learning models that provide a deeper semantic understanding of the text.</p><p>These models generate <a href="https://www.elastic.co/what-is/vector-embedding">vector embeddings</a>, which are numeric representations capturing the text meaning. These embeddings are stored alongside your document data, enabling <a href="https://www.elastic.co/what-is/vector-search">vector search techniques</a> that take into account the word meaning and context instead of pure lexical matches.</p><h2>How to perform semantic search</h2><p>To perform semantic search, you need to go through the following steps:</p><ul><li><p><a href="https://www.elastic.co/search-labs/blog/semantic-search-simplified-semantic-text#choosing-an-inference-model">Choose an inference mode</a>l to create embeddings, both for indexing documents and performing queries.</p></li><li><p><a href="https://www.elastic.co/search-labs/blog/semantic-search-simplified-semantic-text#creating-your-index-mapping">Create your index mapping</a> to store the inference results, so they can be efficiently searched afterwards.</p></li><li><p><a href="https://www.elastic.co/search-labs/blog/semantic-search-simplified-semantic-text#setting-up-indexing">Setting up indexing</a> so inference results are calculated for new documents added to your index.</p></li><li><p><a href="https://www.elastic.co/search-labs/blog/semantic-search-simplified-semantic-text#automatically-handling-long-text-passages">Automatically handle long text documents</a>, so search can be accurate and cover the entire document.</p></li><li><p><a href="https://www.elastic.co/search-labs/blog/semantic-search-simplified-semantic-text#querying-your-data">Querying your data</a> to retrieve results.</p></li></ul><p>Configuring semantic search from the ground up can be complex. It requires setting up mappings, ingestion pipelines, and queries tailored to your chosen inference model. Each step offers opportunities for fine-tuning and optimization, but also demands careful configuration to ensure all components work together seamlessly.</p><p>While this offers a great degree of control, it makes using semantic search a detailed and deliberate process, requiring you to configure separate pieces that are all related to each other and to the inference model.</p><p><a href="https://www.elastic.co/guide/en/elasticsearch/reference/current/semantic-text.html"><code>semantic_text</code></a> simplifies this process by focusing on what matters: the inference model. Once you have selected the inference model, <code>semantic_text</code> will make it easy to start using semantic search by providing sensible defaults, so you can focus on your search and not on how to index, generate, or query your embeddings.</p><p>Let's take a look at each of these steps, and how <code>semantic_text</code> simplifies this setup.</p><h3>Choosing an inference model</h3><p>The inference model will generate embeddings for your documents and queries. Different models have different tradeoffs in terms of:</p><ul><li><p>Accuracy and relevance of the results</p></li><li><p>Scalability and performance</p></li><li><p>Language and multilingual support</p></li><li><p>Cost</p></li></ul><p>Elasticsearch supports both internal and external inference services:</p><ul><li><p>Internal services are deployed in the Elasticsearch cluster. You can use already included models like <a href="https://www.elastic.co/guide/en/machine-learning/current/ml-nlp-elser.html">ELSER</a> and <a href="https://www.elastic.co/guide/en/machine-learning/current/ml-nlp-e5.html">E5</a>, or import <a href="https://www.elastic.co/guide/en/machine-learning/current/ml-nlp-model-ref.html#ml-nlp-model-ref-text-embedding">external models</a> into the cluster using <a href="https://www.elastic.co/guide/en/machine-learning/current/ml-nlp-import-model.html">eland</a>.</p></li><li><p>External services are deployed by model providers. Elasticsearch supports the following:   </p><ul><li><p><a href="https://www.elastic.co/search-labs/blog/elasticsearch-cohere-embeddings-support">Cohere</a></p></li><li><p><a href="https://www.elastic.co/search-labs/integrations/hugging-face">Hugging Face</a></p></li><li><p><a href="https://www.elastic.co/search-labs/integrations/mistral">Mistral</a></p></li><li><p><a href="https://www.elastic.co/search-labs/integrations/open-ai">OpenAI</a></p></li><li><p><a href="https://www.elastic.co/search-labs/blog/elasticsearch-azure-ai-studio-support">Azure AI Studio</a></p></li><li><p><a href="https://www.elastic.co/search-labs/blog/elasticsearch-azure-openai-embeddings-support">Azure OpenAI</a></p></li><li><p>Google AI Studio</p></li></ul></li></ul><p>Once you have chosen the inference mode, <a href="https://www.elastic.co/guide/en/elasticsearch/reference/current/put-inference-api.html">create an inference endpoint</a> for it. The inference endpoint identifier will be the only configuration detail that you will need to set up <code>semantic_text</code>.</p>PUT _inference/sparse_embedding/my-elser-endpoint
{
  "service": "elser",
  "service_settings": {
    "num_allocations": 1,
    "num_threads": 1
  }
}
<h3>Creating your index mapping</h3><p>Elasticsearch will need to index the embeddings generated by the model so they can be efficiently queried later.</p><p>Before semantic_text, you needed to understand about the two main field types used for storing embeddings information:</p><ul><li><p><a href="https://www.elastic.co/guide/en/elasticsearch/reference/current/sparse-vector.html"><code>sparse_vector</code></a>: It indexes sparse vector embeddings, like the ones generated by ELSER. Each embedding consists of pairs of tokens and weights. There is a small number of tokens generated per embedding.</p></li><li><p><a href="https://www.elastic.co/guide/en/elasticsearch/reference/current/dense-vector.html"><code>dense_vector</code></a>: It indexes vectors of numbers, which contains the embedding information. A model produces vectors of a fixed size, called the vector dimension.</p></li></ul><p>The field type to use is conditioned by the model you have chosen. If using dense vectors, you will need to <a href="https://www.elastic.co/guide/en/elasticsearch/reference/current/dense-vector.html#dense-vector-params">configure</a> the field to include the dimension count, the similarity function used to calculate vectors proximity, and storage customizations like quantization or the specific data type used for each element.</p><p>Now, if you're using semantic_text, you define a <a href="https://www.elastic.co/guide/en/elasticsearch/reference/current/semantic-text.html">semantic_text field mapping</a> by just specifying the inference endpoint identifier for your model:</p>PUT test-index
{
  "mappings": {
    "properties": {
      "infer_field": {
        "type": "semantic_text",
        "inference_id": "my-elser-endpoint"
      }
    }
  }
}
<p>That's it. No need for you to define other mapping options, or to understand which field type you need to use.</p><h3>Setting up indexing</h3><p>Once your index is ready to store the embeddings, it's time to generate them.</p><p>Before <code>semantic_text</code>, to generate embeddings automatically on document ingestion you needed to set up an <a href="https://www.elastic.co/guide/en/elasticsearch/reference/current/ingest.html">ingestion pipeline</a>.</p><p>Ingestion pipelines are used to automatically enrich or transform documents when ingested into an index, or when explicitly specified as part of the ingestion process.</p><p>You need to use the <a href="https://www.elastic.co/guide/en/elasticsearch/reference/current/inference-processor.html">inference processor</a> to generate embeddings for your fields. The processor needs to be configured using:</p><ul><li><p>The text fields from which to generate the embeddings</p></li><li><p>The output fields where the generated embeddings will be added</p></li><li><p>Specific inference configuration for text embeddings or sparse embeddings, depending on the model type</p></li></ul><p>With <code>semantic_text</code>, you simply add documents to your index. semantic_text fields will automatically calculate the embeddings using the specified inference endpoint.</p><p>This means there's no need to create an inference pipeline to generate the embeddings. Using bulk, index, or update APIs will do that for you automatically:</p>PUT test-index/_doc/doc1
{
  "infer_field": "These are not the droids you're looking for. He's free to go around"
}
<p>Inference requests in <code>semantic_text</code> fields are also batched. If you have 10 documents in a bulk API request, and each document contains 2 <code>semantic_text</code> fields, then that request will perform a single inference request with 20 texts to your inference service in one go, instead of making 10 separate inference requests of 2 texts each.</p><h3>Automatically handling long text passages</h3><p>Part of the challenge of selecting a model is the number of tokens that the model can generate embeddings for. Models have a limited number of tokens they can process. This is referred to as the model’s context window.</p><p>If the text you need to work with is longer than the model’s context window, you may <strong>truncate</strong> the text and use just part of it to generate embeddings. This is not ideal as you'll lose information; the resulting embeddings will not capture the full context of the input text.</p><p>Even if you have a long context window, having a long text means a lot of content will be reduced to a single embedding, making it an inaccurate representation.</p><p>Also, returning a long text will be difficult for the users to understand, as they will have to scan the text to check it's what they are looking for. Using smaller snippets would be preferable instead.</p><p>Another option is to use <strong>chunking</strong> to divide long texts into smaller fragments. These smaller chunks are added to each document to provide a better representation of the complete text. You can then use a nested query to search over all the individual fragments and retrieve the documents that contain the best-scoring chunks.</p><p>Before <code>semantic_text</code>, chunking was not done out of the box - the inference processor did not support chunking. If you needed to use chunking, you needed to do it before ingesting your documents or use the script processor to perform the chunking in Elasticsearch.</p><p>Using semantic_text means that chunking will be done on your behalf when indexing. Long documents will be split into 250-word sections with a 100-word overlap so that each section shares 100 words with the previous section. This overlap ensures continuity and prevents vital contextual information in the input text from being lost by a hard break.</p><p>If the model and inference service support batching the chunked inputs are automatically batched together into as few requests as possible, each optimally sized for the Inference Service. The resulting chunks will be stored in a nested object structure so you can check the text contained in each chunk.</p><h3>Querying your data</h3><p>Now that the documents and their embeddings are indexed in Elasticsearch, it's time to do some queries!</p><p>Before <code>semantic_text</code>, you needed to use a different query depending on the type of embeddings the model generates (dense or sparse). A <a href="https://www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl-sparse-vector-query.html">sparse vector query</a> is needed to query sparse_vector field types, and either a <a href="https://www.elastic.co/guide/en/elasticsearch/reference/current/knn-search.html">knn search</a> or a <a href="https://www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl-knn-query.html">knn query</a> can be used to search dense_vector field types.</p><p>The query process can be further customized for performance and relevance. For example, sparse vector queries can define <a href="https://www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl-sparse-vector-query.html#sparse-vector-query-with-pruning-config-and-rescore-example">token pruning</a> to avoid considering irrelevant tokens. Knn queries can specify the number of candidates to consider and the top k results to be returned from each shard.</p><p>You don't need to deal with those details when using <code>semantic_text</code>. You use a <a href="https://www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl-semantic-query.html">single query type</a> to search your documents:</p>GET test-index/_search
{
  "query": {
    "semantic": {
      "field": "infer_field",
      "query": "robots you're searching for"
    }
  }
}
<p>Just include the field and the query text. There’s no need to decide between sparse vector and knn queries, semantic text does this for you.</p><p>Compare this with using a specific <code>knn</code> search with all its configuration parameters:</p>{
  "knn": {
    "field": "infer_field",
    "k": 10,
    "num_candidates": 100,
    "query_vector_builder": {
      "text_embedding": { 
        "model_id": "my-dense-vector-embedding-model", 
        "model_text": "robots you're searching for" 
      }
    }
  }
}
<h2>Under the hood: How <code>semantic_text</code> works</h2><p>To understand how <code>semantic_text</code> works, you can create a <code>semantic_text</code> index and check what happens when you ingest a document. When the first document is ingested, the inference endpoint calculates the embeddings. When indexed, you will notice changes in the index mapping:</p>GET test-index
{
  "test-index": {
    "mappings": {
      "properties": {
        "infer_field": {
          "type": "semantic_text",
          "inference_id": "my-elser-endpoint",
          "model_settings": {
            "task_type": "sparse_embedding"
          }
        }
      }
    }
  }
}
<p>Now there is additional information about the model settings. Text embedding models will also include information like the number of dimensions or the similarity function for the model.</p><p>You can check the document already includes the embedding results:</p>GET test-index/_doc/doc1
{
  "_index": "test-sparse",
  "_id": "doc1",
  "_source": {
    "infer_field": {
      "text": "these are not the droids you're looking for. He's free to go around",
      "inference": {
        "inference_id": "my-elser-endpoint",
        "model_settings": {
          "task_type": "sparse_embedding"
        },
        "chunks": [
          {
            "text": "these are not the droids you're looking for. He's free to go around",
            "embeddings": {
              "##oid": 1.9103845,
              "##oids": 1.768872,
              "free": 1.693662,
              "dr": 1.6103356,
              "around": 1.4376559,
              "these": 1.1396849

              …
            }
          }
        ]
      }
    }
  }
}
<p>The field does not just contain the input text, but also a structure storing the original text, the model settings, and information for each chunk the input text has been divided into.</p><p>This structure consists of an object with two elements:</p><ul><li><p><em>text</em>: Contains the original input text</p></li><li><p><em>inference</em>: Inference information added by the inference endpoint, that consists of: </p><ul><li><p><em>inference_id</em> of the inference endpoint</p></li><li><p><em>model_settings</em> that contain model properties</p></li><li><p><em>chunks</em>: Nested object that contains an element for each chunk that has been created from the input text. Each chunk contains:</p><ul><li><p>The <em>text</em> for the chunk</p></li><li><p>The calculated <em>embeddings</em> for the chunk text</p></li></ul></li></ul></li></ul><h2>Customizing <code>semantic_text</code></h2><p><code>semantic_text</code> simplifies semantic search by making default decisions about indexing and querying your data:</p><ul><li><p>uses <code>sparse_vector</code> or <code>dense_vector</code> field types depending on the inference model type</p></li><li><p>Automatically defines the number of dimensions and similarity according to the inference results</p></li><li><p>Uses <code>int8_hnsw</code> index type for dense vector field types to leverage <a href="https://www.elastic.co/search-labs/blog/evaluating-scalar-quantization">scalar quantization</a>.</p></li><li><p>Uses query defaults. No token pruning is applied for <code>sparse_vector</code> queries, nor custom <code>k</code> and <code>num_candidates</code> are set for knn queries.</p></li></ul><p>Those are sensible defaults and allow you to quickly and easily start working with semantic search. Over time, you may want to customize your queries and data types to optimize search relevance, index and query performance, and index storage.</p><h3>Query customization</h3><p>There are no customization options - yet - for semantic queries. If you want to customize queries against <code>semantic_text</code> fields, you can perform <a href="https://www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl-semantic-query.html#advanced-search">advanced semantic_text search</a> using explicit knn and sparse vector queries.</p><p>We're planning to add <a href="https://www.elastic.co/guide/en/elasticsearch/reference/current/retrievers-overview.html">retrievers support</a> for <code>semantic_text</code>, and adding configuration options to the <code>semantic_text</code> field so they won't be needed at query time. Stay tuned!</p><h3>Data type customization</h3><p>If you need deeper customization for the data indexing, you can use the <code>sparse_vector</code> or <code>dense_vector</code> field types. These field types give you full control over how embeddings are generated, indexed, and queried.</p><p>You need to create an ingest pipeline with an inference processor to generate the embeddings. <a href="https://www.elastic.co/guide/en/elasticsearch/reference/current/semantic-search-inference.html">This tutorial</a> walks you through the process.</p><h2>What's next with <code>semantic_text</code>?</h2><p>We're just getting started with <code>semantic_text</code>! There are quite a few enhancements that we will keep working on, including:</p><ul><li><p>Better inference error handling</p></li><li><p>Customize the chunking strategy</p></li><li><p>Hiding embeddings in _source by default, to avoid cluttering the search responses</p></li><li><p>Inner hits support, to retrieve the relevant chunks of information for a query</p></li><li><p>Filtering and retrievers support</p></li><li><p>Kibana support</p></li></ul><h2>Try it out!</h2><p><code>semantic_text</code>is available on <a href="https://www.elastic.co/elasticsearch/serverless">Elasticsearch Serverless</a> now! It will be available soon on Elasticsearch 8.15 version for <a href="https://www.elastic.co/cloud">Elastic Cloud</a> and on <a href="https://www.elastic.co/downloads/elasticsearch">Elasticsearch downloads</a>.</p><p>If you already have an Elasticsearch serverless cluster, you can see a complete example for testing semantic search using <code>semantic_text</code> in <a href="https://www.elastic.co/search-labs/blog/elasticsearch-cohere-rerank">this tutorial</a>, or try it with <a href="https://colab.research.google.com/github/elastic/elasticsearch-labs/blob/main/notebooks/search/09-semantic-text.ipynb">this notebook</a>.</p><p>We'd love to hear about your experience with <code>semantic_text</code>! Let us know what you think in the <a href="https://www.elastic.co/community">forums</a>, or open an issue in the <a href="https://github.com/elastic/elasticsearch">GitHub repository</a>. Let's make semantic search easier together!</p>]]></content:encoded>
    <link>https://www.elastic.co/search-labs/blog/semantic-search-simplified-semantic-text</link>
    <guid isPermaLink="true">https://www.elastic.co/search-labs/blog/semantic-search-simplified-semantic-text</guid>
    <category><![CDATA[Vector Database]]></category>
    <category><![CDATA[Basics]]></category>
    <dc:creator><![CDATA[Carlos Delgado,Mike Pellegrini]]></dc:creator>
    <enclosure url="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt31515f5dc8f12092/6a170c170c48570fa101aabd/dc08f5c15b12a0e686b8922ad8d2b997ca1227d7-1024x1024.jpg" length="0" type="image/jpeg"/>
    <pubDate>Mon, 24 Jun 2024 00:00:00 GMT</pubDate>
  </item>
  </channel>
</rss>