璐噜噜 發表於 2025-5-8 21:20:00

【Elasticsearch】一文读懂ES向量搜索:原理剖析与技术全景

<p><code>大家好,我是大任,今天给大家分享一下Elasticsearch的向量搜索技术</code></p>
<blockquote>
<p>注:本文若未说明ES版本则为7.10,其他版本会特别标记,由于ES版本不同,部分差异较大,具体请以官方文档为准</p>
</blockquote>
<h1 id="一向量搜索的核心原理">一、向量搜索的核心原理</h1>
<h2 id="11-向量化表示的本质">1.1 向量化表示的本质</h2>
<p>现代AI技术将文本、图像等非结构化数据转化为高维向量(通常128-1024维),这些向量在数学空间中携带语义特征。如:</p>
<ul>
<li>文本嵌入(Embedding):BERT等模型生成768维向量</li>
<li>图像特征:ResNet提取2048维特征向量</li>
</ul>
<h2 id="12-向量搜索简介">1.2 向量搜索简介</h2>
<p><strong>向量搜索</strong>,简单来说,就是在向量空间中进行的搜索操作。在传统的文本搜索中,我们通常基于关键词进行匹配,这种方式对于精确匹配的场景效果较好,但在处理语义理解、模糊匹配等复杂场景时显得力不从心。而向量搜索则将文本、图像、音频等各种数据转化为向量表示,通过计算向量之间的相似度来进行搜索。这种方式能够更好地捕捉数据的语义信息,从而实现更精准、更智能的搜索。</p>
<p>例如,对于文本数据,我们可以使用词嵌入(Word Embedding)技术将每个单词映射为一个向量,然后将整个文本的向量表示为其包含单词向量的某种聚合(如平均、求和等)。在搜索时,将查询文本也转化为向量,通过计算查询向量与文档向量之间的相似度,找出与查询最相关的文档。</p>
<h1 id="二elasticsearch两种核心向量搜索方式">二、Elasticsearch两种核心向量搜索方式</h1>
<p>Elasticsearch为了满足向量搜索的需求,在7.0版本新增了两个字段类型<code>dense_vector</code> 和 <code>sparse_vector</code>,分别是密集向量和稀疏向量,其中 <code>sparse_vector</code>在7.6中被弃用,在8.0-8.10版本中是没有该字段类型的。</p>
<h2 id="21-近似knnhnsw算法">2.1 近似kNN(HNSW算法)</h2>
<blockquote>
<p>在7.10版本中,没有该方式,因此按照2025/3/1最新版本8.17版本介绍.</p>
</blockquote>
<p>k-nearest neighbor(kNN)搜索通过相似性度量所测量等算法查询向量最近的k个向量。</p>
<h3 id="前置条件">前置条件</h3>
<p>要使用KNN搜索,首先必须要有<code>dense_vector</code>或 <code>sparse_vector</code>字段,这些字段的维度一定要和查询的维度相同,所以一定要确定好维度。关于向量,可以在Elasticsearch中使用自然语言处理(NLP)模型创建这些向量,或者在Elasticsearch外部生成它们,比如使用阿里云的向量模型API获取文本向量后赋值到搜索参数中。</p>
<ol>
<li>首先需要显式映射一个或多个<code>dense_vector</code>字段。</li>
</ol>
<blockquote>
<p>在8.0版中添加了对近似kNN搜索的支持。在此之前,<code>dense_vector</code>字段不支持在映射中启用索引。如果在8.0之前的版本创建了一个包含<code>dense_vector</code>字段的索引,那么为了支持近似kNN搜索,必须使用设置<code>index:true</code>(默认选项)的新字段映射重新索引数据。</p>
</blockquote>
<pre><code class="language-json">PUT image-index
{
"mappings": {
    "properties": {
      "image-vector": {
      "type": "dense_vector",
      "dims": 3,       //向量维度
      "index": true,   //8.0之前使用KNN搜索
      "similarity": "l2_norm"//获取knn相似度的函数,不设置改值默认为cosine(余弦),且只能在index设置为true时指定
      },
      "title-vector": {
      "type": "dense_vector",
      "dims": 5,
      "similarity": "l2_norm"
      },
      "title": {
      "type": "text"
      },
      "file-type": {
      "type": "keyword"
      }
    }
}
}
</code></pre>
<p><code>similarity</code>的选择有(详情可看密集向量字段的参数):</p>
<ul>
<li><strong>l2_norm</strong></li>
<li><strong>dot_product</strong></li>
<li><strong>cosine</strong></li>
<li><strong>max_inner_product</strong></li>
</ul>
<ol start="2">
<li>创建数据</li>
</ol>
<pre><code class="language-json">POST image-index/_bulk?refresh=true
{ "index": { "_id": "1" } }
{ "image-vector": , "title-vector": , "title": "moose family", "file-type": "jpg" }
{ "index": { "_id": "2" } }
{ "image-vector": , "title-vector": , "title": "alpine lake", "file-type": "png" }
{ "index": { "_id": "3" } }
{ "image-vector": , "title-vector": , "title": "full moon", "file-type": "jpg" }
...
</code></pre>
<ol start="3">
<li>使用knn选项或knn查询</li>
</ol>
<pre><code class="language-json">POST image-index/_search
{
"knn": {
    "field": "image-vector",
    "query_vector": [-5, 9, -12],
    "k": 10, //查询的数量
    "num_candidates": 100
},
"fields": [ "title", "file-type" ]
}
</code></pre>
<ol start="4">
<li>原理</li>
</ol>
<p>为了收集结果,kNN搜索API在每个分片上找到<code>num_candidates</code>数量的近似最近邻候选。搜索计算这些候选向量与查询向量的相似度,选择k个最相似的结果。然后,搜索将来自 每个分片返回全局前k个最近邻向量。</p>
<p>可以增加<code>num_candidates</code>以获得更准确的结果,但代价是搜索速度变慢。<code>num_candidates</code>值比较较高的搜索 会从每个分片中考虑更多的候选者。会花费更多的时间,但也会提高搜索真正的k个最接近的向量的概率。</p>
<h2 id="22精确暴力搜索">2.2精确暴力搜索</h2>
<p>计算查询向量与所有过滤后的文档向量的余弦/欧式距离,保证100%召回率但计算成本高。使用精确KNN搜索的话,需要使用带有vector函数的<code>script_score</code>查询。</p>
<ol>
<li>首先需要显式映射一个或多个<code>dense_vector</code>字段。如果不打算将该字段使用近似kNN搜索,可以将索引映射选项设置为<code>false</code>。这可以显著提高索引速度。</li>
</ol>
<pre><code class="language-json">PUT product-index
{
"mappings": {
    "properties": {
      "product-vector": {
      "type": "dense_vector",
      "dims": 5,
      "index": false
      },
      "price": {
      "type": "long"
      }
    }
}
}
</code></pre>
<ol start="2">
<li>创建数据</li>
</ol>
<pre><code class="language-json">POST product-index/_bulk?refresh=true
{ "index": { "_id": "1" } }
{ "product-vector": , "price": 1599 }
{ "index": { "_id": "2" } }
{ "product-vector": [-0.5, 100.0, -13.0, 14.8, -156.0], "price": 799 }
{ "index": { "_id": "3" } }
{ "product-vector": , "price": 1099 }
...
</code></pre>
<ol start="3">
<li>使用<code>search</code> API运行包含<code>向量函数</code>的<code>script_score</code>查询。<br>
向量函数的类型有:
<ol>
<li><code>cosineSimilarity</code>-计算余弦相似度</li>
<li><code>dotProduct</code>-计算点积</li>
<li><code>l1 norm</code>-计算L1距离</li>
<li><code>Hamming</code>-计算Hamming距离</li>
<li><code>l2 norm</code>-计算L2距离</li>
<li><code>doc[&lt;field&gt;].vectorValue</code>-以浮点数组的形式返回向量的值</li>
<li><code>doc[&lt;field&gt;].magnitude</code>-返回向量的大小<br>
接下来以<code>cosineSimilarity</code>为例,这是一个通过计算两个向量的余弦大小来判断向量相似度的函数。我们通过传入的参数向量<code>queryVector</code>,这个名称是自定义的,但是需要与<code>source</code>的脚本中<code>params.queryVector</code>一致,下面方法将会查询所有文档</li>
</ol>
</li>
</ol>
<pre><code class="language-json">POST product-index/_search
{
"query": {
    "script_score": {
      "query" : {
      "match_all" : {}
       },
      "script": {
      "source": "cosineSimilarity(params.queryVector, 'product-vector') + 1.0",
      "params": {
          "queryVector": [-0.5, 90.0, -10, 14.8, -156.0]
      }
      }
    }
}
}
</code></pre>
<p>但是使用<code>match_all</code>查询来全量匹配所有文档会显著增加搜索延迟,为了限制传递给<code>vector</code>函数的匹配文档的数量,可以在<code>script_score.query</code>参数中指定一个过滤查询。例如下面示例:</p>
<pre><code class="language-json">POST product-index/_search
{
"query": {
    "script_score": {
      "query" : {
      "bool" : {
          "filter" : {
            "range" : {
            "price" : {
                "gte": 1000
            }
            }
          }
      }
      },
      "script": {
      "source": "cosineSimilarity(params.queryVector, 'product-vector') + 1.0",
      "params": {
          "queryVector": [-0.5, 90.0, -10, 14.8, -156.0]
      }
      }
    }
}
}
</code></pre>
<h2 id="23-两种搜索的对比">2.3 两种搜索的对比</h2>
<h3 id="原理差异">原理差异</h3>
<ul>
<li>精确暴力搜索:在进行搜索时,精确暴力搜索会遍历数据集中的每一个向量,逐一计算查询向量与它们之间的相似度(如欧几里得距离、余弦相似度等),然后根据相似度的大小对所有向量进行排序,最后选取最相似的<code>k</code>个向量作为结果返回。这种方法简单直接,能够保证搜索结果的精确性,但在数据量较大时,计算量会非常庞大。</li>
<li>近似 k - NN 搜索:近似 k - NN 搜索则采用了一些优化策略来减少计算量。例如,利用上述提到的 KD - Tree、HNSW 等索引结构,通过构建数据的层次化表示或图结构,在搜索时能够快速定位到可能包含相似向量的区域,而无需遍历整个数据集。以 HNSW 为例,它通过多层图结构,在高层图中快速跳过大量不相关的向量,仅在底层图中对局部区域进行更精确的搜索,从而在保证一定搜索精度的前提下,大大提高了搜索效率。</li>
</ul>
<h3 id="性能表现">性能表现</h3>
<ul>
<li>搜索时间:精确暴力搜索的搜索时间与数据集的大小成正比,数据量越大,搜索时间越长。在大规模数据集上,搜索可能需要很长时间才能完成。而近似 k - NN 搜索借助索引结构,能够显著减少搜索过程中需要计算相似度的向量数量,搜索时间相对较短,尤其在处理海量数据时,优势更为明显。</li>
<li>内存使用:精确暴力搜索不需要额外的内存来存储索引结构,仅需存储数据集本身。然而,近似 k - NN 搜索需要构建和维护索引结构,这会占用额外的内存空间。例如,HNSW 索引在构建过程中会生成多层图结构,每个节点都需要存储一定的连接信息,因此会消耗更多的内存。</li>
</ul>
<h3 id="适用场景">适用场景</h3>
<ul>
<li>精确暴力搜索:适用于数据集较小、对搜索结果的精确性要求极高且对搜索时间没有严格限制的场景。例如,在一些数据量有限的科研项目中,研究人员可能更关注搜索结果的准确性,而不太在意搜索时间,此时精确暴力搜索可以满足需求。</li>
<li>近似 k - NN 搜索:在实际应用中,尤其是面对大规模数据集时,近似 k - NN 搜索更为适用。例如,在图像搜索引擎中,可能需要处理数百万甚至数十亿张图像的向量数据,如果使用精确暴力搜索,搜索效率将极低。而近似 k - NN 搜索能够在较短的时间内返回近似最优的结果,满足用户对实时性的需求</li>
</ul>
<ol>
<li><strong>用精确搜索的情况</strong></li>
</ol>
<ul>
<li>公司只有200份合同,需要100%确保找到所有相关文件</li>
<li>医疗诊断系统,不能容忍任何漏诊可能</li>
</ul>
<ol start="2">
<li><strong>用近似搜索的情况</strong></li>
</ol>
<ul>
<li>淘宝有10亿商品图片,用户想快速找到相似款衣服</li>
<li>抖音推荐视频,允许少量误差但要求毫秒级响应</li>
</ul>
<p>精确的强力kNN保证了准确的结果,但无法很好地扩展大型数据集。如果必须要使用精确暴力搜索,您可以通过使用查询参数来限制传递给函数的匹配文档的数量。如果过滤数据到一个小的文档子集,就可以得到很好的搜索结果。</p>
<h1 id="三elasticsearch支持的其他语义搜索方式">三、Elasticsearch支持的其他语义搜索方式</h1>
<p>Elasticsearch使用自然语言处理(NLP)和向量搜索提供各种语义搜索功能。使用NLP模型使您能够从文本中提取文本嵌入。嵌入是提供文本的数字表示的向量。具有相似含义的内容片段具有相似的表示。</p>
<p>Elasticsearch中的<code>semantic_text</code>字段也支持语义文本搜索。在使用时只需要将文档的字段设置为<code>semantic_text</code>, 不需要指定如何为数据生成嵌入,或者如何对其进行索引。推理端点自动确定要使用的嵌入生成,索引和查询。<br>
<img src="https://github.com/user-attachments/assets/ad47b19b-fdb0-47ce-a64b-f8fabb9f7bac" alt="image" loading="lazy"></p>
<blockquote>
<p><strong>翻译</strong>   警告:此功能处于测试阶段,可能会发生变化。设计和代码不如官方GA功能成熟,并且按原样提供,没有任何保证。Beta版功能不受正式GA功能的支持SLA的限制。使用时需要注意风险</p>
</blockquote>
<h2 id="使用">使用</h2>
<ol>
<li>创建Index Mapping<br>
默认是使用预配置的<code>.elser-2-elasticsearch</code>端点,使用以下API请求设置<code>semantic_text</code>:</li>
</ol>
<pre><code class="language-json">PUT semantic-embeddings
{
"mappings": {
    "properties": {
      "content": {
      "type": "semantic_text"
      //"inference_id": "my-elser-endpoint", 默认是使用预配置的``.elser-2-elasticsearch``端点,也可以自己设置推理端点
      }
    }
}
}
</code></pre>
<ol start="2">
<li>加载数据<br>
将文档添加到索引时,Elasticsearch 会自动使用指定的推理端点计算嵌入,并将其存储在 <code>semantic_text</code> 字段中。您无需手动生成嵌入向量。以下是索引文档的示例:</li>
</ol>
<pre><code class="language-json">PUT semantic-embeddings/_doc/1
{
"content": "Elasticsearch 8.17 introduces semantic search capabilities."
}

