前言
假设你在做一个旅行相册应用。用户拍了几千张照片,文件名全是 IMG_20260924_183207.jpg,按时间排序帮不上忙,标签系统又没人愿意手动打。
用户实际会怎么找照片?他会说:"把海边日落那张给我看看。"
传统两条路都不划算:云端多模态检索要上传用户全部相册,隐私和带宽都是问题;本地自建向量库要引模型、做推理、管索引,工程量劝退。
HarmonyOS 7(API 26)在 Core Vision Kit 里新增了这个能力:文搜图(textSearchImage)。把图片特征存进本地索引,然后用一句自然语言把匹配的图片捞回来,全程不出设备。
这篇先讲怎么用,再讲一个更重要的事:它到底按什么在匹配——这个直接决定你的产品文案怎么写。
能用在哪些场景
- 旅行、家庭相册的自然语言检索(最贴合)
- 自媒体、设计类的本地素材库
- 工程、家装类的参考图归档
- 任何"图片多、命名烂、标签没人打"的本地相册
需要注意的是,它不是通用图片搜索引擎。第二节末尾会看到它明确的脾气边界。
接口与约束
调用面六个方法,都是 textSearchImage 命名空间下的静态函数,不需要 create/destroy 分析器:
import { textSearchImage } from '@kit.CoreVisionKit';
init(): Promise<boolean>
insertImage(imagePath: string, scope: string): Promise<boolean>
search(query: string, scope: string, topKey?: number): Promise<ImageObject[]>
deleteImage(imagePath: string, scope: string): Promise<boolean>
clearData(): Promise<boolean>
release(): Promise<boolean>返回的 ImageObject 有三个字段:imagePath、scope、similarity(取值范围 [-1, 1],越大越相似)。
- 文搜图(Text-to-Image Search):用文本作为查询条件,从已建立索引的图片集合中检索匹配图像。
- scope:图片索引的逻辑分区,用来隔离不同图库,类似一张表里的命名空间。
- 端侧索引:图片特征提取与检索都在设备本地完成,不上传原图。
参数约束(来自 SDK 声明,实测有效):
| 参数 | 约束 |
|---|---|
imagePath |
应用沙箱路径,1–128 字符 |
scope |
1–32 字符,只允许字母或数字 |
query |
1–100 字符,允许中文 |
topKey |
整数 0–100,默认 100 |
三个错误码值得提前处理:
1013100001Invalid image path —— 传了媒体库 URI 而不是沙箱路径1013100002Service abnormal —— 服务异常,需要降级1013100003The capability has been updated —— 模型能力升级后旧索引失效,官方要求先clearData()再重新insertImage()
最后这个坑比较隐蔽:用户升级系统后你的索引可能整体作废。不处理 1013100003,表现就是"搜索突然全空",而且界面上没有任何提示。
开发步骤
第一步:把图片落到沙箱
这是最容易卡住的地方。insertImage 只吃沙箱路径,你从相册选图拿到的 file://media/Photo/... URI 直接传进去就是 1013100001。
所以流程必须是:取图 → 拷进 context.filesDir → 用沙箱路径建索引。
async stageFiles(ctx: Context, names: string[]): Promise<string[]> {
const paths: string[] = [];
for (let i = 0; i < names.length; i++) {
const bytes: Uint8Array = await ctx.resourceManager.getRawFileContent(names[i]);
const dest: string = `${ctx.filesDir}/${names[i]}`;
const file = fileIo.openSync(dest,
fileIo.OpenMode.READ_WRITE | fileIo.OpenMode.CREATE | fileIo.OpenMode.TRUNC);
fileIo.writeSync(file.fd,
bytes.buffer.slice(bytes.byteOffset, bytes.byteOffset + bytes.byteLength));
fileIo.closeSync(file);
paths.push(dest);
}
return paths;
}从相册导入同理:fileIo.open(uri) 读出内容,写进 filesDir,再拿这个路径建索引。
还有一点:scope 只能用字母和数字。我一开始想用 trip_2026,下划线不在允许字符集里,最后用了 triplens2026。scope 是索引的逻辑分区,多用户、多图库都靠它隔离,别浪费它。
第二步:建索引,并把耗时暴露出来
async build(paths: string[], scope: string): Promise<number> {
await this.init();
const start: number = Date.now();
for (let i = 0; i < paths.length; i++) {
try {
await textSearchImage.insertImage(paths[i], scope);
} catch (err) {
const e = err as BusinessError;
if (e.code === 1013100003) {
await this.reset(); // 模型升级,旧索引作废
return this.build(paths, scope); // 重建一次
}
throw new TsError(e.code, `建索引失败(${e.code}):${e.message}`);
}
}
return Date.now() - start;
}实测单张 640×480 图片的 insertImage 约 380 ms,6 张约 2.3 秒。
这个数字决定了产品形态:建索引必须是可后台执行、可中断、有进度的任务,不能放在首屏等待里。
第三步:检索
async search(query: string, scope: string, top: number): Promise<Hit[]> {
await this.init();
const list: textSearchImage.ImageObject[] = await textSearchImage.search(query, scope, top);
// 转成 UI 需要的结构:文件名 + similarity
}检索很快,实测 13–36 ms,用户体感是即时的。
第四步:生命周期
init() 自己保证幂等;release() 放 aboutToDisappear。因为 insertImage 是同步等返回的,索引期间不要并发调用 search。
真机运行效果
测试机:HUAWEI MatePad Pro(MRDI-W00),HarmonyOS 7.0.0.107,API 26。图库是 6 张旅行照:海边日落、雪山湖泊、都市夜景、古镇小巷、森林溪流、沙漠星空。
精确命中一张:

