混合搜索与多重嵌入:一次有趣又毛茸茸的猫咪搜索之旅!(一)

178 阅读9分钟

作者:来自 Elastic  Jo Ann de Leon

演示如何在多个嵌入(文本和图像)上实现不同类型的搜索 - 词汇、向量和混合。它使用一个简单而有趣的猫搜索应用程序。

你知道 Elastic 可以用作强大的向量数据库吗?在本博客中,我们将探讨如何在传统词汇搜索的同时生成、存储和查询向量嵌入。Elastic 的优势在于其灵活性和可扩展性,使其成为现代搜索用例的绝佳选择。通过将向量嵌入与 Elastic 集成,你可以提高搜索相关性,并增强各种数据类型(包括图像等非文本文档)的搜索能力。

但它会变得更好!学习 Elastic 的搜索功能也很有趣。在本文中,我们将向你展示如何使用 Elastic 搜索你最喜欢的猫,以搜索猫的文本描述和图像。通过本文附带的一个简单的 Python 应用程序,你将学习如何实现基于向量和关键字的搜索。我们将指导你生成自己的向量嵌入,将它们存储在 Elastic 中并运行混合查询 - 所有这些都是在搜索可爱的猫科动物朋友的同时进行的。

无论你是经验丰富的开发人员还是 Elasticsearch 新手,这个有趣的项目都是了解现代搜索技术如何运作的好方法。此外,如果你喜欢猫,你会发现它更具吸引力。所以让我们深入研究并设置 Elasticats 应用程序,同时探索 Elasticsearch 的强大功能。

在开始之前,让我们确保你已准备好 Elastic 云 ID 和 API 密钥。复制 .env-template 文件,将其另存为 .env 并插入你的 Elastic 云凭据。

应用程序架构

以下是描述我们应用程序架构的高层次图表:

生成和存储向量嵌入

在执行任何类型的搜索之前,我们首先需要有数据。我们的 data.json 包含我们将在 Elasticsearch 中索引的猫文档列表。每个文档描述一只猫并具有以下映射:



1.  mappings= {
2.        	"properties": {
3.              	"img_embedding": {
4.                         "type": "dense_vector",
5.                         "dims": 512,
6.                         "index": True,
7.                         "similarity": "cosine"
8.                   },
9.                   "photo": {
10.                         "type": "keyword"
11.                   },
12.                   "cat_id": {
13.                         "type": "keyword"
14.                   },
15.                   "name": {
16.                         "type" : "text"
17.                   },
18.                   "url" : {
19.                         "type" : "keyword"
20.                   },
21.                   "summary" : {
22.                         "type" : "text"
23.                   },
24.                   "summary_embedding": {
25.                         "type": "dense_vector",
26.                         "dims": 384
27.                   },
28.                   "age": {
29.                         "type": "keyword"
30.                   },
31.                   "gender": {
32.                         "type": "keyword"
33.                   },
34.                   "size": {
35.                         "type": "keyword"
36.                   },
37.                   "coat": {
38.                         "type": "keyword"
39.                   },
40.                   "breed": {
41.                         "type": "keyword"
42.                   }
43.         }
44.  }


每只猫的照片属性都指向猫图像的位置。当我们在应用程序中调用 reindex 函数时,它将生成两个嵌入:

1) 首先是每只猫图像的向量嵌入。我们使用 clip-ViT-B-32 模型。图像模型允许你将图像和文本嵌入到同一个向量空间中。这允许你将图像搜索实现为文本到图像或图像到图像的搜索。

 self.img_model = SentenceTransformer('clip-ViT-B-32')
 1.     def get_img_embedding(self, text='', image_path=''):
2.         if text:
3.             print(f'Encoding text: {text}')
4.             return self.img_model.encode(text)
5.         else:
6.             print(f'Encoding image: {image_path}')
7.             temp_image = Image.open(image_path)
8.             return self.img_model.encode(temp_image)

2) 第二个嵌入是针对每只待领养猫的摘要文本。我们使用了不同的模型,即 all-MiniLM-L6-v2。

 self.text_model = SentenceTransformer('all-MiniLM-L6-v2')
 1.     def get_text_embedding(self, text):
2.         return self.text_model.encode(text)

然后我们将嵌入存储为文档的一部分。

 1.     def insert_documents(self, documents):
