在移动应用开发中,高效实现全文搜索功能是提升用户体验的关键。本文将深入探讨如何利用SQLite的FTS4模块实现高性能全文搜索,并提供完整的Kotlin实现方案。
一、FTS4技术原理与优势
FTS4与普通SQLite表的对比
特性 | 普通表 | FTS4虚拟表 |
---|---|---|
搜索速度 | 慢(全表扫描) | 极快(倒排索引) |
模糊匹配 | 仅支持简单LIKE | 支持高级搜索语法 |
词干处理 | 不支持 | 支持Porter等词干分析器 |
结果排序 | 无相关性排序 | 支持相关性排名 |
存储空间 | 小 | 较大(1-3倍原始数据) |
FTS4核心原理
FTS4通过创建倒排索引实现高效搜索:
- 将文本分解为词元(token)
- 建立词元到文档的映射关系
- 使用BM25算法计算匹配相关性
graph TD
A[原始文本] --> B[分词处理]
B --> C[创建倒排索引]
C --> D[存储词元位置信息]
D --> E[高效查询处理]
二、完整实现步骤
1. 创建FTS4虚拟表
kotlin
const val CREATE_FTS_TABLE = """
CREATE VIRTUAL TABLE IF NOT EXISTS documents USING fts4(
id INTEGER PRIMARY KEY,
title TEXT,
content TEXT,
tokenize=porter -- 使用Porter词干分析器
)
"""
fun createFtsTable(db: SQLiteDatabase) {
db.execSQL(CREATE_FTS_TABLE)
}
2. 插入与索引数据
kotlin
fun insertDocument(db: SQLiteDatabase, title: String, content: String) {
val values = ContentValues().apply {
put("title", title)
put("content", content)
}
db.insert("documents", null, values)
}
// 批量插入优化
fun bulkInsertDocuments(db: SQLiteDatabase, documents: List<Pair<String, String>>) {
db.beginTransaction()
try {
documents.forEach { (title, content) ->
insertDocument(db, title, content)
}
db.setTransactionSuccessful()
} finally {
db.endTransaction()
}
}
3. 执行高级搜索
kotlin
data class SearchResult(
val id: Long,
val title: String,
val snippet: String,
val score: Double
)
fun searchDocuments(db: SQLiteDatabase, query: String): List<SearchResult> {
val results = mutableListOf<SearchResult>()
// 使用MATCH操作符进行全文搜索
val sql = """
SELECT
docid AS id,
title,
snippet(documents, '<b>', '</b>', '...', 1, 20) AS snippet,
matchinfo(documents) AS matchInfo
FROM documents
WHERE documents MATCH ?
ORDER BY bm25(matchinfo) ASC
""".trimIndent()
db.rawQuery(sql, arrayOf(query)).use { cursor ->
while (cursor.moveToNext()) {
val matchInfo = cursor.getBlob(cursor.getColumnIndex("matchInfo"))
val score = calculateBm25Score(matchInfo) // 计算BM25相关性分数
results.add(SearchResult(
id = cursor.getLong(cursor.getColumnIndex("id")),
title = cursor.getString(cursor.getColumnIndex("title")),
snippet = cursor.getString(cursor.getColumnIndex("snippet")),
score = score
))
}
}
return results
}
// BM25相关性计算
fun calculateBm25Score(matchInfo: ByteArray): Double {
// 简化的BM25计算(实际实现需解析matchinfo二进制结构)
val hits = matchInfo.size / 4 // 估算匹配次数
return 1.0 - (1.0 / (hits + 1)) // 实际项目应使用标准BM25算法
}
4. 支持的高级搜索语法
kotlin
// 前缀搜索
fun searchPrefix(db: SQLiteDatabase, prefix: String) {
val query = "$prefix*" // 添加星号表示前缀搜索
searchDocuments(db, query)
}
// 短语搜索
fun searchExactPhrase(db: SQLiteDatabase, phrase: String) {
val query = "\"$phrase\"" // 使用双引号包裹短语
searchDocuments(db, query)
}
// 布尔搜索
fun booleanSearch(db: SQLiteDatabase, term1: String, term2: String) {
val queries = listOf(
"$term1 AND $term2", // 必须同时包含
"$term1 OR $term2", // 包含任意一个
"$term1 NOT $term2" // 包含term1但不包含term2
)
queries.forEach { query ->
searchDocuments(db, query)
}
}
三、性能优化技巧
1. 索引维护策略
kotlin
// 定期优化索引
fun optimizeFtsIndex(db: SQLiteDatabase) {
db.execSQL("INSERT INTO documents(documents) VALUES('optimize')")
}
// 重建索引(数据变更量大时使用)
fun rebuildFtsIndex(db: SQLiteDatabase) {
db.execSQL("INSERT INTO documents(documents) VALUES('rebuild')")
}
// 删除旧数据后优化
fun deleteOldDocuments(db: SQLiteDatabase, cutoffDate: Long) {
db.delete("documents", "timestamp < ?", arrayOf(cutoffDate.toString()))
optimizeFtsIndex(db)
}
2. 分页查询优化
kotlin
fun searchWithPagination(db: SQLiteDatabase, query: String,
page: Int, pageSize: Int): List<SearchResult> {
val offset = page * pageSize
val sql = """
SELECT ...
WHERE documents MATCH ?
ORDER BY bm25(matchinfo) ASC
LIMIT $pageSize OFFSET $offset
"""
// 执行分页查询...
}
3. 存储优化策略
kotlin
// 使用内容压缩
fun insertCompressedContent(db: SQLiteDatabase, title: String, content: String) {
val compressed = GZIP.compress(content) // 使用GZIP压缩
val values = ContentValues().apply {
put("title", title)
put("content", compressed)
}
db.insert("documents", null, values)
}
// 查询时解压
fun getDocumentContent(db: SQLiteDatabase, id: Long): String {
val cursor = db.query("documents", arrayOf("content"),
"docid=?", arrayOf(id.toString()),
null, null, null)
return cursor.use {
if (it.moveToFirst()) {
val compressed = it.getBlob(it.getColumnIndex("content"))
GZIP.decompress(compressed) // 解压内容
} else ""
}
}
四、FTS4与FTS5对比
功能对比表
特性 | FTS4 | FTS5 |
---|---|---|
SQLite版本要求 | ≥ 3.7.4 | ≥ 3.9.0 |
索引大小 | 较大 | 更小 |
搜索性能 | 快 | 更快 |
自定义分词器 | 复杂 | 简单 |
BM25排序 | 需手动计算 | 内置支持 |
结果高亮 | 支持 | 支持更优 |
迁移到FTS5示例
kotlin
// 创建FTS5表
const val CREATE_FTS5_TABLE = """
CREATE VIRTUAL TABLE IF NOT EXISTS documents_fts5 USING fts5(
title,
content,
tokenize = 'porter'
)
"""
// 迁移数据
fun migrateToFts5(db: SQLiteDatabase) {
db.execSQL("ALTER TABLE documents RENAME TO documents_old")
db.execSQL(CREATE_FTS5_TABLE)
db.execSQL("""
INSERT INTO documents_fts5(title, content)
SELECT title, content FROM documents_old
""")
db.execSQL("DROP TABLE documents_old")
}
五、完整示例应用
文档搜索管理器
kotlin
class DocumentSearcher(context: Context) {
private val dbHelper = DatabaseHelper(context)
inner class DatabaseHelper(context: Context) :
SQLiteOpenHelper(context, "docs.db", null, 1) {
override fun onCreate(db: SQLiteDatabase) {
db.execSQL(CREATE_FTS_TABLE)
}
override fun onUpgrade(db: SQLiteDatabase, oldVersion: Int, newVersion: Int) {
// 升级处理
}
}
fun addDocument(title: String, content: String) {
val db = dbHelper.writableDatabase
insertDocument(db, title, content)
}
fun search(query: String): List<SearchResult> {
val db = dbHelper.readableDatabase
return searchDocuments(db, query)
}
fun optimize() {
val db = dbHelper.writableDatabase
optimizeFtsIndex(db)
}
}
// 使用示例
fun main() {
val searcher = DocumentSearcher(context)
// 添加文档
searcher.addDocument(
"Kotlin Coroutines",
"Coroutines are lightweight threads for asynchronous programming."
)
searcher.addDocument(
"SQLite Optimization",
"Advanced techniques for optimizing SQLite performance."
)
// 执行搜索
val results = searcher.search("optimize OR performance")
results.forEach {
println("${it.title}: ${it.snippet} [Score: ${it.score}]")
}
// 优化索引
searcher.optimize()
}
六、关键点总结
-
索引创建:
- 使用
VIRTUAL TABLE
语法创建FTS4表 - 选择合适的tokenizer(porter适合英文)
- 仅索引必要字段
- 使用
-
高效搜索:
- 使用
MATCH
操作符而非LIKE
- 利用前缀(
term*
)、短语("exact phrase"
)和布尔搜索 - 实现BM25相关性排序
- 使用
-
性能优化:
- 批量插入使用事务
- 定期执行
optimize
命令 - 大文本内容使用压缩
- 分页处理搜索结果
-
进阶技巧:
- 使用
snippet()
实现结果高亮 - 解析
matchinfo
获取详细匹配数据 - 考虑迁移到FTS5获得更好性能
- 使用
-
适用场景:
- 移动端本地搜索(Android/iOS)
- 桌面应用全文检索
- 中小型数据集的快速搜索需求