基于Airgram(TDLib)实现Telegram全局聊天搜索耗时过长问题
问题描述
我正尝试复刻Telegram官方的全局聊天搜索功能,功能效果参考如下图:
我当前使用getChats方法执行搜索,但该方法仅返回聊天ID列表,而我还需要获取对应聊天的名称、头像信息,预期返回字段参考如下图:
目前我只能遍历返回的chatId列表,针对每个ID单独调用getChat方法拉取所需数据,但该方案存在严重的查询耗时问题,总耗时长达14秒,远高于Telegram官方搜索约2秒的响应速度。我已通读全部官方文档,未找到可传入聊天搜索关键词、同时返回聊天ID、标题、头像信息的接口,想确认是否存在更高效的实现方案。我的当前实现代码如下:
import BaseAction from "./BaseAction"; import airgram from "../airgram/airgram"; import { ChatsUnion, ChatUnion } from 'airgram'; class SearchChatsAction implements BaseAction { async run(name: string): Promise<any> { const output = await airgram.api.searchPublicChats({ query: name }); const promises: Array<any> = []; const result: Array<any> = []; for (const chatId of (output.response as ChatsUnion).chatIds) { promises.push( airgram.api.getChat({ chatId: chatId }).then(output => { result.push({ id: (output.response as ChatUnion).id, title: (output.response as ChatUnion).title }); }) ); } await Promise.all(promises); return result; } } export default SearchChatsAction;
解决方案
耗时问题和接口缺失无关,是实现逻辑的问题,按以下顺序优化即可把响应速度压到和官方客户端一致的水平:
- 先纠正接口选型错误:不要用
searchPublicChats做全局搜索,这个接口只会搜索带公开用户名的频道/机器人,搜不到本地私人聊天、已加入的私有群,和官方全局搜索的范围不一致。做全局搜索应该用searchChats接口,传入搜索关键词、需要返回的结果数量即可,这个接口返回chatId列表的同时,TDLib会自动把对应聊天的基础信息预加载到本地数据库,不需要全量重复拉取。 - 必须做本地聊天缓存:TDLib本身在本地持久化存储了所有已接触过的聊天详情,不需要每次搜索都调用
getChat拉取数据。初始化Airgram的时候新建一个Map<number, ChatUnion>结构的缓存池,监听updateNewChat、updateChatTitle、updateChatPhoto、updateChatLastMessage这几个事件,事件触发时直接把返回的chat对象写入/更新缓存。后续搜索拿到chatId列表后,优先从缓存里取标题、头像信息,90%以上的场景根本不需要发起额外的getChat请求。 - 控制接口请求并发数:如果遇到缓存未命中的chatId,不要一次性把所有请求丢进
Promise.all,Airgram底层的TDLib默认对同时处理的请求数有限制,无节制并发会导致请求排队等待,这就是当前实现14秒耗时的核心原因。写个简单的异步并发池,把同时发起的getChat请求数控制在3-5个即可,避免触发限流。 - 头像加载不要阻塞搜索结果返回:聊天头像是文件资源,不需要等所有头像都下载完再返回搜索结果。拿到chat对象里的头像文件ID后,优先返回标题、ID等文本信息,头像用懒加载的方式异步拉取,拉取时设置最高请求优先级,已经下载过的头像会直接返回本地路径,不会产生网络耗时。
优化后的参考实现代码:
import BaseAction from "./BaseAction"; import airgram from "../airgram/airgram"; import { ChatsUnion, ChatUnion } from 'airgram'; // 全局聊天缓存,初始化时通过update事件预填充 const chatCache = new Map<number, ChatUnion>(); // 初始化阶段注册缓存更新逻辑 airgram.on('updateNewChat', ({ response }) => { chatCache.set(response.chat.id, response.chat); }); airgram.on('updateChatTitle', ({ response }) => { const chat = chatCache.get(response.chatId); if (chat) chat.title = response.title; }); airgram.on('updateChatPhoto', ({ response }) => { const chat = chatCache.get(response.chatId); if (chat) chat.photo = response.photo; }); // 并发控制工具,限制同时请求数为3 async function asyncPool<T, R>(concurrency: number, items: T[], fn: (item: T) => Promise<R>): Promise<R[]> { const results: R[] = []; const executing = new Set<Promise<R>>(); for (const item of items) { const task = Promise.resolve().then(() => fn(item)); results.push(task); executing.add(task); task.finally(() => executing.delete(task)); if (executing.size >= concurrency) await Promise.race(executing); } return Promise.all(results); } class SearchChatsAction implements BaseAction { async run(name: string): Promise<any> { // 用searchChats替代searchPublicChats,匹配全局搜索范围 const output = await airgram.api.searchChats({ query: name, limit: 20 // 按需求设置返回结果数 }); const chatIds = (output.response as ChatsUnion).chatIds; const result: Array<any> = []; const missChatIds: number[] = []; // 优先查缓存 for (const chatId of chatIds) { const cacheChat = chatCache.get(chatId); if (cacheChat) { result.push({ id: cacheChat.id, title: cacheChat.title, photo: cacheChat.photo }); } else { missChatIds.push(chatId); } } // 缓存未命中的才发起请求,控制并发数为3 if (missChatIds.length > 0) { const missChats = await asyncPool(3, missChatIds, async (chatId) => { const res = await airgram.api.getChat({ chatId }); const chat = res.response as ChatUnion; chatCache.set(chatId, chat); // 写入缓存下次直接复用 return { id: chat.id, title: chat.title, photo: chat.photo }; }); result.push(...missChats); } return result; } } export default SearchChatsAction;
内容的提问来源于stack exchange,提问作者pibocik125
相关产品推荐
相关产品推荐