搜「夜景」命中两张(都市夜景 + 沙漠星空):

搜「风景」返回空:

单特征词「城市夜景」:

它到底按什么匹配
我用 9 个查询词做了完整对照,结果如下:
| 查询词 | 命中 | 相似度 | 检索耗时 | 解读 |
|---|---|---|---|---|
| 海边日落 | 1 张(海边) | 33.7% | 36 ms | 精确 |
| 雪山湖泊 | 1 张(雪山) | 35.9% | 21 ms | 精确 |
| 城市夜景 | 1 张(都市夜景) | 35.3% | 17 ms | 精确 |
| 沙漠星空 | 1 张(沙漠星空) | 34.8% | 19 ms | 精确 |
| 夜景 | 2 张(都市夜景 32.9% / 沙漠星空 31.7%) | — | 17 ms | 跨图共同特征 |
| 雪 | 1 张(雪山) | 31.2% | — | 单字也能命中 |
| 一张猫 | 0 张 | — | 13 ms | 图库无猫,正确拒绝 |
| 风景 | 0 张 | — | 17 ms | 6 张全是风景,却返回 0 |
| 海边的雪山 | 0 张 | — | — | 跨图元素拼接,正确拒绝 |
两个结论,直接决定产品怎么写。
结论一:它匹配"画面里有什么",不匹配类别。
「风景」返回 0 是最有信息量的一条——6 张图全是风景,但"风景"不描述任何画面内容,模型无从对齐。而「夜景」返回 2 张,因为都市夜景和沙漠星空在画面上共享"夜"这个可视特征。
所以搜索框的引导文案不能写"输入图片类别",要写**"描述照片里的东西"**。示例词要具体到画面元素:海边日落、下雪的山、夜晚的霓虹灯,而不是 风景、旅行、好看。
结论二:相似度绝对值不能直接给用户看。
精确命中的分数也只有 31%–36%,因为返回值落在 [-1,1] 全区间,实际分布被压缩在 0.3 附近。如果 UI 上显示"匹配度 33.7%",用户会认为结果很差。
建议做法:只做相对排序,不做绝对展示;一定要显示,就按本次结果集归一化(最高分记 100),或者干脆显示名次而不是百分比。
上生产前还要补的三件事
- 索引持久化与增量。
insertImage是往系统数据库里写,但你需要自己记住"哪些文件已经建过",否则每次冷启重建,2.3 秒/6 张会放大成几百秒。 - 删除同步。 用户从你的应用里删照片时,必须同步
deleteImage,否则结果里会返回已经不存在的文件。 1013100003的自动恢复。 别把它当普通错误抛给用户,检测到就静默clearData()+ 重建,并在 UI 上提示"正在重新整理相册索引"。
总结
什么场景值得用它:图片多、命名烂、标签没人打的本地相册——旅行、家庭、自媒体素材、工程参考图都算,全程端侧跑完,原图不出设备。
不适合:需要按"类别、事件"检索的场景。它认的是画面里有什么,不认"风景""旅行"这种抽象标签,硬套只会得到一片空白。
💡 一句话:搜索框要引导用户"描述照片里的东西",别让他选分类;相似度只做相对排序。
参考文献
- Core Vision Kit 开发指南 · 通过文本搜索图片:https://developer.huawei.com/consumer/cn/doc/harmonyos-guides/core-vision-text-search-image
- Core Vision Kit API 参考 · textSearchImage(通过文本搜索图片):https://developer.huawei.com/consumer/cn/doc/harmonyos-references/core-vision-text-search-image-api
- Core Vision Kit 错误码:https://developer.huawei.com/consumer/cn/doc/harmonyos-references/errorcode-core-vision
- HarmonyOS 7(API 26) 官方新能力解读 & 开发者实战开发案例合集:https://developer.huawei.com/consumer/cn/forum/topic/0208224160644765091