在云搜索服务的 ML 服务中,支持创建 Image Embedding 模型,该模型是将图像转换为低维的向量表示,以便于在各种机器学习任务中进行处理、分析和比较。豆包的视觉理解模型通过分析和理解图像内容,将视觉信息转化为可识别、可处理的语义信息,从而实现对图像的智能化理解和应用。本文介绍基于云搜索服务 Image Embedding 模型和豆包视觉理解模型,快速搭建一套支持以图搜图、以文搜图、图文混合搜图的图文检索应用。
图文检索在电商、广告、设计、搜索引擎等热门领域被广泛应用。常见的图文检索包括以图搜图和以文搜图,用户通过文字描述或上传图片就可以在海量的图片库中快速找到同款或者相似图片。
图片的文本描述和图片作为检索对象,分别对 image 和 text 进行特征提取,并在模型中对文本和图片建立相关联系,然后在海量图片数据库进行特征向量检索,返回与检索对象最相关的记录集合。其中文本描述由豆包视觉理解模型 Doubao-1.5-vision-pro-32k 生成,特征提取部分采用 CLIP 模型,向量检索采用火山引擎云搜索服务在海量图片特征中进行快速搜索。
在构建图文检索应用前,您需要提前创建 TOS Bucket,后续图片文件将被上传到该 TOS Bucket 中。
picture
。opensearch-remote-inference
插件本文通过在 Ingest Pipeline 中指定使用的 Image Embedding 模型,实现将指定字段(图片信息)转换为向量后嵌入回去。
在创建 Ingest Pipeline 前,需要提前在实例中安装opensearch-remote-inference
插件,否则会报错。安装插件会触发重启集群,建议在业务低峰期操作。如果已经安装该插件,则无需执行以下步骤。
opensearch-remote-inference
插件的操作列中勾选安装并选择需要安装的插件版本,然后单击批量提交。说明
安装插件会触发重启集群,建议在业务低峰期操作。
OpenSearch 实例创建后,您可以在实例中创建 ML 服务,用于后续的模型创建和部署。
如何创建 ML 服务,请参见创建 ML 服务。
说明
1:4
资源比例,能够保障模型服务应用的性能更佳。您可以在 ML 服务中创建 Image Embedding 模型,也可以选择使用豆包的图文向量化模型。ML 服务中的 Image Embedding 模型类型支持 CLIP 和 ChineseCLIP 及其衍生模型,本文以 CLIP 模型为例。
云搜索服务为您提供了模型示例文件,您可以自行下载上传。
上传 Image Embedding 模型文件到 TOS Bucket。
创建模型。
参数 | 说明 |
---|---|
模型名称 | 自定义设置模型名称,比如ImageSearch-Demo。说明Image Embedding 模型具备以下两种能力:支持直接对图片和文字进行向量化,返回向量化结果。如何获取向量化,请参见调用 Embedding 模型。代理 OpenSearch,使用类似 OpenSearch 接口同时进行向量化和写入操作。 |
模型类型 | 此处需要选择创建 Image Embedding 模型。 |
选择存储桶 | 从下拉列表中选择存放模型文件的对象存储桶。 |
模型文件 | 选择对象存储文件,表示从选择的对象存储桶中选择模型文件,此时还需要设置对象存储文件夹,即从该文件夹中读取已经存在的模型文件。 |
描述 | 设置模型的描述语句。 |
创建并启动推理服务。
您可以创建一个推理服务,关联新建的 Image Embedding 模型,并根据业务需求配置网络、资源和自定义参数等信息。
配置 | 说明 |
---|---|
服务名称 | 自定义设置推理服务的名称,比如“image-search”。首字符仅支持字母或下划线()。可包含字母、数字、特殊字符仅支持英文句号(.)、下划线()、短横线(-)、反斜杠(/),长度为 1~128 个字符。最多只能包含一个反斜杠(/)字符。 |
选择模型 | 从拉列表框选择新建的 Image Embedding 模型。 |
可用区 | 选择需要部署推理服务的可用区。如果 OpenSearch 实例是单可用区,那么您的推理服务也只有一个可用区。如果需要多可用区部署推理服务,则需要确保 OpenSearch 实例有多个可用区。如何为实例添加可用区,请参见添加可用区。 |
是否开启高可用区 | 单可用区部署推理服务时,支持为推理服务开启高可用。 |
资源类型 | 从下拉列表中选择推理服务使用的资源类型,您可以选择 CPU 类型和 GPU 两种资源类型。 |
规格 | 从下拉列表中选择规格。 |
节点数量 | 设置推理服务的节点数量。单可用区部署,未开启高可用:节点数量可设范围为 1~512。单可用区部署,开启高可用:节点数量为 2 的整数倍,最大值为 512。多可用区部署:节点数量为可用区个数的整数倍,最大值为 512。比如可用区个数为 2,节点数量就为 2 的整数倍。 |
网络配置 | 是否开通私网访问。 |
描述 | 自定义设置推理服务的描述信息。 |
高级选项 | 为推理服务配置自定义参数,默认提供UserConfig、MaxReplicasPerNode两个参数。 |
启动推理服务。
获取 Image Embedding 模型调用信息。
推理服务启动后,在推理服务详情页面单击查看调用信息,然后获取模型的调用信息。
创建 Ingest Pipeline,需要指定使用的 Image Embedding 模型,可以将指定字段(图片信息)转换为向量后嵌入回去。
image_remote_embedding
的 Ingest Pipeline。url
:设置为从模型服务的调用信息中获取模型的url
。model
:设置从模型服务的调用信息中获取模型的model
,比如ImageSearch-Demo
。"vector_source_image": "vector"
:索引中的vector_source_image
字段含义为图片的 TOS Bucket 存放路径,该配置表示将vector_source_image
字段转为向量存储到vector
中。PUT _ingest/pipeline/image_remote_embedding { "description": "image embedding pipeline for remote inference", "processors": [ { "remote_text_embedding": { "remote_config": { "method": "POST", "url": "http://d-182800***-serve-svc.r-00g**:8000/v1/embeddings", "params": { }, "headers": { "Content-Type": "application/json" }, "advance_request_body": { "model": "ImageSearch-Demo" } }, "field_map": { "vector_source_image": "vector" } } } ] }
创建 Search Pipeline,需要指定使用的 Image Embedding 模型。
image_remote_embedding_search_pipeline
的 Search Pipeline。url
:设置为从模型服务的调用信息中获取模型的url
。model
:设置从模型服务的调用信息中获取模型的model
,比如ImageSearch-Demo
。PUT _search/pipeline/image_remote_embedding_search_pipeline { "description": "image embedding pipeline for remote inference", "request_processors": [ { "remote_embedding": { "remote_config": { "method": "POST", "url": "http://d-1828002***-serve-svc.r-00g***:8000/v1/embeddings", "params": {}, "headers": { "Content-Type": "application/json" }, "advance_request_body": { "model": "ImageSearch-Demo" } } } } ] }
OpenSearch 实例创建后,您可以在实例中创建一个索引,用于存储图片的向量信息。
image_search
的索引。
default_pipeline
为新建的 Ingest Pipeline,ID 为image_remote_embedding
。vector
设置为knn_vector
,dimension
需要和使用的模型保持相同维度,比如768。PUT imagesearch { "settings": { "default_pipeline": "image_remote_embedding", "index": { "refresh_interval": "60s", "number_of_shards": "3", "knn.space_type": "cosinesimil", "knn": "true", "number_of_replicas": "1" } }, "mappings": { "properties": { "id": { "type": "text" }, "description": { "type": "text" }, "vector_source_image": { "type": "text" }, "vector": { "type": "knn_vector", "dimension": 768, "method": { "name": "hnsw", "engine": "nmslib", "space_type": "cosinesimil" } } } } }
使用 Python 语言利用 Gradio 库搭建交互式的图文检索应用。
安装依赖
pip install gradio opensearch-py tos openai
生成图片描述
配置环境变量,将<ARK_API_KEY>
替换为您的方舟 API Key。方舟 API Key 获取方式,请参见API Key 管理。
export ARK_API_KEY="ARK_API_KEY"
利用豆包视觉模型推理接入点生成图片描述,注意填写以下信息:
ark_model:设置为豆包视觉模型推理接入点 ID。
import os import base64 from openai import OpenAI from PIL import Image def gen_image_description(image, prompt="提取图中商品的关键词,直接输出结果", image_path=None): ark_model = 'ep-****' llm = OpenAI(api_key=os.environ.get("ARK_API_KEY"), base_url="https://ark.cn-beijing.volces.com/api/v3") if image_path: image = Image.open(image_path) if image is None: return "" response = llm.chat.completions.create( model=ark_model, temperature=0.1, max_tokens=64, messages=[ { "role": "user", "content": [ {"type": "text", "text": prompt}, { "type": "image_url", "image_url": { "url": "data:image/jpeg;base64," + image_to_base64(image)} } ] } ]) return response.choices[0].message.content def image_to_base64(pil_image): buffered = BytesIO() pil_image.save(buffered, format="JPEG") img_str = base64.b64encode(buffered.getvalue()) return img_str.decode()
配置 OpenSearch 和 TOS 连接
连接 OpenSearch 实例,请注意填写以下信息:
from opensearchpy import OpenSearch host = 'opensearch-o-***.escloud.volces.com' port = '9200' auth = ('username', 'password') index = 'imagesearch' client = OpenSearch( hosts=[{'host': host, 'port': port}], http_compress=True, http_auth=auth, use_ssl=True, verify_certs=False, ssl_assert_hostname=False, ssl_show_warn=False, )
建立 TOS 连接,请注意填写以下信息:
import tos accessKey = 'AK**' secretKey = '****' tos_path = 'tos://doc-test/picture/image_temp' tos_region = 'cn-beijing' tos_endpoint = 'https://tos-' + tos_region + '.volces.com' tos_client = tos.TosClientV2(accessKey, secretKey, tos_endpoint, tos_region)
上传图片
将图片上传到 TOS Bucket,并生成图片向量和图片描述存储到 OpenSearch。
import gradio as gr def upload_images(file_objs, ark_prompt: str = None, progress=gr.Progress()): result = [] for file_obj in progress.tqdm(file_objs, desc="Uploading files"): file_name = file_obj.name base_file_name = os.path.basename(file_name) tos_file_path = upload_to_tos(file_obj) upload_to_opensearch(index, tos_file_path, file_name, ark_prompt) result.append([base_file_name, "Done"]) return result def upload_to_tos(file): from urllib.parse import urlparse bucket_name = urlparse(tos_path).netloc prefix = urlparse(tos_path).path[1:] object_key = os.path.join(prefix,os.path.basename(file)) tos_client.put_object_from_file(bucket_name, object_key, file) return os.path.join(tos_path, os.path.basename(file)) def upload_to_opensearch(index, tos_file_path, local_file_path, ark_prompt): base_name = os.path.basename(tos_file_path) document = { "id": base_name, "vector_source_image": tos_file_path, "description": gen_image_description(None, prompt=ark_prompt, image_path=local_file_path) } print(document['description']) pipeline_id = 'image_remote_embedding' client.index(index=index, body=document, id=base_name, params={"pipeline": pipeline_id})
搜索图片
from concurrent.futures import ThreadPoolExecutor def search_images(img: Image.Image, description=None, percent=None, size=None): result = query_opensearch(img, description, percent, size) image_list = [] data_list = [] for item in result: image_list.append(item["_source"]["vector_source_image"]) data_list.append( [item["_id"], item["_score"], item["_source"]["vector_source_image"], item["_source"]["description"]]) return data_list, download_from_tos(image_list) def query_opensearch(img: Image.Image, description=None, percent=None, size=None): if not size: size = 20 bool_queries = {"must": [], "should": []} bool_queries["must"].append({ "remote_neural": { "vector": { "query_text": "data:image," + image_to_base64(img), "k": size * 10, "boost": 100 } } }) if description and percent and percent > 0: bool_queries["must"].append({ "remote_neural": { "vector": { "query_text": description, "k": size * 10, "boost": percent } } }) bool_queries["should"].append({ "dis_max": { "queries": [ { "match": { "description": { "query": description, "boost": percent/10 } } } ] } }) search_query = { "_source": { "exclude": ["vector"] }, "size": size, "query": { "function_score": { "query": { "bool": bool_queries } } } } search_pipeline_id = 'image_remote_embedding_search_pipeline' result = client.search(index=index, body=search_query, params={"search_pipeline": search_pipeline_id}) return result["hits"]["hits"] def download_from_tos(keys): with ThreadPoolExecutor() as executor: images = list(executor.map(get_image_from_tos, keys)) return images def get_image_from_tos(url): from urllib.parse import urlparse bucket_name = urlparse(url).netloc prefix = urlparse(url).path[1:] object_stream = tos_client.get_object(bucket_name, prefix) return Image.open(object_stream)
创建搜索应用界面
import gradio as gr with gr.Blocks(analytics_enabled=False) as demo: local_storage = gr.State([]) gr.Markdown("# CloudSearch Image Search") with gr.Tab("Upload Files"): with gr.Column(): with gr.Row(): with gr.Column(): gr.Markdown("# 上传图像文件") file_input = gr.File(file_count="multiple", label="支持 png,jpeg,jpg,webp 类型 ", file_types=['image']) radio_input = gr.Radio([False, True], label="是否开启图像增强,开启后会调用大模型生成图像描述", value=True) prompt = gr.Textbox(lines=1, label="Prompt", value='提取图中商品的关键词,直接输出结果') upload_button = gr.Button("Uploads") result = gr.Dataframe( headers=["File", "Status"], datatype=["str", "str"], label="Upload Result", ) upload_button.click(upload_images, inputs=[file_input, prompt], outputs=[result]) with gr.Tab("Search Image"): with gr.Column(): with gr.Row(): image_input = gr.Image(type="pil", sources=["upload"], height=450, width=450) with gr.Column(): gr.Markdown("图片描述内容用来提供混合搜索。可以调用大模型生成图片描述,也可以手动填写内容,如果描述内容为空,则只进行图片搜索") prompt = gr.Textbox(lines=1, label="Prompt", value='提取图中商品的关键词,直接输出结果') generate_des_btn = gr.Button(value="生成图片描述") description_input = gr.Textbox(lines=1, label="描述", value='') temp_slider = gr.Slider(0, 100, value=1, step=0.1, interactive=True, label="文字描述搜索占比") generate_des_btn.click(gen_image_description,inputs=[image_input, prompt],outputs=[description_input]) size = gr.Number(minimum=10, maximum=1000, step=10, value=10) image_button = gr.Button("Search Image") data_output = gr.Dataframe( headers=["Id", "Score", "ImageUrl","Description"], datatype=["str", "number", "str", "str"], label="Image List", ) image_output = gr.Gallery(show_label=False, elem_id="gallery", columns=[4], object_fit="contain", height="auto") image_button.click(search_images, inputs=[image_input, description_input, temp_slider, size], outputs=[data_output, image_output]) demo.launch(server_name="0.0.0.0", server_port=7860)
启动图文搜索应用后,在浏览器中打开 http://0.0.0.0:7860/, 从页面上传图片。
启动图文搜索应用后,在浏览器中打开 http://0.0.0.0:7860/,从页面进行图片检索。