12、应用列表(AllApps)的搜索与排序:搜索索引的构建、模糊匹配与拼音搜索、应用排序规则(安装时间、使用频率)

应用列表,也就是我们常说的 AllApps,是桌面系统里用户最常打交道的界面之一。用户装了几百个应用后,想快速找到某个应用,靠手动翻页?那体验可就太糟糕了。所以,搜索和排序就成了 AllApps 的核心能力。

这一节,我们就来聊聊搜索索引怎么建、模糊匹配和拼音搜索怎么实现,以及排序规则怎么定。嗯,这里面的坑不少,我当年踩过好几个,咱们一个一个说。

核心知识点一览:

  • 搜索索引的构建策略(内存 vs 数据库)
  • 模糊匹配算法(编辑距离、前缀匹配)
  • 拼音搜索的实现(拼音库 + 分词)
  • 排序规则(安装时间、使用频率、字母序)
AllApps 搜索与排序知识体系 AllApps 搜索与排序 搜索索引构建 • 内存索引(HashMap + Trie) • 数据库索引(SQLite FTS) • 增量更新 vs 全量重建 模糊匹配与拼音搜索 • 编辑距离(Levenshtein) • 拼音首字母匹配 • 全拼 + 模糊音支持 排序规则 • 安装时间(倒序) • 使用频率(LRU 统计) • 字母序(A-Z) 三者协同:索引加速匹配 → 匹配结果排序 → 展示给用户

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」,结果匹配到「银行」和「航行」——因为「行」是多音字。后来我加了上下文判断,才解决这个问题。

拼音搜索的实现思路:

  1. 把应用名转成拼音全拼,比如「微信」→「weixin」
  2. 把应用名转成拼音首字母,比如「微信」→「wx」
  3. 用户输入时,同时匹配全拼和首字母
  4. 支持模糊音,比如「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