使用 Jina CLIP v2 和 Elasticsearch 实现多语言图片搜索

62 阅读12分钟

作者:来自 Elastic Jeffrey Rengifo

使用 Jina CLIP v2 和 Elasticsearch 构建多语言图片搜索系统。无需翻译流水线,即可使用 89 种语言查询你的图片集合,并利用 Matryoshka Representations 将索引大小减少 75%。

Elasticsearch 原生集成了业界领先的 Gen AI 工具和服务提供商。欢迎查看我们关于超越 RAG 基础,或使用 Elastic Vector Database 构建生产就绪应用的网络研讨会。

为了针对你的使用场景构建最佳搜索解决方案,现在即可开始免费的云端试用,或在你的本地机器上体验 Elastic。


上一篇文章中,我们探讨了用于多模态搜索的 OpenAI 对比语言–图像预训练(Contrastive Language–Image Pre-training - CLIP)替代方案,其中包括 Jina CLIP v1。在本文中,我们将进一步介绍 Jina CLIP v2。这是一款多语言、多模态嵌入模型,使你能够使用同一个 Elasticsearch 索引和同一个模型,以 89 种语言搜索图片集合。我们还将介绍 Matryoshka Representations,这是 v2 的一项功能,可帮助你将索引大小减少 75%。

前提条件

  • Elasticsearch 9.x 集群(开始免费试用

  • Python 3.9+

  • Jina API 密钥(可在 jina.ai 免费获取,提供 100K 免费 tokens,足够完成本演示)

你可以参考完整 Notebook 获取全部代码并跟随实践。

Jina CLIP v1 与 v2 对比

在编写代码之前,先了解有哪些变化是值得的。最重要的特性是多语言支持,但除此之外还有一些其他重要改进:

特性Jina CLIP v1Jina CLIP v2
语言仅支持英语89 种语言
最大图片分辨率224×224512×512
文本编码器JinaBERTJina XLM-RoBERTa
Matryoshka Representations
嵌入维度7681024
最大文本长度512 个 tokens8192 个 tokens

JinaBERT 升级到 Jina XLM-RoBERTa 的文本编码器,正是实现多语言支持的关键。现在,你可以使用法语编写查询,并检索带有英语标签的图片;模型会将两者映射到同一个嵌入空间中。

在 v2 中,长度最多为 8,192 个 tokens 的查询都会被完整嵌入;如果启用了 truncate 选项,超出部分将被截断。

设置

作为向量数据库,Elasticsearch 允许我们原生存储和搜索稠密嵌入。我们使用一个具有 1024 维度并采用余弦相似度(cosine similarity)的 dense_vector 字段。对于 CLIP 风格的嵌入来说,这是一个合适的选择,因为余弦相似度会在索引时对向量进行归一化:

`

1.  INDEX_NAME = "clip-v2-stock-images"

3.  if es_client.indices.exists(index=INDEX_NAME):
4.      es_client.indices.delete(index=INDEX_NAME)

6.  es_client.indices.create(
7.      index=INDEX_NAME,
8.      mappings={
9.          "properties": {
10.              "image_embedding": {
11.                  "type": "dense_vector",
12.                  "dims": 1024,
13.                  "index": True,
14.                  "similarity": "cosine",
15.              },
16.              "tags": {
17.                  "type": "text",
18.                  "fields": {"keyword": {"type": "keyword"}},
19.              },
20.          }
21.      },
22.  )

`AI写代码![](https://csdnimg.cn/release/blogv2/dist/pc/img/runCode/icon-arrowwhite.png)

Jina Embeddings API

我们使用 Jina Embeddings API,这是一个 REST API,能够使用同一个模型同时处理文本和图片输入:

`

1.  import requests
2.  import base64
3.  from io import BytesIO

5.  JINA_API_URL = "https://api.jina.ai/v1/embeddings"
6.  JINA_HEADERS = {
7.      "Content-Type": "application/json",
8.      "Authorization": f"Bearer {JINA_API_KEY}",
9.  }

12.  def image_to_base64(image, max_size=512):
13.      """Convert a PIL image to a base64 data URL, resizing to max_size."""
14.      image = image.copy()
15.      image.thumbnail((max_size, max_size))
16.      buffer = BytesIO()
17.      image.save(buffer, format="PNG")
18.      b64 = base64.b64encode(buffer.getvalue()).decode("utf-8")
19.      return f"data:image/png;base64,{b64}"

22.  def encode_texts(texts, dimensions=1024):
23.      """Encode a list of text strings using Jina CLIP v2."""
24.      data = {
25.          "input": [{"text": t} for t in texts],
26.          "model": "jina-clip-v2",
27.          "dimensions": dimensions,
28.      }
29.      response = requests.post(JINA_API_URL, headers=JINA_HEADERS, json=data)
30.      response.raise_for_status()
31.      return [item["embedding"] for item in response.json()["data"]]

34.  def encode_images(images, dimensions=1024):
35.      """Encode a list of PIL images using Jina CLIP v2."""
36.      data = {
37.          "input": [{"image": image_to_base64(img)} for img in images],
38.          "model": "jina-clip-v2",
39.          "dimensions": dimensions,
40.      }
41.      response = requests.post(JINA_API_URL, headers=JINA_HEADERS, json=data)
42.      response.raise_for_status()
43.      return [item["embedding"] for item in response.json()["data"]]

`AI写代码![](https://csdnimg.cn/release/blogv2/dist/pc/img/runCode/icon-arrowwhite.png)

dimensions 参数用于控制输出向量的大小,也是支持 Matryoshka 的关键,我们将在本文最后进行介绍。目前,我们使用完整的 1024 维向量。

加载数据集

我们使用 StockImages-CC0 数据集,其中包含约 4,000 张采用 CC0 许可协议的图库照片,并附带描述性标签。图片宽度约为 1200 像素,远高于 CLIP v2 的 512×512 输入尺寸,因此我们会在生成嵌入时对图片进行缩放。

为了让演示运行更快,并且结果更容易理解,我们选取了 20 张涵盖不同类别的图片:

`

1.  from datasets import load_dataset

3.  full_dataset = load_dataset("KoalaAI/StockImages-CC0", split="train")
4.  print(f"Total images: {len(full_dataset)}")

6.  selected_indices = [
7.      0,   # technology: smartphone, macbook
8.      8,   # coastal landscape: driftwood, sea, ocean
9.      34,  # waterfall: rock, waterfall, creek
10.      40,  # fashion: highheel, shoe, red
11.      61,  # vineyard: vine, wine, fruit
12.      82,  # fruit: raspberry, berry
13.      90,  # night sky: milky way, stars
14.      95,  # music: acoustic guitar
15.      111, # town: hot air balloon
16.      120, # vehicle: vw van, vintage
17.      150, # city: eiffel tower, paris
18.      153, # animal: puppy, canine
19.      191, # sport: skateboard, kickflip
20.      197, # drink: tea, honey
21.      286, # wildlife: brown bear
22.      305, # architecture: palace, cathedral
23.      312, # coffee: latte, cappuccino
24.      317, # flowers: tulip, bouquet
25.      371, # nature: waterfall, river, cascade
26.      418, # pet: kitten, cat
27.  ]

29.  dataset = full_dataset.select(selected_indices)
30.  print(f"Selected {len(dataset)} images")

`AI写代码![](https://csdnimg.cn/release/blogv2/dist/pc/img/runCode/icon-arrowwhite.png)

生成图片嵌入

下图展示了两阶段处理流程:

首先,使用 CLIP v2 对图片生成嵌入,并将其存储到 Elasticsearch 中;然后,使用同一个模型对文本查询或图片查询生成嵌入,并通过 k 最近邻(kNN)相似度搜索进行检索。

我们在一次 API 调用中对全部 20 张图片进行编码。CLIP v2 模型将图像和文本嵌入到同一个向量空间中,这正是实现文本到图像搜索的关键:

`1.  images = [item["image"].convert("RGB") for item in dataset]

3.  image_embeddings = encode_images(images)
4.  print(f"Generated {len(image_embeddings)} embeddings of {len(image_embeddings[0])} dimensions")
5.  # Generated 20 embeddings of 1024 dimensions` AI写代码

索引文档

我们使用 Elasticsearch 的 bulk helper 一次性将所有文档批量写入索引:

`

1.  from elasticsearch import helpers

4.  def build_bulk_actions(dataset, image_embeddings, index_name):
5.      for i, item in enumerate(dataset):
6.          yield {
7.              "_index": index_name,
8.              "_id": i,
9.              "_source": {
10.                  "image_embedding": image_embeddings[i],
11.                  "tags": item.get("tags", ""),
12.              },
13.          }

16.  success, failed = helpers.bulk(
17.      es_client,
18.      build_bulk_actions(dataset, image_embeddings, INDEX_NAME),
19.      refresh=True,
20.  )

22.  print(f"Indexed {success} documents")
23.  # Indexed 20 documents

`AI写代码![](https://csdnimg.cn/release/blogv2/dist/pc/img/runCode/icon-arrowwhite.png)

多语言文本到图像搜索

我们使用与图像相同的 clip-v2 模型对文本查询进行编码,然后对图像嵌入执行 kNN 搜索。由于 Jina CLIP v2 将所有支持语言的文本和图像映射到同一个嵌入空间,因此不同语言的查询会检索到相同的图像:

`

1.  import matplotlib.pyplot as plt

4.  def search_by_text(query, k=3):
5.      """Encode a text query and search Elasticsearch."""

7.      query_embedding = encode_texts([query])[0]
8.      results = es_client.search(
9.          index=INDEX_NAME,
10.          knn={
11.              "field": "image_embedding",
12.              "query_vector": query_embedding,
13.              "k": k,
14.              "num_candidates": 50,
15.          },
16.      )

18.      return results["hits"]["hits"]

`AI写代码![](https://csdnimg.cn/release/blogv2/dist/pc/img/runCode/icon-arrowwhite.png)

我们使用三组查询进行测试,每组都被翻译成英语、西班牙语、法语和葡萄牙语:

`

1.  multilingual_queries = [
2.      {
3.          "English": "a cat sleeping",
4.          "Spanish": "un gato durmiendo",
5.          "French": "un chat qui dort",
6.          "Portuguese": "um gato dormindo",
7.      },
8.      {
9.          "English": "red flowers",
10.          "Spanish": "flores rojas",
11.          "French": "fleurs rouges",
12.          "Portuguese": "flores vermelhas",
13.      },
14.      {
15.          "English": "waterfall in nature",
16.          "Spanish": "cascada en la naturaleza",
17.          "French": "cascade dans la nature",
18.          "Portuguese": "cascata na natureza",
19.      },
20.  ]

22.  for query_set in multilingual_queries:
23.      print(f"\n{'='*60}")

25.      for lang, query in query_set.items():
26.          print(f'\n{lang}: "{query}"')
27.          hits = search_by_text(query, k=3)
28.          display_results(hits, query=f"[{lang}] {query}") # Function to display the images

`AI写代码![](https://csdnimg.cn/release/blogv2/dist/pc/img/runCode/icon-arrowwhite.png)

如下图所示,每个查询的四种语言版本都会返回相同的顶部结果。不同语言之间的排序分数几乎完全一致:

图像到图像搜索

除了文本查询之外,你还可以使用图像作为查询来查找视觉上相似的图片。方法是一样的:将查询图像编码到嵌入空间中,然后执行 kNN 搜索:

`

1.  def search_by_image(image, k=5):
2.      """Encode an image and search Elasticsearch."""

4.      query_embedding = encode_images([image])[0]
5.      results = es_client.search(
6.          index=INDEX_NAME,
7.          knn={
8.              "field": "image_embedding",
9.              "query_vector": query_embedding,
10.              "k": k,
11.              "num_candidates": 50,
12.          },
13.      )

15.      return results["hits"]["hits"]

18.  # Use image at index 10 (Eiffel Tower) as query
19.  query_image = dataset[10]["image"]
20.  hits = search_by_image(query_image)
21.  display_results(hits, query="Similar to query image")

`AI写代码![](https://csdnimg.cn/release/blogv2/dist/pc/img/runCode/icon-arrowwhite.png)

我们使用下面这张埃菲尔铁塔的图片来进行图像搜索:

以埃菲尔铁塔作为查询,该模型会首先返回图片本身,其次是一个教堂和一个有热气球的小镇;这两者在视觉和语义上都与城市地标接近。葡萄园和滑板公园的匹配则不那么明显;由于索引中只有 20 张图片,kNN 会始终返回 k 个结果,而不考虑相关性强弱。

Matryoshka Representations - 套娃表示

Jina CLIP v2 支持 Matryoshka Representation Learning(MRL)。其核心思想是:模型在训练时被设计为使 embedding 的前 N 个维度已经能够表达大部分信息,因此你可以截断后面的维度,从而获得更小的向量,同时几乎不损失效果。

Jina API 通过 dimensions 参数直接支持这一能力,该参数可以在 64 到 1024 之间取任意整数。

根据 Jina 的基准测试,从 1024 维降到 256 维,在文本、图像以及跨模态任务上仍能保持超过 99% 的检索质量

要使用低维向量,你需要创建一个单独的 Elasticsearch 索引,并将 dims 设置为目标维度大小。因为 Elasticsearch 的 dense_vector 字段在创建索引时是固定维度的,你不能用 256 维向量去查询一个 1024 维的索引:

`

1.  MATRYOSHKA_DIMS = 256
2.  MATRYOSHKA_INDEX = "clip-v2-stock-images-256d"

4.  if es_client.indices.exists(index=MATRYOSHKA_INDEX):
5.      es_client.indices.delete(index=MATRYOSHKA_INDEX)

7.  es_client.indices.create(
8.      index=MATRYOSHKA_INDEX,
9.      mappings={
10.          "properties": {
11.              "image_embedding": {
12.                  "type": "dense_vector",
13.                  "dims": MATRYOSHKA_DIMS,
14.                  "index": True,
15.                  "similarity": "cosine",
16.              },
17.              "tags": {
18.                  "type": "text",
19.                  "fields": {"keyword": {"type": "keyword"}},
20.              },
21.          }
22.      },
23.  )

25.  # Generate 256-dim embeddings
26.  image_embeddings_256 = encode_images(images, dimensions=MATRYOSHKA_DIMS)
27.  print(f"Generated {len(image_embeddings_256)} embeddings of {len(image_embeddings_256[0])} dimensions")

29.  # Index documents
30.  success, _ = helpers.bulk(
31.      es_client,
32.      build_bulk_actions(dataset, image_embeddings_256, MATRYOSHKA_INDEX),
33.      refresh=True,
34.  )
35.  print(f"Indexed {success} documents in {MATRYOSHKA_INDEX}")

`AI写代码![](https://csdnimg.cn/release/blogv2/dist/pc/img/runCode/icon-arrowwhite.png)

现在比较 1024 维索引和 256 维索引之间的结果:

`

1.  query = "a cat sleeping"

3.  print("Results with 1024 dimensions:")
4.  hits_1024 = search_by_text(query, k=3)
5.  display_results(hits_1024, query=f"{query} (1024 dims)")

7.  print("\nResults with 256 dimensions:")
8.  query_embedding_256 = encode_texts([query], dimensions=MATRYOSHKA_DIMS)[0]
9.  hits_256 = es_client.search(
10.      index=MATRYOSHKA_INDEX,
11.      knn={
12.          "field": "image_embedding",
13.          "query_vector": query_embedding_256,
14.          "k": 3,
15.          "num_candidates": 50,
16.      },
17.  )["hits"]["hits"]
18.  display_results(hits_256, query=f"{query} (256 dims)")

20.  ids_1024 = [hit["_id"] for hit in hits_1024]
21.  ids_256 = [hit["_id"] for hit in hits_256]
22.  print(f"1024d ranking: {ids_1024}")
23.  print(f" 256d ranking: {ids_256}")
24.  print(f"Same top results: {ids_1024 == ids_256}")

`AI写代码![](https://csdnimg.cn/release/blogv2/dist/pc/img/runCode/icon-arrowwhite.png)

这些是结果:

在 256 维和 1024 维索引中,返回的最相关结果是相同的。在更大规模的部署中,256 维嵌入可以按比例减少存储占用和查询延迟,因此 Matryoshka 在生产系统中是一种很实用的优化方式,尤其是在索引规模较大的情况下。不过,仍然需要针对你的具体数据集来评估检索质量。

多模态差距

需要注意的是,CLIP 风格的双编码器模型存在一个已知限制,称为多模态差距(multimodal gap):文本和图像的嵌入在向量空间中往往形成分离的簇,这会导致跨模态相似度分数的可靠性下降。Jina 在 jina-embeddings-v4 中通过用统一模型替代双编码器架构来解决这一问题,并且正在开发多模态 v5。如果你的用例对跨模态对齐要求很高,建议关注这些更新的模型。

结论

Jina CLIP v2 在 v1 的基础上进行了扩展,支持 89 种语言的多语言能力、更大的嵌入向量、更高的图像分辨率,以及 Matryoshka embeddings,使你可以在索引大小和少量质量损失之间进行权衡。其 API 保持一致,因此你可以像使用第一版一样使用该模型。

下一步

这篇内容对你有多大帮助?

原文:www.elastic.co/search-labs…