〖共创稿事节〗HarmonyOS 7 新特性实战(07):用自然语言检索本地图鉴,管理图片索引与服务生命周期
图鉴用户不一定记得条目的名字却常能描述画面“长着翅膀的鸟”“背着龟壳的动物”“带翅膀的龙”。传统标题检索要求这些词已经存在于文本字段文搜图则提供另一条入口把图片加入索引再通过描述返回匹配图片及相似度。本例使用 Core Vision Kit 的 textSearchImage建立三张插画的小型图库。页面提供导入、搜索和移除索引三个动作结果展示图片及原始相似度。业务侧没有预设哪个查询必须命中哪一张图排序来自真实服务返回。图片语义与知识检索承担不同工作文搜图适合识别颜色、形状、物体及视觉关系未必掌握神话专名、原典出处和地域归属。用户搜索某个神兽专名时仍应优先考虑标题、别名和结构化知识检索用户描述画面时再使用视觉语义路径。实验中的图片是项目插画不是真实文物照片。固定样本能帮助比较相同查询下的结果变化但三张图不足以推导普遍的召回率。扩大实验时需要加入外观接近的干扰样本并保留人工判定的相关性标签。API 26 新增文搜图能力接口入口见 textSearchImage 文档。服务初始化、入库、检索、删除和释放形成完整调用链。用 scope 区分图库本例使用固定 scopeArticleLab7。scope 由字母和数字组成不把中文标题、路径或用户输入直接用作作用域名称。一张图片先复制到应用 filesDir再将沙箱路径交给 insertImage。返回结果中的 imagePath 也用于显示图片。因此图片索引的生命周期必须与源文件对应如果应用删除了文件却保留索引检索结果可能仍指向已经失效的资源。if(!awaittextSearchImage.init()){thrownewError(文搜图服务初始化返回 false);}if(!awaittextSearchImage.insertImage(path,ImageSearchService.scope)){thrownewError(图片入库失败);}Promise 正常返回不一定代表业务成功初始化和插入都要检查 boolean。批量导入逐张处理发生失败时报告已完成的位置不能把“发出了三次请求”当作“三张图全部入库”。这份最小实验采用稳定路径重用样本。生产图库还需要持久化文件摘要和索引版本针对新增、修改、删除做增量更新模型能力变更时按平台错误码处理索引重建避免每次进页面都全库重建。查询校验与结果展示页面限制输入长度服务层仍再次校验避免未来从语音、链接或其他入口调用时绕过约束。constnormalizedquery.trim();if(normalized.length0||normalized.length100){thrownewError(请输入 1–100 字的图片描述);}awaitthis.open();returntextSearchImage.search(normalized,ImageSearchService.scope,10);返回的相似度范围是 [-1, 1]值越大表示越相似。这个数值不应直接乘以 100 写成“识别准确率”也不能预设一个阈值适用于所有数据集。实验先显示原始分数便于观察同一组图在不同描述下的相对排序。结果为空是一种正常业务结果。此时可以提示用户改用更具体的颜色、动作或外形描述不要把零结果改写成默认推荐再称它是语义命中。三个动作共用一个串行入口导入与搜索同时发生可能让搜索读到只完成一部分的图库搜索还没结束就释放服务也可能打断请求。因此页面通过 busy 互斥执行一次动作按钮同步禁用。每次 run 记录开始时间得到结果后才更新状态。页面退出把 active 设为 false未完成请求仍会结束但不会将结果写回已退出的页面。服务释放遵循两个分支没有请求时立即释放正在运行时由 finally 等待请求结束再释放。这种处理不声称“取消了平台计算”。它取消的是结果对页面的提交并保证服务不会在请求中途被本页面提前销毁。若后续加入多页面共享检索应将服务所有权提升到应用层通过串行队列或引用计数统一管理。删除只作用于本实验图片本例调用 deleteImage(path, scope) 移除三张样本的索引保留源图片方便重新导入。不调用 clearData 清空整个数据库。for(constpathofpaths){if(!awaittextSearchImage.deleteImage(path,ImageSearchService.scope)){thrownewError(删除图片索引失败请检查日志后重试);}}clearData 的作用域比单条删除大适合按平台要求处理能力升级后的全库重建不能随意绑定到某个图库的“移除样本”按钮。多账号应用还需要稳定的账号标识映射与退出策略避免不同账号共用错误的索引集合。哪一种失败应该让用户重试结果页面行为索引含义输入校验失败提示修改描述尚未发起查询部分图片插入后失败报告失败位置允许检查后再导入不能称为完整图库查询返回空结果显示空状态不据此推断初始化失败服务返回错误保留错误信息不生成预置相似度补位删除中途失败提示检查后重试已删除项与剩余项可能并存部分成功尤其需要保留阶段信息。导入失败后立即查询可能得到仅覆盖已入库图片的结果删除失败后也不能将界面清空当作数据库已清空。最小 Demo 通过逐项 boolean 与异常反馈暴露失败生产版本再增加可持久化的逐项状态及恢复策略。一套可重复的查询实验先完成样本导入再依次输入“长着翅膀的鸟”“带有龟壳的动物”“带翅膀的龙”。每次记录查询、返回路径、排序、相似度和耗时。另加入无关描述观察服务对不相关查询的返回行为不要求它一定返回空数组。操作检查点首次导入三张图片文件存在逐张入库成功固定查询集显示真实结果与相似度没有硬编码答案空白输入服务调用前给出输入错误移除索引后检索本实验已删除记录不继续出现在返回结果再次导入图库可恢复观察重复导入是否产生重复记录请求中退出没有迟到页面更新服务在请求结束后释放能力升级错误保留错误码按官方约定安排重建正式比较查询质量时将图集、查询集和人工标注固定下来再计算 RecallK 或排序指标。冷启动服务初始化、特征入库和检索耗时分开统计只有三张图时的速度不能外推到上万张图库。接入神兽图鉴后文本检索与视觉检索可以并列提供。专名查询负责找到准确条目视觉描述负责帮助“不知道它叫什么”的读者发现候选最后仍由图鉴资料解释名称与出处。参考文搜图 API、Core Vision Kit API 26 新增接口。服务串行化与输入快照按钮禁用只保护当前页面服务复用时仍需要实例级串行队列。初始化、入库、删除、查询和释放共享队列任务失败原样交给调用方链尾恢复为可继续执行的 Promise。入队前复制路径避免等待期间被调用方修改。import{textSearchImage}fromkit.CoreVisionKit;exportclassImageSearchService{privateready:booleanfalse;privatequeue:PromisevoidPromise.resolve();staticreadonlyscope:stringArticleLab7;privateenqueueT(task:()PromiseT):PromiseT{constresult:PromiseTthis.queue.thenT(task);this.queueresult.thenvoid,void(():void{},():void{});returnresult;}privateasyncensureReady():Promisevoid{if(this.ready){return;}if(!awaittextSearchImage.init()){thrownewError(文搜图服务初始化返回 false);}this.readytrue;}asyncopen():Promisevoid{awaitthis.enqueuevoid(async():Promisevoid{awaitthis.ensureReady();});}asyncinsert(paths:string[]):Promisenumber{constsnapshot:string[]paths.slice();returnthis.enqueuenumber(async():Promisenumber{awaitthis.ensureReady();letinserted0;for(constpathofsnapshot){if(path.length128){thrownewError(图片沙箱路径超过 128 字符);}if(!awaittextSearchImage.insertImage(path,ImageSearchService.scope)){thrownewError(第${inserted1}张图片入库失败可重试导入);}inserted;}returninserted;});}asyncsearch(query:string):PromisetextSearchImage.ImageObject[]{constnormalizedquery.trim();if(normalized.length0||normalized.length100){thrownewError(请输入 1–100 字的图片描述);}returnthis.enqueuetextSearchImage.ImageObject[](async():PromisetextSearchImage.ImageObject[]{awaitthis.ensureReady();returntextSearchImage.search(normalized,ImageSearchService.scope,10);});}asyncremove(paths:string[]):Promisevoid{constsnapshot:string[]paths.slice();awaitthis.enqueuevoid(async():Promisevoid{awaitthis.ensureReady();for(constpathofsnapshot){if(!awaittextSearchImage.deleteImage(path,ImageSearchService.scope)){thrownewError(删除图片索引失败请检查日志后重试);}}});}asyncclose():Promisevoid{awaitthis.enqueuevoid(async():Promisevoid{if(!this.ready){return;}try{if(!awaittextSearchImage.release()){thrownewError(文搜图服务释放返回 false);}}finally{this.readyfalse;}});}}操作或边界应检查的结果并发入库平台最大并发为一查询中释放查询结束后再 release调用方修改 paths已入队任务使用原快照三张样本的真机查询与索引恢复2026-09-20在 HBN-AL80API 26运行随包的 bird、turtle、dragon 三张插画导入完成后回读“已导入 3 张图片 · 1802 ms”。使用固定图库依次输入三条视觉描述页面得到以下实际结果查询结果数页面显示的相似度查询耗时长着翅膀的鸟0无19 ms带有龟壳的动物10.332923 ms带翅膀的龙10.354419 ms鸟类描述首次返回空结果页面没有插入默认候选。三张样本和三条描述只用于观察调用结果不用于推导普遍召回率入库成功也不保证某个描述必然命中。随后移除本实验三张图片的索引页面回读完成耗时 15 ms。再次查询“带翅膀的龙”返回 0 个结果、16 ms重新导入三张图片耗时 590 ms再查同一描述恢复为 1 个结果、14 ms显示相似度 0.3481。重新入库前后的分数不同因此不要把某次分数作为固定业务断言。2026-09-22 在同一 HBN-AL80 真机重新执行导入页面回读“已导入 3 张图片 · 486 ms”。随后立即查询默认描述“长着翅膀的鸟”本次回读“返回 0 个结果 · 18 ms”保留了真实空结果没有用预置图片补位。当前截图用于记录这次重新导入和查询状态