功能实现:全局搜索
在ACGNGAME V1.0正式版本开发时,为了实现推荐游戏的在线召回,我们将数据库正式从MySQL迁移至了PostgreSQL。
同时将Tag,Platform,Company等信息抽离为单独实体,方便了快速筛选。但与此同时,Game表中原本的冗余字段被移除,原本的搜索功能变成了只能搜索标题与文章中的文本信息,给使用带来了不便。
所以我们重新设计了搜索功能,实现了更好的全文搜索
解决方案:PostgreSQL 的 trigram 索引
我们采用 PostgreSQL 的 gist_trgm_ops 操作符类构建 GIST 索引,实现基于 trigram(三元组) 的高效相似度搜索。核心思路是将文本拆分为重叠的三字符片段(如 "abc" → ["abc"]),通过比较这些片段的相似度计算匹配度。
数据库设计:关键表与索引
CREATE TABLE game_keywords (
game_id INTEGER NOT NULL
CONSTRAINT game_keywords_pk PRIMARY KEY,
keywords TEXT
);
CREATE INDEX game_keywords_game_id_index
ON game_keywords USING GIST (keywords GIST_TRGM_OPS);
其中此表的更新依赖于 updateGameSearchIndex 函数
fun updateGameSearchIndex(gameId: Int): Boolean {
val gameData = getGameById(gameId) ?: throw NotFoundException()
// 关键:构建统一关键词(所有字段 + 大写化)
val gameKeywords = StringBuilder()
.append(gameData.title)
.append(gameData.description)
.append(gameData.tags.map { "${it.chinese}-${it.english}-${it.japanese}" })
.append(gameData.companies.companyName + gameData.companies.companyDes)
.toString()
.uppercase() // 统一转大写,避免大小写干扰
// 更新或插入索引
val rows = database.insertOrUpdate(GameKeywordsTable) {
set(GameKeywordsTable.gameId, gameData.id)
set(GameKeywordsTable.keywords, gameKeywords)
onConflict(GameKeywordsTable.gameId) {
set(GameKeywordsTable.keywords, gameKeywords)
}
}
return rows >= 1
}
在每次GameTable被update或create时创建聚合数据,同时出于保险起见,设计一个定时任务每天空闲时间全量更新索引
@Scheduled(cron = "0 00 6 * * *")
fun updateAllGamesSearchIndex() {
val logger = logger()
executor.execute {
val gameIds = gameServices.listAllIds()
gameIds.chunked(BATCH_SIZE).forEach { batchIts ->
batchIts.forEach {
updateGameSearchIndex(it)
}
}
}
}