</code></pre>
<ol start="3">
<li>语义搜索</li>
</ol>
<pre><code class="language-json">GET semantic-embeddings/_search
{
"query": {
    "semantic": {
      "field": "content",
      "query": "如何在跑步时避免肌肉酸痛?"
    }
}
}

</code></pre>
<p>使用嵌入丰富数据集之后,可以使用语义搜索查询数据。在语义查询类型中提供<code>semantic_text</code>字段名称和查询文本。用于为<code>semantic_text</code>字段生成嵌入的推理端点将用于处理查询文本。</p>
<p>通过上述步骤,您可以在 Elasticsearch 8.17 中利用 <code>semantic_text</code> 字段实现高效的语义搜索功能。该功能使搜索更加智能,能够理解查询的意图和上下文,从而提供更相关的搜索结果。</p>
<h1 id="向量搜索典型应用场景">向量搜索典型应用场景</h1>
<ul>
<li>基于自然语言处理(NLP)算法的相关性排序</li>
<li>产品推荐和推荐引擎</li>
<li>图像或视频的相似性搜索</li>
</ul>
<p>转载请标明:文章来源博客园《程序员大任》<br>
欢迎关注本人公众号,分享更多技术经验<br>
<img src="https://img2024.cnblogs.com/blog/2476029/202505/2476029-20250519103813710-795838511.jpg" alt="" loading="lazy"></p><br><br>
来源:https://www.cnblogs.com/renyi537/p/18867146
頁: [1]
查看完整版本: 【Elasticsearch】一文读懂ES向量搜索:原理剖析与技术全景