ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

HarmonyOS 7 + ArkTS-AI Kit:文搜图增量索引与结果一致性【鸿蒙心迹】

HarmonyOS 7 + ArkTS-AI Kit:文搜图增量索引与结果一致性【鸿蒙心迹】 一、搜索结果没错错的是它属于昨天PhotoIndex Lab的第一版已经能用文字搜索本地照片。输入“海边日落中的红色风筝”端侧语义检索会返回相似图片演示时很顺。真正放进相册整理流程后一个不太显眼的问题出现了用户删除照片、重新编辑或从其他设备同步新照片后搜索结果仍可能指向旧缩略图。算法给出的相似度没有错错的是索引版本落后于媒体库。这类问题在 Demo 阶段很容易被掩盖。开发者通常准备一批固定图片启动时完整建库之后只测查询速度。真实相册却一直变化新图加入、图片被删除、编辑后的资源 ID 保持不变但内容指纹改变后台扫描还可能因为应用切换而中断。如果每次都全量重建3000 张图片尚可接受三四万张时耗电、发热和等待时间都会变得明显如果只追加新图删除和修改又无法正确反映。本次优化没有改动语义模型而是在模型外增加一层增量索引协议。测试数据固定为索引版本IDX-20260930-1357-07增量批次BATCH-071本轮发现 37 个变化资源其中新增或修改 33 个、删除 4 个查询返回 12 张图片首条资源IMG-2048相似度0.912端到端耗时 86 ms。二、先给“图片变化”一个稳定定义媒体库通知只能告诉我们“可能变了”不能直接等价为“重新算向量”。例如用户只修改收藏标记语义内容没有变化相反编辑器可能覆盖原图而保留同一个业务资源 ID。我们给每张图片建立三元版本assetId modifiedTime contentFingerprint。资源 ID 用于定位修改时间用于快速筛选内容指纹负责最终确认。索引记录还要保存向量模型版本。文搜图能力升级或更换预处理方式后即使图片没变旧向量也不能与新查询向量混用。本项目把模型版本写成text-image-v3把预处理版本写成crop-center-2两者任一变化都会触发有控制的重建而不是悄悄合并两种向量空间。这段代码解决什么问题。它把媒体资源快照与现有索引做差生成新增、修改、删除三类确定操作避免把一次变更通知粗暴地变成全量重建。interfaceAssetSnapshot{assetId:stringmodifiedTime:numberfingerprint:string}interfaceIndexEntryMetaextendsAssetSnapshot{modelVersion:stringpreprocessVersion:string}interfaceIndexDelta{upserts:AssetSnapshot[]deletes:string[]}exportclassDeltaPlanner{plan(current:AssetSnapshot[],indexed:Mapstring,IndexEntryMeta):IndexDelta{constupserts:AssetSnapshot[][]constcurrentIdsnewSetstring()current.forEach(asset{currentIds.add(asset.assetId)constoldindexed.get(asset.assetId)constchanged!old||old.modifiedTime!asset.modifiedTime||old.fingerprint!asset.fingerprint||old.modelVersion!text-image-v3||old.preprocessVersion!crop-center-2if(changed)upserts.push(asset)})constdeletes:string[][]indexed.forEach((_,assetId){if(!currentIds.has(assetId))deletes.push(assetId)})return{upserts,deletes}}}这里没有仅靠modifiedTime。时间戳适合快速过滤却可能因为批量导入、文件恢复或编辑器行为产生碰撞指纹成本更高但可以只对候选变化资源计算。实际项目应把轻量元数据检查放在前面把像素级指纹放在后台任务中避免每次进入页面都读取原图。状态从SCANNING开始得到差异后进入EMBEDDING。若模型版本变化差异计划会把全部资源放入upserts但仍沿用分批提交和断点恢复不必把“全量重建”写成另一套逻辑。三、一次增量不是 37 次独立写入早期实现每生成一个向量就立刻写数据库。任务中途退出时索引已经处于半新半旧状态新图可以搜到删除图仍然存在版本号却被提前更新。下一次启动看见“版本一致”就不会再补做剩余工作。我们改成批次提交。BATCH-071有自己的暂存区33 个向量与 4 个删除标记全部完成后才原子切换当前索引版本。搜索线程始终读取上一个完整版本只有批次进入COMMITTED后新版本才对查询可见。这段代码解决什么问题。它用暂存批次和提交指针保证查询只能看到完整索引并让中断任务可以从最后完成的资源继续。typeBatchStateCREATED|EMBEDDING|MERGING|COMMITTED|FAILEDinterfaceIndexBatch{batchId:stringtargetVersion:stringstate:BatchState completed:numbertotal:number}exportclassIndexBatchRunner{asyncrun(batch:IndexBatch,delta:IndexDelta):Promisevoid{batch.stateEMBEDDINGfor(constassetofdelta.upserts){if(awaitStagingIndex.has(batch.batchId,asset.assetId))continueconstvectorawaitSemanticEncoder.encodeImage(asset.assetId)awaitStagingIndex.put(batch.batchId,asset.assetId,vector)batch.completedawaitBatchStore.save(batch)}batch.stateMERGINGawaitStagingIndex.markDeletes(batch.batchId,delta.deletes)awaitIndexStore.commit(batch.batchId,batch.targetVersion)batch.stateCOMMITTEDawaitBatchStore.save(batch)}}completed只是进度不是提交依据。真正决定版本切换的是暂存区完整性校验目标条目数、删除标记数、模型版本和校验和都正确才更新活动索引指针。若应用在第 21 个资源后进入后台下次恢复会跳过暂存区已经存在的 21 个向量继续完成剩余 12 个而不是从头计算。删除采用 tombstone而不是立刻物理移除。这样旧查询快照仍能完成读取新版本又不会返回被删除资源。后台压缩任务在没有读者持有旧版本时再回收向量页。4 个 tombstone 的空间很小却换来了清晰的并发边界。DevEco Studio 图中左侧工程目录区分DeltaPlanner、IndexBatchRunner和SearchRepository中间代码显示BATCH-071的提交逻辑右侧模拟器停在MERGING 37/37底部 HiLog 明确打印目标版本IDX-20260930-1357-07。红色标注只指向活动索引指针和 tombstone 数量。四、查询要把“版本”带到结果页增量索引完成后还有一个 UI 层问题。用户发起查询时活动版本是...-06结果返回前批次切换到...-07如果页面随后按新版本加载缩略图可能出现列表项与资源详情不一致。解决办法不是锁住整个索引而是让查询返回一个不可变快照 ID。这段代码解决什么问题。它把查询文本、活动索引版本和结果集绑定成一次快照页面翻页与打开详情时始终使用同一版本。interfaceSearchHit{assetId:stringscore:number}interfaceSearchSnapshot{queryId:stringindexVersion:stringhits:SearchHit[]elapsedMs:number}exportclassSearchRepository{asyncsearch(text:string):PromiseSearchSnapshot{conststartedDate.now()constversionawaitIndexStore.getActiveVersion()consttextVectorawaitSemanticEncoder.encodeText(text)consthitsawaitIndexStore.query(version,textVector,12)return{queryId:Q-1357-019,indexVersion:version,hits,elapsedMs:Date.now()-started}}}页面不再自己读取“当前版本”而是显示快照携带的IDX-20260930-1357-07。打开IMG-2048时也把queryIdQ-1357-019和索引版本传入详情服务。即使后台已经开始BATCH-072当前结果仍可解释、可复现。相似度0.912只能说明在当前模型和候选集合中的相对接近程度不应直接翻译成“91.2% 正确”。UI 用“相关度高”作为用户语言同时在诊断页保留原始分值既避免误导也方便工程调试。运行截图展示查询“海边日落中的红色风筝”共 12 个结果首条为IMG-2048相关分值0.912耗时 86 ms。状态栏、查询 ID 和索引版本都完整呈现红色箭头强调结果属于哪个快照。五、断点恢复比跑得快更重要增量任务适合放在设备空闲、充电或用户明确触发时运行但应用仍可能随时被切走。我们没有把“进入后台”视为错误而是保存批次游标当前批次、已完成条目、模型版本、暂存区校验和。恢复时先核对环境是否仍然兼容再继续任务。若模型文件在中断期间升级旧暂存向量不能继续合并批次会进入FAILED_MODEL_CHANGED清理暂存区后重新规划。若媒体库再次变化不必取消当前批次先提交BATCH-071随后把新变化归入BATCH-072避免一个永远追不上变化的长任务。本轮压力测试在第 21/37 项主动终止进程。再次启动后扫描耗时 18 ms恢复点命中剩余 16 项完成后进入MERGING最终校验和CHK-7A31没有重复编码已经完成的资源。诊断记录如下13:57:08.112 I PhotoIndex: batchBATCH-071 stateEMBEDDING progress21/37 13:57:12.450 I PhotoIndex: resumetrue checkpoint21 targetIDX-20260930-1357-07 13:57:13.806 I PhotoIndex: stateMERGING upserts33 tombstones4 13:57:13.892 I PhotoIndex: stateREADY checksumCHK-7A31诊断页显示批次恢复、33 个 upsert、4 个 tombstone 和最终校验和。红圈标在“21/37 恢复点”箭头指向READY说明这张图承担的是任务完整性解释而不是单纯展示漂亮界面。六、性能优化要看总账只看单次查询86 ms 已经足够流畅真正影响体验的是后台索引成本。全量重建测试需要读取 18.6 GB 原图数据设备明显发热增量方案只读取 33 个变化资源I/O 降到 428 MB。向量编码时间从 11 分 42 秒降到 14.8 秒代价是增加批次表、暂存区和版本回收逻辑。这笔复杂度值得付出是因为它解决的不只是速度。版本快照消除了半成品查询tombstone 解决删除一致性检查点解决生命周期中断模型版本字段解决升级兼容。性能只是结果之一数据可解释性才是结构收益。索引并非越新越好。如果用户正在连续搜索后台频繁切版本会让相邻两次查询结果跳动。项目把合并窗口设为 30 秒变化先聚合用户停止交互后再切换活动版本。这个值不是平台规则应结合图片变化频率和产品容忍度测量。七、能力边界与上线检查文搜图能力负责把文本语义与图像内容连接起来应用仍要处理媒体权限、资源可见范围、缩略图生命周期和隐私提示。索引只保存必要的向量与资源引用不保存用户查询原文的长期日志调试日志使用查询 ID不输出完整图片路径。端侧检索也不等于结果永远正确。抽象词、地域性表达、截图中的小字和高度相似的连拍都可能降低区分度。产品需要允许用户按时间、地点或相册继续过滤并提供“结果不相关”的反馈出口而不是把所有责任压给一个相似度分数。最终上线前我们固定检查五件事模型与预处理版本是否写入索引删除资源是否通过 tombstone 立即对新查询不可见批次中断是否能从检查点恢复查询结果是否携带不可变索引版本日志是否避免输出图片内容与完整路径。做到这些文搜图才从一个会演示的 AI 功能变成能长期维护的相册能力。参考资料Harmony Intelligence 开放能力与服务HarmonyOS 文搜图社区资料
RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进