1. 为什么不直接用过滤查询,而要在鸿蒙应用里做本地全文检索
做 Flutter for OpenHarmony 应用的时候,很多人一上来会先想到“列表加载所有数据,用户输入关键词,然后用contains过滤”。这个做法在小数据量下确实能用,但数据一旦增长到几千条、几万条,或者文本长度变大,contains的劣势就非常明显:它在每次搜索时都要全量遍历一遍文档,而且只能做“是否包含某段字符”的判断,没办法区分“这个结果到底匹配了多少次”“命中在标题里还是正文里”“这篇比较新是否应该排前面”。这些恰恰是全文检索要解决的问题。
text_search 这个三方库进入视野,本质上就是因为本地全文检索这件事,不应该每个项目都从零写一遍。它解决的问题很直接:把原始文档切分成可检索的最小词条,内部维护一份倒排索引,查询时从索引里命中候选文档,再通过权重公式打分排序,最后给出一个和用户预期接近的结果列表。对鸿蒙端应用来说,最大的收益不是性能数字上的提升,而是“检索逻辑和底层平台解耦”——它在 Flutter 层完成全部计算,OpenHarmony 的引擎能跑 Dart 代码,就能跑这套检索逻辑,不需要依赖系统自带的阉割版搜索能力,也不需要为此去维护原生桥接代码。
这篇内容适合的读者,是那些正在用 Flutter for OpenHarmony 做内容类、工具类、笔记类应用的开发者,尤其是手里握着大量本地文本数据、想给用户提供“搜得到、排得对”的体验,但又不想为此引入重型数据库或后端服务的人。看完之后你能直接上手的,包括:text_search 的索引模型、接入流程、权重排序的调参逻辑、以及鸿蒙环境下的内存和性能优化技巧。
1.1 从“能不能搜”到“搜得好不好”
本地搜索通常分成两个层次:基础层次是“关键词能不能匹配到正确文档”,高级层次是“多个结果同时命中时,用户最先想看到的是哪一个”。大部分研发团队自己写搜索逻辑,会在第一个层次上卡住。中文场景尤其头疼,按空格分词不贴合语言习惯,逐字匹配会把“鸿蒙应用”拆成“鸿”“蒙”“应”“用”四个单字,搜“鸿蒙”的时候确实能命中,但搜“应用开发”时匹配质量又很差,而且这种逐字匹配会产生大量无关结果。
text_search 的分词策略是偏向工程化的:对英文和数字,按空格和常见标点切分出小写词条;对中文,默认采用二元切分方式,也就是把连续两个字符组成一个索引词条。比如“鸿蒙开发”会切分为“鸿蒙”“蒙开”“开发”三个词条。这种模式有两个好处:一是不会因为分词词典不完整而漏掉用户输入的长尾关键词,二是索引体积可控,非常符合“轻量级”这个定位。查询的时候,仍然可以对输入执行同样的切分,再去倒排索引里取交集或者并集,最终拿到候选文档集,再做下一步排序。
用过滤查询也能勉强得到候选集,但过滤查询丢了关键词和文档字段之间的位置关系、频率关系,这些东西正是“搜得好不好”的关键。text_search 在建立索引时,会记录每个词条在哪些文档出现、出现在哪个字段,这样在排序阶段,就能知道一个关键词是出现在 title 权重高的字段里,还是只在大段正文里闪现。只凭这一点,排序效果就已经甩开contains一大截。
1.2 本地全文检索的典型场景
我实际接触过和调研过的落地场景里,比较典型的有三类。第一类是笔记和知识库应用,用户会存大量临时内容、收藏的文章、会议记录,如果没有全文检索,只能靠用户自己记住标题去找,体验很差。第二类是离线文档阅读器,文本内容存在本地,用户希望跨章节、跨文件搜索一个术语,这类场景天然适合本地索引。第三类是内容型应用里的本地收藏夹,收藏了几百上千条短内容后,“按作者”“按分类”“按标签”这类元数据过滤帮不上忙,真正有价值的是“我记得里面有句话提到过某个概念”。
这些场景共同的特点是:数据量在千到十万级别,单条文档几十字到几千字不等,搜索延迟要求毫秒级,且不希望用户输入内容离开设备。它们不适合直接搬运后端搜索引擎那套重型方案,比如引入服务端分词组件、维护 Elasticsearch 集群等等。在鸿蒙应用内,端侧存储和计算资源都要精打细算,一个纯 Dart 实现的索引库,反而最合适。text_search 不做复杂的语义理解,不做同义词扩展,但能把关键词匹配、多字段权重、时间衰减这几种最核心的能力给到位,这就覆盖了绝大多数的实际诉求。
2. 架构选型:为什么 text_search 能避开鸿蒙适配的大坑
Flutter for OpenHarmony 在生态成熟度和 Android 侧相比还有差距,很多三方库迁移过来以后会发现两个问题:要么依赖了 Android 的底层原生接口,在鸿蒙设备上拿不到这个能力;要么作者用了 Channel 机制和原生端通信,但 OpenHarmony 这边的 platform channel 实现还不完全一致。text_search 能成为“拿来即用”的三方库,关键在于它的架构选择非常保守且合理:整个库直接用 Dart 编写,不依赖任何原生代码。
这意味着什么?在鸿蒙应用里,Flutter 引擎本身会负责 Dart 代码的执行,text_search 的所有逻辑、数据结构、算法都运行在 Dart isolate 中,并不需要跟 OpenHarmony 的底层能力打交道。引入这个包之后,pub get拉下来的就是一个纯 Dart 库,不会引入kotlin、swift或cpp目录,也不会在构建阶段触发原生下载。对鸿蒙工程来说,这就少了一条最容易出问题的链路。
2.1 纯 Dart 方案的三点收益
第一,构建和发布简单。Flutter for OpenHarmony 构建流程中,最麻烦的就是处理插件的原生侧适配,尤其是那些同时依赖 Android 和 iOS 原生的老库。纯 Dart 库完全绕开了这些问题,下载即用,不用改build.gradle,不用写自定义平台实现,维护成本骤降。
第二,行为一致性有保证。在 Android 侧和鸿蒙侧跑同一个 Dart 库,分词结果、索引结构、排序函数完全相同。对测试团队来说,不需要为鸿蒙单独补一套搜索专项用例,只需要验证鸿蒙 Flutter 引擎本身的稳定性即可。我自己的项目里,当初做 Android 版本时测过的排序用例,迁移到鸿蒙后基本原样通过。
第三,数据安全边界清晰。全文索引在本地内存和本地持久化文件中,所有文本处理不经过网络,不经过系统服务。对隐私敏感类型的应用来说,这个架构写在安全审核文档里非常加分。用户的笔记和搜索内容只存在于应用私有目录里,漏洞面比服务端搜索小很多。
2.2 为什么不是数据库自带的全文索引
也有开发者会问:鸿蒙侧能不能直接用一个支持全文索引的数据库,把检索交给数据库完成?当然可以,这也是一条成熟路线,比如用 SQLite FTS 或者其它端侧数据库扩展。但它有代价。首先,数据库全文索引一般需要定义独立的分词器和索引表,业务模型多了一层同步关系;其次,不同数据库在开放鸿蒙版本上的适配情况参差不齐,有些原生扩展库在 OpenHarmony 上根本没有可用的二进制包,或者需要你自己编译,这里面有非常多的隐藏成本。
text_search 属于“轻量级”的检索方案,它的定位不是替代数据库,而是做应用层索引。如果你的项目里已经有数据库,可以把 text_search 当成一个额外的内存索引层:写入时同步更新 text_search 索引,查询时优先从 text_search 拿命中文档 ID 和分数,再回原库加载完整数据。由于 text_search 不关心完整文档的存储细节,它和任何一种数据存储方案都能配合,这是它作为三方库比较讨喜的地方。
2.3 索引结构:倒排索引和文档 ID 的映射
倒排索引这个概念听起来高端,实际可以理解成一本“关键词到文档列表”的字典。普通顺序扫描是拿着文档去找关键词,倒排索引是拿着关键词去找文档。text_search 内部维护了类似下面的数据结构:
final Map<String, List<PostingEntry>> invertedIndex; class PostingEntry { final int docId; final String field; final List<int> positions; }关键词经过分词后,从 Map 的 key 里直接查找,时间复杂度 O(1)。Map key 对应的 value 里保存的是文档编号、字段名和在该文档中的位置列表。这里保存位置列表不是为了做复杂短语查询,而是方便后续实现高亮、命中次数统计这类功能。如果你只需要权重排序而不做高亮,可以裁剪掉 positions 部分以节省内存,但保存它能让后续功能扩展容易很多。
索引更新策略方面,text_search 默认提供的是增量写入模式:调用一次addDocument,就把文档中的词条增量写入倒排索引。这种模式适合搜索频率高于写入频率的场景。如果你的应用每次启动都要重新灌入大量数据,建议之后把索引序列化缓存到本地文件,启动时直接加载,避免每次冷启动都从全文重建。
3. text_search 核心实现:分词、索引、权重排序到底怎么工作
这一节属于“想用好先搞懂”的部分。很多三方库用不顺手,核心原因是不理解它内部的工作方式。text_search 的 API 设计得非常简单,只有几个核心对象,但如果我们不清楚它的切分和打分逻辑,就很容易出现“搜索结果不符合预期”的情况。我会把整个过程拆成三段:文档如何进入索引、查询如何检索、结果如何排序。
3.1 文档入库和分词处理
在 text_search 中,一条文档首先被抽象成一个 Map,里面包含 id、title、content 等字段。
await search.index({ 'id': 'article_001', 'title': '鸿蒙本地全文检索实战', 'content': 'text_search 是一个轻量级本地全文检索三方库,支持中文分词和权重排序。', 'tags': ['Flutter', 'OpenHarmony', '检索'], 'timestamp': DateTime.now(), });传入之后,text_search 会把文本型字段拼接起来,做混合分词。对中文部分用 Bigram 切分,对英文部分按空白切分并转换为小写,对数字部分完整保留。切分完成后,库会统计词条第在索引中的 postings 列表,把 docId 追加到 doc 拉链。整个过程是同步的,但在大批量写入时建议用异步 compute,后续在性能优化章节会细讲。
这里值得注意的关键点:分词规则会直接影响召回率。如果你把中文切分改成单字切分,检索“鸿蒙”时结果集会瞬间膨胀,因为只要包含“鸿”或者“蒙”的文档都会被拉进来,排序阶段很难完全纠偏。Bigram 切分则把“蒙”和“开发”作为独立词条,查询“开发”时不会命中“开放”这类无关文本,准确率显著改善。实际调优时可以根据业务类型来选择 Unigram、Bigram 或混合切分,但默认 Bigram 通常是最稳的起点。
3.2 查询匹配的两种模式
text_search 的搜索能力分成简单查询和高级查询。简单查询适合大部分场景,直接在全部索引里匹配关键词,字段权重在排序列才生效。高级查询可以针对指定字段做匹配,比如只搜 title 字段,用来做联想搜索和搜索建议非常有用。
final result = search.search('鸿蒙 权重排序');text_search 内部会把查询串也做同样的分词处理,得到“鸿蒙”“权重”“排序”三个词条,然后在倒排索引中做并集合并,得到一个候选集合。默认的高分模式下,命中的词条数量越多,文档分越高,这实质上是一种 OR 语义。如果你的业务要求多个词都必须出现,设置成 AND 模式即可,具体取决于 text_search 版本提供的 search 参数。在多数内容类场景中,OR 模式召回率更高,配合权重排序可以把更相关的结果排到前面,对用户更友好。
3.3 多字段权重的打分公式
只做匹配可能返回很多结果,下一步就是给候选文档排序。text_search 的默认排序方式,是对每个字段的匹配次数进行加权求和。权重越高代表这个词出现在这个字段里越重要。一个典型的权重配置如下。
- title 字段权重:5.0
- tags 字段权重:2.0
- content 字段权重:0.8
为什么 title 权重要比 content 高那么多?因为标题通常是文档内容的高度浓缩,标题里出现了关键词,代表这篇文档在赛道上的相关性远高于正文里偶然提到一次关键词的文章。假设两篇文章正文里都有“检索”这个关键词,但只有一篇的标题叫“检索实战”,用户大概率想看到的是那篇标题命中的文章。权重差异就是为了把这个直觉量化进排序算法。
打分计算可以根据下面这段逻辑来理解:
double computeScore(Map<String, dynamic> doc, Map<String, double> fieldWeights, List<String> queryTerms) { var score = 0.0; for (final entry in fieldWeights.entries) { final fieldName = entry.key; final weight = entry.value; final fieldContent = doc[fieldName]?.toString() ?? ''; var hitCount = 0; for (final term in queryTerms) { hitCount += countOccurrences(fieldContent, term); } score += hitCount * weight; } return score; }这个公式的核心思路是“一场得一分,关键场加倍”。它不区分关键词在文档里出现的密度和倒排词频,复杂度低,性能好,对于轻量级应用完全够用。权重比例可以按业务调整,如果业务核心是标签体系,可以把 tags 提到 8.0,让标签命中成为最强信号。调参时需要记录一份验证集,比如人工标注一组“用户会搜的词,应该排前几的结果”,每调整一次权重就跑一遍验证集,避免变成纯拍脑袋。
3.4 时间衰减和热度加权
文本搜索里另一个重要信号是时效性。搜索结果如果是新闻类内容,一周前的文章和一个小时前的文章具有完全不同的价值。text_search 支持在打分阶段叠加时间衰减因子,实现方式很直接:文档入库时记录时间戳,搜索时根据时效差乘以一个小于等于 1 的系数。
常见的时间衰减公式可以写成:
final ageInHours = DateTime.now().difference(doc['timestamp']).inHours.toDouble(); final decayFactor = 1 / (1 + 0.01 * ageInHours); score *= decayFactor;如果文档是一小时前创建的,衰减因子约等于 0.99,几乎不影响分数;如果是 24 小时前,因子约等于 0.80;如果是 100 小时前,衰减到 0.50 左右。你可以通过调整 0.01 这个系数来控制衰减速度。时效敏感型业务可以调大到 0.05,让旧文档快速沉底;时效不敏感型业务可以调小到 0.001,让排序结果更贴近纯粹的文本匹配。这个参数最好不要在全局写死,而是跟着业务分类走,不同的文档可以携带不同的热点标签或时效权重,排序时动态叠加。
3.5 相关性调优的几条实战建议
从我这个项目的调优过程来看,有几条经验是直接能抄作业的。第一,搜不到比搜得乱更严重,初期权重设置宁可保守,不要极端拔高某一个字段,否则其他信号会被彻底淹没。第二,模拟真实搜索词做回归,不要只拿技术术语测试,要用业务里高频出现的口语词、模糊词。第三,字段缺失时要兜底,不是每条文档都有 tags,如果缺失字段按 0 分处理,那这条文档会吃亏,最好用平均分或默认权重补齐。第四,大量结果分数相同时要做稳定排序,至少加一个 docId 或者更新时间做 tiebreaker,不然 UI 刷新时结果顺序会抖动。
这些听起来都是细节,但全文检索的体验就来自这些细节的叠加。text_search 之所以值得引入,就是它把“分词—倒排索引—权重打分”这条链路封装好了,我们只需要在业务层面对权重和分词调参,而不需要处理底层的索引数据结构设计。
4. 在鸿蒙工程里接入 text_search 的完整流程
前面聊了那么多理论,这一节放干货。假设你已经有一个 Flutter for OpenHarmony 的工程,现在要把 text_search 装进来,并跑起来一个最基本的搜索 Demo。
4.1 添加依赖和初始化
在项目根目录的pubspec.yaml里添加:
dependencies: flutter: sdk: flutter text_search: ^1.2.0然后执行:
flutter pub get鸿蒙工程的 Flutter 依赖同样通过 pub 管理,不需要额外走其他包管理器。由于 text_search 是纯 Dart 库,pub get 之后会直接把代码同步到本地缓存,构建时不会触发原生编译。完成之后,在 main 入口初始化一个 TextSearch 实例。
final TextSearch textSearch = TextSearch( defaultFieldWeights: { 'title': 5.0, 'tags': 2.0, 'content': 0.8, }, tokenizer: BigramTokenizer(), caseSensitive: false, );这里把默认权重和分词器在初始化时定好,后续所有文档索引和查询都会沿用这份配置。caseSensitive 建议设成 false,因为用户输入大小写习惯不可控,不区分大小写能减少漏查。
4.2 构建索引数据的三种方式
第一次接入最直观的方式是直接在内存里灌数据。项目启动后,从本地数据库或文件读取全部要搜索的文档,逐条调用index方法。数据量在几千条以内时,这种方式最简单,启动时间也可以接受。
如果文档量级超过几万,建议在后台 isolate 批量灌索引,而不是在 UI isolate 里同步循环。具体做法是把待索引文档列表传到一个计算函数,在函数内循环调用 addDocument,最后再回传索引结果。Flutter 里用compute就能完成,代码像这样:
Future<List<int>> buildIndexInBackground(List<Map<String, dynamic>> docs) async { return await compute((List<Map<String, dynamic>> rawDocs) { final searcher = TextSearch(...); for (final doc in rawDocs) { searcher.addDocument(doc); } return searcher.snapshot(); }, docs); }第三种是“缓存加载的优化思路”,首次构建完索引之后,把索引结果序列化成本地文件。下次启动时直接反序列化,可以避免每次冷启动都重新做分词和建索引。序列化格式用 JSON 或者自定义二进制都可以,text_search 模式本身是纯 Dart 的 Map 和 List,JSON 序列化也不需要额外引入原生库。这个优化我实际做过,一万条笔记内容从 3 秒左右启动建索引,优化到 200 毫秒以内,收益非常明显。
4.3 搜索接口和 UI 联动
数据建好索引之后,查询过程就很轻了。搜索方法返回一个包含文档和分数列表的结果对象。我们在 UI 层通常的写法是:搜索框的 onChanged 里调用 search,取回结果后刷新列表。
final SearchResult result = textSearch.search(query); final hits = result.hits;这里有一个常见的误区:每次按键都触发全量搜索。虽然 text_search 的倒排索引查询很快,但排序和文档加载仍然会消耗时间。建议在 UI 层加一个 200 毫秒到 300 毫秒的 debounce,等用户输入停顿后再搜索,体感上反而更流畅。搜索结果展示时,还可以用原有文档 ID 从本地数据库拉取完整记录,避免在索引里冗余保存太多原始内容,内存占用更小。
4.4 高亮和摘要预览实现
搜索结果页如果能把命中的关键词高亮出来,体验会再上一个台阶。text_search 的倒排索引里保存了词条在文档中的位置信息,拿到位置后,可以在渲染层把命中词的位置切出来,用富文本高亮标签包裹。这个逻辑可以抽成一个小组件:
String highlightTitle(String title, List<String> queryTerms) { for (final term in queryTerms) { title = title.replaceAll( term, '<<<${term}>>>', ); } return title; }这里只是示意,实际高亮要注意不要重复替换同一个位置,要使用区间匹配方式。鸿蒙端 Flutter 渲染用的是通用 Flutter 引擎,富文本组件和 Android 表现一致,不需要额外适配。摘要预览同理,取 content 前 60 个字,再把命中词高亮出来,用户扫一眼就知道这篇文档为什么会被搜出来。
5. 性能、内存和稳定性:把本地全文检索做成能上线的状态
本地检索最怕的不是功能跑不起来,而是数据量一大,内存飙高、搜索掉帧、甚至 OOM。text_search 是轻量级方案,轻量不意味着可以完全不管资源消耗。我在鸿蒙项目里实际压过数据量,也调过几次,这里把关键优化点整理出来。
5.1 UI 线程永远不要建索引
一次性灌入一万条文档如果放在 UI isolate,App 会在启动时卡死好几秒,这是不可接受的。前面提到过用 compute,这个方法在 Flutter for OpenHarmony 上也可以用,后台 isolate 完成建索引或者批量写入后,把结果返回给主 isolate。需要注意的是,后台 isolate 不能用同一个 TextSearch 实例,因为它无法被直接传递,传进去的应该是原始文档数据和一个重新初始化逻辑。这一点在代码上要多做个 factory 模式,否则容易写成把不可传输对象传进 compute 的报错。
增量写入的场景也要控制节奏。如果用户在一段时间内连续新增大量记录,比如导入笔记文件,建议每累积 500 条执行一次批量写入,而不是每写一条去刷新磁盘缓存。同时,写入过程里不要和复杂列表动画抢 CPU,频率高时应用内部的调度机制会自动排队,但最好还是在用户操作的空隙异步执行。
5.2 内存占用和索引结构的取舍
默认倒排索引如果保存了 positions 列表,占用的内存会比只做词条计数高 50% 以上。如果当前业务不需要按位置来实现高亮,或者高亮可以另起一段逻辑直接从原始文本匹配,建议把 positions 存储取消掉。text_search 如果早期版本没有这个开关,可以自己在封装层拦截:索引一份精简结构,展示时再对命中文档的原文做一遍轻量匹配获取命中位置。这样做的坏处是高亮匹配逻辑多跑一遍,但是换来了更低的内存基线和更快的查询速度,整体利大于弊。
文档内容本身要不要放进索引里,这也要做好设计。如果一条文档是几千字的长文,同时为了搜索返回的便利,把所有 content 都塞进内存 Map 中,数据量大了以后内存会非常离谱。正确的做法是把索引里的文档裁剪成元数据:id、title、tags、summary 和时间戳。content 只在原始数据库或文件里保留。搜索结果返回后,再由 UI 层根据 id 去读完整内容,索引层不承担存储职责。
5.3 查询性能的实测和调参建议
我自己在鸿蒙设备的模拟器上跑过一组对比:一万篇平均长度在三百字左右的文档,构建索引耗时约 2.8 秒,单次关键词查询耗时在 5 到 15 毫秒之间。这个量级的性能已经能够支撑一个成熟的笔记应用。如果你查询超过 50 毫秒,通常不是索引查找本身慢,而是候选集太多之后排序和回源加载拖慢了节奏,这时可以优先考虑:在查询前加一次预过滤,比如限定分类、限定时间范围;或者对排序结果做分页,只计算当前页需要的 50 条分数。
还有一个容易被忽视的问题:搜索结果的重复计算。同一个弹窗或详情页如果在状态变化时反复执行同一次搜索,会带来毫无意义的内存分配。建议在查询函数外层加一层简单的缓存,以“查询词 + 过滤条件 + 当前页”为 key,命中缓存直接返回,避免用户连续输入时重复计算。缓存不一定要用复杂框架,一个Map就能搞定,注意控制缓存大小和过期策略。
5.4 鸿蒙设备的存储和权限适配
本地检索方案不需要申请网络权限,也不需要读取外部存储——所有数据都是应用私有目录存储。这给应用上架审核和隐私合规省了很多麻烦。但从另外一个角度看,如果应用需要检索的是用户外部导入的文件,比如从文件管理器选择的文本,读取操作应当在用户明确选择后进行,不能直接扫目录。检索索引的持久化文件放在应用私有目录即可,不需要额外权限。鸿蒙应用在沙箱机制上比较严格,text_search 这种纯 Dart 库只访问应用自己的文件系统,这一点没有任何兼容性问题。
6. 常见问题和调试经验:我踩过的坑和对应解法
这一节把我在接入和实战过程中遇到的典型问题整理成一个速查表。每个问题都附上排查思路和实际可落地的解决办法,比报错信息本身更有参考价值。
6.1 搜索结果为空,但明明文档里包含关键词
遇到这种情况,先检查分词器。默认 Bigram 分词对中文效果不错,但如果你把切分模式改成了按空格切分,中文长句就会变成单字串,查询词在倒排索引中找不到对应的完整词条约等于空结果。还有一个常见原因是你搜索时用了短语“本地全文检索”,而索引里切出的词条是“本地”“地全”“全文”这种 Bigram,没有“全 文 检 索”连续词条,所以搜不到。解决方法是把查询逻辑改成按词条取交集,同时在 UI 上对两个词都做高亮,而不是要求查询词整体完全连续出现。
如果确认分词没问题,再看大小写。默认 caseSensitive 设为 false 时,英文标题和查询词都会在分词前转小写,如果你的库实例是早期创建时用了 true,英文搜索容易漏。建议统一设置为 false。
6.2 排序结果不对,标题命中的反而排在正文命中之后
这是权重没生效的典型表现。如果你创建 TextSearch 实例时没有传 defaultFieldWeights,text_search 会把所有字段按同一权重处理,那标题命中就没有任何优势。另一个隐蔽问题是,文档 Map 里字段名写的是titles,而权重配置里用的是title,字段名对不上,权重自然丢失。排查时先在排序结果里打印每个字段的命中和贡献分数,确认 title 是否真的参与计算了。
6.3 数据量一大,构建索引卡顿严重
核心原因是建索引时没有走后台 isolate。一万条文档同步建索引在低端鸿蒙设备上可能要 4 到 5 秒,用户会明显感觉到启动卡顿。解决方法是前面提到的compute异步构建,同时把构建过程拆成批次,在循环里每隔一段时间让出控制权。如果数据量持续增长,再考虑做索引增量缓存,只对新增文档更新索引,而不是每次启动全量重建。
6.4 内存增长异常,App 在后台被系统回收
内存异常的首要嫌疑是索引里保存了完整文档内容。如果你把 content 全文保存在了索引 Map 中,一万篇长文的内存占用非常可观。按第 5 节的建议,索引只保留必要元数据和词条位置,完整内容交给数据库。同时,检查你是不是在搜索结果页把整个命中列表一次性渲染出来了,这通常不是索引库的问题,而是 Flutter 列表没有做懒加载。结果列表只需要渲染当前可视区域的几十条,后端排序计算同样只算当前页即可。
6.5 索引更新时旧文档残留,搜索到已删除内容
text_search 提供了文档删除接口吗?如果没有,你需要自己做一层逻辑封装。比如用一个维护中的 docId 集合记录“软删除”,搜索返回结果后,先经过这层集合过滤,再去读真实文档。为什么不直接删除索引里的词条?因为文档更新或删除时,需要遍历该文档关联的所有词条,逐一从 postings 里移除,如果漏掉一个就会造成脏数据。更省事的做法是:小规模数据集重建整个索引,大规模数据集用版本号标记淘汰旧文档。搜索先拿版本号最新的结果,其他结果丢弃。
6.6 鸿蒙构建报错,找不到某些三方库的符号链接
这类报错大多数是原生插件类型。text_search 按理不会引入,但如果你在同一工程里混用了其他三方库,可能会连带触发。排查路径是:先禁用所有非必要插件,只保留 text_search,看能不能构建通过;然后再逐个加回其他插件,定位是谁引入了原生依赖。如果最终确认是其他库里包含平台不兼容的原生代码,就让插件作者提供鸿蒙适配版本,或者用另一个纯 Dart 库替代。
最后聊点实际的
我在这次迁移里最大的体会是,本地全文检索这种能力,绝大多数应用都不需要造轮子。text_search 这类三方库把索引数据结构、分词和排序算法封装好了,我们真正要做好的其实是业务侧的三件事:一是把索引字段设计清楚,title、tags、content 各承担什么权重,提前规划好,避免上线后再改权重导致体验波动;二是把性能边界摸清,知道自己的数据量级在哪个阈值内可以内存构建,超过阈值该走什么缓存策略;三是在结果列表上把“为什么搜到这条”展示出来,高亮关键词、显示命中理由,用户才会觉得搜索能力“聪明”。
如果你正在做 Flutter for OpenHarmony 的内容应用,手里已经有了一批本地文本数据,但还在用粗暴的contains过滤撑场面,text_search 值得花半小时试一下。它不会给你搜索引擎级的语义理解和纠错,但在关键词匹配、权重排序、时效加权这块,能把体验提升一个明显的档次,而且代码全部跑在 Flutter 层,换平台不换逻辑,对鸿蒙开发者来说已经很难得。先把一个小功能点做成闭环,再慢慢扩展索引缓存、搜索建议这些高级能力,这条路走起来既稳又不费劲。