博客

使用 Elasticsearch Go 客户端在 Elasticsearch 中执行矢量搜索

通过实际示例了解如何使用 Elasticsearch Go 客户端在 Elasticsearch 中执行矢量搜索。

使用包括 Go 语言在内的任何编程语言构建软件,都需要终身学习。在大学和工作生涯中,卡莉涉猎过多种编程语言和技术,包括最新、最先进的矢量搜索技术。但这还不够!所以最近 Carly 也开始玩围棋了。

就像动物、编程语言和您友好的作者一样,搜索也经历了不同实践的演变,您可能很难在自己的搜索用例中做出选择。在本博客中,我们将分享矢量搜索的概述,以及使用 Elasticsearch Elasticsearch Go 客户端 的每种方法的 示例 。这些示例将向您展示如何使用 Elasticsearch 和 Go 中的矢量搜索查找地鼠并确定它们吃什么。

准备工作

要学习本示例,请确保满足以下前提条件:

  1. 安装 Go 1.21 或更高版本

  2. 使用

  3. 创建您自己的 Elasticsearch 集群,其中包含一组基于啮齿动物的页面,包括维基百科中对我们友好的Gopher 的介绍:

维基百科 Gopher 网页

连接到 Elasticsearch

在我们的示例中,我们将使用 Go 客户端提供的类型 API。要为任何查询建立安全连接,都需要使用以下两种方法之一配置客户端:

  1. 云 ID 和 API 密钥(如果使用弹性云)。

  2. 集群 URL、用户名、密码和证书。

连接到位于 Elastic Cloud 上的集群的过程如下:

func GetElasticsearchClient() (*elasticsearch.TypedClient, error) {
	var cloudID = os.Getenv("ELASTIC_CLOUD_ID")
	var apiKey = os.Getenv("ELASTIC_API_KEY")

	var es, err = elasticsearch.NewTypedClient(elasticsearch.Config{
		CloudID: cloudID,
		APIKey:  apiKey,
		Logger:  &elastictransport.ColorLogger{os.Stdout, true, true},
	})

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

	return es, nil
}

如后续章节所示,client 连接可用于矢量搜索。

向量搜索

矢量搜索试图通过将搜索问题转换为使用矢量的数学比较来解决这一问题。文件嵌入过程还有一个额外的阶段,即使用模型将文件转换成密集的向量表示,或者简单地转换成数字流。这种方法的优势在于,通过将非文本文档(如图像和音频)翻译成矢量并与查询一起进行搜索。

矢量搜索图

简单地说,矢量搜索是一组矢量距离计算。在下图中,我们将查询Go Gopher的向量表示与向量空间中的文档进行比较,并返回最接近的结果(用常数k 表示):

Gopher 矢量空间示例

根据为文档生成嵌入式的方法,有两种不同的方法可以找出地鼠吃什么。

方法 1:自带模型

有了白金许可,就可以通过上传模型和使用推理 API 在 Elasticsearch 中生成嵌入。建立模型有六个步骤:

  1. 从模型库中选择一个 PyTorch 模型上传。在本例中,我们使用 Hugging Face 的句子转换器/msmarco-MiniLM-L-12-v3来生成嵌入。

  2. 使用 Elasticsearch 集群的凭据和任务类型text_embeddings ,使用Python 版 Eland 机器学习客户端将模型加载到 Elastic 中。如果没有安装 Eland,可以使用 Docker 运行导入步骤,如下图所示:

docker run -it --rm --network host \
    docker.elastic.co/eland/eland \
    eland_import_hub_model \
      --cloud-id $ELASTIC_CLOUD_ID \
      --es-api-key $ELASTIC_API_KEY \
      --hub-model-id sentence-transformers/msmarco-MiniLM-L-12-v3 \
      --task-type text_embedding
  1. 上传后,请使用样本文档快速测试模型sentence-transformers__msmarco-minilm-l-12-v3 ,以确保生成的嵌入结果符合预期:

弹性测试训练模型示例
  1. 创建包含推理处理器的摄取管道。这将允许使用上传的模型生成矢量表示:

PUT _ingest/pipeline/search-rodents-vector-embedding-pipeline
{
  "processors": [
    {
      "inference": {
        "model_id": "sentence-transformers__msmarco-minilm-l-12-v3",
        "target_field": "text_embedding",
        "field_map": {
          "body_content": "text_field"
        }
      }
    }
  ]
}
  1. 创建一个新索引,其中包含类型为dense_vector 的字段text_embedding.predicted_value ,用于存储为每个文档生成的向量嵌入:

PUT vector-search-rodents
{
  "mappings": {
    "properties": {
      "text_embedding.predicted_value": {
        "type": "dense_vector",
        "dims": 384,
        "index": true,
        "similarity": "cosine"
      },
      "text": {
        "type": "text"
      }
    }
  }
}
  1. 使用新创建的摄取管道重新索引文档,生成文本嵌入作为每个文档的附加字段text_embedding.predicted_value

POST _reindex
{
  "source": {
    "index": "search-rodents"
  },
  "dest": {
    "index": "vector-search-rodents",
    "pipeline": "search-rodents-vector-embedding-pipeline"
  }
}

现在,我们可以使用新的索引vector-search-rodents ,在同一个搜索 API 中使用Knn 选项,如下例所示:

