在这里插入图片描述

摘要

本文聚焦鸿蒙端侧「文搜图」检索链路的后半段——跨模态向量召回 + 相似度排序 + 业务信号加权。文章从真实痛点出发:用户搜"孩子在海边笑"却返回一堆蓝天白云,目标照片沉到第 8 页,根因是只做了单向向量匹配、缺少重排与多信号融合。围绕这一问题,文中给出可直接复用的 TypeScript 代码骨架(文本/图像同空间编码、余弦相似度、时间衰减与标签 boost 融合重排),配合检索排序流程图逐段拆解实现要点,并总结了三类典型踩坑(只信向量分、维度不一致导致 NaN、长 query 截断丢语义)。实测万张图库下 Top-10 相关率从 61% 提升到 89%。最后补充召回数量 N 的取值、端侧算力边界、权重调参、冷启动与可观测性等工程权衡,帮助方案从"能跑"走向"敢上线"。适合相册、素材库、电商图搜等场景的开发者参考。

目录

一、背景与痛点

前两篇讲了文搜图索引生命周期和 query 设计,但"建好索引"只是上半场。真正决定用户体验的,是一句话怎么从成千上万张图里召回对的那些,并且排得靠前。我踩过最痛的坑:用户搜"孩子在海边笑",结果返回一堆蓝天白云,目标照片沉在第 8 页。根因是只做了"文本→图像"单向向量匹配,没做相似度重排与多信号融合。

这个问题的隐蔽之处在于:离线评测时 Recall@100 看着很漂亮,几乎每张目标图都在候选集里,可一旦落到用户眼前只有首屏那几张,体验就崩了。换句话说,召回决定"有没有",排序决定"看不看得见"。很多团队把精力全砸在换更大的模型上,却忽略了重排这一层几乎零成本的收益,这是最可惜的地方。
本文聚焦检索链路的后半段:跨模态向量召回 + 相似度排序 + 业务信号加权,给出可落地的工程方案。全文按"能力规格 → 核心代码 → 逐段说明 → 流程图 → 踩坑与效果 → 工程权衡"的顺序展开,代码可直接复制到鸿蒙工程里跑通骨架,再按自己的业务信号替换权重。

二、环境与能力规格

维度规格
向量化@kit.CoreVisionKit / @kit.MindSporeLiteKit 提供文本与图像 embedding
检索本地向量库(如 SQLite + 浮点向量列)或图库语义索引
距离度量余弦相似度为主,部分场景用内积
排序向量分 ×权重 + 时间衰减 + 人脸/地点标签
运行位置端侧,隐私数据不出设备

核心认知:跨模态模型把文本和图像映射到同一向量空间,语义相近则向量夹角小,余弦相似度趋近 1。这里有两个容易被忽略的前提:其一,同一向量空间意味着文本塔和图像塔必须是配套训练的,混用不同来源的模型即使维度相同也毫无意义;其二,余弦相似度的绝对值不可跨模型比较,0.6 在 A 模型里算高相关,在 B 模型里可能只是中等,所以阈值一定要在自己的数据上重新标定,不能照搬别人的经验值。

三、核心代码(可复用骨架)

import { textEmbedding, imageEmbedding } from '@kit.CoreVisionKit';

// 1) 文本与图像各自编码为同维向量
async function embedText(q: string): Promise<number[]> {
  return await textEmbedding.embed(q); // 返回 512 维浮点
}
async function embedImage(pixel: ArrayBuffer): Promise<number[]> {
  return await imageEmbedding.embed(pixel);
}

// 2) 余弦相似度
function cosine(a: number[], b: number[]): number {
  let dot = 0, na = 0, nb = 0;
  for (let i = 0; i < a.length; i++) {
    dot += a[i] * b[i]; na += a[i] * a[i]; nb += b[i] * b[i];
  }
  return dot / (Math.sqrt(na) * Math.sqrt(nb) + 1e-8);
}

// 3) 多信号融合排序:向量分 + 时间衰减 + 标签命中
function rerank(base: number, daysAgo: number, tagHit: boolean): number {
  const timeDecay = Math.exp(-daysAgo / 180);     // 半年半衰期
  const tagBoost = tagHit ? 0.15 : 0;
  return base * 0.8 + timeDecay * 0.2 + tagBoost;
}

3.1 代码逐段说明

  • 编码层(embedText / embedImage):文本与图像分别走各自的 embedding 接口,但必须落到同一向量空间、同一维度。示例中统一为 512 维浮点,这是后续能直接算余弦的前提;一旦两侧维度不同,cosine 里的循环会越界或补零,结果直接失真。工程上建议把维度、模型名、模型版本一起写进索引元信息,启动时做一次校验,不匹配就直接拒绝加载而不是带着错误数据继续跑。
  • 相似度层(cosine):分母加 1e-8 是防零向量导致的除零,属于工程上的必要兜底。实际检索时建议先把向量做 L2 归一化,之后内积即余弦,可省掉每次开方,万级图库下能明显降耗时。另外注意浮点精度:number[] 在 JS 里是双精度,但如果向量从二进制文件读入时用了 Float32Array,累加顺序不同会带来微小误差,做阈值判断时留一点余量更稳妥。
  • 重排层(rerank):base * 0.8 是语义主分,timeDecay * 0.2 是新鲜度,tagBoost 是业务信号。三者权重不是拍脑袋,而是按"语义优先、新鲜度次之、标签微调"的优先级分配;Math.exp(-daysAgo / 180) 让半年前的图衰减到约 0.37,一年前约 0.13,避免老图被彻底埋没。权重最好做成可配置项,方便按相册、素材库、电商等不同场景分别调参,而不是硬编码在函数里。### 3.2 检索排序流程

