当前位置: 首页 > news >正文

HarmonyOS RDB 模糊搜索变慢怎么办:中式美食菜名、别名和食材关键词怎么分层查询

本地菜谱越来越多以后,搜索响应慢,结果排序还经常把弱相关菜排前面。 中式美食里同一个菜谱会从搜索、分类、收藏、推荐和购物清单多个入口出现,状态一旦没拆清楚,用户看到的就是点了没反应、返回位置丢了、统计不准或者卡片状态串到别的菜上。

项目说明
应用名称中式美食
这篇解决什么本地菜谱越来越多以后,搜索响应慢,结果排序还经常把弱相关菜排前面。
谁会搜这篇做 HarmonyOS 本地搜索、菜谱库、食材库的开发者会搜这篇
读完能解决什么明确状态归属、Repository 边界、ArkUI/ArkTS 代码拆法和回归验收
当前项目证据中式美食已经有搜索、分类、详情、收藏、购物清单和最近浏览,多入口状态会互相影响

先把问题说清楚

这个问题要拆到页面、状态和数据层一起看。只改页面文案解决不了根因,因为页面看到的是结果,真正决定结果的是 ViewModel 怎么保存状态、Repository 怎么查数据、路由或存储层怎么留下稳定事实。

章节重点
问题从哪里来先把用户能看到的异常讲明白
状态怎么拆页面状态、查询状态、路由状态不要混用
Repository 怎么接数据层只负责事实,不负责 UI 文案
ArkUI 怎么写组件只做展示和事件出口,不偷改全局状态
怎么验收用连续点击、刷新、返回、筛选这些动作压一遍

验证环境和文件边界

项目说明
DevEco Studio6.0.0 Release 系列
HarmonyOS SDKAPI 12
应用模型Stage 模型
语言和 UIArkTS、ArkUI 声明式页面
数据 API@ohos.data.relationalStoreRdbStoreRdbPredicatesexecuteSqlquery
相关模块RecipeRepositoryIngredientAliasRepositorySearchViewModelRDBSchema
层级负责什么不应该做什么
ArkUI 页面展示状态、收集点击、给用户反馈不直接拼查询、不偷偷改仓储
ViewModel保存当前页面状态,调用 Repository,屏蔽旧结果不关心 UI 细节
Repository查数据、写数据、做归一化和事务边界不决定页面怎么排版
路由或存储提供稳定事实不承担临时交互判断

RDB 表和索引先定下来

模糊搜索变慢,很多时候不是因为 RDB 不能用,而是表和索引一开始就没给搜索留位置。中式美食这里把菜谱主表、别名表和食材索引表拆开,别把所有词都塞进一个description字段里。

importrelationalStorefrom'@ohos.data.relationalStore'constCREATE_RECIPE_INDEX=`CREATE TABLE IF NOT EXISTS recipe_index ( id TEXT PRIMARY KEY, recipe_id TEXT NOT NULL, normalized_name TEXT NOT NULL, category_key TEXT NOT NULL, rating REAL DEFAULT 0, updated_at INTEGER NOT NULL )`constCREATE_ALIAS_INDEX=`CREATE TABLE IF NOT EXISTS ingredient_alias ( alias_key TEXT PRIMARY KEY, target_key TEXT NOT NULL, weight INTEGER DEFAULT 10 )`constCREATE_INGREDIENT_INDEX=`CREATE TABLE IF NOT EXISTS recipe_ingredient_index ( id TEXT PRIMARY KEY, recipe_id TEXT NOT NULL, ingredient_key TEXT NOT NULL, amount_text TEXT )`

建表之后再补索引。这里的目标不是让所有LIKE '%xx%'都变快,而是让更常见的前缀、别名、食材 key 查询先走窄集合,最后才做宽模糊。

asyncfunctionprepareSearchSchema(rdb:relationalStore.RdbStore):Promise<void>{awaitrdb.executeSql(CREATE_RECIPE_INDEX)awaitrdb.executeSql(CREATE_ALIAS_INDEX)awaitrdb.executeSql(CREATE_INGREDIENT_INDEX)awaitrdb.executeSql('CREATE INDEX IF NOT EXISTS idx_recipe_name ON recipe_index(normalized_name)')awaitrdb.executeSql('CREATE INDEX IF NOT EXISTS idx_recipe_category ON recipe_index(category_key)')awaitrdb.executeSql('CREATE INDEX IF NOT EXISTS idx_ingredient_key ON recipe_ingredient_index(ingredient_key)')awaitrdb.executeSql('CREATE INDEX IF NOT EXISTS idx_alias_target ON ingredient_alias(target_key)')}
查什么为什么单独拆
recipe_index菜名、分类、评分先命中最准确的菜谱
ingredient_alias番茄/西红柿、土豆/马铃薯解决用户叫法和数据叫法不一致
recipe_ingredient_index食材命中菜名没命中时还能按食材兜底

先把状态模型写出来

