本文介绍如何通过火山引擎云搜索服务使用混合检索、Rerank 等功能。
在搜索应用中,传统的 Keyword Search 一直是主要的搜索方法,它适合精确匹配查询的场景,能够提供低延迟和良好的结果可解释性,但是 Keyword Search 并没有考虑上下文信息,可能产生不相关的结果。最近几年,基于向量检索技术的搜索增强技术 Semantic Search 越来越流行,通过使用机器学习模型将数据对象(文本、图像、音视频等)转化成向量,向量距离代表对象间的相似性,如果使用的模型和问题领域相关性高,则往往能更好地理解上下文和搜索意图,进而提高搜索结果的相关性,反之,如果模型和问题领域相关性不高,则效果会大打折扣。
Keyword Search 和 Semantic Search 都存在明显的优劣势,那么是否可以通过组合它们的优点来整体提高搜索的相关性?答案是,简单的算术组合并不能收到预期的效果,主要原因有两个:
综上,我们需要寻找一种理想的查询类型来解决这些问题,它能单独执行每个查询子句,同时收集分片级别的查询结果,最后对所有查询的评分进行归一化合并后返回最终的结果,这就是混合搜索(Hybrid Search) 方案。
暂时无法在飞书文档外展示此内容
通常一次混合搜索查询可以分为以下几步:
从前面原理介绍,我们可以看到要实现一个混合检索应用,至少需要用到这些基础技术设施
火山引擎云搜索构建在开源的 Elasticsearch 和 OpenSearch 项目上,从第一天上线就支持了完善成熟的文本检索和向量检索能力,同时针对混合搜索场景也进行了一系列的功能迭代和演进,提供了开箱即用的混合搜索解决方案。本文将以图像搜索应用为例,介绍如何借助火山引擎云搜索服务的解决方案快速开发一个混合搜索应用。
暂时无法在飞书文档外展示此内容
其端到端流程概括如下:
后面在写入/检索数据时使用到了推理服务,所以需要启用 OpenSearch 的相关插件。
插件安装完成后,实例状态变为 “运行中”。
ML 服务创建成功:
说明
本示例中选用了 Doubao-embedding-large 模型,实际使用请根据实际需求选择合适模型。
后文在 OpenSearch 中创建 Pipeline 时使用该推理服务的的访问地址。
OpenSearch 实例中 可视化配置 -> 公网访问地址
本示例创建一个名为 text_index
的索引。
PUT /text_index { "settings": { "index": { "number_of_shards": "1", "number_of_replicas": "1", "refresh_interval": "10s", "knn": "true", # 注意使用该配置,否则可能会导致向量检索失效 "indexing": { "slowlog": { "level": "info", "threshold": { "index": { "warn": "200ms", "trace": "20ms", "debug": "50ms", "info": "100ms" } }, "source": "1000" } }, "search": { "slowlog": { "level": "info", "threshold": { "query": { "warn": "500ms", "trace": "50ms", "debug": "100ms", "info": "200ms" }, "fetch": { "warn": "200ms", "trace": "50ms", "debug": "80ms", "info": "100ms" } } } } } }, "mappings": { "properties": { "text": { "type": "text", "analyzer": "ik_max_word" }, "text_knn": { "type": "knn_vector", "dimension": 4096, ## 需要和推理服务中选择的 embedding 模型 dimension 一致。 "method": { "engine": "nmslib", "space_type": "cosinesimil", "name": "hnsw", "parameters": {} } } } } }
如需修改为 DiskANN 配置,可参考 使用 DiskANN 向量引擎。
API Key 是推理服务接口调用时进行身份认证的关键信息,在正式调用推理服务之前需要创建 API Key,请注意妥善保管 API Key,请勿泄露。
创建 Pipeline 时使用该推理服务的的访问地址以及model信息。
本示例创建一个名为post_embedding
的 pipeline,该 Pipeline 在向 OpenSearch 中写入数据时使用。
PUT _ingest/pipeline/post_embedding { "description": "model embedding pipeline for community inference", "processors": [ { "remote_text_embedding": { "remote_config" : { "method" : "POST", "url" : "http://ark.cn-beijing.volces.com/api/v3/embeddings", ## 豆包embedding服务固定url "params" : { }, "headers" : { "Content-Type" : "application/json", "Authorization": "Bearer {api—key}" ##创建的api-key }, "advance_request_body" : { "model" : "ep-xxxxxxx-xxxxxx" ##上一步embedding推理服务调用信息中的model字段值 } }, "field_map": { "text": "text_knn" } } } ] }
本示例创建一个名为search_pipeline
的 pipeline,该 Pipeline 在 OpenSearch 检索数据时使用。
PUT _search/pipeline/search_pipeline { "description": "text embedding pipeline for remote inference", "request_processors": [ { "remote_embedding": { "remote_config": { "method": "POST", "url" : "https://ark.cn-beijing.volces.com/api/v3/embeddings", "params" : { }, "headers" : { "Content-Type" : "application/json", "Authorization": "Bearer {api—key}" ##创建的api-key }, "advance_request_body" : { "model" : "ep-xxxxxxx-xxxxxx" ##上一步embedding推理服务调用信息中的model字段值 } } } } ], "phase_results_processors": [ { "normalization-processor": { "normalization": { "technique": "min_max" }, "combination": { "technique": "arithmetic_mean", "parameters": {} } } } ] }
本示例中写入 100 条测试数据;
POST /text_index/_bulk?pipeline=post_embedding # 与前文创建的 insert pipeline 一致 { "index": { "_id": 1 } } { "text": "口风琴" } { "index": { "_id": 2 } } { "text": "电子琴" } { "index": { "_id": 3 } } { "text": "吉他" } { "index": { "_id": 4 } } { "text": "钢琴" } { "index": { "_id": 5 } } { "text": "小提琴" } { "index": { "_id": 6 } } { "text": "鼓" } { "index": { "_id": 7 } } { "text": "萨克斯" } { "index": { "_id": 8 } } { "text": "低音提琴" } { "index": { "_id": 9 } } { "text": "电子合成器" } { "index": { "_id": 10 } } { "text": "风琴" } { "index": { "_id": 11 } } { "text": "竖琴" } { "index": { "_id": 12 } } { "text": "口琴" } { "index": { "_id": 13 } } { "text": "电子打击乐器" } { "index": { "_id": 14 } } { "text": "手风琴" } { "index": { "_id": 15 } } { "text": "鼓组" } { "index": { "_id": 16 } } { "text": "电子小号" } { "index": { "_id": 17 } } { "text": "铜管乐器" } { "index": { "_id": 18 } } { "text": "木管乐器" } { "index": { "_id": 19 } } { "text": "打击乐器" } { "index": { "_id": 20 } } { "text": "电子小号" } { "index": { "_id": 21 } } { "text": "牙刷" } { "index": { "_id": 22 } } { "text": "洗发水" } { "index": { "_id": 23 } } { "text": "漱口水" } { "index": { "_id": 24 } } { "text": "纸巾" } { "index": { "_id": 25 } } { "text": "洗衣粉" } { "index": { "_id": 26 } } { "text": "洗洁精" } { "index": { "_id": 27 } } { "text": "沐浴露" } { "index": { "_id": 28 } } { "text": "毛巾" } { "index": { "_id": 29 } } { "text": "垃圾袋" } { "index": { "_id": 30 } } { "text": "牙膏" } { "index": { "_id": 31 } } { "text": "香皂" } { "index": { "_id": 32 } } { "text": "洗面奶" } { "index": { "_id": 33 } } { "text": "清洁剂" } { "index": { "_id": 34 } } { "text": "洗手液" } { "index": { "_id": 35 } } { "text": "护肤霜" } { "index": { "_id": 36 } } { "text": "面膜" } { "index": { "_id": 37 } } { "text": "卫生纸" } { "index": { "_id": 38 } } { "text": "食品保鲜膜" } { "index": { "_id": 39 } } { "text": "水杯" } { "index": { "_id": 40 } } { "text": "器具清洗液" } { "index": { "_id": 41 } } { "text": "餐具消毒液" } { "index": { "_id": 42 } } { "text": "保鲜盒" } { "index": { "_id": 43 } } { "text": "便签纸" } { "index": { "_id": 44 } } { "text": "《活着》" } { "index": { "_id": 45 } } { "text": "《百年孤独》" } { "index": { "_id": 46 } } { "text": "《小王子》" } { "index": { "_id": 47 } } { "text": "《1984》" } { "index": { "_id": 48 } } { "text": "《骆驼祥子》" } { "index": { "_id": 49 } } { "text": "《红楼梦》" } { "index": { "_id": 50 } } { "text": "《西游记》" } { "index": { "_id": 51 } } { "text": "《三国演义》" } { "index": { "_id": 52 } } { "text": "《围城》" } { "index": { "_id": 53 } } { "text": "《悲惨世界》" } { "index": { "_id": 54 } } { "text": "《哈利·波特》" } { "index": { "_id": 55 } } { "text": "《无声告白》" } { "index": { "_id": 56 } } { "text": "《时间简史》" } { "index": { "_id": 57 } } { "text": "《追风筝的人》" } { "index": { "_id": 58 } } { "text": "《平凡的世界》" } { "index": { "_id": 59 } } { "text": "《三体》" } { "index": { "_id": 60 } } { "text": "《人类简史》" } { "index": { "_id": 61 } } { "text": "《安娜·卡列尼娜》" } { "index": { "_id": 62 } } { "text": "《月亮和六便士》" } { "index": { "_id": 63 } } { "text": "《未来简史》" } { "index": { "_id": 64 } } { "text": "北京" } { "index": { "_id": 65 } } { "text": "上海" } { "index": { "_id": 66 } } { "text": "广州" } { "index": { "_id": 67 } } { "text": "深圳" } { "index": { "_id": 68 } } { "text": "成都" } { "index": { "_id": 69 } } { "text": "重庆" } { "index": { "_id": 70 } } { "text": "杭州" } { "index": { "_id": 71 } } { "text": "武汉" } { "index": { "_id": 72 } } { "text": "西安" } { "index": { "_id": 73 } } { "text": "南京" } { "index": { "_id": 74 } } { "text": "青岛" } { "index": { "_id": 75 } } { "text": "郑州" } { "index": { "_id": 76 } } { "text": "福州" } { "index": { "_id": 77 } } { "text": "厦门" } { "index": { "_id": 78 } } { "text": "天津" } { "index": { "_id": 79 } } { "text": "大连" } { "index": { "_id": 80 } } { "text": "厦门" } { "index": { "_id": 81 } } { "text": "济南" } { "index": { "_id": 82 } } { "text": "青岛" } { "index": { "_id": 83 } } { "text": "昆明" } { "index": { "_id": 84 } } { "text": "哈尔滨" } { "index": { "_id": 85 } } { "text": "长沙" } { "index": { "_id": 86 } } { "text": "广州" } { "index": { "_id": 87 } } { "text": "合肥" } { "index": { "_id": 88 } } { "text": "南宁" } { "index": { "_id": 89 } } { "text": "太原" } { "index": { "_id": 90 } } { "text": "南昌" } { "index": { "_id": 91 } } { "text": "狗" } { "index": { "_id": 92 } } { "text": "猫" } { "index": { "_id": 93 } } { "text": "狐狸" } { "index": { "_id": 94 } } { "text": "大象" } { "index": { "_id": 95 } } { "text": "老虎" } { "index": { "_id": 96 } } { "text": "狮子" } { "index": { "_id": 97 } } { "text": "熊" } { "index": { "_id": 98 } } { "text": "长颈鹿" } { "index": { "_id": 99 } } { "text": "斑马" } { "index": { "_id": 100 } } { "text": "鳄鱼" }
能够从 OpenSearch 索引中查询出数据,代表写入成功。
示例:
GET /text_index/_search?search_pipeline=search_pipeline # 注意与创建的 search pipeline 保持一致 { "_source": "text", "query": { "hybrid": { "queries": [ { "remote_neural": { "text_knn": { "query_text": "电吉他", "k": 10 } } }, { "intervals": { "text": { "match": { "query": "电子琴", "ordered": true, "max_gaps": 3 } } } } ] } } }
操作步骤同上述部署 Embedding 模型步骤,此处不再赘述。
说明
本示例中选用了 BAAI/bge-reranker-v2-m3
模型,实际使用请根据实际需求选择合适模型。
API Key 是推理服务接口调用时进行身份认证的关键信息,在正式调用推理服务之前需要创建 API Key,请注意妥善保管 API Key,请勿泄露。
export API_KEY="5cad31c3-**************-e5b7d3c9294d"
进入到 ML 服务 -> 推理服务 ,找到 Rerank 服务查看调用信息
curl http://{addr}/v1/rerank \ -H "Content-Type: application/json" \ -H "Authorization: Bearer $API_KEY" \ -X POST \ -d '{ "model": "BAAI/bge-reranker-v2-m3", "query": "电子琴", "top_n": 7, "documents": [ "口风琴", "吉他", "北京", "牙刷", "电脑", "显示器", "电子琴", "电子", "鼓" ] }' | jq .
在 OpenSearch 中通过 Pipeline 使用 Rerank 模型需要通过 Rerank 推理服务的内部地址进行访问,首先需要获取 Rerank 访问地址。
该 Pipeline 在查询 OpenSearch 时使用。本示例创建一个名为 search_pipeline_with_rerank
的 pipeline,包含 Embedding 和 Rerank 模型。
PUT _search/pipeline/search_pipeline_with_rerank { "description": "text embedding pipeline for remote inference", "request_processors": [ { "remote_embedding": { "remote_config": { "method": "POST", "url" : "https://ark.cn-beijing.volces.com/api/v3/embeddings", "params" : { }, "headers" : { "Content-Type" : "application/json", "Authorization": "Bearer {api-key}" ##创建的api-key }, "advance_request_body" : { "model" : "ep-xxxxx-xxxxx" ##embedding推理服务,调用信息中的model字段值 } } } } ] , "phase_results_processors": [ { "normalization-processor": { "normalization": { "technique": "min_max" }, "combination": { "technique": "arithmetic_mean", "parameters": { "weights": [ 0.3, 0.7 ] } } } } ] , "response_processors": [ { "remote_rerank": { "ml_opensearch": { "remote_config": { "method": "POST", "url": "{rerank_url}", "params": { "token": "{api_key}" }, "headers": { "Content-Type": "application/json" }, "advance_request_body": { "model": "{rerank_model}" } } }, "context": { "document_fields": [ "text" ] } } } ] }
说明
请根据业务实际情况替换上述代码中的以下变量:
使用 Rerank pipeline 时在 Query 中需要添加 ext 信息(如下示例中的 search_pipeline_with_rerank 变量)。
其中 ext.remote_rerank.query_context.query_text
设置相应提问内容。
GET /text_index/_search?search_pipeline=search_pipeline_with_rerank { "_source": "text", "query": { "hybrid": { "queries": [ { "remote_neural": { "text_knn": { "query_text": "电吉他", "k": 10 } } }, { "intervals": { "text": { "match": { "query": "电子琴", "ordered": true, "max_gaps": 3 } } } } ] } }, "ext": { "remote_rerank": { "query_context": { "query_text": "哪个答案和电吉他、电子琴最匹配?" } } } }