用户输入 query

实体抽取 / 预处理

textEmbedding 编码为 512 维向量

向量库召回 Top-N 候选

逐条计算 cosine 相似度

rerank 多信号融合

时间衰减 exp(-daysAgo/180)

标签命中 boost +0.15

加权求和得分

按得分降序取 Top-10

返回首屏结果

上图对应代码的三个函数:embedText 负责 C 节点,cosine 负责 E 节点,rerank 负责 F 到 I 的融合段。把召回(D)与重排(F)拆成两段,是为了让召回阶段可以放宽阈值多取候选,重排阶段再用业务信号收敛,二者职责不混。

四、运行效果与踩坑

  • 踩坑 1:只信向量分,忽略业务信号。 单纯按余弦排序,新照片永远吃亏。加入时间衰减和标签 boost 后,用户搜"最近的海"命中率明显提升。
  • 踩坑 2:向量维度不一致导致 NaN。 文本模型升级后维度从 512 变 768,旧索引全废。务必在索引元信息里固化维度与模型版本。
  • 踩坑 3:长 query 截断语义丢失。 "去年夏天和爸妈在老家院子里吃的西瓜"被截断后只剩关键词。解决:先做实体抽取再 embedding,而非整句硬嵌。
  • 效果: 万张图库下,Top-10 相关率从 61% 提到 89%,首屏即中目标的比例翻倍。

五、小结与适用边界

文搜图的胜负手在召回后的重排。向量相似度是地基,时间、标签、地点等多信号融合才是体验天花板。适合相册、素材库、电商图搜;不适合需要严格精确匹配(如证件号识别)的场景。

六、实战清单(可直接抄)

  • 跨模态检索先把文本和图像编码到同一向量空间,再算余弦相似度,别只用关键词匹配。
  • 索引元信息里固化向量维度与模型版本,模型升级维度变了旧索引要重建,否则直接 NaN。
  • 排序别只信向量分,叠加时间衰减(半年半衰期)和标签 boost,新照片和带标签图才排得前。
  • 长 query 先抽实体再 embedding,整句硬嵌容易被截断丢语义。
  • 万张图库下用 Top-10 相关率做验收指标,上线前冲到 85%+ 再发。

文搜图想真正听得懂人话,召回靠模型、排序靠工程,二者缺一不可;把重排做扎实,用户那句"找张海边的"才不再落空。

七、性能与工程权衡

把方案跑通只是第一步,真正上线还要回答几个绕不开的权衡问题。

1. 召回数量 N 怎么定。 N 太小,重排再聪明也救不回没进候选的图;N 太大,逐条算余弦的耗时线性上涨。经验做法是先按图库规模定一个基线(万级图库取 N=100 左右),再用离线评测画出"Recall@N 曲线",找到曲线开始变平的拐点,取拐点略偏上的值,既保证召回又不浪费算力。

2. 端侧算力的边界。 端侧做 embedding 和重排,好处是隐私数据不出设备,代价是算力受限。建议把 embedding 放到后台任务里增量计算并落盘,检索时只做向量读取和余弦计算;重排阶段只对 Top-N 候选做,避免全库扫描。实测万级图库下,Top-100 重排的耗时通常在几十毫秒量级,用户几乎无感。

3. 权重调参不要靠感觉。 时间衰减系数、标签 boost 幅度这些参数,最好用一批带标注的真实 query 做网格搜索或简单贝叶斯优化,以 Top-10 相关率为目标函数。手工调参容易过拟合到某几个 case,换个 query 分布就失效。

4. 冷启动与增量更新。 新导入的图片没有历史行为信号,标签也可能缺失,此时应适当提高语义分权重、降低业务信号权重,等积累了一定交互数据再逐步回归正常权重,否则新图会被老图长期压制。

5. 可观测性。 上线后要能回答"这次搜索为什么排成这样"。建议在调试模式下记录每条候选的向量分、时间衰减值、标签命中情况和最终得分,出问题时能快速定位是召回漏了还是重排压错了,而不是只能靠猜。

这些权衡没有标准答案,取决于你的图库规模、端侧机型和业务对延迟的容忍度。但把上面几个问题想清楚,方案就从"能跑"变成了"敢上线"。

Logo

讨论HarmonyOS开发技术,专注于API与组件、DevEco Studio、测试、元服务和应用上架分发等。

更多推荐