假設你需要在前端展示 5000 條甚至更多的資料,每一條資料的資料結構是乙個物件,裡面有各種各樣的屬性。每個屬性的值又可以是基本型別,物件,甚至陣列。這裡的物件或者陣列內部的元素又可以繼續包含物件或者陣列並且允許無限巢狀下去。比如
, "age": 23, "roles": ['developer', 'admin'], "projects":
}
如何設計這個功能,讓搜尋功能盡可能的快?
如果你稍有程式設計師的敏感度,此時你的腦海裡應該有兩個念頭:
的確,遍歷是最簡單但也是最慢的。所以通常的優化方法之一是通過空間換取時間;而另乙個方法……稍後再引出。
這裡我們嘗試通過建立字典樹(trie)來優化搜尋。
如果你還不了解什麼是字典樹,下面做簡單的介紹:假設我們有乙個簡單的物件,鍵值的對應關係如下:
我們根據「鍵」的字母出現順次構建出一棵樹出來,葉子節點值即有可能是某個「鍵」的值
那麼此時無論使用者想訪問任何屬性的值,只要從樹的根節點出發,依據屬性字母出現的順序訪問樹的葉子節點,即可得到該屬性的值。比如當我們想訪問tea
時:
但是在我們需要解決的場景中,我們不需要關心「屬性」,我們只關心「值」是否匹配上搜尋的內容。所以我們只需要對「值」建立字典樹。
假設有以下的物件值
const o =
建立的樹狀結構如下:
root--a |--c |--k |--p |--p |--l |--e |--n |--n |--a
但實際工作中我們會有非常多個物件值,多個物件值之間可能有重複的值,所以匹配時,我們要把所有可能的匹配結果都返回。比如
[
, ,
]
上面兩個物件有相同的值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
時,我們能返回所有的匹配項
ok,有了思路之後我們開始實現**。
首先要解決的乙個問題是如果快速的偽造 5000 條資料?這裡我們使用 開源 api。為了簡單起見,我們讓它只返回gender
,email
,phone
,cell
,nat
基本資料型別的值,而不返回巢狀結構(物件和陣列)。注意這裡只是為了便於**展示和理解,略去了複雜的結構,也就避免了複雜的**。加入複雜結構之後**其實也沒有大的變化,只是增加了遍歷的邏輯和遞迴邏輯而已。
請求 ?results=5000&inc=gender,email,phone,cell,nat 結果如下:
,
//...
]}
根據思路中的描述,資料結構描述如下:
class leaf ;
} share(id)
}
share
方法用於向該葉子節點新增多個相同的匹配的id
在編碼的過程中我們需要一些幫助函式,比如:
這兩個函式可以借用lodash
類庫實現,即使手動實現起來也很簡單,這裡就不贅述了
另乙個重要的方法是normalize
,我更習慣將normalize
翻譯為「扁平化」(而不是「標準化」),因為這樣更形象。該方法用於將乙個陣列裡的物件拆分為 id 與物件的對映關係。
比如將
[
, ,
]
扁平化之後為
,
'2':
}
之所以要這麼做是為了當檢索結果返回乙個 id 陣列時:[1, 2, 3]
,我們只需要遍歷一邊返回結果就能通過 id 在扁平化的map
裡立即找到對應的資料。否則還要不停的遍歷原始資料陣列找到對應的資料.
因為 randomuser.me 返回的資訊中不包含 id 資訊,所以我們暫時用 email 資訊作為唯一標示。normalize
的實現如下:
function normalize(identify, data) ;
data.foreach(item => ); return id2value;
}
這部分**就沒有什麼秘密了,完全是按照遞演算法歸構建一顆樹了
fetch("?results=5000&inc=gender,email,phone,cell,nat")
.then(response => )
.then(data => = data; const root = new leaf(); const identifykey = "email";
results.foreach(item => else
temproot = temproot.children[character];
}});
});});
搜尋部分**也沒有什麼秘密,按圖索驥而已:
function searchblurry(root, keyword, usermap) else if (keywordarr.length - 1 === i)
} return distinct(result).map(id => );
}
為了對比效率,並且為了測試搜尋結果的正確性,我們仍然需要編寫乙個常規的遍歷的搜尋方法:
function regularsearch(searchkeyword)
}}); return regularsearchresults
}
效能的對比結果是很有意思的:
效率反而低的問題不難想到是為什麼:當你搜尋詞簡單時,訪問的葉子節點會少,所以只能掃瞄children
收集子節點的所有的可能 id,這步操作中遍歷的過程占用了大部分時間
但是我們仍然需要滿足這部分的查詢需求,所以我們要針對這個場景做一些優化
我們回想一下簡單搜尋的場景,效能的瓶頸主要在於我們需要遍歷葉子節點下的所有子節點。好辦,鑑於樹構建完之後不會再發生變化,那麼我們只需要提前計算好每個葉子節點的所有子 id 就好了,這就是文章開頭說的第二類優化方案,即預計算。
我編寫了乙個新的方法,用於遞迴的給每個葉子節點新增它所有子節點的 id:
function decoratewithchildrenids(root) = root;
root.childrenids = collectchildreninsideids(root.children); for (const character in children)
}
Data Retrieval 資料檢索
index 索引 定義 分類 1 結構化資料 固定格式 有限長度 應用 資料庫 元資料 2 非結構化資料 非定格式 非限長度 應用 磁碟檔案 查詢方式 1 結構化查詢 資料庫搜尋 2 非結構化查詢 a 順序掃瞄 b 全文檢索定義 根據使用者需求,從資料庫提取資料,生成資料表。資料表 可放回資料庫,也...
基本資料檢索
2016.11.28 二 基本資料檢索 select from table select 和 from 號是特殊符號,它表示所有的列,這句話的意思就是從 table 中查詢所有的列。在mysql和 oracle 中要求每句話的末尾要加乙個分號 但在 sqlserver 中不適用。2.1 查詢指定列 ...
Nutla 全文檢索千億資料檢索框架
全文檢索千億資料檢索框架 nutla 核心結構 lucene hadoop 分布式搜尋執行框架 概述不管程式效能有多高,機器處理能力有多強,都會有其極限。能夠快速方便的橫向與縱向擴充套件是nut設計最重要的原則,以此原則形成以分布式平行計算為核心的架構設計。以分布式平行計算為核心的架構設計是nut區...