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

Electron 鸿蒙 PC 上做本地搜索,Fuse.js 比 SQLite 快 6 倍——但我愣是选了最慢的方案

先说结论:在鸿蒙 PC 的 Electron 应用里做本地搜索,如果你只盯着查询毫秒数,大概率会选错方案。

我等下会贴三段能直接跑的代码,分别是线性扫描、Fuse.js 和 SQLite FTS5。测下来,10000 条案例数据下,平均单次查询耗时大概是这个量级:

方案1000 条5000 条10000 条内存占用
线性扫描4.2 ms21 ms45 ms约 12 MB
Fuse.js0.3 ms1.1 ms2.0 ms约 18 MB
SQLite FTS51.8 ms7.5 ms12 ms约 3 MB

你看,Fuse.js 把 SQLite 按在地上摩擦。但结果你猜怎么着?我把项目里的实现换成了 SQLite。先别急着骂我标题党,听我把原因说完。

等一下,这里我漏说一个前提——我说的“本地搜索”不是指输入法那种即时提示,而是用户输入关键词后回车触发、需要返回排序结果列表的搜索。这个边界很重要,因为两种场景对延迟的容忍度完全不一样。输入法差 50ms 用户都能感知,但搜索结果页慢 10ms 基本无感。

测试环境也交代一下:华为 MateBook 鸿蒙 PC 工程机,CPU 是麒麟 9000S 桌面版,Electron 版本 33.x,Node 22。数据是我从公开渠道整理的一人公司案例库,每条记录包含标题、标签、正文摘要,平均 180 个汉字。测试前我先把其它进程杀干净,避免内存抖动干扰结果。

方案一,线性扫描。代码写起来最爽,一把梭:

// main/search/linear.tsinterfaceCaseItem{id:string;title:string;tags:string[];content?:string;}exportfunctionlinearSearch(list:CaseItem[],keyword:string):CaseItem[]{constlower=keyword.toLowerCase();returnlist.filter(item=>item.title.toLowerCase().includes(lower)||item.tags.some(t=>t.toLowerCase().includes(lower))).slice(0,30);}

逻辑够直白吧?但这玩意儿有两个暗坑。第一,数据一多,主进程会被卡死。鸿蒙 PC 上 Electron 的主线程要是阻塞超过 200ms,窗口就会出现明显卡顿。我 5000 条数据时搜了一次,整个窗口冻结了将近 80ms,用户要是正在滚动列表,体验直接炸裂。第二,它只能做包含匹配,搜“鸿蒙”能命中“鸿蒙开发”,但搜“鸿蒙发”就凉了。你稍微想做点模糊匹配,代码就丑得不像样。

方案二,Fuse.js。这个库我挺喜欢的,轻量、API 友好,模糊搜索的效果也确实能打。

// main/search/fuse.tsimportFusefrom'fuse.js';constfuseOptions={keys:['title','tags'],threshold:0.35,includeScore:true,limit:30,};exportfunctioncreateFuseIndex(list:CaseItem[]){returnnewFuse(list,fuseOptions);}exportfunctionfuseSearch(fuse:Fuse<CaseItem>,keyword:string):CaseItem[]{returnfuse.search(keyword).map(r=>r.item);}

初始化一次索引后,查询速度飞起。问题是,Fuse 会把整个索引结构挂在内存里。我测下来,10000 条案例数据大概占 18MB 左右。听上去不多?但 Electron 主进程的内存本来就被各种框架底层吃得很紧,鸿蒙 PC 上再给它加 18MB,我那个应用的启动速度肉眼可见地慢了一截。更难受的是,每次新增或删除数据,你都得重建索引。如果你的数据是离线下载、分批更新的,那这个重建过程会让用户感受到明显的卡顿。

还有个点很多人会忽略:Fuse.js 的索引构建本身也要时间。10000 条数据在我这台鸿蒙 PC 上初始化大概 220ms。也就是说,用户每次打开应用,都要先等 200 多毫秒把索引塞到内存里,搜索才能变快。如果数据还在增长,这个等待会越来越长。

方案三,SQLite FTS5。这是三种方案里最“重”的,但也是我最心动的。

// main/search/sqlite.tsimportDatabasefrom'better-sqlite3';importpathfrom'path';import{app}from'electron';letdb:Database.Database;exportfunctioninitSearchDB(){constdbPath=path.join(app.getPath('userData'),'search.db');db=newDatabase(dbPath);db.exec(`CREATE VIRTUAL TABLE IF NOT EXISTS cases USING fts5( id UNINDEXED, title, tags, content ); PRAGMA journal_mode = WAL; PRAGMA synchronous = NORMAL;`);returndb;}exportfunctionupsertCases(items:CaseItem[]){constinsert=db.prepare(`INSERT OR REPLACE INTO cases (id, title, tags, content) VALUES (@id, @title, @tags, @content)`);consttagStringify=(tags:string[])=>tags.join(' ');consttx=db.transaction((rows:CaseItem[])=>{for(constrowofrows){insert.run({id:row.id,title:row.title,tags:tagStringify(row.tags),content:row.content||''});}});tx(items);}exportfunctionsqliteSearch(keyword:string):CaseItem[]{conststmt=db.prepare(`SELECT id, title, tags FROM cases WHERE cases MATCH ? ORDER BY rank LIMIT 30`);returnstmt.all(`${keyword}*`)asCaseItem[];}

查询 10000 条数据 12ms,确实比 Fuse.js 慢。但 SQLite 有三个 Fuse.js 给不了的东西。

