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

