前端实现大数据前后模糊搜索

大数据
后台-插件-广告管理-内容页头部广告(手机)

一、背景 在特定的一些场景中,服务端可能会返回我们大量的数据,并希望在前端处理模糊查询。我们可以参考以下方案,如果有更好的实现方案可以直接添加。 二、解决方案 1、indexedDB 目前主流前端存储方案:

特性

cookie

localStorage

sessionStorage

indexedDB

数据生命周期

一般由服务器生成,可以设置过期时间;前端采用和js-cookie等组件也可以生成

除非被清理,否则一直存在;浏览器关闭还会保存在本地,但是不支持跨浏览器

页面关闭就清理刷新依然存在,不支持跨页面交互

限制为可用磁盘空间的 50%。

数据存储大小

4K

5M

5M

不限制大小

与服务端通信

每次都会携带在请求的header 中,对于请求性能有影响;同时由于请求中都带有,所以也容易出现安全问题

不参与

不参与

不参与

特点

字符串键值对在本地存储数据

字符串键值对在本地存储数据

字符串键值对在本地存储数据

可以存储大量数据,提供接口来查询,还可以建立索引

IndexedDB是个非关系型数据库,那我们也可以去进行查询搜索。对于搜索速度方面可能不是最优解,但是要考虑业务实现,例如:我们有一个接口,接口数据更新不是那么频繁,并且数据量非常大,调用接口时间可能会很久,搜索某一项或者某个开始的可能会更好点。

