12、应用列表(AllApps)的搜索与排序:搜索索引的构建、模糊匹配与拼音搜索、应用排序规则(安装时间、使用频率)
应用列表,也就是我们常说的 AllApps,是桌面系统里用户最常打交道的界面之一。用户装了几百个应用后,想快速找到某个应用,靠手动翻页?那体验可就太糟糕了。所以,搜索和排序就成了 AllApps 的核心能力。
这一节,我们就来聊聊搜索索引怎么建、模糊匹配和拼音搜索怎么实现,以及排序规则怎么定。嗯,这里面的坑不少,我当年踩过好几个,咱们一个一个说。
核心知识点一览:
- 搜索索引的构建策略(内存 vs 数据库)
- 模糊匹配算法(编辑距离、前缀匹配)
- 拼音搜索的实现(拼音库 + 分词)
- 排序规则(安装时间、使用频率、字母序)
12.1 搜索索引的构建
搜索快不快,索引是关键。我见过不少项目,一开始图省事,每次搜索都遍历所有应用信息,结果应用一多,卡得不行。后来老老实实建索引,效果立竿见影。
索引的构建,通常有两种思路:
- 内存索引:把应用名、包名、拼音等数据加载到内存里,用 HashMap 或 Trie 树组织。优点是查询极快,缺点是占用内存,且需要处理数据同步。
- 数据库索引:用 SQLite 的 FTS(全文搜索)功能。优点是持久化、支持复杂查询,缺点是每次查询都有 IO 开销。
我个人习惯的做法是:内存索引做热数据,数据库索引做冷备份。什么意思呢?就是应用列表加载时,先把常用应用(比如用户最近打开的)建一份内存索引,剩下的走数据库。这样既保证了速度,又控制了内存。
小技巧:索引构建的时机很重要。我建议在应用安装/卸载时做增量更新,而不是每次都全量重建。全量重建放在应用启动后的后台线程里做,避免阻塞 UI。
来看一段索引构建的伪代码:
class AppIndexBuilder {
// 内存索引:应用名 -> 应用信息
private val nameIndex = HashMap<String, AppInfo>()
// 拼音索引:拼音首字母 -> 应用列表
private val pinyinIndex = HashMap<String, List<AppInfo>>()
fun buildIndex(appList: List<AppInfo>) {
nameIndex.clear()
pinyinIndex.clear()
for (app in appList) {
// 按应用名建立索引
nameIndex[app.appName] = app
// 按拼音首字母建立索引
val pinyin = PinyinUtil.toPinyin(app.appName)
val firstChar = pinyin.first().toString()
pinyinIndex.getOrPut(firstChar) { mutableListOf() }.add(app)
}
}
fun search(query: String): List<AppInfo> {
// 先查内存索引
val exactMatch = nameIndex[query]
if (exactMatch != null) return listOf(exactMatch)
// 再查拼音索引
val pinyinResult = pinyinIndex[query] ?: emptyList()
return pinyinResult
}
}
12.2 模糊匹配与拼音搜索
用户输入「微信」,你只匹配「微信」?那用户输入「wx」怎么办?或者输入「weixin」呢?这就是模糊匹配和拼音搜索要解决的问题。
模糊匹配,说白了就是允许用户输入不完整、有拼写错误的情况。常用的算法有:
- 编辑距离(Levenshtein Distance):计算两个字符串之间需要多少次编辑操作(插入、删除、替换)才能变成一样。距离越小,匹配度越高。我一般设置阈值为 2,超过 2 就不匹配了,否则结果太多。
- 前缀匹配:用户输入「微」,就匹配所有以「微」开头的应用。这个实现简单,效果也不错。
拼音搜索这块,坑比较多。你想想看,中文有多音字,比如「重庆」的「重」读 chóng,但「重要」的「重」读 zhòng。如果拼音库不准,搜索结果就会乱。
注意:拼音库的选择很关键。我建议用开源的 pinyin4j 或者 Android 自带的 PinyinUtils。但要注意,这些库对多音字的处理不一定完美。我曾经遇到过用户搜「银行」输「yinhang」,结果匹配到「银行」和「航行」——因为「行」是多音字。后来我加了上下文判断,才解决这个问题。
拼音搜索的实现思路:
- 把应用名转成拼音全拼,比如「微信」→「weixin」
- 把应用名转成拼音首字母,比如「微信」→「wx」
- 用户输入时,同时匹配全拼和首字母
- 支持模糊音,比如「zh」和「z」不分,「sh」和「s」不分
代码示例:
fun matchPinyin(appName: String, query: String): Boolean {
val fullPinyin = PinyinUtil.toPinyin(appName) // "weixin"
val firstLetters = PinyinUtil.toFirstLetters(appName) // "wx"
// 全拼匹配
if (fullPinyin.contains(query, ignoreCase = true)) return true
// 首字母匹配
if (firstLetters.contains(query, ignoreCase = true)) return true
// 模糊音匹配(简化版)
val fuzzyQuery = query.replace("zh", "z").replace("sh", "s").replace("ch", "c")
val fuzzyPinyin = fullPinyin.replace("zh", "z").replace("sh", "s").replace("ch", "c")
if (fuzzyPinyin.contains(fuzzyQuery, ignoreCase = true)) return true
return false
}
12.3 应用排序规则
搜索结果是出来了,但怎么排?总不能随机排吧?排序规则直接影响用户体验。我总结下来,主要有三种排序维度:
| 排序规则 | 说明 | 适用场景 |
|---|---|---|
| 安装时间 | 按应用安装时间倒序,新安装的排前面 | 用户刚装了个新应用,想快速找到它 |
| 使用频率 | 按用户打开次数或最近使用时间排序 | 常用应用排前面,减少查找时间 |
| 字母序 | 按应用名拼音首字母 A-Z 排序 | 用户知道应用名,按字母快速定位 |
实际项目中,我通常会把这三种规则组合起来用。比如:默认按使用频率排序,但用户搜索时,按匹配度排序,匹配度相同时再按使用频率排。
这里有个细节:使用频率怎么统计?我建议用 LRU(最近最少使用) 的思想。每次用户打开应用,就记录一个时间戳。排序时,按时间戳倒序。这样既简单又有效。
我的经验:排序规则不要搞得太复杂。用户其实就两个诉求:一是常用的应用好找,二是新装的应用能快速看到。所以,我一般把「最近安装」和「最常使用」作为两个独立的排序选项,让用户自己选。默认用「最常使用」,但新装应用在 24 小时内会置顶显示。
排序的代码实现:
fun sortApps(apps: List<AppInfo>, sortBy: SortBy): List<AppInfo> {
return when (sortBy) {
SortBy.INSTALL_TIME -> apps.sortedByDescending { it.installTime }
SortBy.FREQUENCY -> apps.sortedByDescending { it.lastUsedTime }
SortBy.ALPHABET -> apps.sortedBy { PinyinUtil.toPinyin(it.appName) }
}
}
// 组合排序:先按匹配度,再按使用频率
fun sortSearchResults(results: List<AppInfo>, query: String): List<AppInfo> {
return results.sortedWith(compareByDescending<AppInfo> {
// 匹配度:完全匹配 > 前缀匹配 > 模糊匹配
when {
it.appName == query -> 3
it.appName.startsWith(query) -> 2
else -> 1
}
}.thenByDescending { it.lastUsedTime })
}
嗯,到这里,搜索索引、模糊匹配、拼音搜索和排序规则就都讲完了。说白了,这三块是环环相扣的:索引让搜索变快,匹配让搜索变准,排序让结果变好用。少了哪一块,用户体验都会打折扣。
我当年第一次做 AllApps 搜索时,只做了精确匹配,结果被用户吐槽「连微信都搜不到」。后来加上拼音搜索和模糊匹配,反馈就好多了。所以,别嫌麻烦,该做的功能一个都不能少。
公众号:蓝海资料掘金营,微信deep3321