如果页面上来就用几个零散变量凑逻辑,后面一定会乱。中式美食这类菜谱应用的麻烦点在于:一个操作会影响列表、详情、收藏、购物清单和最近浏览。先把状态模型写出来,后面才知道该更新谁。

exporttypeLoadStatus='idle'|'loading'|'ready'|'empty'|'error'exportinterfacePageState{keyword:stringstatus:LoadStatus selectedIds:string[]source:'search'|'favorite'|'recommend'|'shopping'anchorId?:stringmessage?:string}

这段模型不追求复杂,关键是把“用户正在看什么”和“页面为什么这样显示”分开。比如空结果不是数组长度为 0 这么简单,它还可能带着关键词、来源和下一步建议。

字段作用出错表现
keyword当前搜索词或筛选词返回后输入框空了
status页面当前状态加载、空态、错误态互相覆盖
selectedIds当前勾选或命中项刷新后统计不准
source从哪个入口进入详情返回错页
anchorId列表恢复位置返回后回到顶部

ViewModel 不要把动作写散

页面里每个按钮都直接改状态,短期看很快,后期调试很痛。我的习惯是让页面只调用动作,动作里统一处理旧状态、请求版本和刷新边界。

exportclassFeatureViewModel{state:PageState={keyword:'',status:'idle',selectedIds:[],source:'search'}privaterequestVersion=0asyncreload(keyword:string):Promise<void>{constversion=++this.requestVersionthis.state.keyword=keyword.trim()this.state.status='loading'constresult=awaitthis.repository.query(this.state.keyword)if(version!==this.requestVersion)returnthis.state.status=result.length>0?'ready':'empty'}}

这里的requestVersion很土,但很管用。用户连续输入或连续切换 tab 时,旧请求回来不能覆盖新结果。这个坑在搜索页、购物清单页和推荐页都会遇到。

Repository 分三层查,不要一把 LIKE 打到底

我会把查询拆成三段:菜名前缀和完全命中、别名命中、食材兜底。这样做的好处是结果排序可以解释,调试时也能知道慢在哪里。

exportinterfaceSearchHit{recipeId:stringsource:'name'|'alias'|'ingredient'score:numberexplain:string}functionnormalizeKeyword(input:string):string{returninput.trim().toLowerCase().replace(/\s+/g,'')}

第一层查菜名。精确命中和前缀命中要比包含命中更靠前。

asyncfunctionqueryByRecipeName(rdb:relationalStore.RdbStore,keyword:string):Promise<SearchHit[]>{constkey=normalizeKeyword(keyword)constpredicates=newrelationalStore.RdbPredicates('recipe_index').beginsWith('normalized_name',key)constresultSet=awaitrdb.query(predicates,['recipe_id','normalized_name','rating'])consthits:SearchHit[]=[]while(resultSet.goToNextRow()){constrecipeId=resultSet.getString(resultSet.getColumnIndex('recipe_id'))constname=resultSet.getString(resultSet.getColumnIndex('normalized_name'))constrating=resultSet.getDouble(resultSet.getColumnIndex('rating'))hits.push({recipeId,source:'name',score:name===key?100+rating:80+rating,explain:`菜名命中:${name}`})}resultSet.close()returnhits}

第二层查别名。用户搜“番茄”,数据里可能是“西红柿”;用户搜“马铃薯”,菜谱里可能写“土豆”。这一步不要在页面里硬编码,放到别名表里更稳。

asyncfunctionqueryAliasTarget(rdb:relationalStore.RdbStore,keyword:string):Promise<string[]>{constp=newrelationalStore.RdbPredicates('ingredient_alias').equalTo('alias_key',normalizeKeyword(keyword))constresultSet=awaitrdb.query(p,['target_key'])constkeys:string[]=[]while(resultSet.goToNextRow()){keys.push(resultSet.getString(resultSet.getColumnIndex('target_key')))}resultSet.close()returnkeys}

第三层用食材兜底。只有菜名和别名都不够时,才扩大到食材索引。

asyncfunctionqueryByIngredient(rdb:relationalStore.RdbStore,ingredientKey:string):Promise<SearchHit[]>{constp=newrelationalStore.RdbPredicates('recipe_ingredient_index').equalTo('ingredient_key',ingredientKey)constresultSet=awaitrdb.query(p,['recipe_id','ingredient_key'])consthits:SearchHit[]=[]while(resultSet.goToNextRow()){hits.push({recipeId:resultSet.getString(resultSet.getColumnIndex('recipe_id')),source:'ingredient',score:50,explain:`食材命中:${ingredientKey}`})}resultSet.close()returnhits}

最后把三层结果合并去重。同一个菜谱可能被菜名和食材同时命中,保留分数最高的那条。

functionmergeSearchHits(groups:SearchHit[][]):SearchHit[]{constmap=newMap<string,SearchHit>()groups.flat().forEach((hit:SearchHit)=>{constold=map.get(hit.recipeId)if(!old||hit.score>old.score){map.set(hit.recipeId,hit)}})returnArray.from(map.values()).sort((a,b)=>b.score-a.score)}

