10、联系人搜索:使用LIKE查询、SearchSnippet列、全文搜索(FTS3/FTS4)、搜索建议(SearchRecentSuggestionsProvider)

搜索功能,说白了就是联系人的「门面」。用户打开通讯录,第一件事就是搜名字。如果搜得慢、搜不准,体验直接崩盘。我早期做联系人模块时,就吃过这个亏——用户输入「张三」,结果只匹配了「张」字开头的,中间带「三」的反而没出来。嗯,今天咱们就把搜索这块彻底讲透。

10.1 基础搜索:LIKE查询的坑与优化

最朴素的搜索方式,就是用SQL的LIKE语句。比如这样:

String selection = ContactsContract.Contacts.DISPLAY_NAME + " LIKE ?";
String[] selectionArgs = new String[]{"%" + query + "%"};
Cursor cursor = getContentResolver().query(
    ContactsContract.Contacts.CONTENT_URI,
    null, selection, selectionArgs, null);

这段代码看起来没问题,对吧?但我在项目中遇到过一个问题:当用户输入「李明」时,LIKE "%李明%" 只能匹配到「李明」,却匹配不到「李小明」。为什么?因为LIKE的%通配符只能匹配任意字符,但它是从左到右匹配的。说白了,你搜「李明」,它只找「李明」开头或结尾的,中间插了字就不行。

⚠️ 注意:LIKE查询在数据量大时性能极差。因为它是全表扫描,不走索引。如果你的联系人超过5000条,用户输入第一个字符时,界面会卡顿。

我个人习惯的做法是:小数据量(<1000条)用LIKE没问题;大数据量必须上全文搜索。另外,LIKE查询还有个坑——大小写敏感。Android的SQLite默认是二进制比较,所以「zhang」和「Zhang」可能匹配不上。解决方案是用COLLATE NOCASE:

String selection = ContactsContract.Contacts.DISPLAY_NAME + " LIKE ? COLLATE NOCASE";

10.2 SearchSnippet列:高亮匹配结果

光搜出来还不够,你得告诉用户「为什么这条记录被搜到了」。比如用户搜「张」,结果里「张三」的「张」字应该高亮。这就是SearchSnippet列的用途。

SearchSnippet是ContactsContract.Contacts表里的一个虚拟列。它返回匹配片段的HTML格式字符串,自带高亮标签。用法如下:

String[] projection = new String[]{
    ContactsContract.Contacts._ID,
    ContactsContract.Contacts.DISPLAY_NAME,
    ContactsContract.Contacts.SEARCH_SNIPPET  // 注意这个列
};

Cursor cursor = getContentResolver().query(
    ContactsContract.Contacts.CONTENT_URI,
    projection,
    ContactsContract.Contacts.DISPLAY_NAME + " LIKE ?",
    new String[]{"%" + query + "%"},
    null);

if (cursor != null) {
    while (cursor.moveToNext()) {
        String snippet = cursor.getString(cursor.getColumnIndex(
            ContactsContract.Contacts.SEARCH_SNIPPET));
        // snippet 内容类似:<b>张</b>三
    }
}

你想想看,这个列返回的内容里,匹配的关键词会被<b>标签包裹。你直接把它渲染到TextView里,设置textAllCaps为false,然后调用Html.fromHtml()就能显示高亮。我曾经踩过一个坑:如果查询条件里用了多个关键词,SearchSnippet只会高亮第一个匹配的。所以如果你要做多关键词高亮,得自己解析。

💡 小技巧:SearchSnippet列在LIKE查询和全文搜索中都能用。但它的内容格式取决于底层实现,不同厂商的ROM可能有差异。建议在真机上测试。

10.3 全文搜索:FTS3/FTS4

好了,聊点硬核的。如果你觉得LIKE查询太慢,SearchSnippet又不够灵活,那就得上全文搜索了。Android的SQLite内置了FTS3和FTS4引擎。它们的工作原理是:把文本拆成词条(token),建立倒排索引。搜索时直接查索引,速度比LIKE快几个数量级。

FTS3和FTS4的区别,我直接给你列个表:

特性 FTS3 FTS4
性能 基础 优化(压缩、合并)
matchinfo 支持 更丰富
排名 需手动 内置bm25排序
兼容性 Android 2.2+ Android 3.0+

我个人建议:直接上FTS4。因为Android 4.0以上的设备已经占绝大多数了。FTS4的bm25排序算法能自动把匹配度高的结果排前面,省得你写排序逻辑。

怎么用?首先,你得创建一个FTS虚拟表:

