【共创稿事节】HarmonyOS 7.0 文本搜图实战:零标注语义检索,随手记图片“一句话找图“
HarmonyOS 7.0 文本搜图实战:零标注语义检索,随手记图片"一句话找图"
本文是鸿蒙 AI 视觉系列第二篇(前作:图像超分),基于 HarmonyOS 7.0(API 26)CoreVisionKit 的文本搜索图片能力(
textSearchImage),结合真实项目 的完整落地实践,讲清楚文本搜图"是什么、怎么用、怎么用好"。所有 API 用法均来自华为官方文档,工程实践部分全部来自项目已验证的代码。
一、什么是文本搜索图片,以及本项目的使用场景
1.1 官方定义
按官方文档的说法:文本搜索图片提供基于文本语意的图片检索能力。用户通过输入文本语意,从已插入的图片库中搜索匹配的图像结果,返回图片沙箱路径、作用域和相似度。 HarmonyOS 从 API 26.0.0 版本开始提供该能力(phone / 2in1 / tablet 均支持),官方给出的典型场景是图片检索、相册管理、内容推荐。
三个关键事实,决定了它的接入方式与前一篇的图像超分完全不同:
- 它管的是"特征库"而非像素。
insertImage只把图片特征提取进系统数据库,不做像素处理;图片文件本身的增删仍由业务负责。API 面就是一套"特征库增删查":init / insertImage / search / deleteImage / clearData / release。 - 它是系统服务级单例。不像超分需要
create()/destroy()配对管理分析器实例,textSearchImage初始化一次即可全程使用。 - 特征库是个黑盒。业务无法查询库里有什么,删除、错位、能力更新都不会主动通知——这正是本项目第三章"记账 + 对账"体系要解决的问题。
1.2 通用使用场景
- 图片检索:自然语言搜图——“海边日落”“停车罚单”“猫咪趴在键盘上”,无需人工打标;
- 相册管理:给本地图片库加语义搜索入口,自动归类检索;
- 内容推荐:按用户输入语意推荐相关图片素材。
与两条"看起来很像"的路线对比,它的价值更清楚:
| 方案 | 检索依据 | 前置成本 | 局限 |
|---|---|---|---|
| 文件名/标签检索 | 人工打的标签 | 每张图都要打标 | 漏标即搜不到 |
| OCR 文字检索 | 图中的文字 | 抽取文本入索引 | 只搜得到"有字的图" |
| 文本搜索图片 | 画面内容语义 | 零标注,入库即索引 | 搜"画了什么",不是"写了什么" |
1.3 本项目的用法:图片附件零标注检索,文字 + 图片双通道搜索
本项目的随手记支持为每条日志挂载图片附件(日报实拍、扫描文档、收据凭证),图片按 filesDir/worklog_images/{logId}/ 沙箱目录归档。日志多了之后,"上周那张停车费收据的截图记在哪条日志里?"只能靠回忆翻找。
接入文本搜索图片后的完整链路:
① 索引侧:图片落盘即入特征库(零标注的源头)

工作日志保存/新建/删图时在图片文件增删的同一事务点调用TextSearchImageUtil 的 indexWorkLogImage / removeWorkLogImageIndex——打标成本为零,语义特征由系统模型自动提取。
② 搜索侧:自然语言命中图片,反解日志直达