func VectorSearch(client *elasticsearch.TypedClient, term string) ([]Rodent, error) {
  var k = 10
	var numCandidates = 10

	res, err := client.Search().
		Index("vector-search-rodents").
		Knn(types.KnnSearch{
      # Field in document containing vector
			Field:         "text_embedding.predicted_value",
      # Number of neighbors to return
			K:             &k,
      # Number of candidates to evaluate in comparison
			NumCandidates: &numCandidates,
      # Generate query vector using the same model used in the inference processor
			QueryVectorBuilder: &types.QueryVectorBuilder{
				TextEmbedding: &types.TextEmbedding{
					ModelId:   "sentence-transformers__msmarco-minilm-l-12-v3",
					ModelText: term,
				},
			}}).Do(context.Background())

	if err != nil {
		return nil, fmt.Errorf("error in rodents vector search: %w", err)
	}

	return getRodents(res.Hits.Hits)
}

通过解分叉转换 JSON 结果对象的方法与关键字搜索示例完全相同。常量KNumCandidates 允许我们配置返回的邻接文件数和每个分区要考虑的候选文件数。需要注意的是,增加候选结果的数量会提高结果的准确性,但由于要进行更多的比较,查询的运行时间会更长。

当使用查询What do Gophers eat? 执行代码时,返回的结果与下图类似,突出显示 Gopher 文章包含所需的信息,与之前的关键字搜索不同:

[
  {ID:64f74ecd4acb3df024d91112 Title:Gopher - Wikipedia Url:https://en.wikipedia.org/wiki/Gopher} 
  {ID:64f74ed34acb3d71aed91fcd Title:Squirrel - Wikipedia Url:https://en.wikipedia.org/wiki/Squirrel} 
  //Other results omitted
]

方法 2:拥抱脸推理应用程序接口

另一种方法是在 Elasticsearch 之外生成相同的嵌入,并将其作为文档的一部分进行摄取。由于该选项不使用 Elasticsearch 机器学习节点,因此可以在免费层上完成。

Hugging Face 公开了一个免费使用、有费率限制的推理 API,只要有账户和 API 令牌,就可以手动生成相同的嵌入式数据,用于实验和原型开发,帮助你开始工作。不建议用于生产。也可以使用类似的方法在本地调用自己的模型来生成嵌入,或使用付费 API。

在下面的函数GetTextEmbeddingForQuery 中,我们根据查询字符串使用推理 API 生成从POST 请求端点返回的向量:

// HuggingFace text embedding helper
func GetTextEmbeddingForQuery(term string) []float32 {
    // HTTP endpoint
    model := "sentence-transformers/msmarco-minilm-l-12-v3"
    posturl := fmt.Sprintf("https://api-inference.huggingface.co/pipeline/feature-extraction/%s", model)

    // JSON body
    body := []byte(fmt.Sprintf(`{
        "inputs": "%s",
        "options": {"wait_for_model":True}
    }`, term))

    // Create a HTTP post request
    r, err := http.NewRequest("POST", posturl, bytes.NewBuffer(body))

    if err != nil {
        log.Fatal(err)
        return nil
    }

    token := os.Getenv("HUGGING_FACE_TOKEN")
    r.Header.Add("Authorization", fmt.Sprintf("Bearer %s", token))

    client := &http.Client{}
    res, err := client.Do(r)
    if err != nil {
        panic(err)
    }

    defer res.Body.Close()

    var post []float32
    derr := json.NewDecoder(res.Body).Decode(&post)

    if derr != nil {
        log.Fatal(derr)
        return nil
    }

    return post
}

生成的[]float32 类型的向量将作为QueryVector 传递,而不是使用QueryVectorBuilder 选项来利用之前上传到 Elastic 的模型。

func VectorSearchWithGeneratedQueryVector(client *elasticsearch.TypedClient, term string) ([]Rodent, error) {
	vector, err := GetTextEmbeddingForQuery(term)
	if err != nil {
		return nil, err
	}

	if vector == nil {
		return nil, fmt.Errorf("unable to generate vector: %w", err)
	}

  var k = 10
	var numCandidates = 10

	res, err := client.Search().
		Index("vector-search-rodents").
		Knn(types.KnnSearch{
      # Field in document containing vector
			Field:         "text_embedding.predicted_value",
      # Number of neighbors to return
			K:             &k,
      # Number of candidates to evaluate in comparison
			NumCandidates: &numCandidates,
      # Query vector returned from Hugging Face inference API
			QueryVector:   vector,
		}).
		Do(context.Background())

	if err != nil {
		return nil, err
	}

	return getRodents(res.Hits.Hits)
}

请注意,无论使用哪两个选项,KNumCandidates 选项都是一样的,而且生成的结果也是一样的,因为我们使用的是同一个模型来生成嵌入结果

结论

在此,我们讨论了如何使用 Elasticsearch Go 客户端 在 Elasticsearch 中执行矢量搜索。查看GitHub 仓库中本系列的所有代码。请继续阅读第 3 部分,了解如何将矢量搜索与 Go 语言第一部分中的关键字搜索功能相结合。

在此之前,祝您打地鼠愉快!

资源

  1. Elasticsearch 指南

  2. Elasticsearch Go 客户端

  3. 什么是矢量搜索?| 弹性

相关内容

LINQ to Elasticsearch ES|QL:编写 C# 代码,查询 Elasticsearch

Florian Bernd

利用 Elasticsearch 中的 NLP 和向量搜索增强聊天机器人功能

Priscilla Parodi

Elasticsearch 与 OpenSearch:向量搜索性能比较

Ugo Sangiorgi

人工智能剽窃利用 Elasticsearch 检测剽窃行为

Priscilla Parodi

高级 RAG 技术第 2 部分:查询和测试

Han Xiang Choong

准备好打造最先进的搜索体验了吗?

足够先进的搜索不是一个人的努力就能实现的。Elasticsearch 由数据科学家、ML 操作员、工程师以及更多和您一样对搜索充满热情的人提供支持。让我们联系起来,共同打造神奇的搜索体验,让您获得想要的结果。

亲自试用