如何在JavaScript中实现类Typesense的网站即时搜索功能
客户端前缀实时搜索的轻量替代方案(10K级数据)
针对你10K级客户端数据的前缀实时搜索需求,这里有几个轻量替代方案,完全不用依赖Typesense这类服务:
方案1:原生JS数组过滤(最简便)
直接利用数组的filter方法结合startsWith做前缀匹配,10K数据量下性能完全够用,不用额外构建数据结构,代码零成本。
示例代码:
// 假设加载后的JSON数据是一个键数组 const keywords = ["stranger", "strongman", "stranded", "apple", "banana"]; const inputBox = document.getElementById("search-input"); const resultList = document.getElementById("result-list"); inputBox.addEventListener("input", (e) => { const prefix = e.target.value.trim().toLowerCase(); if (!prefix) { resultList.innerHTML = ""; return; } // 过滤出所有以前缀开头的关键词(忽略大小写) const matches = keywords.filter(keyword => keyword.toLowerCase().startsWith(prefix) ); // 渲染结果 resultList.innerHTML = matches.map(word => `<li>${word}</li>`).join(""); });
优点:代码极简,无需额外逻辑,适合快速实现;缺点:每次输入都要遍历整个数组,10K数据虽然没问题,但极端情况下(比如输入单个字符)会遍历全部元素,不过客户端完全能扛住。
方案2:提前构建前缀索引(优化查询速度)
如果想进一步提升查询效率,可以提前把所有可能的前缀和对应的关键词做映射,查询时直接取结果,不用遍历数组。
示例代码:
const keywords = ["stranger", "strongman", "stranded", "apple", "banana"]; const prefixIndex = {}; // 构建前缀索引 keywords.forEach(keyword => { const lowerKeyword = keyword.toLowerCase(); // 生成该关键词的所有可能前缀(从1个字符到完整长度) for (let i = 1; i <= lowerKeyword.length; i++) { const prefix = lowerKeyword.slice(0, i); if (!prefixIndex[prefix]) { prefixIndex[prefix] = []; } // 去重(如果有重复关键词的话) if (!prefixIndex[prefix].includes(keyword)) { prefixIndex[prefix].push(keyword); } } }); // 搜索逻辑 inputBox.addEventListener("input", (e) => { const prefix = e.target.value.trim().toLowerCase(); const matches = prefixIndex[prefix] || []; resultList.innerHTML = matches.map(word => `<li>${word}</li>`).join(""); });
优点:查询时直接O(1)取结果,响应更快;缺点:构建索引需要一点初始化时间,且会占用额外内存(10K数据的话,内存开销完全可以忽略)。
方案3:Trie前缀树(专业前缀匹配结构)
如果追求最专业的前缀匹配性能,实现一个轻量Trie树,插入所有关键词后,搜索前缀时直接遍历树结构收集结果,查询效率最优。
示例代码:
// 实现一个简单的Trie树节点 class TrieNode { constructor() { this.children = {}; this.isEnd = false; this.word = null; // 存储完整关键词 } } class Trie { constructor() { this.root = new TrieNode(); } // 插入关键词 insert(word) { let node = this.root; const lowerWord = word.toLowerCase(); for (const char of lowerWord) { if (!node.children[char]) { node.children[char] = new TrieNode(); } node = node.children[char]; } node.isEnd = true; node.word = word; } // 搜索前缀,返回所有匹配的关键词 searchPrefix(prefix) { let node = this.root; const lowerPrefix = prefix.toLowerCase(); for (const char of lowerPrefix) { if (!node.children[char]) { return []; // 没有匹配的前缀 } node = node.children[char]; } // 收集当前节点下所有的关键词 const matches = []; this.collectWords(node, matches); return matches; } // 递归收集所有子节点的关键词 collectWords(node, matches) { if (node.isEnd) { matches.push(node.word); } for (const char in node.children) { this.collectWords(node.children[char], matches); } } } // 初始化Trie树 const trie = new Trie(); keywords.forEach(word => trie.insert(word)); // 搜索逻辑 inputBox.addEventListener("input", (e) => { const prefix = e.target.value.trim(); const matches = prefix ? trie.searchPrefix(prefix) : []; resultList.innerHTML = matches.map(word => `<li>${word}</li>`).join(""); });
优点:前缀查询效率极高,尤其是前缀较长时,性能优势明显;缺点:需要额外写Trie的实现代码,不过代码量不大,且可以复用。
方案选择建议
- 快速实现选方案1,代码最少;
- 追求查询速度选方案2,初始化代价小;
- 频繁搜索或未来数据可能扩容选方案3,扩展性最好。
内容的提问来源于stack exchange,提问作者StrangerDanger
相关产品推荐
相关产品推荐