搜索框输入"猫",searchLogImages 命中图片后按路径反解出所属日志,聚合展示"日志标题 + 命中缩略图",点击直达详情如上图。
③ 一致性侧:删图即删索引,错位自动对账
日志删除、图片移除、备份恢复、模型能力更新,每一条会破坏"文件 ↔ 特征库"一致性的路径都有对应清理或重建动作(详见第三章)。
三段配合的结果:检索质量由系统语义模型保证,数据一致性由业务工程保证——这是本项目用文本搜图的核心思路。
二、鸿蒙文本搜索图片 API 介绍与基本使用
2.1 API 定位
文本搜索图片能力由 CoreVisionKit 提供,模块为 textSearchImage,仅支持 Stage 模型,系统能力为 SystemCapability.AI.Vision.VisionBase,phone / 2in1 / tablet 均从 API 26.0.0 起支持。
导入方式:
import { textSearchImage } from '@kit.CoreVisionKit';
2.2 核心方法与返回结构
textSearchImage 共六个方法:
| 方法 | 签名 | 说明 |
|---|---|---|
init | init(): Promise<boolean> | 初始化服务,true/false |
insertImage | insertImage(imagePath: string, scope: string): Promise<boolean> | 插入图片特征到数据库 |
search | search(query: string, scope: string, topKey?: number): Promise<Array<ImageObject>> | 文本检索,返回命中列表 |
deleteImage | deleteImage(imagePath: string, scope: string): Promise<boolean> | 删除单条图片记录 |
clearData | clearData(): Promise<boolean> | 清空数据库(模型能力更新后必须执行) |
release | release(): Promise<boolean> | 释放服务 |
返回对象 ImageObject:
| 名称 | 类型 | 说明 |
|---|---|---|
imagePath | string | 图片沙箱路径 |
scope | string | 图片作用域(插入时写入的分组标识) |
similarity | number | 图文相似度,取值 [-1, 1],越大越相似 |
2.3 参数与输入约束(官方硬性规定)
| 参数 | 约束 |
|---|---|
imagePath | 沙箱绝对路径(不带 file://),长度 [1, 128] |
scope | 作用域,长度 [1, 32],仅字母或数字 |
query | 查询词长度 [1, 100],不支持纯数字和纯字母,支持中文 |
topKey | 返回数量上限,[0, 100] 整数,默认 100 |
输入图像尺寸约束(Core Vision Kit 介绍 · 约束与限制):成像质量合适的前提下,100px < 宽度/高度 < 10000px,高宽比例建议 10:1 以下,接近手机屏幕高宽比例为宜。
错误码:
| 错误码 | 含义 |
|---|---|
1013100001 | Invalid image path(路径非法) |
1013100002 | Service abnormal(服务异常) |
1013100003 | 模型能力已更新——旧特征全部失效,须 clearData 后重新插入 |
2.4 基本使用:官方六步全流程
以下开发步骤与示例代码均复用自官方文档《通过文本搜索图片》。
第一步:初始化。
import { textSearchImage } from '@kit.CoreVisionKit';
import { hilog } from '@kit.PerformanceAnalysisKit';
async function initTextSearchImage() {
try {
const initResult = await textSearchImage.init();
hilog.info(0x0000, 'textSearchImageSample', `Text search image initialization result:${initResult}`);
if (initResult) {
hilog.info(0x0000, 'textSearchImageSample', 'Text search image initialized successfully');
} else {
hilog.error(0x0000, 'textSearchImageSample', 'Failed to initialize text search image');
}
} catch (error) {
hilog.error(0x0000, 'textSearchImageSample', `Init failed. Code: ${error.code}, message: ${error.message}`);
}
}
第二步:插入图片特征。 官方示例通过 context.getApplicationContext().filesDir 拿应用沙箱目录拼接绝对路径:
import { textSearchImage } from '@kit.CoreVisionKit';
import { hilog } from '@kit.PerformanceAnalysisKit';
import { BusinessError } from '@kit.BasicServicesKit';
import { common } from '@kit.AbilityKit';
async function insertImage(context: common.UIAbilityContext) {
// 正确获取应用级别的沙箱路径
const applicationContext = context.getApplicationContext();
const filesDir = applicationContext.filesDir;
// 请确保该路径下确实存在对应的图片文件
const imagePath = filesDir + '/haps/entry/files/image.jpg';
const scope = 'default_scope';
try {
const result = await textSearchImage.insertImage(imagePath, scope);
hilog.info(0x0000, 'textSearchImageSample', `Insert image result: ${result}`);
} catch (error) {
const err = error as BusinessError;
hilog.error(0x0000, 'textSearchImageSample', `Insert image failed. Code: ${err.code}, message: ${err.message}`);
}
}
第三步:文本搜索。 topKey 限制返回数量,similarity 越大越相似:
async function searchImages() {
const query = 'landscape';
const scope = 'default_scope';
const topKey = 100;
try {
const results = await textSearchImage.search(query, scope, topKey);
hilog.info(0x0000, 'textSearchImageSample', `Search results count: ${results.length}`);
results.forEach((imageObject, index) => {
hilog.info(0x0000, 'textSearchImageSample', `Result ${index}: imagePath=${imageObject.imagePath}, similarity=${imageObject.similarity}`);
});
} catch (error) {
const err = error as BusinessError;
hilog.error(0x0000, 'textSearchImageSample', `Search failed. Code: ${err.code}, message: ${err.message}`);
}
}
第四步:删除单张。 API 形态与 insertImage 对称:
const delResult = await textSearchImage.deleteImage(imagePath, scope);
第五步:清空数据库。 模型能力更新后(错误码 1013100003)旧特征全部失效,必须清库重建,否则搜索/插入持续报错:
const clearResult = await textSearchImage.clearData();
第六步:释放服务。
const releaseResult = await textSearchImage.release();
官方心智模型一句话:
init一次 → 图片生命周期事件驱动insertImage/deleteImage→ 业务入口search→ 异常或能力更新时clearData重建。release 在服务级单例场景下可按需取舍(本项目选择不调,见 3.2)。
2.5 配套能力:scope 分组与路径转换
scope 是特征库逻辑分组的唯一手段:不同业务域(日志附件/卡面图/文档扫描件)用不同 scope 隔离,搜索时按 scope 收窄,避免跨域噪声。本项目用 worklog 一个作用域承载全部日志图片。
另注意 imagePath 要求沙箱绝对路径,而业务侧通常持有 file:// URI——两者转换是接入的固定配套(本项目封装为 tsiAbsPath,双向兼容两种形态)。
三、结合本项目的最佳实践
官方示例是"最短可用路径",生产环境还差好几层防护。本项目把全部系统能力收口在 TextSearchImageUtil,业务层只看到三个动词:存图时 indexWorkLogImage、删图时 removeWorkLogImageIndex、搜索时 searchLogImages。以下是踩坑后沉淀的六个工程要点。
3.1 兼容性与版本控制:双门控 + 静默降级
与图像超分同款探测。文本搜图是 API 26 新增能力,低版本设备上模块未定义,直接访问属性就会 crash,所以所有对外函数第一行都是:
/** 设备是否支持文本搜索图片(API 版本 + 系统能力双门控,与超分同款) */
export function isTsiSupported(): boolean {
return getSdkApiVersion() >= 26 && canIUse('SystemCapability.AI.Vision.VisionBase');
}
与超分的差异在降级策略:超分探测失败是"隐藏按钮"(UI 动作前置),文本搜图则是"函数内部静默降级"——索引函数直接 return、搜索函数返回空数组,业务层与 UI 层完全不用写版本分支,低版本设备上搜索框照常工作(只搜文本通道),图片语义通道静默缺席。差异的根源:超分是用户主动触发的显式功能,搜图是搜索链路里的隐式增强。
3.2 生命周期:服务级单例,懒 init 一次、不 release
textSearchImage 是系统服务级单例(对比超分 analyzer 实例的 create/destroy 配对),项目实测后采取的策略:
/** 服务初始化 promise(成功后缓存复用;失败置空以便下次重试) */
let initPromise: Promise<boolean> | null = null;
function ensureInit(): Promise<boolean> {
if (!initPromise) {
initPromise = textSearchImage.init()
.then((ok: boolean): boolean => {
if (!ok) {
initPromise = null; // init 返回 false 时置空,允许下次重试
}
return ok;
})
.catch((e: object): boolean => {
initPromise = null;
return false;
});
}
return initPromise;
}
三个细节:
- 懒初始化:首次真正用到才 init,不用能力就零开销;
- 失败可重试:init 返回 false 或抛错时把 promise 置空,下次调用重新 init,而不是把失败结果缓存到永远;
- 存续期内不调用 release:所有调用都是 fire-and-forget,无法安全配对 release,引用计数容易漏,应用退出交给系统回收。
3.3 并发治理:FIFO 串行队列
官方文档约束之外,实测发现同一特性不支持进程内并发调用(并发时返回"系统繁忙"类错误)。而业务的典型场景恰恰是并发的:用户删 3 张图的同时搜索框防抖触发。解法是一个 12 行的 FIFO 队列:
let opQueue: Promise<void> = Promise.resolve();
/** 串行执行一次系统调用(FIFO 队列;前序成功/失败均继续) */
function enqueue<T>(op: () => Promise<T>): Promise<T> {
const run: Promise<T> = opQueue.then(op, op);
opQueue = run.then((): void => {}, (): void => {});
return run;
}
insertImage / deleteImage / search / clearData 全部经 enqueue 入队:前序失败不阻断后续(then(op, op) 两个分支都继续执行),调用方拿到的 promise 值不变,但底层严格串行。
3.4 数据一致性:registry 记账 + 双向对账 + 备份重建
系统特征库是独立于业务数据的黑盒(无法查询),删了图忘了删索引就会出现"搜得到、点进去 404"的幽灵结果。项目的三层防线:
① 本地记账:preferences 持久化"已索引路径集合"(按 scope 分组的 registry),插入成功才记账、删除先删索引再删记账——记账与系统库的增删严格同事务点。
② 业务钩子全覆盖:删单图(LogEditPage)、删整条日志(WorkLogDataManager)、清空重置(WorkLogDataManager)三条路径都同步调用索引清理,不留死角。
③ 启动延迟增量对账:前两层仍挡不住"历史漏删""进程中途被杀"等错位,启动后扫描目录与记账的双向差集兜底:
// 图片搜索索引后台补索引(API 26+ 且系统能力满足时):延迟避开启动高峰,
// 扫 worklog_images 与本地记账差集增量插入 + 清理失效记录(目录扫描不依赖 DataManager 加载)
if (isTsiSupported()) {
setTimeout((): void => {
void syncIndexInBackground(this.context);
}, 5000);
}
对账主体(syncIndexInBackground)扫描 worklog_images 目录与 registry 做双向差集:
- 目录有、记账无 → 补索引(insertImage + 记账);
- 记账有、目录无 → 删索引(deleteImage + 删记账)。
幂等、可重跑、防重入(syncingPromise 单飞:进行中再调用直接复用同一 promise)。
④ 备份恢复后全量重建:registry 不参与备份,恢复后与本机系统库必然错位——先 clearTsiIndex()(会先等待进行中的对账完成,防止旧记账写回;然后清系统库 + 清记账),延迟 3 秒后 syncIndexInBackground 按恢复后的目录全量重建。
3.5 路径设计:把业务 id 编码进沙箱路径
系统库只认识"沙箱路径 + 相似度",搜出来的结果要回到业务(“哪条日志”)就需要映射。本项目图片按 filesDir/worklog_images/{logId}/xxx.jpg 归档——这不是随手起的名,而是搜索结果反解业务键的关键:
/** 从图片路径反解日志 id:worklog_images/{logId}/xxx;解析失败返回空串 */
export function tsiLogIdFromPath(path: string): string {
const abs = tsiAbsPath(path);
const m = abs.match(/worklog_images\/([^/]+)\//);
return m ? m[1] : '';
}
如果业务 id 不在路径里,就得自己维护一张 path → logId 映射表——又一个要和对账系统保持一致的状态源。把 id 编码进目录名,search 返回的 imagePath 直接正则反解,零额外存储。配套的 tsiAbsPath 统一处理 file:// 前缀与绝对路径的双向转换;超长路径(顶到 128 字符上限的)直接跳过索引且对账不补,避免无意义重试。
3.6 搜索侧工程化:防抖、防过期、去重、自愈
查询词预过滤:util 层对系统不支持的查询词(纯数字/纯字母)先行校验返回空,把"系统报错"转化为"返回空";命中结果按 similarity 降序排序后返回。
UI 层四层保护:
/** 图片语义搜索(关键词非空时并行执行;seq 防过期结果覆盖;按 logId 去重取最高相似度) */
private async runImageSearch(): Promise<void> {
const seq = ++this.imageSearchSeq; // ① 序号防过期
const kw = this.searchKeyword.trim();
if (kw.length === 0) { this.imageHits = []; return; }
const hits: LogImageHit[] = await searchLogImages(getContext(this), kw, 50);
if (seq !== this.imageSearchSeq) {
return; // ② 期间有新搜索,丢弃本批结果
}
const out: LogImageHitDisplay[] = [];
const seen: Record<string, LogImageHit> = {};
for (let i = 0; i < hits.length; i++) {
const h = hits[i];
// hits 已按 similarity 降序:首次出现的 logId 即该日志最高相似度命中
if (seen[h.logId]) { continue; } // ③ 按日志去重,一条日志只露一张图
seen[h.logId] = h;
const entry = this.manager.getLogByIdSnapshot(h.logId);
if (!entry) {
continue; // 日志已删(索引清理有时差),跳过 // ④ 业务侧兜底过滤
}
out.push({ logId: h.logId, fileUri: h.fileUri, title: entry.title, date: entry.date });
}
this.imageHits = out;
}
外层与文本搜索共用 300ms 防抖;searchLogImages 的 topKey 取 50 而非默认 100,控制单次搜索的渲染量。
模型能力更新自愈:insert 或 search 捕获错误码 1013100003(模型能力更新,旧特征全部失效)时,自动 clearData + 清记账:插入路径就地重试当前图片(其余图片靠对账差集自动重插),搜索路径触发 syncIndexInBackground 后台全量重建——全程幂等,无需用户干预。
四、注意事项
结合官方文档与本项目踩坑,文本搜图落地时重点盯住以下 11 条,前三条是硬性红线:
- API 26+ 才有此能力。低版本设备上
textSearchImage模块未定义,任何属性访问都会 crash——先getSdkApiVersion()再碰模块,这是生命线。 - query 不支持纯数字和纯字母(须含中文或数字字母符号拼接)。纯 “123”、“abc” 直接搜索会报错——搜索入口要正则预过滤(必须命中
/[^0-9a-zA-Z]/),把不支持转化为"返回空"而非异常。 - 同一特性不支持同进程并发调用。并发插入 + 搜索会返回"系统繁忙"类错误——所有系统调用走串行队列(3.3),这 12 行代码不能省。
- 图片尺寸约束:100px < 宽/高 < 10000px,宽高比建议 10:1 以下。超小缩略图、超长截图(聊天长图)特征质量差甚至失败——本项目压缩存图统一到最长边 1440px,天然落在舒适区。
- imagePath 是沙箱绝对路径且 ≤128 字符,不带
file://前缀。业务侧若存 URI,入库前必须转换;深层嵌套目录 + 长文件名很容易顶到 128 上限,路径设计要短(本项目超长路径直接跳过索引,避免无意义重试)。 - scope 仅字母数字、≤32 字符,是特征库逻辑分组的唯一手段——不同业务域用不同 scope 隔离,搜索时按 scope 收窄,避免跨域噪声。
- similarity ∈ [-1, 1] 没有官方"及格线"。不同关键词的相似度分布差异大,硬编码阈值容易"要么全出要么全无"。项目不设截断:按相似度降序 + 按 logId 去重后全量交给 UI,由用户自己判断相关性。
- 特征库与业务数据必须同步增删。删图、删日志时漏删索引 → 幽灵结果;只删索引漏删文件 → 对账时又补回来。原则:文件删除与索引删除在同一事务点执行,再用对账兜底。
- 备份/迁移不覆盖系统特征库。恢复备份后索引必然错位,必须 clearData + 全量重建;同理"克隆到新设备"场景也要走重建流程。
- insertImage 的结果要做记账校验。返回 false 或抛错时系统库可能没收到特征,但调用方无从查询——项目用"插入成功才记账 + 记账缺失延迟 3 秒重试 + 启动对账兜底"三层策略,避免"当次搜不到、重启才可见"。
- init 与首次 insert 有特征提取耗时。别在启动关键路径同步等待:项目启动延迟 5 秒才对账、插入失败延迟 3 秒重试,全部 fire-and-forget,宁可晚一点可搜,不卡启动。
五、总结
HarmonyOS 7.0(API 26)的文本搜索图片给了应用一套"图片零标注检索"的开箱能力:入库即索引,一句话找图。本项目随手记的完整链路——“落盘即索引(业务钩子同步增删)→ 一句话搜图(语义命中反解日志直达)→ 错位自愈(对账 + 重建)”——验证了把系统级搜图嵌进真实业务检索链路完全可行。
API 层面的心智模型一句话:init 一次 → 生命周期事件驱动 insertImage / deleteImage → 业务入口 search → 能力更新时 clearData 重建——六方法三注意(版本拦截、参数约束、并发串行)。
而真正决定落地质量的,是官方示例之外的工程化功夫:双门控让老设备无感降级,FIFO 串行队列挡住并发报错,"记账 + 对账 + 重建"三道防线守住黑盒一致性,业务 id 编码进路径免掉映射表,防抖 + seq + 去重管住搜索体验。API 决定能不能用,工程实践决定好不好用——与图像超分一样,两者齐备,文本搜图才能从演示走向生产。
参考文档
- 通过文本搜索图片(开发指南):https://developer.huawei.com/consumer/cn/doc/harmonyos-guides/core-vision-text-search-image
- textSearchImage(通过文本搜索图片)API 参考: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-guides/core-vision-introduction
- 图像超分实战(系列前作):./harmonyos7-image-super-resolution.md
更多推荐



所有评论(0)