CREATE VIRTUAL TABLE contacts_fts USING fts4(
    content="",  // 不存储原始数据,只存索引
    display_name,
    phone_number,
    email
);

然后,把联系人数据插入到这个虚拟表里:

INSERT INTO contacts_fts(docid, display_name, phone_number, email)
VALUES(1, '张三', '13800138000', 'zhangsan@example.com');

搜索时用MATCH语法:

SELECT * FROM contacts_fts WHERE contacts_fts MATCH '张*';

注意这里的星号是前缀匹配符。FTS4默认的tokenizer是simple,它会把中文按字符拆分。所以「张*」能匹配「张三」、「张伟」等。但如果你搜「张三」,它也能匹配,因为「张」和「三」都是独立词条。

⚠️ 重要:FTS4的虚拟表不存储原始数据。你需要在应用层维护一个映射关系:docid对应到ContactsContract中的_ID。否则你搜到了docid,却不知道对应哪个联系人。

我在项目中遇到过一个问题:FTS4的索引文件会占用额外存储空间。如果你的联系人数量超过10万,索引文件可能达到几十MB。所以建议在后台线程异步构建索引,并且只在首次启动或联系人变更时更新。

10.4 搜索建议:SearchRecentSuggestionsProvider

用户搜完一次,下次再搜同样的关键词,如果能直接显示历史记录,体验会好很多。Android提供了SearchRecentSuggestionsProvider,专门用来做这个。

实现步骤很简单。第一步,继承SearchRecentSuggestionsProvider:

public class ContactSearchSuggestionsProvider extends SearchRecentSuggestionsProvider {
    public static final String AUTHORITY = "com.example.contacts.provider";
    public static final int MODE = DATABASE_MODE_QUERIES;

    public ContactSearchSuggestionsProvider() {
        setupSuggestions(AUTHORITY, MODE);
    }
}

第二步,在AndroidManifest.xml里注册:

<provider
    android:name=".ContactSearchSuggestionsProvider"
    android:authorities="com.example.contacts.provider"
    android:exported="false" />

第三步,在用户执行搜索时,保存查询记录:

SearchRecentSuggestions suggestions = new SearchRecentSuggestions(
    context,
    ContactSearchSuggestionsProvider.AUTHORITY,
    ContactSearchSuggestionsProvider.MODE);
suggestions.saveRecentQuery(query, null);

第四步,在SearchView或SearchDialog中启用搜索建议:

SearchView searchView = findViewById(R.id.search_view);
searchView.setSearchableInfo(SearchManager.getSearchableInfo(getComponentName()));
// 系统会自动读取SearchRecentSuggestionsProvider中的数据

嗯,这里要注意:SearchRecentSuggestionsProvider默认只保存最近50条记录。如果你想调整这个数量,可以在setupSuggestions时传入第三个参数:

setupSuggestions(AUTHORITY, MODE, 100);  // 保存100条

我曾经犯过一个错误:在每次搜索时都调用saveRecentQuery,结果用户输入一个字符就保存一次,导致搜索建议里全是「张」、「张三」、「张三1」这种半截子查询。后来我改成只在用户点击搜索按钮或按回车时才保存,问题就解决了。

💡 建议:搜索建议的数据可以定期清理。比如在设置页面加一个「清除搜索历史」的选项,调用suggestions.clearHistory()即可。

10.5 知识体系总览

下面这张图,我把本章的核心知识点串起来了。你可以看到,从基础的LIKE查询到高级的FTS4全文搜索,再到搜索建议,是一条完整的链路。

联系人搜索知识体系 基础搜索 全文搜索 搜索建议 LIKE查询 + COLLATE NOCASE SearchSnippet列(高亮) FTS3 vs FTS4 对比 虚拟表 + MATCH语法 bm25排序 + 索引维护 SearchRecentSuggestionsProvider 保存/清除历史记录 核心原则:小数据用LIKE,大数据用FTS4,搜索建议提升体验

说白了,搜索这件事没有银弹。你得根据数据量、用户场景、设备性能来权衡。我个人习惯的做法是:先用LIKE查询快速实现,等用户量上来后再迁移到FTS4。搜索建议则是一开始就加上,因为它的实现成本很低,但对体验的提升很明显。

核心要点回顾:

  • LIKE查询适合小数据量,记得加COLLATE NOCASE
  • SearchSnippet列返回HTML格式的高亮片段,直接渲染即可
  • FTS4比FTS3更强大,推荐直接使用FTS4
  • FTS4的MATCH语法支持前缀匹配,但需要维护docid映射
  • SearchRecentSuggestionsProvider实现搜索建议,注意保存时机

公众号:蓝海资料掘金营,微信deep3321