{ "name": { "firstName": "yi", "lastName": "li" }, "age": 23, "roles": ['developer', 'admin'], "projects": [{ "name": "demo", "repo": "" }] }页面上提供一个搜索框,用户通过输入搜索的内容可以找到包含这个内容的数据。注意,只要任意数据对象的任意属性值 (比如在上面的数据结构中,只要 name, age, roles 任何一个属性的值)包含这个关键词即可。如果属性值是数组或者对象,那么数组的元素或者对象的值继续对输入内容进行匹配检测,并递归的检测下去,只要有命中,便算该数据匹配。如何设计这个功能,让搜索功能尽可能的快?
2.如果要求够快的话遍历我就输了
const o = { message: 'ack' fruit: 'apple', unit: 'an', name: 'anna', }建立的树状结构如下:
root--a |--c |--k |--p |--p |--l |--e |--n |--n |--a当用户搜索 apple 时,从a开始访问,至最后访问到字母 e 时,若在树中有对应的节点,表示命中;当用户搜索 aha 时,在访问 h 时就已经无法在树中找到对应的节点了,表示该对象不符合搜索条件。但实际工作中我们会有非常多个对象值,多个对象值之间可能有重复的值,所以匹配时,我们要把所有可能的匹配结果都返回。比如
[ { id: 1, message: 'ack' fruit: 'apple', unit: 'an', name: 'anna', }, { id: 2, message: 'ack' fruit: 'banana', unit: 'an', name: 'lee', }, ]上面两个对象有相同的值 ack 和 an,所以在树上的叶子节点中我们还要添加对象的 id 辨识信息
root--a |--c |--k (ids: [1,2]) |--p |--p |--l |--e (ids: [1]) |--n (ids: [1, 2]) |--n |--a (ids: [1])这样当用户搜索 an 时,我们能返回所有的匹配项。
{ "results": [ { "gender": "male", "email": "enzo.dumont@example.com", "phone": "02-65-13-26-00", "cell": "06-09-02-19-99", "nat": "FR" }, { "gender": "male", "email": "gerald.omahony@example.com", "phone": "011-376-3811", "cell": "081-697-1414", "nat": "IE" } //... ] }叶子节点数据结构
class Leaf { constructor(id = "", value = "") { this.ids = id ? [id] : []; this.value = value; this.children = {}; } share(id) { this.ids.push(id); } }share方法用于向该叶子节点添加多个相同的匹配的id
distinct: 移除一个数组中的重复元素
[ { id: 1, message: 'ack' fruit: 'apple', unit: 'an', name: 'anna', }, { id: 2, message: 'ack' fruit: 'banana', unit: 'an', name: 'lee', }, ]扁平化之后为
{ '1': { id: 1, message: 'ack' fruit: 'apple', unit: 'an', name: 'anna', }, '2': { id: 2, message: 'ack' fruit: 'banana', unit: 'an', name: 'lee', } }之所以要这么做是为了当检索结果返回一个 id 数组时:[1, 2, 3],我们只需要遍历一边返回结果就能通过 id 在扁平化的 Map 里立即找到对应的数据。否则还要不停的遍历原始数据数组找到对应的数据。因为 randomuser.me 返回的信息中不包含 id 信息,所以我们暂时用 email 信息作为唯一标示。normalize 的实现如下:
function normalize(identify, data) { const id2Value = {}; data.forEach(item => { const idValue = item[identify]; id2Value[idValue] = item; }); return id2Value; }构建一棵树
fetch("https://randomuser.me/api/?results=5000&inc=gender,email,phone,cell,nat") .then(response => { return response.json(); }) .then(data => { const { results } = data; const root = new Leaf(); const identifyKey = "email"; results.forEach(item => { const identifyValue = item[identifyKey]; Object.values(item).forEach(itemValue => { // 注意这里会把 Number 和 Boolean 类型也字符串化 const stringifiedValue = String(itemValue); let tempRoot = root; const arraiedStringifiedValue = Array.from(stringifiedValue); arraiedStringifiedValue.forEach((character, characterIndex) => { const reachEnd = characterIndex === arraiedStringifiedValue.length - 1; if (!tempRoot.children[character]) { tempRoot.children[character] = new Leaf( reachEnd ? identifyValue : "", character ); tempRoot = tempRoot.children[character]; } else { if (reachEnd) { tempRoot.children[character].share(identifyValue); } tempRoot = tempRoot.children[character]; } }); }); });模糊搜索
function searchBlurry(root, keyword, userMap) { const keywordArr = Array.from(String(keyword)); let tempRoot = root; let result = []; for (let i = 0; i < keywordArr.length; i++) { const character = keywordArr[i]; if (!tempRoot.children[character]) { break; } else { tempRoot = tempRoot.children[character]; } if (keywordArr.length - 1 === i) { result = [ ...tempRoot.ids, ...collectChildrenInsideIds(tempRoot.children) ]; } } return distinct(result).map(id => { return userMap[id]; }); }注意这里有一个collectChildrenInsideIds方法,这个方法用于收集该叶子节点下所有的子节点的 id。这么做是因为当前操作模糊匹配,当你搜索a时,apple, anna, ack 都算匹配。
function regularSearch(searchKeyword) { const regularSearchResults = []; results.forEach(item => { for (const key in item) { const value = item[key]; if (String(value).startsWith(searchKeyword)) { regularSearchResults.push(item); break; } } }); return regularSearchResults }注意在测试对象值是否匹配搜索词时,我们使用了startsWith,而不是indexOf,这是因为字典树的缺陷在于只能匹配以搜索词开头的词!比如当你搜索a时,只能匹配apple、anna而不能匹配banana。为了便于对比,我们不得不使用startsWith
2.当数据量较大时,比如 5000 条的情况下,当你的搜索词非常短小,比如a,那么字典树的查找效率会比遍历搜索低,也就是反而花费的时间长;当搜索词变得具体时,比如ali,字典树的查找效率会比遍历搜索高。
function decorateWithChildrenIds(root) { // 堆代码 duidaima.com const { children } = root; root.childrenIds = collectChildrenInsideIds(root.children); for (const character in children) { const characterLeaf = children[character]; characterLeaf.childrenIds = collectChildrenInsideIds( characterLeaf.children ); decorateWithChildrenIds(characterLeaf); } }那么在构建完树之后,用这个方法把所有叶子节点「装饰」一遍就好了。