直接用searchText字段建索引+IDBKeyRange.bound实现毫秒级前缀匹配,无需倒排索引;需小写去标点预处理、防空查、去重更新、本地时间排序,并结合AbortController节流。
直接用 searchText 字段建索引 + 游标范围查询,就能实现毫秒级前缀匹配,不需要倒排索引或分词库。
用户输入“net”想搜“network”“netlify”,searchText.includes('net') 能命中,但也会误中“internet”“connect”——这是子串匹配的固有缺陷;而正则在 IndexedDB 游标里根本不能用于索引扫描,只能遍历后过滤,10 万条记录就是 10 万次字符串操作,卡顿明显。更关键的是,IDBKeyRange.bound() 对字符串索引天然支持字典序范围查询,前缀匹配正是它的强项。
前缀匹配对大小写和符号极其敏感。如果用户搜 “React”,而存的是 "react" 或 "React!",IDBKeyRange.bound('react', 'reactuFFFF') 就会漏掉。所以写入时必须统一预处理:
searchTerm = input.trim().toLowerCase().replace(/[^ws]/g, '')searchPrefix: searchTerm
searchPrefix 上建**非唯一索引**:objectStore.createIndex('idx_prefix', 'searchPrefix', { unique: false })
核心不是遍历后过滤,而是让游标只扫「可能匹配」的键区间。例如搜 “net”,就构造从 "net" 开始、到 "netuFFFF" 结束的范围(uFFFF 是 Unicode 最大码点,确保覆盖所有以 “net” 开头的字符串):
const range = IDBKeyRange.bound(query, query + 'uFFFF');const index = objectStore.index('idx_prefix');const cursorRequest = index.openCursor(range);
这样游标只落在 "net"、"network"、"netlify" 等键上,完全跳过无关记录。实测 5 万条历史记录下,查询响应稳定在 3–8ms。
注意两个坑:
IDBKeyRange.bound('', 'uFFFF') 会扫全量,务必提前拦截:if (!query) return []
搜索历史不是日志流,要防刷屏式重复提交(比如用户连敲 “a”、“ab”、“abc”)。建议:
index.get(query) 查是否存在;存在就用 put() 更新 updatedAt 字段,不新增keyPath: 'searchPrefix' —— 这样同词天然去重,且索引查询时能直接定位timestamp: Date.now() 字段,查完前缀匹配结果后,用 Array.sort((a, b) => b.timestamp - a.timestamp) 本地排序(数据量不大时比建复合索引更轻量)真正难的不是查得快,而是让用户觉得“刚输完就出来了”——这取决于你是否把游标查询包进了 AbortController,并在输入停顿 200ms 后才触发。没做这个,再快的查询也显得卡。