function init() {  // 打开 IndexedDB 数据库  const openDB = window.indexedDB.open("myDatabase", 1);  // 新建数据库和数据库版本号发生变化时才被调用  openDB.onupgradeneeded = function (event) {    const db = event.target.result;    // 创建对象存储空间    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;    // 获取事务对象    const transaction = db.transaction(["myObjectStore"], "readwrite");    // 获取对象存储空间    const objectStore = transaction.objectStore("myObjectStore");    // 向对象存储添加对象    dataArray.forEach(function (data) {      const addRequest = objectStore.add(data);      addRequest.onsuccess = function () {        console.log("数据已成功添加到对象存储");      };      addRequest.onerror = function (error) {        console.error("添加数据时发生错误", error);      };    });  };}
function search() {  window.result = [];  // 打开数据库  const openRequest = window.indexedDB.open("myDatabase", 1);  // 打开数据库成功回调  openRequest.onsuccess = function (event) {    // 数据库对象    const db = event.target.result;    // 打开只读事务并获取对象存储 readwrite 读写  readonly 只读    const transaction = db.transaction(["myObjectStore"], "readonly");    // 获取myObjectStore对象存储空间的引用    const objectStore = transaction.objectStore("myObjectStore");    // 获取 name 索引    const index = objectStore.index("name");    // 打开游标    const request = index.openCursor(); // 全部数据    // 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 是否包含搜索关键字 如果是后模糊的话 直接拿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("执行搜索时发生错误");    };  };  // 打开数据库失败回调  openRequest.onerror = function () {    console.error("打开数据库时发生错误");  };}

2、Trie tree(字典树)

其原理:就是将所有的字都拆分,并将后面的字全部拆分加入自己的孩子节点里面。其实更加契合后模糊匹配,如果是需要前后模糊匹配,需要全部遍历,性能也不是最佳。

class TrieNode {  constructor() {    this.children = new Map(); // 用于存储当前节点的子节点    this.isEndOfWord = false; // 表示当前节点是否是一个单词的结尾    this.values = []; // 用于存储与当前节点关联的值  }}class Trie {  constructor() {    this.root = new TrieNode();  }  // 如果当前字符不存在于当前节点的子节点中,则创建一个新的子节点,  // 并将其添加到子节点Map中。然后,将当前节点更新为新的子节点,继续遍历下一个字符。  // 当遍历完所有字符后,将当前节点的isEndOfWord属性设置为true,表示该节点是一个单词的结尾。  // 同时,将与该单词关联的值添加到当前节点的values数组中。  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);  }  // 递归地收集以给定节点为根的子树中所有单词的关联值。如果当前节点是一个单词的结尾  // 则将该节点的所有关联值添加到结果集合中。然后,对当前节点的每个子节点  // 递归调用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);    }  }  // 模糊搜索与给定关键字匹配的单词,并返回匹配到的所有关联值。该方法首先创建一个空的  // Set对象results,用于存储结果。然后,调用collectSuffixValues方法,  // 从根节点开始递归地搜索与关键字匹配的单词,并将匹配到的关联值添加到results中。  // 最后,将results转换为数组并返回。  fuzzySearch(keyword) {    const results = new Set();    this.collectSuffixValues(this.root, keyword, results);    return Array.from(results);  }  // 递归地搜索与给定关键字后缀匹配的单词,并将匹配到的关联值添加到结果集合中。  // 如果关键字的长度为0,表示已经匹配到了关键字的末尾,此时调用collectValues方法,  // 将当前节点及其子树中的所有关联值添加到结果集合中。  // 否则,取关键字的第一个字符char,并获取当前节点的子节点childNode。  // 如果childNode存在,则递归调用collectSuffixValues方法,  // 将关键字的剩余部分和childNode作为参数传递。  // 然后,对当前节点的每个子节点,递归调用collectSuffixValues方法,  // 将关键字和结果集合作为参数传递  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中用于实现模糊搜索的核心算法之一,其主要思路是利用位运算来计算模式串和目标串之间的相似度。具体来说,Bitap算法首先将模式串转换为二进制掩码,并利用位运算来计算模式串和目标串之间的相似度,然后采用一些启发式策略来提高算法的准确性和效率。其他讲解

function search() {  const startTime = new Date().getTime();  const fuseOptions = {    // isCaseSensitive: false, // 指示比较是否应区分大小写。    // includeScore: true, // 分数是否应包含在结果集中。分数0表示完全匹配,分数1表示完全不匹配。    // shouldSort: true, // 是否按分数对结果列表进行排序。    // includeMatches: true, // 匹配是否应包含在结果集中。当 时true,结果集中的每条记录都将包含匹配字符的索引。因此,这些可以用于突出显示的目的。    // findAllMatches: true, // 当为 true 时,匹配函数将继续到搜索模式的末尾,即使已在字符串中找到完美匹配。    // minMatchCharLength: 1, // 仅返回长度超过该值的匹配项。(例如,如果您想忽略结果中的单个字符匹配,请将其设置为2)。    // location: 0, // 确定预期在文本中找到的模式的大概位置。    // threshold: 0.6, // 匹配算法在什么时候放弃。阈值0.0需要完美匹配(字母和位置),阈值1.0可以匹配任何内容    // distance: 100, // 确定匹配与模糊位置的接近程度(由 指定location)。distance远离模糊位置的字符的精确字母匹配将被评分为完全不匹配。A distanceof0要求匹配精确指定location。距离1000需要在使用of找到的800字符内有完美匹配。locationthreshold0.8    // ignoreLocation: true, // true,搜索将忽略location和distance,因此模式出现在字符串中的位置并不重要。    // ignoreFieldNorm: true, //true,相关性得分(用于排序)的计算将忽略字段长度范数。    // fieldNormWeight: 1, // 确定字段长度范数对评分的影响程度。值0相当于忽略字段长度范数。值0.5将大大降低字段长度范数的影响,而值2.0将大大增加字段长度范数的影响。    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

要搜索的键的列表。

它支持嵌套路径、加权搜索、在字符串和对象数组中搜索。

isCaseSensitive

是否区分大小写

false

includeScore

分数是否应包含在结果集中

false

分数0表示完全匹配,分数1表示完全不匹配。

shouldSort

是否按分数对结果列表进行排序。

true

includeMatches

匹配是否应包含在结果集中。

findAllMatches

匹配是否应全部在结果集中。

false

当为 true 时,匹配函数将继续到搜索模式的末尾,即使已在字符串中找到完美匹配。

minMatchCharLength

仅返回长度超过该值的匹配项。

1

如果您想忽略结果中的单个字符匹配,请将其设置为2

location

确定预期在文本中找到的模式的大概位置。

0

threshold

匹配算法在什么时候放弃

0.6

阈值0.0需要完美匹配(字母和位置),阈值1.0可以匹配任何内容

distance

确定匹配与模糊位置的接近程度(由 指定location)

100

distance远离模糊位置的字符的精确字母匹配将被评分为完全不匹配。A distanceof0要求匹配精确指定location。距离1000需要在使用of找到的800字符内有完美匹配。locationthreshold0.8

ignoreLocation

搜索将忽略location和distance

false

ignoreFieldNorm

相关性得分(用于排序)的计算将忽略字段长度范数

false

fieldNormWeight

确定字段长度范数对评分的影响程度。

1

值0相当于忽略字段长度范数。值0.5将大大降低字段长度范数的影响,而值2.0将大大增加字段长度范数的影响。

4、前缀(Trie)树+交集实现模糊搜索 之前我们提过的第二种方案更适合前缀查询,但是我们通过循环整个树的话,是可以实现前后模糊查询,但不是最佳方案,如果是前后模糊查询,更加推荐以下这种方案。 我们之前是将数组种的文字第一个进行一个拆解。其实我们可以转换一种思路,通过将所有的字都拆解,然后每个树结点中有一个字段储存所有包含这个字的唯一索引。

let array = [{id:1,name:"总部"},{id:2,name:"总"},{id:3,name:"部"}]let result = {"总":{ids:[1,2]},"部":{ids:[1,3]}}

当我们输入“总”字时,结果应该为1,2; 输入部时,结果应该为1,3; 输入总部时,结果应该为1; 其实在输入总部的时候也就是取两个字的交集,他们共同含有的1 如果搜索的是多个字的话也是同样的道理,取每个字的索引,找出它们的交集,也就是他们的搜索结果,同时也可以实现,总*部、总部**等,这种包含的情况。 具体实现可以参考这个:

class Trie {  constructor() {    this.root = new Map();  }  insert(words, ids) {    let node = this.root;    for (let index = 0; index < words.length; index++) {      const char = words[index];      if (!node.has(char)) {        let set = new Set();        set.add(ids);        node.set(char, set);      } else {        let set = node.get(char);        set.add(ids);      }    }  }  search(words) {    let collectIdsSet = new Array();    for (let index = 0; index < words.length; index++) {      const char = words[index];      if (this.root.has(char)) {        collectIdsSet.push(this.root.get(char));      }    }    const result = Array.from(      collectIdsSet.reduce((a, b) => new Set([...a].filter((x) => b.has(x))))    );    return result;  }}async function init() {  let trie = new Trie();  try {    const data = await getJSON();    data.forEach((element) => {      trie.insert(element.name, element.code);    });    const startTime = new Date().getTime();    const result = trie.search("内用");    const endTime = new Date().getTime();    console.log("耗时时间:", endTime - startTime);    console.log(result, "result");  } catch (error) {    console.log(error);  }}init();

总结 1、indexedDB更加适用于数据变动不大,需要缓存到浏览器的大量数据的前缀模糊搜索。 2、Trie tree更加适用于前缀搜索。 3、Fuse更加适用于前后模糊搜索以及根据配置计算匹配得分等。 4、Trie+Set 交集更推荐一点,前后模糊搜索+包含搜索都是可以做到的,效率也是很强的! 最后,如果对本文存在疑惑,可以在评论区留言,我会及时回复的,欢迎大家指出文中的错误观点。

后台-插件-广告管理-内容页尾部广告(手机)
标签:

评论留言

我要留言

◎欢迎参与讨论,请在这里发表您的看法、交流您的观点。