2.         operations = []
3.         for document in documents:
4.             operations.append({'index': {'_index': self.index}})
5.             operations.append({
6.                 **document,
7.                 'img_embedding': self.get_img_embedding(image_path="static/"+document['photo']),
8.                 'summary_embedding': self.get_text_embedding(document['summary'])
9.             })
10.         return self.es.bulk(operations=operations)

我们现在准备调用 reindex 函数。



1.  @app.cli.command()
2.  def reindex():
3.     """Regenerate the Elasticsearch index."""
4.     response = es.reindex()
5.     print (response)
6.     print(f'Index with {len(response["items"])} documents created '
7.           f'in {response["took"]} milliseconds.')


 1.     def reindex(self):
2.         self.create_index()
3.         with open('data.json', 'rt') as f:
4.             documents = json.loads(f.read())
5.         return self.insert_documents(documents)

从终端运行以下命令:

(.venv) $> flask reindex

现在我们可以运行我们的 Web 应用程序了:

(.venv) $> flask run

我们的初始表单如下所示:

如你所见,我们已经将一些关键词作为过滤器(例如年龄、性别、尺寸等)公开,我们将在查询中使用它们。

执行不同类型的搜索

以下工作流程图显示了我们的 Web 应用程序中可用的不同搜索路径。我们将介绍每种情况。

词汇搜索

最简单的情况是 “match all” 查询,基本上会返回索引中的所有猫。我们不使用任何过滤器,也不输入描述或上传图片。

 1.         search_query = {
2.             'must': {
3.                 'match_all': {}
4.             }
5.         }

如果表单中提供了任何过滤器,我们将执行布尔查询。在这种情况下,没有输入任何描述,因此我们将在 “match all” 查询中应用过滤器。



1.  def extract_filters(form_data):
2.     filters = []

5.     for key, val in form_data.items():
6.         if (key == "imageQuery" or key == "inputQuery" or key == "from_"):
7.             continue

10.         if (key != "age" and key != "breed"):
11.             if (val[0] != ''): #only apply the filter if value is not empty
12.                 filters.append({
13.                     "term": {
14.                         f"{key}": {
15.                             "value": val[0]
16.                         }
17.                     },
18.                 })
19.         else:
20.             #remove any empty values first
21.             cleaned_list = [item for item in val if item]

24.             if (len(cleaned_list) > 0): #only apply the filter if list is not empty
25.                 filters.append({
26.                     "terms": {
27.                         f"{key}": cleaned_list
28.                     },
29.                 })

32.     return {'filter': filters}


 filters = extract_filters(form_data)
 1.     if search_query:
2.         search_params['query'] = {
3.             'bool': {
4.                 **search_query,
5.                 **filters
6.             }
7.         }

向量搜索

在我们的 Web 表单中,我们可以上传猫的类似图像。通过上传图像,我们可以进行向量搜索,方法是将上传的图像转换为嵌入,然后对之前存储的图像嵌入执行 knn 搜索。

首先,我们将上传的图像保存在 uploads 文件夹中。

 1.     if 'imageQuery' in request.files:
2.         file = request.files['imageQuery']

5.         if file:
6.             filename = file.filename
7.             filepath = os.path.join(app.config['UPLOAD_FOLDER'], filename)
8.             # Process the image as needed
9.             file.save(filepath)
10.             imageSearch = True;

然后我们为图像嵌入创建一个 knn 查询。

 1.     elif imageSearch:
2.         search_query = None
3.         # add knn image if there's image
4.         knn_query.append({
5.             'field': 'img_embedding',
6.             'query_vector': es.get_img_embedding(image_path=filepath),
7.             'k': 5,
8.             'num_candidates': 15,
9.             **filters,
10.         })
 1.     search_params = {
2.         'knn': knn_query,
3.         'from_': from_,
4.         'size': 5
5.     }

请注意,可以使用或不使用过滤器(来自布尔查询)来执行向量搜索。另外,请注意 k=5,这意味着我们只返回前 5 个相似文档(猫)。

尝试存储在 images/ 文件夹中的以下任何图像:

  1. Abyssinian
    1. Dahlia - 72245105_3.jpg
  2. American shorthair
    1. Uni - 64635658_2.jpg
    2. Sugarplum - 72157682_4.jpeg
  3. Persian
    1. Sugar - 72528240_2.jpeg

