前端JavaScript實(shí)現(xiàn)大數(shù)據(jù)前后模糊搜索的方法詳解
一、背景
在特定的一些場(chǎng)景中,服務(wù)端可能會(huì)返回我們大量的數(shù)據(jù),并希望在前端處理模糊查詢。我們可以參考一下方案,如果有更好的實(shí)現(xiàn)方案可以直接添加。
二、解決方案
1、indexedDB
目前主流前端存儲(chǔ)方案:
特性 | cookie | localStorage | sessionStorage | indexedDB |
---|---|---|---|---|
數(shù)據(jù)生命周期 | 一般由服務(wù)器生成,可以設(shè)置過(guò)期時(shí)間;前端采用和js-cookie等組件也可以生成 | 除非被清理,否則一直存在;瀏覽器關(guān)閉還會(huì)保存在本地,但是不支持跨瀏覽器 | 頁(yè)面關(guān)閉就清理刷新依然存在,不支持跨頁(yè)面交互 | 限制為可用磁盤空間的 50%。 |
數(shù)據(jù)存儲(chǔ)大小 | 4K | 5M | 5M | 不限制大小 |
與服務(wù)端通信 | 每次都會(huì)攜帶在請(qǐng)求的header 中,對(duì)于請(qǐng)求性能有影響;同時(shí)由于請(qǐng)求中都帶有,所以也容易出現(xiàn)安全問(wèn)題 | 不參與 | 不參與 | 不參與 |
特點(diǎn) | 字符串鍵值對(duì)在本地存儲(chǔ)數(shù)據(jù) | 字符串鍵值對(duì)在本地存儲(chǔ)數(shù)據(jù) | 字符串鍵值對(duì)在本地存儲(chǔ)數(shù)據(jù) | 可以存儲(chǔ)大量數(shù)據(jù),提供接口來(lái)查詢,還可以建立索引 |
IndexedDB是個(gè)非關(guān)系型數(shù)據(jù)庫(kù),那我們也可以去進(jìn)行查詢搜索。對(duì)于搜索速度方面可能不是最優(yōu)解,但是要考慮業(yè)務(wù)實(shí)現(xiàn),例如:我們有一個(gè)接口,接口數(shù)據(jù)更新不是那么頻繁,并且數(shù)據(jù)量非常大,調(diào)用接口時(shí)間可能會(huì)很久,搜索某一項(xiàng)或者某個(gè)開(kāi)始的可能會(huì)更好點(diǎn)。
function init() { // 打開(kāi) IndexedDB 數(shù)據(jù)庫(kù) const openDB = window.indexedDB.open("myDatabase", 1); // 新建數(shù)據(jù)庫(kù)和數(shù)據(jù)庫(kù)版本號(hào)發(fā)生變化時(shí)才被調(diào)用 openDB.onupgradeneeded = function (event) { const db = event.target.result; // 創(chuàng)建對(duì)象存儲(chǔ)空間 const objectStore = db.createObjectStore("myObjectStore", { keyPath: "code", }); // 添加索引 // objectStore.createIndex("code", "code"); objectStore.createIndex("pCode", "pCode", { unique: false }); objectStore.createIndex("name", "name", { unique: false }); }; openDB.onsuccess = async function (event) { const db = event.target.result; const dataArray = data; // 獲取事務(wù)對(duì)象 const transaction = db.transaction(["myObjectStore"], "readwrite"); // 獲取對(duì)象存儲(chǔ)空間 const objectStore = transaction.objectStore("myObjectStore"); // 向?qū)ο蟠鎯?chǔ)添加對(duì)象 dataArray.forEach(function (data) { const addRequest = objectStore.add(data); addRequest.onsuccess = function () { console.log("數(shù)據(jù)已成功添加到對(duì)象存儲(chǔ)"); }; addRequest.onerror = function (error) { console.error("添加數(shù)據(jù)時(shí)發(fā)生錯(cuò)誤", error); }; }); }; }
function search() { window.result = []; // 打開(kāi)數(shù)據(jù)庫(kù) const openRequest = window.indexedDB.open("myDatabase", 1); // 打開(kāi)數(shù)據(jù)庫(kù)成功回調(diào) openRequest.onsuccess = function (event) { // 數(shù)據(jù)庫(kù)對(duì)象 const db = event.target.result; // 打開(kāi)只讀事務(wù)并獲取對(duì)象存儲(chǔ) readwrite 讀寫 readonly 只讀 const transaction = db.transaction(["myObjectStore"], "readonly"); // 獲取myObjectStore對(duì)象存儲(chǔ)空間的引用 const objectStore = transaction.objectStore("myObjectStore"); // 獲取 name 索引 const index = objectStore.index("name"); // 打開(kāi)游標(biāo) const request = index.openCursor(); // 全部數(shù)據(jù) // const request = index.openCursor(searchKey); // 精準(zhǔn)匹配 // const request = index.openCursor( // IDBKeyRange.bound(searchKey, searchKey + "\uffff", false, false) // ); // 后模糊 const startTime = new Date().getTime(); request.onsuccess = function (event) { const cursor = event.target.result; if (cursor) { const name = cursor.value.name; // 判斷 name 是否包含搜索關(guān)鍵字 如果是后模糊的話 直接拿name就行 if (regex.test(name)) { result.push(cursor.value); } cursor.continue(); } else { const endTime = new Date().getTime(); console.log(`搜索耗時(shí):${endTime - startTime}`); console.log(result); } }; request.onerror = function () { console.error("執(zhí)行搜索時(shí)發(fā)生錯(cuò)誤"); }; }; // 打開(kāi)數(shù)據(jù)庫(kù)失敗回調(diào) openRequest.onerror = function () { console.error("打開(kāi)數(shù)據(jù)庫(kù)時(shí)發(fā)生錯(cuò)誤"); }; }
2、Trie tree(字典樹(shù))
其原理:就是將所有的字都拆分,并將后面的字全部拆分加入自己的孩子節(jié)點(diǎn)里面。其實(shí)更加契合后模糊匹配,如果是需要前后模糊匹配,需要全部遍歷,性能也不是最佳。
class TrieNode { constructor() { this.children = new Map(); // 用于存儲(chǔ)當(dāng)前節(jié)點(diǎn)的子節(jié)點(diǎn) this.isEndOfWord = false; // 表示當(dāng)前節(jié)點(diǎn)是否是一個(gè)單詞的結(jié)尾 this.values = []; // 用于存儲(chǔ)與當(dāng)前節(jié)點(diǎn)關(guān)聯(lián)的值 } } class Trie { constructor() { this.root = new TrieNode(); } // 如果當(dāng)前字符不存在于當(dāng)前節(jié)點(diǎn)的子節(jié)點(diǎn)中,則創(chuàng)建一個(gè)新的子節(jié)點(diǎn), // 并將其添加到子節(jié)點(diǎn)Map中。然后,將當(dāng)前節(jié)點(diǎn)更新為新的子節(jié)點(diǎn),繼續(xù)遍歷下一個(gè)字符。 // 當(dāng)遍歷完所有字符后,將當(dāng)前節(jié)點(diǎn)的isEndOfWord屬性設(shè)置為true,表示該節(jié)點(diǎn)是一個(gè)單詞的結(jié)尾。 // 同時(shí),將與該單詞關(guān)聯(lián)的值添加到當(dāng)前節(jié)點(diǎn)的values數(shù)組中。 insert(word, value) { let node = this.root; for (let i = 0; i < word.length; i++) { const char = word[i]; if (!node.children.has(char)) { node.children.set(char, new TrieNode()); } node = node.children.get(char); } node.isEndOfWord = true; node.values.push(value); } // 遞歸地收集以給定節(jié)點(diǎn)為根的子樹(shù)中所有單詞的關(guān)聯(lián)值。如果當(dāng)前節(jié)點(diǎn)是一個(gè)單詞的結(jié)尾 // 則將該節(jié)點(diǎn)的所有關(guān)聯(lián)值添加到結(jié)果集合中。然后,對(duì)當(dāng)前節(jié)點(diǎn)的每個(gè)子節(jié)點(diǎn) // 遞歸調(diào)用collectValues方法。 collectValues(node, results) { if (node.isEndOfWord) { for (const value of node.values) { results.add(value); } } for (const childNode of node.children.values()) { this.collectValues(childNode, results); } } // 模糊搜索與給定關(guān)鍵字匹配的單詞,并返回匹配到的所有關(guān)聯(lián)值。該方法首先創(chuàng)建一個(gè)空的 // Set對(duì)象results,用于存儲(chǔ)結(jié)果。然后,調(diào)用collectSuffixValues方法, // 從根節(jié)點(diǎn)開(kāi)始遞歸地搜索與關(guān)鍵字匹配的單詞,并將匹配到的關(guān)聯(lián)值添加到results中。 // 最后,將results轉(zhuǎn)換為數(shù)組并返回。 fuzzySearch(keyword) { const results = new Set(); this.collectSuffixValues(this.root, keyword, results); return Array.from(results); } // 遞歸地搜索與給定關(guān)鍵字后綴匹配的單詞,并將匹配到的關(guān)聯(lián)值添加到結(jié)果集合中。 // 如果關(guān)鍵字的長(zhǎng)度為0,表示已經(jīng)匹配到了關(guān)鍵字的末尾,此時(shí)調(diào)用collectValues方法, // 將當(dāng)前節(jié)點(diǎn)及其子樹(shù)中的所有關(guān)聯(lián)值添加到結(jié)果集合中。 // 否則,取關(guān)鍵字的第一個(gè)字符char,并獲取當(dāng)前節(jié)點(diǎn)的子節(jié)點(diǎn)childNode。 // 如果childNode存在,則遞歸調(diào)用collectSuffixValues方法, // 將關(guān)鍵字的剩余部分和childNode作為參數(shù)傳遞。 // 然后,對(duì)當(dāng)前節(jié)點(diǎn)的每個(gè)子節(jié)點(diǎn),遞歸調(diào)用collectSuffixValues方法, // 將關(guān)鍵字和結(jié)果集合作為參數(shù)傳遞 collectSuffixValues(node, keyword, results) { if (keyword.length === 0) { this.collectValues(node, results); } else { const char = keyword[0]; const childNode = node.children.get(char); if (childNode) { this.collectSuffixValues(childNode, keyword.slice(1), results); } } for (const childNode of node.children.values()) { this.collectSuffixValues(childNode, keyword, results); } } } async function search() { // 示例用法 const trie = new Trie(); const dataArray = await getJSON(); dataArray.forEach((item) => { trie.insert(item.name, item); }); const startTime = new Date().getTime(); const results = trie.fuzzySearch("總部"); const endTime = new Date().getTime(); console.log(`搜索耗時(shí):${endTime - startTime}`); console.log(trie, "results", results); } search();
3、Fuse(第三方組件庫(kù))
Bitap算法是fuse.js中用于實(shí)現(xiàn)模糊搜索的核心算法之一,其主要思路是利用位運(yùn)算來(lái)計(jì)算模式串和目標(biāo)串之間的相似度。具體來(lái)說(shuō),Bitap算法首先將模式串轉(zhuǎn)換為二進(jìn)制掩碼,并利用位運(yùn)算來(lái)計(jì)算模式串和目標(biāo)串之間的相似度,然后采用一些啟發(fā)式策略來(lái)提高算法的準(zhǔn)確性和效率。其他講解
function search() { const startTime = new Date().getTime(); const fuseOptions = { // isCaseSensitive: false, // 指示比較是否應(yīng)區(qū)分大小寫。 // includeScore: true, // 分?jǐn)?shù)是否應(yīng)包含在結(jié)果集中。分?jǐn)?shù)0表示完全匹配,分?jǐn)?shù)1表示完全不匹配。 // shouldSort: true, // 是否按分?jǐn)?shù)對(duì)結(jié)果列表進(jìn)行排序。 // includeMatches: true, // 匹配是否應(yīng)包含在結(jié)果集中。當(dāng) 時(shí)true,結(jié)果集中的每條記錄都將包含匹配字符的索引。因此,這些可以用于突出顯示的目的。 // findAllMatches: true, // 當(dāng)為 true 時(shí),匹配函數(shù)將繼續(xù)到搜索模式的末尾,即使已在字符串中找到完美匹配。 // minMatchCharLength: 1, // 僅返回長(zhǎng)度超過(guò)該值的匹配項(xiàng)。(例如,如果您想忽略結(jié)果中的單個(gè)字符匹配,請(qǐng)將其設(shè)置為2)。 // location: 0, // 確定預(yù)期在文本中找到的模式的大概位置。 // threshold: 0.6, // 匹配算法在什么時(shí)候放棄。閾值0.0需要完美匹配(字母和位置),閾值1.0可以匹配任何內(nèi)容 // distance: 100, // 確定匹配與模糊位置的接近程度(由 指定location)。distance遠(yuǎn)離模糊位置的字符的精確字母匹配將被評(píng)分為完全不匹配。A distanceof0要求匹配精確指定location。距離1000需要在使用of找到的800字符內(nèi)有完美匹配。locationthreshold0.8 // ignoreLocation: true, // true,搜索將忽略location和distance,因此模式出現(xiàn)在字符串中的位置并不重要。 // ignoreFieldNorm: true, //true,相關(guān)性得分(用于排序)的計(jì)算將忽略字段長(zhǎng)度范數(shù)。 // fieldNormWeight: 1, // 確定字段長(zhǎng)度范數(shù)對(duì)評(píng)分的影響程度。值0相當(dāng)于忽略字段長(zhǎng)度范數(shù)。值0.5將大大降低字段長(zhǎng)度范數(shù)的影響,而值2.0將大大增加字段長(zhǎng)度范數(shù)的影響。 keys: ["name"], }; const fuse = new Fuse(data, fuseOptions); const searchPattern = "肉食總部人力資源部"; result = fuse.search(searchPattern); const endTime = new Date().getTime(); console.log(`搜索耗時(shí):${endTime - startTime}`); console.log(result); }
配置項(xiàng) | 描述 | 默認(rèn)值 | 說(shuō)明 |
---|---|---|---|
keys | 要搜索的鍵的列表。 | 它支持嵌套路徑、加權(quán)搜索、在字符串和對(duì)象數(shù)組中搜索。 | |
isCaseSensitive | 是否區(qū)分大小寫 | false | |
includeScore | 分?jǐn)?shù)是否應(yīng)包含在結(jié)果集中 | false | 分?jǐn)?shù)0表示完全匹配,分?jǐn)?shù)1表示完全不匹配。 |
shouldSort | 是否按分?jǐn)?shù)對(duì)結(jié)果列表進(jìn)行排序。 | true | |
includeMatches | 匹配是否應(yīng)包含在結(jié)果集中。 | ||
findAllMatches | 匹配是否應(yīng)全部在結(jié)果集中。 | false | 當(dāng)為 true 時(shí),匹配函數(shù)將繼續(xù)到搜索模式的末尾,即使已在字符串中找到完美匹配。 |
minMatchCharLength | 僅返回長(zhǎng)度超過(guò)該值的匹配項(xiàng)。 | 1 | 如果您想忽略結(jié)果中的單個(gè)字符匹配,請(qǐng)將其設(shè)置為2 |
location | 確定預(yù)期在文本中找到的模式的大概位置。 | 0 | |
threshold | 匹配算法在什么時(shí)候放棄 | 0.6 | 閾值0.0需要完美匹配(字母和位置),閾值1.0可以匹配任何內(nèi)容 |
distance | 確定匹配與模糊位置的接近程度(由 指定location) | 100 | distance遠(yuǎn)離模糊位置的字符的精確字母匹配將被評(píng)分為完全不匹配。A distanceof0要求匹配精確指定location。距離1000需要在使用of找到的800字符內(nèi)有完美匹配。locationthreshold0.8 |
ignoreLocation | 搜索將忽略location和distance | false | |
ignoreFieldNorm | 相關(guān)性得分(用于排序)的計(jì)算將忽略字段長(zhǎng)度范數(shù) | false | |
fieldNormWeight | 確定字段長(zhǎng)度范數(shù)對(duì)評(píng)分的影響程度。 | 1 | 值0相當(dāng)于忽略字段長(zhǎng)度范數(shù)。值0.5將大大降低字段長(zhǎng)度范數(shù)的影響,而值2.0將大大增加字段長(zhǎng)度范數(shù)的影響。 |
4、前綴(Trie)樹(shù)+交集實(shí)現(xiàn)模糊搜索
之前我們提過(guò)的第二種方案更適合前綴查詢,但是我們通過(guò)循環(huán)整個(gè)樹(shù)的話,是可以實(shí)現(xiàn)前后模糊查詢,但不是最佳方案,如果是前后模糊查詢,更加推薦以下這種方案。
我們之前是將數(shù)組種的文字第一個(gè)進(jìn)行一個(gè)拆解。其實(shí)我們可以轉(zhuǎn)換一種思路,通過(guò)將所有的字都拆解,然后每個(gè)樹(shù)結(jié)點(diǎn)中有一個(gè)字段儲(chǔ)存所有包含這個(gè)字的唯一索引。
let array = [{id:1,name:"總部"},{id:2,name:"總"},{id:3,name:"部"}] let result = {"總":{ids:[1,2]},"部":{ids:[1,3]}}
當(dāng)我們輸入“總”字時(shí),結(jié)果應(yīng)該為1,2;
輸入部時(shí),結(jié)果應(yīng)該為1,3;
輸入總部時(shí),結(jié)果應(yīng)該為1;
其實(shí)在輸入總部的時(shí)候也就是取兩個(gè)字的交集,他們共同含有的1
如果搜索的是多個(gè)字的話也是同樣的道理,取每個(gè)字的索引,找出它們的交集,也就是他們的搜索結(jié)果,同時(shí)也可以實(shí)現(xiàn),總*部、總部**等,這種包含的情況。
以上就是前端JavaScript實(shí)現(xiàn)大數(shù)據(jù)前后模糊搜索的方法詳解的詳細(xì)內(nèi)容,更多關(guān)于JavaScript模糊搜索的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
詳解小程序如何改變onLoad的執(zhí)行時(shí)機(jī)
這篇文章主要介紹了詳解小程序如何改變onLoad的執(zhí)行時(shí)機(jī),文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2019-11-11javascript面向?qū)ο笾L問(wèn)對(duì)象屬性的兩種方式分析
這篇文章主要介紹了javascript面向?qū)ο笾L問(wèn)對(duì)象屬性的兩種方式分析,實(shí)例分析了直接訪問(wèn)對(duì)象屬性的方式與數(shù)組訪問(wèn)方式,需要的朋友可以參考下2015-01-01javascript實(shí)現(xiàn)狀態(tài)欄文字首尾相接循環(huán)滾動(dòng)的方法
這篇文章主要介紹了javascript實(shí)現(xiàn)狀態(tài)欄文字首尾相接循環(huán)滾動(dòng)的方法,實(shí)例分析了javascript定時(shí)函數(shù)及頁(yè)面元素屬性操作的相關(guān)技巧,具有一定參考借鑒價(jià)值,需要的朋友可以參考下2015-07-07深入理解Canvas模糊問(wèn)題產(chǎn)生的原因與解決辦法
我們?cè)谑褂肅anvas進(jìn)行繪圖時(shí),經(jīng)常會(huì)出現(xiàn)繪制的文字或者圖片比較模糊,這篇文章我們就來(lái)討論一下Canvas模糊問(wèn)題產(chǎn)生的原因與解決辦法吧2024-04-04Autocomplete Textbox Example javascript實(shí)現(xiàn)自動(dòng)完成成功
Autocomplete Textbox Example javascript實(shí)現(xiàn)自動(dòng)完成成功...2007-08-08