第一,崩溃恢复。WAL 模式下,就算用户直接杀进程或者鸿蒙 PC 突然断电,数据库文件也不容易损坏。我前面用 Fuse.js 时,索引是放在内存里的,崩溃后一切归零,重启得重新加载。而 SQLite 的数据躺在磁盘上,打开即用。

第二,查询能力。FTS5 支持前缀匹配、短语匹配、布尔组合。用户搜“鸿蒙 NOT 游戏”这种高级语法,SQLite 直接能处理,Fuse.js 得你自己写过滤逻辑,而且效率不一定好。你甚至可以在同一张表上同时做普通过滤和全文搜索,代码不用拆成两套。

第三,增量更新。新增一条案例?一条 INSERT 就行,不需要重建整个索引。这对数据会持续增长的应用太重要了。我实测批量插入 1000 条数据,FTS5 自动维护索引,耗时 340ms,用户几乎无感知。

我实测的那个对比数据,是在同一台鸿蒙 PC 上跑了 50 次取的平均值。测试脚本长这样:

// scripts/benchmark.tsimport{performance}from'perf_hooks';exportfunctionbench(name:string,fn:(kw:string)=>unknown,keyword:string,rounds=50){// 预热for(leti=0;i<5;i++)fn(keyword);conststart=performance.now();for(leti=0;i<rounds;i++)fn(keyword);constms=(performance.now()-start)/rounds;console.log(`${name}:${ms.toFixed(2)}ms/次`);}

跑之前记得把better-sqlite3在鸿蒙 PC 上重新编译一遍。我踩过的坑是,直接用 npm 装好的预编译二进制在鸿蒙定制内核上会报NODE_MODULE_VERSION不匹配,必须走electron-rebuild。命令大概是:

npmrun electron-rebuild ---f-wbetter-sqlite3

如果你的 CI 环境还没有鸿蒙 PC 的交叉编译链,这一步能卡你半天。我建议先在真机上编一次,把产物提交到仓库,后面就不用每次都折腾。

那到底怎么选?我直接给判断标准,不按编号,想到哪儿说到哪儿。

如果你数据量不到 500 条,或者搜索需求就是简单的标题匹配,Fuse.js 更合适。启动快、代码少、效果柔和,用户输入体验也好。SQLite 这套方案,重就重在它给你留了后路。

如果你数据会增长,或者需要离线稳定、崩溃不丢数据,直接上 SQLite。别被那 10ms 的差距迷惑,真实用户根本感觉不出来。倒是应用 crash 后索引没了,用户下次打开要重新等 200ms 建索引,那才叫恼火。

顺带一提,我做的 App 雷达鸭(鸿蒙版已上架华为应用市场)里,那个案例搜索功能最终用的就是 SQLite FTS5 这套方案。

反正我现在做 Electron 跨端项目,本地搜索这块基本不会选 Fuse.js 做主存储方案了。毫秒数好看,但扛不住数据增长和异常场景。

你目前在 Electron 项目里用的是什么搜索方案?遇到过类似的选择纠结吗?


关于作者

老三,10 年以上软件开发经验,软件设计师、人工智能应用工程师。目前专注鸿蒙应用开发(ArkTS)北向开发与 Web 前端,也在折腾 AI 自动化。不定期在 CSDN 分享鸿蒙和 AI 方向的技术文章。

本文遵循 MIT 协议,转载请注明出处。

http://www.jsqmd.com/news/1135043/

相关文章:

  • Kimi LeetCode 3485. 删除元素后 K 个字符串的最长公共前缀 Java实现
  • SV基础之连接设计和测试平台
  • OpenClaw专栏第三篇:环境搭建前的准备工作清单(保姆级避坑指南)
  • YOLO26 TensorBoard集成:可视化训练过程的完整配置与高级用法
  • Wave-ViT: Unifying Wavelet and Transformersfor Visual Representation Learning ——ECCV
  • 解决UE4SS在Palworld游戏中的崩溃问题:GUI控制台兼容性深度解析
  • 西天取经四人组(Liunx小测试)
  • 在上一篇文章NHibernate实践总结(一)中,主要罗列了自己的一部分实践总结,其中包括:
  • C++ Primer Plus 重读精讲 _ 名称空间与代码组织
  • Windows 7用户必备:让Blender 3.x+在旧系统上焕发新生的终极方案
  • 实验一:IP 地址规划、静态路由及 DHCP 配置
  • EldenRingSaveCopier:三步完成艾尔登法环存档迁移的终极解决方案
  • 计算机组成原理4
  • 如何下载沪深股票市场的行情数据,包括委托、成交、订单薄、沪深股票、etf、可转债、指数五档订单薄数据,买一到买五、卖一到卖五历史行情数据笔记
  • Claude Code 是什么
  • 5个常见泰拉瑞亚创作难题,TEdit地图编辑器如何帮你轻松解决?
  • 好久不在这里写文章了。先祝各位六一儿童节快乐!
  • 第一章 为什么90%的Camera项目,都死在第一步?
  • 基于YOLOv8的铁路障碍物检测:从数据准备到边缘部署全流程实践
  • MySQL 视图详解
  • 吾爱大佬最新出品,牛批Plus!
  • Jenkins与SonarQube集成实战:5步构建代码质量自动化分析体系
  • MediaPipe BlazePose 33关键点实战:Python+OpenCV 实现深蹲计数与角度可视化
  • Java程序设计(第3版)第四章——类加载
  • 计算机毕业设计之基于JSP绿色旅游生态园管理系统
  • 解锁B站评论区:如何用Python获取完整视频评论数据
  • 一文读懂服务器:撑起整个互联网的底层核心
  • IDEA集成Git
  • Terraform 状态文件深度解析:从原理到远程Backend的6大核心作用
  • pandas实战指南:从数据加载到业务分析的完整闭环