Elasticsearch 向量搜索:余弦相似度匹配

在Elasticsearch中,向量搜索是一种高效处理高维数据(如文本嵌入或图像特征)的方法。余弦相似度是一种常用的相似度度量方式,它通过计算两个向量之间的夹角余弦值来评估相似度,公式为:$\cos \theta = \frac{\mathbf{A} \cdot \mathbf{B}}{|\mathbf{A}| |\mathbf{B}|}$,其中$\mathbf{A}$和$\mathbf{B}$是两个向量,$\cdot$表示点积,$|\cdot|$表示向量的范数。余弦相似度范围在$[-1, 1]$之间,值越大表示相似度越高($1$表示完全相同)。

下面,我将逐步解释如何在Elasticsearch中实现余弦相似度匹配,包括设置索引、执行查询和注意事项。整个过程基于Elasticsearch 7.x 或更高版本(推荐使用8.x以获得更好的性能)。

步骤1: 理解余弦相似度在Elasticsearch中的原理
  • Elasticsearch使用dense_vector字段类型存储向量数据。
  • 余弦相似度计算需要点积和范数,但Elasticsearch的向量默认未归一化(即范数不为1)。因此,在查询时,我们需要手动计算归一化版本或使用脚本处理。
  • 核心公式推导:余弦相似度可重写为$\frac{\mathbf{A} \cdot \mathbf{B}}{|\mathbf{A}| |\mathbf{B}|} = \left( \frac{\mathbf{A}}{|\mathbf{A}|} \right) \cdot \left( \frac{\mathbf{B}}{|\mathbf{B}|} \right)$,这表示我们应先将向量归一化(单位化),然后计算点积。
  • 在Elasticsearch中,常用script_score查询来实现,因为它允许自定义相似度计算。
步骤2: 设置索引和存储向量

在Elasticsearch中,首先需要创建一个索引,并定义dense_vector字段来存储向量。以下是一个索引设置的JSON示例:

  • 假设每个文档有一个向量字段embedding,维度为128(根据您的数据调整)。
  • 索引名称设为vector_index
PUT /vector_index
{
  "mappings": {
    "properties": {
      "embedding": {
        "type": "dense_vector",
        "dims": 128  // 向量维度,例如128
      },
      "content": {  // 可选:存储原始文本或其他元数据
        "type": "text"
      }
    }
  }
}

  • 创建索引后,添加一些文档数据。例如:
    POST /vector_index/_doc/1
    {
      "embedding": [0.1, 0.2, ..., 0.5],  // 实际向量值,长度需匹配维度
      "content": "示例文本1"
    }
    

    确保向量值以数组形式提供。
步骤3: 执行余弦相似度搜索查询

使用script_score查询来计算余弦相似度并排序结果。查询时,需要提供一个查询向量(query vector),并编写Painless脚本(Elasticsearch的脚本语言)实现归一化和点积计算。

查询示例:

  • 假设查询向量为$\mathbf{q} = [q_1, q_2, \dots, q_{128}]$。
  • 脚本逻辑:
    1. 归一化查询向量和文档向量。
    2. 计算点积作为相似度分数。
  • 公式在脚本中:$\text{score} = \frac{\mathbf{q}}{|\mathbf{q}|} \cdot \frac{\mathbf{doc_vector}}{|\mathbf{doc_vector}|}$。
GET /vector_index/_search
{
  "query": {
    "script_score": {
      "query": {
        "match_all": {}  // 匹配所有文档,但根据分数排序
      },
      "script": {
        "source": """
          // 归一化查询向量
          double qNorm = 0.0;
          for (double v : params.query_vector) {
            qNorm += v * v;
          }
          qNorm = Math.sqrt(qNorm);
          
          // 归一化文档向量
          double docNorm = 0.0;
          for (double v : doc['embedding']) {
            docNorm += v * v;
          }
          docNorm = Math.sqrt(docNorm);
          
          // 计算点积并返回余弦相似度
          double dotProduct = 0.0;
          for (int i = 0; i < params.query_vector.length; i++) {
            dotProduct += (params.query_vector[i] / qNorm) * (doc['embedding'][i] / docNorm);
          }
          return dotProduct;
        """,
        "params": {
          "query_vector": [0.3, 0.4, ..., 0.6]  // 替换为实际查询向量,长度128
        }
      }
    }
  }
}

  • 解释
    • script_score查询为每个文档计算一个自定义分数(即余弦相似度)。
    • 脚本中,先计算查询向量和文档向量的范数($|\mathbf{q}|$和$|\mathbf{doc_vector}|$),然后归一化并计算点积。
    • 返回的分数用于排序:分数越高,相似度越大。
  • 结果:Elasticsearch会返回按余弦相似度降序排列的文档列表。
注意事项
  1. 向量归一化:在索引前或查询时归一化向量可提高效率和准确性。建议在数据导入时预处理向量(例如,使用Python的scikit-learn库归一化),这样脚本中无需重复计算范数,从而提升性能。
  2. 性能优化:对于大规模数据集,使用Elasticsearch 8.x的KNN搜索特性更高效。它支持dense_vectorindex选项和近似最近邻算法,但需注意KNN默认使用欧氏距离;如果必须用余弦相似度,仍需自定义脚本。
  3. 维度限制:Elasticsearch的dense_vector维度上限通常为1024(具体取决于版本),确保向量维度匹配设置。
  4. 错误处理:在脚本中添加范数检查(如if (docNorm == 0) return 0;)避免除以零错误。
  5. 测试验证:在实际应用中,先用小数据集测试查询,确保脚本正确性。您可以使用Kibana或Elasticsearch客户端执行查询。

通过以上步骤,您可以在Elasticsearch中高效实现余弦相似度匹配。如果您有具体向量数据或问题,提供更多细节我可以进一步优化示例!

Logo

火山引擎开发者社区是火山引擎打造的AI技术生态平台,聚焦Agent与大模型开发,提供豆包系列模型(图像/视频/视觉)、智能分析与会话工具,并配套评测集、动手实验室及行业案例库。社区通过技术沙龙、挑战赛等活动促进开发者成长,新用户可领50万Tokens权益,助力构建智能应用。

更多推荐