混合搜索

我们的应用程序中最复杂的场景是当一些文本输入到描述字段时。在这里,我们执行 3 种不同类型的搜索并将它们组合成混合搜索。首先,我们对实际的文本输入执行词汇 “match” 查询。

 1.     # add text search
2.     if textQuery:
3.         search_query = {
4.             'must': {
5.                 'match': {
6.                     'summary': textQuery
7.                 }
8.             }
9.         }

我们还创建了 2 个 knn 查询:

  1. 使用文本嵌入模型,我们为文本输入生成一个嵌入,并对摘要嵌入执行 knn 搜索。
  2. 使用图像嵌入模型,我们为文本输入生成另一个嵌入,并对图像嵌入执行 knn 搜索。我之前提到过,图像模型不仅允许你执行我们在上面的向量搜索场景中看到的图像到图像搜索,还允许你执行文本到图像搜索。这意味着如果我在描述中输入“black”,它将搜索可能包含或类似于黑猫的图像!
 1.         # add knn text and image search if there's a description
2.         knn_query.append({
3.             'field': 'summary_embedding',
4.             'query_vector': es.get_text_embedding(textQuery),
5.             'k': 5,
6.             'num_candidates': 15,
7.             **filters,
8.         })

11.         knn_query.append({
12.             'field': 'img_embedding',
13.             'query_vector': es.get_img_embedding(textQuery),
14.             'k': 5,
15.             'num_candidates': 15,
16.             **filters,
17.         })

然后,我们利用倒数秩融合 (RRF) 检索器有效地将所有三个查询的结果组合并排序为单个有凝聚力的结果集。

 1.     rank = None
2.     if len(knn_query) > 0 and search_query:
3.         rank = {
4.             'rrf': {}
5.         }
 1.     # Conditionally add the 'rank' parameter
2.     if rank:
3.         search_params['rank'] = rank

RRF 是一种将多个结果集(每个结果集可能具有不同的相关性指标)合并为一个统一集的方法。与简单地连接结果数组不同,RRF 应用特定公式根据文档在各个结果集中的位置对其进行排名。这种方法可确保出现在多个查询中的文档具有更高的重要性,从而提高最终结果的相关性和质量。通过使用 RRF,我们避免了手动调整每个查询权重的复杂性,并实现了不同搜索策略的平衡整合。

更多有关 RRF 的描述,请阅读如下的文章:

为了进一步说明,下表显示了我们搜索 “sisters” 时各个结果集的排名。使用 RRF 公式(默认排名常数 k=60),我们可以得出每个文档的最终分数。然后按降序对最终分数进行排序,即可得出文档的最终排名。“Willow & Nova” 是我们的最佳匹配项(cat)!

Cat (document)Lexical rankingknn (on img_embedding) rankingknn (on summary_embedding) rankingFinal ScoreFinal Ranking
Sugarplum130.03226645852
Willow & Nova2110.04891591751
Zoe & Zara20.016129032264
Sage320.032002048133
Primrose40.0156255
Dahlia50.015384615387
Luke & Leia40.0156256
Sugar & Garth50.015384615388

以下是你可以用于描述的其他一些测试:

  1. “sisters” vs “siblings”
  2. “tuxedo”
  3. “black cats” with “American shorthair” breed filter
  4. “white”

结论

除了显而易见的 cats 之外,Elasticats 是了解 Elasticsearch 的绝佳方式。这是一个有趣且实用的项目,可让你探索搜索技术,同时提醒我们技术可以带来的乐趣。随着你深入研究,你还会发现 Elasticsearch 处理向量嵌入的能力如何解锁新级别的搜索功能。无论是针对猫、图像还是其他数据类型,Elastic 都能让搜索既强大又令人愉快!

欢迎为项目做出贡献或分叉存储库以进一步定制它。祝你搜索愉快,希​​望你能找到梦寐以求的猫!😸

准备好自己尝试一下了吗?开始免费试用或使用此自定进度的动手学习来学习 Search AI。

Elasticsearch 集成了 LangChain、Cohere 等工具。加入我们的高级语义搜索网络研讨会,构建你的下一个 GenAI 应用程序!

原文:Hybrid search with multiple embeddings: A fun and furry search for cats! - Search Labs