给Easysearch以文搜图搭座桥,结果 Haiku 把藕片看成了炒饭 (下)
上一篇《我用 Easysearch 搭了个「以文搜图」然后被一盘藕片上了一课》结尾我立了个 flag整图语义和局部细节之间那道坎模型自己迈不过去得靠工程手段搭桥。下一篇就来搭这座桥。这篇就来还债。桥搭成了「藕片」也终于排到了第一——但中间被一个模型摆了一道那段值得单拎出来讲。先把上回的病情复述一句搜「海鲜汤」准得不行搜「藕片」却翻车。根子在于 Cohere Embed v4 给整张图只生成一个向量代表的是整盘菜是什么藕片只是小龙虾里的配菜占画面一成语义被稀释了向量根本指不到它。一、这座桥叫 Hybrid 检索思路其实朴素得很向量看不清的让文字来补。向量检索管「语义相似」——你搜「红烧的炖菜」它能捞回一堆你没提过具体名字的菜。这是它的长处得留着兜底。关键词检索BM25管「精确命中」——你搜「藕片」只要哪张图的描述里明明白白写着「藕片」两个字它就能一把揪出来。这正是向量的短板。两路一起打分、融合排序就叫Hybrid 检索。向量兜住语义的下限关键词捞回精确的漏网之鱼。可问题来了图片本身没有文字啊BM25 拿什么去 match所以搭桥的第一根桩是给每张图先配上一段文字——让一个能看图的多模态大模型替每张图写句描述、列一串食材标签存进 Easysearch 的全文字段里。这样「藕片」这个词就从「藏在像素里」变成了「白纸黑字」BM25 才够得着。说白了就是花钱请个模型把图翻译成话。那请谁呢二、请 Haiku 来打标签结果它把小龙虾看成了炒饭图便宜活不重我第一反应是叫最轻的Claude Haiku 4.5——快、便宜标注这种粗活它该够了。提示词写得也算讲究让它输出严格 JSONcaption一句话tags逐一列出画面里每种食材尤其是配菜。特意强调了配菜就怕它漏掉藕片。跑第一张就是那张惹事的小龙虾藕片Haiku 张口就来{caption:一道色香味俱全的海鲜炒饭鲜红的龙虾与嫩滑的牛肉块混合配以葱段和香油调味。,tags:[龙虾,牛肉,炒饭,葱段,海鲜,米饭,油炸,中式料理,荤菜]}我盯着这段看了三秒——炒饭牛肉那明明是盘小龙虾配藕片哪来的炒饭哪来的牛肉藕片更是提都没提。它不光没认出配菜连主菜都看岔了还煞有介事地脑补出「香油调味」。主打一个一本正经地胡说八道。这下问题就尖锐了桥能不能承重取决于用什么料造。如果连负责打标签的模型都把菜看错那关键词字段里存的全是错话Hybrid 检索反而会把人往沟里带——你搜「牛肉」它给你端上盘小龙虾。省这点钱等于把桥墩浇成了豆腐渣。得换料。三、换 Opus 上来藕片终于被看见了同一张图、同一段提示词我把模型从 Haiku 换成 Sonnet 4.5又换成 Opus 4.8做了组横评模型认出藕片了吗它说这是什么Haiku 4.5❌「海鲜炒饭龙虾配牛肉」——主配菜全错Sonnet 4.5✅「小龙虾配藕片和蔬菜」tags 里有「莲藕」Opus 4.8✅「蒜蓉小龙虾配藕片、黄瓜和青椒」tags 直接是「藕片」二字高下立判。Sonnet 已经能认出藕片了标签写的是「莲藕」Opus 更狠caption 通顺不说tags 里干脆就是**「藕片」**两个字——和用户会打进搜索框的词一模一样。对 BM25 来说这种「字面精确对上」是顶好的料。一句题外话也是我一直挺认的贵模型未必哪儿都值那个价但在「看清细节」这种硬骨头上参数量的差距是实打实的。标个葱姜蒜Haiku 绰绰有余可要在一盘红油浓酱里把半掩着的藕片给拎出来还得是 Opus。选型这事得看你这一步到底在为什么买单。那就 Opus 了。caption_images.py的核心就是让它看图吐 JSONCAPTION_MODELglobal.anthropic.claude-opus-4-8PROMPT(你是专业的中餐美食标注助手。仔细看这张图用中文输出严格 JSON{caption:一句话描述这道菜,tags:[标签]}。tags 必须逐一列出你在画面里认出的每种食材包括配菜和不显眼的。只输出 JSON不要解释、不要 markdown 代码块。)defcaption(path):b64base64.b64encode(open(path,rb).read()).decode()body{anthropic_version:bedrock-2023-05-31,max_tokens:500,messages:[{role:user,content:[{type:image,source:{type:base64,media_type:image/jpeg,data:b64}},{type:text,text:PROMPT},]}],}txtjson.loads(bedrock.invoke_model(modelIdCAPTION_MODEL,bodyjson.dumps(body))[body].read())[content][0][text]objjson.loads(re.search(r\{.*\},txt,re.S).group(0))# 容错从回复里抠出 JSONreturn{caption:obj.get(caption,),tags:obj.get(tags,[])}⚠️ 一个真会踩的坑Bedrock 上的 Claude 4.x 系列不支持按需直调modelId得带区域前缀us.anthropic.claude-...或global.anthropic.claude-...走 inference profile否则甩你一句Invocation ... with on-demand throughput isnt supported。我第一发就撞上了。给 10 张图挨个打完标签Opus 的表现相当稳——两碗汤认出了「海参鲍鱼花胶」越南沙拉认出了「香茅蘸酱」连那张混进来的高楼照片它都老老实实标了句「并非中餐美食tags 为空」。诚实得可爱。四、字段要落地得先教 Easysearch 断中文词标签生成好了得有地方存。给已有的image_search索引加三个字段curl-sk-uadmin:$ES_PASS-XPUT$ES_URL/image_search/_mapping\-HContent-Type: application/json-d{ properties: { caption: { type: text, analyzer: ik_max_word }, tags: { type: keyword }, caption_text: { type: text, analyzer: ik_max_word, search_analyzer: ik_smart } } }这里的关键是analyzer用了ik_max_word——IK 中文分词器。为什么非它不可说到底Easysearch同 ES 一系默认的分词器不认中文词会把「小龙虾配藕片」按字母/空格切中文一整句被当成一个不可分的整块你搜「藕片」根本 match 不上。IK 分词器才能把「蒜蓉小龙虾配藕片黄瓜青椒」切成「蒜蓉 / 小龙虾 / 藕片 / 黄瓜 / 青椒」这样一颗颗独立的词——藕片这才成了一个能被单独命中的 token。好在这套 Easysearch 自带了analysis-ik插件拿来即用。caption_text这个字段我把 caption 和 tags 拼在一起塞进去专门喂给 BM25。灌标签就一句python3 caption_images.py它用_update局部更新——向量是上一篇index_images.py写好的这回只往每篇文档补 caption/tags不动向量。各司其职。五、把两路拧成一股绳材料齐了改查询。上一篇是光秃秃一个knn_nearest_neighbors这回用bool把它和关键词match拧到一起knn{knn_nearest_neighbors:{field:embedding,vec:{values:vec},model:exact,similarity:cosine,}}ifhybrid:# must 里放 knn 兜底语义召回; should 里放关键词, boost 让命中标签的图显著提分query{bool:{must:[knn],should:[{match:{caption_text:{query:text,boost:4.0}}}]}}else:queryknn# 退回纯向量, 用于对照拆开看这个boolmust放 knn——向量语义是地基保证哪怕关键词一个没中也有语义相似的图垫底不至于开天窗。should放 match——关键词是加分项。哪张图的caption_text里出现了「藕片」BM25 就给它加一大笔分。boost: 4.0——这是个手感活。向量分在 1~2 之间BM25 分天生比它大一截再乘个 4就是明明白白告诉引擎关键词字面命中这件事我很看重。命中标签的图分数会被顶得断层领先。boost具体给多少没有标准答案得看你更信语义还是更信字面调着看效果。我这儿给 4是想让「藕片」这种精确查询的效果最扎眼。六、见桥改完重启搜「藕片」纯向量和 Hybrid 各来一发对照纯向量?hybrid0——老样子藕片图淹在第五前排全是汤#1 1.246 海参鲍鱼花胶汤 #2 1.244 海参鲍鱼汤 #3 1.239 川味红烧牛肉面 ... #5 1.200 蒜蓉小龙虾配藕片 ← 那张真有藕片的还是沉着Hybrid——关键词一介入藕片图直接被顶穿天花板#1 9.129 蒜蓉小龙虾配藕片 ← 从 1.2 飙到 9.13断层第一 #2 1.246 海参鲍鱼花胶汤 #3 1.244 海参鲍鱼汤外一篇给以文搜图搭座桥结果 Haiku 把藕片看成了炒饭上一篇《我用 Easysearch 搭了个「以文搜图」然后被一盘藕片上了一课》结尾我立了个 flag整图语义和局部细节之间那道坎模型自己迈不过去得靠工程手段搭桥。下一篇就来搭这座桥。这篇就来还债。桥搭成了「藕片」也终于排到了第一——但中间被一个模型摆了一道那段值得单拎出来讲。先把上回的病情复述一句搜「海鲜汤」准得不行搜「藕片」却翻车。根子在于 Cohere Embed v4 给整张图只生成一个向量代表的是整盘菜是什么藕片只是小龙虾里的配菜占画面一成语义被稀释了向量根本指不到它。一、这座桥叫 Hybrid 检索思路其实朴素得很向量看不清的让文字来补。向量检索管「语义相似」——你搜「红烧的炖菜」它能捞回一堆你没提过具体名字的菜。这是它的长处得留着兜底。关键词检索BM25管「精确命中」——你搜「藕片」只要哪张图的描述里明明白白写着「藕片」两个字它就能一把揪出来。这正是向量的短板。两路一起打分、融合排序就叫Hybrid 检索。向量兜住语义的下限关键词捞回精确的漏网之鱼。可问题来了图片本身没有文字啊BM25 拿什么去 match所以搭桥的第一根桩是给每张图先配上一段文字——让一个能看图的多模态大模型替每张图写句描述、列一串食材标签存进 Easysearch 的全文字段里。这样「藕片」这个词就从「藏在像素里」变成了「白纸黑字」BM25 才够得着。说白了就是花钱请个模型把图翻译成话。那请谁呢二、请 Haiku 来打标签结果它把小龙虾看成了炒饭图便宜活不重我第一反应是叫最轻的Claude Haiku 4.5——快、便宜标注这种粗活它该够了。提示词写得也算讲究让它输出严格 JSONcaption一句话tags逐一列出画面里每种食材尤其是配菜。特意强调了配菜就怕它漏掉藕片。跑第一张就是那张惹事的小龙虾藕片Haiku 张口就来{caption:一道色香味俱全的海鲜炒饭鲜红的龙虾与嫩滑的牛肉块混合配以葱段和香油调味。,tags:[龙虾,牛肉,炒饭,葱段,海鲜,米饭,油炸,中式料理,荤菜]}我盯着这段看了三秒——炒饭牛肉那明明是盘小龙虾配藕片哪来的炒饭哪来的牛肉藕片更是提都没提。它不光没认出配菜连主菜都看岔了还煞有介事地脑补出「香油调味」。主打一个一本正经地胡说八道。这下问题就尖锐了桥能不能承重取决于用什么料造。如果连负责打标签的模型都把菜看错那关键词字段里存的全是错话Hybrid 检索反而会把人往沟里带——你搜「牛肉」它给你端上盘小龙虾。省这点钱等于把桥墩浇成了豆腐渣。得换料。三、换 Opus 上来藕片终于被看见了同一张图、同一段提示词我把模型从 Haiku 换成 Sonnet 4.5又换成 Opus 4.8做了组横评模型认出藕片了吗它说这是什么Haiku 4.5❌「海鲜炒饭龙虾配牛肉」——主配菜全错Sonnet 4.5✅「小龙虾配藕片和蔬菜」tags 里有「莲藕」Opus 4.8✅「蒜蓉小龙虾配藕片、黄瓜和青椒」tags 直接是「藕片」二字高下立判。Sonnet 已经能认出藕片了标签写的是「莲藕」Opus 更狠caption 通顺不说tags 里干脆就是**「藕片」**两个字——和用户会打进搜索框的词一模一样。对 BM25 来说这种「字面精确对上」是顶好的料。一句题外话也是我一直挺认的贵模型未必哪儿都值那个价但在「看清细节」这种硬骨头上参数量的差距是实打实的。标个葱姜蒜Haiku 绰绰有余可要在一盘红油浓酱里把半掩着的藕片给拎出来还得是 Opus。选型这事得看你这一步到底在为什么买单。那就 Opus 了。caption_images.py的核心就是让它看图吐 JSONCAPTION_MODELglobal.anthropic.claude-opus-4-8PROMPT(你是专业的中餐美食标注助手。仔细看这张图用中文输出严格 JSON{caption:一句话描述这道菜,tags:[标签]}。tags 必须逐一列出你在画面里认出的每种食材包括配菜和不显眼的。只输出 JSON不要解释、不要 markdown 代码块。)defcaption(path):b64base64.b64encode(open(path,rb).read()).decode()body{anthropic_version:bedrock-2023-05-31,max_tokens:500,messages:[{role:user,content:[{type:image,source:{type:base64,media_type:image/jpeg,data:b64}},{type:text,text:PROMPT},]}],}txtjson.loads(bedrock.invoke_model(modelIdCAPTION_MODEL,bodyjson.dumps(body))[body].read())[content][0][text]objjson.loads(re.search(r\{.*\},txt,re.S).group(0))# 容错从回复里抠出 JSONreturn{caption:obj.get(caption,),tags:obj.get(tags,[])}⚠️ 一个真会踩的坑Bedrock 上的 Claude 4.x 系列不支持按需直调modelId得带区域前缀us.anthropic.claude-...或global.anthropic.claude-...走 inference profile否则甩你一句Invocation ... with on-demand throughput isnt supported。我第一发就撞上了。给 10 张图挨个打完标签Opus 的表现相当稳——两碗汤认出了「海参鲍鱼花胶」越南沙拉认出了「香茅蘸酱」连那张混进来的高楼照片它都老老实实标了句「并非中餐美食tags 为空」。诚实得可爱。四、字段要落地得先教 Easysearch 断中文词标签生成好了得有地方存。给已有的image_search索引加三个字段curl-sk-uadmin:$ES_PASS-XPUT$ES_URL/image_search/_mapping\-HContent-Type: application/json-d{ properties: { caption: { type: text, analyzer: ik_max_word }, tags: { type: keyword }, caption_text: { type: text, analyzer: ik_max_word, search_analyzer: ik_smart } } }这里的关键是analyzer用了ik_max_word——IK 中文分词器。为什么非它不可说到底Easysearch同 ES 一系默认的分词器不认中文词会把「小龙虾配藕片」按字母/空格切中文一整句被当成一个不可分的整块你搜「藕片」根本 match 不上。IK 分词器才能把「蒜蓉小龙虾配藕片黄瓜青椒」切成「蒜蓉 / 小龙虾 / 藕片 / 黄瓜 / 青椒」这样一颗颗独立的词——藕片这才成了一个能被单独命中的 token。好在这套 Easysearch 自带了analysis-ik插件拿来即用。caption_text这个字段我把 caption 和 tags 拼在一起塞进去专门喂给 BM25。灌标签就一句python3 caption_images.py它用_update局部更新——向量是上一篇index_images.py写好的这回只往每篇文档补 caption/tags不动向量。各司其职。五、把两路拧成一股绳材料齐了改查询。上一篇是光秃秃一个knn_nearest_neighbors这回用bool把它和关键词match拧到一起knn{knn_nearest_neighbors:{field:embedding,vec:{values:vec},model:exact,similarity:cosine,}}ifhybrid:# must 里放 knn 兜底语义召回; should 里放关键词, boost 让命中标签的图显著提分query{bool:{must:[knn],should:[{match:{caption_text:{query:text,boost:4.0}}}]}}else:queryknn# 退回纯向量, 用于对照拆开看这个boolmust放 knn——向量语义是地基保证哪怕关键词一个没中也有语义相似的图垫底不至于开天窗。should放 match——关键词是加分项。哪张图的caption_text里出现了「藕片」BM25 就给它加一大笔分。boost: 4.0——这是个手感活。向量分在 1~2 之间BM25 分天生比它大一截再乘个 4就是明明白白告诉引擎关键词字面命中这件事我很看重。命中标签的图分数会被顶得断层领先。boost具体给多少没有标准答案得看你更信语义还是更信字面调着看效果。我这儿给 4是想让「藕片」这种精确查询的效果最扎眼。六、见桥改完重启搜「藕片」纯向量和 Hybrid 各来一发对照纯向量?hybrid0——老样子藕片图淹在第五前排全是汤#1 1.246 海参鲍鱼花胶汤 #2 1.244 海参鲍鱼汤 #3 1.239 川味红烧牛肉面 ... #5 1.200 蒜蓉小龙虾配藕片 ← 那张真有藕片的还是沉着Hybrid——关键词一介入藕片图直接被顶穿天花板#1 9.129 蒜蓉小龙虾配藕片 ← 从 1.2 飙到 9.13断层第一 #2 1.246 海参鲍鱼花胶汤 #3 1.244 海参鲍鱼汤从「沉在第五」到「9.13 断层第一」桥就这么架通了。截图里每张卡片底下那句描述、那串标签全是 Opus 标的——你能看见1000016167.jpg挂着「小龙虾 / 莲藕 / 黄瓜 / 青椒」「藕片」这个词如今是捞得起来的实词不再是沉在像素里的一团模糊语义。顺手验了另外几个精确词一个道理「鲍鱼」两碗鲍鱼汤 9.6 / 9.1 领跑「橙汁」那杯橙汁 13.4 断层第一。凡是能被说清、能被打进标签的东西Hybrid 都能精确捞回。前端也加了个开关勾掉 Hybrid 就退回纯向量同一个查询左右横跳那道坎迈没迈过去一眼就看得出来。附新增文件与复现在上一篇的基础上这一篇多了一个脚本、改了一处查询文件变化caption_images.py新增用 Opus 给每张图生成 caption/tags_update进索引server.pysearch_images加hybrid开关查询改bool{knn match}index.html加 Hybrid 开关卡片展示 caption/tagsexportAWS_REGIONus-east-1exportES_PASS你的-Easysearch-admin-密码# 1. 给索引加文本字段(IK 中文分词)curl-sk-uadmin:$ES_PASS-XPUThttps://localhost:9200/image_search/_mapping\-HContent-Type: application/json-d{properties:{ caption:{type:text,analyzer:ik_max_word}, tags:{type:keyword}, caption_text:{type:text,analyzer:ik_max_word,search_analyzer:ik_smart}}}# 2. 用 Opus 打标签(需先跑过上一篇的 index_images.py 建好向量)python3 caption_images.py# 3. 重启服务, 打开 http://localhost:8080, 搜「藕片」, 勾选/取消 Hybrid 对照python3 server.py两篇下来这盘藕片总算是搜出来了。回头看真正的功夫没花在向量、也没花在那个bool查询上——花在了逼着自己承认「模型会看错」这件事上。Haiku 那句「海鲜炒饭配牛肉」要是没多看两眼、顺手就灌进了库这座桥架起来照样是歪的还歪得你浑然不觉。工程里最贵的从来不是搭桥是验桥。下次再有人跟你说「上个多模态模型自动打标签就行了」你可以反问一句你验过它认得出藕片吗技术栈INFINI Labs Easysearch 2.3.0boolknn_nearest_neighbors IK 分词match· Amazon Bedrock Cohere Embed v4向量 Claude Opus 4.8图片标注· Python 标准库 HTTP 服务从「沉在第五」到「9.13 断层第一」桥就这么架通了。截图里每张卡片底下那句描述、那串标签全是 Opus 标的——你能看见1000016167.jpg挂着「小龙虾 / 莲藕 / 黄瓜 / 青椒」「藕片」这个词如今是捞得起来的实词不再是沉在像素里的一团模糊语义。顺手验了另外几个精确词一个道理「鲍鱼」两碗鲍鱼汤 9.6 / 9.1 领跑「橙汁」那杯橙汁 13.4 断层第一。凡是能被说清、能被打进标签的东西Hybrid 都能精确捞回。前端也加了个开关勾掉 Hybrid 就退回纯向量同一个查询左右横跳那道坎迈没迈过去一眼就看得出来。附新增文件与复现在上一篇的基础上这一篇多了一个脚本、改了一处查询文件变化caption_images.py新增用 Opus 给每张图生成 caption/tags_update进索引server.pysearch_images加hybrid开关查询改bool{knn match}index.html加 Hybrid 开关卡片展示 caption/tagsexportAWS_REGIONus-east-1exportES_PASS你的-Easysearch-admin-密码# 1. 给索引加文本字段(IK 中文分词)curl-sk-uadmin:$ES_PASS-XPUThttps://localhost:9200/image_search/_mapping\-HContent-Type: application/json-d{properties:{ caption:{type:text,analyzer:ik_max_word}, tags:{type:keyword}, caption_text:{type:text,analyzer:ik_max_word,search_analyzer:ik_smart}}}# 2. 用 Opus 打标签(需先跑过上一篇的 index_images.py 建好向量)python3 caption_images.py# 3. 重启服务, 打开 http://localhost:8080, 搜「藕片」, 勾选/取消 Hybrid 对照python3 server.py两篇下来这盘藕片总算是搜出来了。回头看真正的功夫没花在向量、也没花在那个bool查询上——花在了逼着自己承认「模型会看错」这件事上。Haiku 那句「海鲜炒饭配牛肉」要是没多看两眼、顺手就灌进了库这座桥架起来照样是歪的还歪得你浑然不觉。工程里最贵的从来不是搭桥是验桥。下次再有人跟你说「上个多模态模型自动打标签就行了」你可以反问一句你验过它认得出藕片吗技术栈INFINI Labs Easysearch 2.3.0boolknn_nearest_neighbors IK 分词match· Amazon Bedrock Cohere Embed v4向量 Claude Opus 4.8图片标注· Python 标准库 HTTP 服务