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的%通配符只能匹配任意字符,但它是从左到右匹配的。说白了,你搜「李明」,它只找「李明」开头或结尾的,中间插了字就不行。
我个人习惯的做法是:小数据量(<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只会高亮第一个匹配的。所以如果你要做多关键词高亮,得自己解析。
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的索引文件会占用额外存储空间。如果你的联系人数量超过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」这种半截子查询。后来我改成只在用户点击搜索按钮或按回车时才保存,问题就解决了。
10.5 知识体系总览
下面这张图,我把本章的核心知识点串起来了。你可以看到,从基础的LIKE查询到高级的FTS4全文搜索,再到搜索建议,是一条完整的链路。
说白了,搜索这件事没有银弹。你得根据数据量、用户场景、设备性能来权衡。我个人习惯的做法是:先用LIKE查询快速实现,等用户量上来后再迁移到FTS4。搜索建议则是一开始就加上,因为它的实现成本很低,但对体验的提升很明显。
核心要点回顾:
- LIKE查询适合小数据量,记得加COLLATE NOCASE
- SearchSnippet列返回HTML格式的高亮片段,直接渲染即可
- FTS4比FTS3更强大,推荐直接使用FTS4
- FTS4的MATCH语法支持前缀匹配,但需要维护docid映射
- SearchRecentSuggestionsProvider实现搜索建议,注意保存时机