ArkUI 页面只做展示和事件出口

页面代码越像“业务大杂烩”,越难定位问题。中式美食里我会把点击动作放到 ViewModel 或 Router,页面只负责把当前状态画出来。

@Componentstruct FeaturePanel{vm:FeatureViewModelbuild(){Column(){if(this.vm.state.status==='loading'){Text('正在加载')}elseif(this.vm.state.status==='empty'){Text('没有找到合适结果,换个关键词试试')}else{List(){ForEach(this.vm.items,(item:RecipeCardModel)=>{ListItem(){Text(item.name)}},(item:RecipeCardModel)=>item.id)}}}}}

ForEach的 key 一定要用业务 id。不要用 index。index 在搜索、筛选、排序、插入、删除之后都会变,卡片复用就容易把上一条的状态带到下一条。

容易忽略的边界

这几个边界如果不提前写清楚,明天换一个页面还会踩同样的坑。

边界错误写法更稳的写法
点击动作子组件里直接改 Repository子组件抛回调,父级 ViewModel 处理
返回来源只传dishId同时传source和上下文快照
搜索结果页面里做字符串匹配Repository 统一归一化和排序
勾选统计底部栏自己算一遍从 ViewModel 派生同一份统计
列表 key用 index用稳定业务 id

回归验收

验收动作预期结果
连续快速点击两次入口不重复写入,不出现旧状态覆盖新状态
筛选后进入详情再返回关键词、分类、排序、滚动位置保持一致
收藏或勾选后刷新页面页面状态和本地数据一致
切换分类再清空条件列表回到默认态,不残留旧筛选
删除或新增一条数据ForEach 不串状态,卡片展示不乱
functionverifyState(current:PageState):boolean{if(current.status==='ready'&&current.keyword.length===0)returntrueif(current.status==='empty'&&current.message===undefined)returnfalsereturncurrent.selectedIds.every((id:string)=>id.length>0)}

我的验收习惯是先用手点出问题,再用表格固定动作。只看代码很容易觉得没问题,但一旦连续点击、返回、刷新、清空一起压,状态边界是否清楚马上就能看出来。

最后小结

RDB 搜索 这类问题不要只看页面表象。真正要处理的是:谁保存状态,谁查数据,谁负责跳转,谁只展示结果。中式美食后面继续扩菜谱、购物清单和推荐入口时,只要这些边界不乱,新功能就不会把旧页面拖出问题。

http://www.cnnetsun.cn/news/3383139.html

相关文章:

  • 【AI智能客服】动态知识中枢:让AI‘越用越聪明‘的知识进化飞轮
  • 人工智能毕业设计2026课题怎么做
  • 全国专业电销外包公司怎么选,不同梯队服务商合规资质对比有哪些?
  • ArkUI 5.0渲染管线:从代码到像素的极致渲染路径(161)
  • 【2024最新】ChatGPT情感分析避坑手册:为什么你的模型总把讽刺当褒义?——基于27万条中文UGC的实证分析
  • 【LINUX】驱动
  • 【1980-2025】中科院30m土地利用/覆盖数据|全国|TIFF
  • 数字电路时序控制:基于74LS193的八路流水灯Multisim仿真与优化
  • 客户现在真的会用豆包找厂家、找本地门店吗?流量多不多?
  • LiteSpeed QUIC与HTTP/3配置优化指南:从原理到实战部署
  • 【LE Audio】CSIS核心缩写全解
  • 开发者AI学习路径:从工具使用到项目实战的3个层次
  • 【译】利用 GitHub Actions 实现 Visual Studio 扩展构建自动化
  • Camera | 瑞芯微RK3568平台VCM马达驱动调试与故障排查指南
  • Matter协议,如何打通设备、品牌与通信协议之间的互通“藩篱”?
  • 打通 AI 编程本地运维边界,利用 MCP 协议简化环境与服务管理
  • Unity性能优化利器:MeshBaker 3.35.0网格合并实战指南
  • 深度学习进阶(十五)卷积神经网络——ResNet残差块原理与实战解析
  • C++宏定义:从基础语法到高级应用与最佳实践
  • C#中如何设置软件开机自启动
  • Windows C盘清理全攻略:从手动操作到自动化工具释放磁盘空间
  • 工业负载控制:TPD2017FN与PIC18LF45K50的智能解决方案
  • 跨平台实战:在C/C++、Python与C#中高效集成CH347DLL驱动
  • iNeuOS Vision Detect:工业视觉模型全生命周期管理平台
  • 为什么你需要Topit:3个技巧彻底掌握macOS窗口置顶神器
  • Docker构建时外网访问配置与优化指南
  • ChatGPT商业价值挖掘最后窗口期:2024Q3前必须掌握的6个合规变现路径(含GDPR/网信办双适配方案)
  • 【一线大厂Java面试题合集】第61篇-分布式链路追踪与配置中心
  • RAG技术实战:从零构建企业级检索增强生成系统
  • YOLOv8轴承缺陷检测:从模型训练到工业部署完整实践