fuse.basic.esm.js 28 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239
  1. /**
  2. * Fuse.js v6.4.3 - Lightweight fuzzy-search (http://fusejs.io)
  3. *
  4. * Copyright (c) 2020 Kiro Risk (http://kiro.me)
  5. * All Rights Reserved. Apache Software License 2.0
  6. *
  7. * http://www.apache.org/licenses/LICENSE-2.0
  8. */
  9. function isArray(value) {
  10. return !Array.isArray
  11. ? getTag(value) === '[object Array]'
  12. : Array.isArray(value)
  13. }
  14. // Adapted from: https://github.com/lodash/lodash/blob/master/.internal/baseToString.js
  15. const INFINITY = 1 / 0;
  16. function baseToString(value) {
  17. // Exit early for strings to avoid a performance hit in some environments.
  18. if (typeof value == 'string') {
  19. return value
  20. }
  21. let result = value + '';
  22. return result == '0' && 1 / value == -INFINITY ? '-0' : result
  23. }
  24. function toString(value) {
  25. return value == null ? '' : baseToString(value)
  26. }
  27. function isString(value) {
  28. return typeof value === 'string'
  29. }
  30. function isNumber(value) {
  31. return typeof value === 'number'
  32. }
  33. // Adapted from: https://github.com/lodash/lodash/blob/master/isBoolean.js
  34. function isBoolean(value) {
  35. return (
  36. value === true ||
  37. value === false ||
  38. (isObjectLike(value) && getTag(value) == '[object Boolean]')
  39. )
  40. }
  41. function isObject(value) {
  42. return typeof value === 'object'
  43. }
  44. // Checks if `value` is object-like.
  45. function isObjectLike(value) {
  46. return isObject(value) && value !== null
  47. }
  48. function isDefined(value) {
  49. return value !== undefined && value !== null
  50. }
  51. function isBlank(value) {
  52. return !value.trim().length
  53. }
  54. // Gets the `toStringTag` of `value`.
  55. // Adapted from: https://github.com/lodash/lodash/blob/master/.internal/getTag.js
  56. function getTag(value) {
  57. return value == null
  58. ? value === undefined
  59. ? '[object Undefined]'
  60. : '[object Null]'
  61. : Object.prototype.toString.call(value)
  62. }
  63. const EXTENDED_SEARCH_UNAVAILABLE = 'Extended search is not available';
  64. const LOGICAL_SEARCH_UNAVAILABLE = 'Logical search is not available';
  65. const INCORRECT_INDEX_TYPE = "Incorrect 'index' type";
  66. const LOGICAL_SEARCH_INVALID_QUERY_FOR_KEY = (key) =>
  67. `Invalid value for key ${key}`;
  68. const PATTERN_LENGTH_TOO_LARGE = (max) =>
  69. `Pattern length exceeds max of ${max}.`;
  70. const MISSING_KEY_PROPERTY = (name) => `Missing ${name} property in key`;
  71. const INVALID_KEY_WEIGHT_VALUE = (key) =>
  72. `Property 'weight' in key '${key}' must be a positive integer`;
  73. const hasOwn = Object.prototype.hasOwnProperty;
  74. class KeyStore {
  75. constructor(keys) {
  76. this._keys = [];
  77. this._keyMap = {};
  78. let totalWeight = 0;
  79. keys.forEach((key) => {
  80. let obj = createKey(key);
  81. totalWeight += obj.weight;
  82. this._keys.push(obj);
  83. this._keyMap[obj.id] = obj;
  84. totalWeight += obj.weight;
  85. });
  86. // Normalize weights so that their sum is equal to 1
  87. this._keys.forEach((key) => {
  88. key.weight /= totalWeight;
  89. });
  90. }
  91. get(keyId) {
  92. return this._keyMap[keyId]
  93. }
  94. keys() {
  95. return this._keys
  96. }
  97. toJSON() {
  98. return JSON.stringify(this._keys)
  99. }
  100. }
  101. function createKey(key) {
  102. let path = null;
  103. let id = null;
  104. let src = null;
  105. let weight = 1;
  106. if (isString(key) || isArray(key)) {
  107. src = key;
  108. path = createKeyPath(key);
  109. id = createKeyId(key);
  110. } else {
  111. if (!hasOwn.call(key, 'name')) {
  112. throw new Error(MISSING_KEY_PROPERTY('name'))
  113. }
  114. const name = key.name;
  115. src = name;
  116. if (hasOwn.call(key, 'weight')) {
  117. weight = key.weight;
  118. if (weight <= 0) {
  119. throw new Error(INVALID_KEY_WEIGHT_VALUE(name))
  120. }
  121. }
  122. path = createKeyPath(name);
  123. id = createKeyId(name);
  124. }
  125. return { path, id, weight, src }
  126. }
  127. function createKeyPath(key) {
  128. return isArray(key) ? key : key.split('.')
  129. }
  130. function createKeyId(key) {
  131. return isArray(key) ? key.join('.') : key
  132. }
  133. function get(obj, path) {
  134. let list = [];
  135. let arr = false;
  136. const deepGet = (obj, path, index) => {
  137. if (!isDefined(obj)) {
  138. return
  139. }
  140. if (!path[index]) {
  141. // If there's no path left, we've arrived at the object we care about.
  142. list.push(obj);
  143. } else {
  144. let key = path[index];
  145. const value = obj[key];
  146. if (!isDefined(value)) {
  147. return
  148. }
  149. // If we're at the last value in the path, and if it's a string/number/bool,
  150. // add it to the list
  151. if (
  152. index === path.length - 1 &&
  153. (isString(value) || isNumber(value) || isBoolean(value))
  154. ) {
  155. list.push(toString(value));
  156. } else if (isArray(value)) {
  157. arr = true;
  158. // Search each item in the array.
  159. for (let i = 0, len = value.length; i < len; i += 1) {
  160. deepGet(value[i], path, index + 1);
  161. }
  162. } else if (path.length) {
  163. // An object. Recurse further.
  164. deepGet(value, path, index + 1);
  165. }
  166. }
  167. };
  168. // Backwards compatibility (since path used to be a string)
  169. deepGet(obj, isString(path) ? path.split('.') : path, 0);
  170. return arr ? list : list[0]
  171. }
  172. const MatchOptions = {
  173. // Whether the matches should be included in the result set. When `true`, each record in the result
  174. // set will include the indices of the matched characters.
  175. // These can consequently be used for highlighting purposes.
  176. includeMatches: false,
  177. // When `true`, the matching function will continue to the end of a search pattern even if
  178. // a perfect match has already been located in the string.
  179. findAllMatches: false,
  180. // Minimum number of characters that must be matched before a result is considered a match
  181. minMatchCharLength: 1
  182. };
  183. const BasicOptions = {
  184. // When `true`, the algorithm continues searching to the end of the input even if a perfect
  185. // match is found before the end of the same input.
  186. isCaseSensitive: false,
  187. // When true, the matching function will continue to the end of a search pattern even if
  188. includeScore: false,
  189. // List of properties that will be searched. This also supports nested properties.
  190. keys: [],
  191. // Whether to sort the result list, by score
  192. shouldSort: true,
  193. // Default sort function: sort by ascending score, ascending index
  194. sortFn: (a, b) =>
  195. a.score === b.score ? (a.idx < b.idx ? -1 : 1) : a.score < b.score ? -1 : 1
  196. };
  197. const FuzzyOptions = {
  198. // Approximately where in the text is the pattern expected to be found?
  199. location: 0,
  200. // At what point does the match algorithm give up. A threshold of '0.0' requires a perfect match
  201. // (of both letters and location), a threshold of '1.0' would match anything.
  202. threshold: 0.6,
  203. // Determines how close the match must be to the fuzzy location (specified above).
  204. // An exact letter match which is 'distance' characters away from the fuzzy location
  205. // would score as a complete mismatch. A distance of '0' requires the match be at
  206. // the exact location specified, a threshold of '1000' would require a perfect match
  207. // to be within 800 characters of the fuzzy location to be found using a 0.8 threshold.
  208. distance: 100
  209. };
  210. const AdvancedOptions = {
  211. // When `true`, it enables the use of unix-like search commands
  212. useExtendedSearch: false,
  213. // The get function to use when fetching an object's properties.
  214. // The default will search nested paths *ie foo.bar.baz*
  215. getFn: get,
  216. // When `true`, search will ignore `location` and `distance`, so it won't matter
  217. // where in the string the pattern appears.
  218. // More info: https://fusejs.io/concepts/scoring-theory.html#fuzziness-score
  219. ignoreLocation: false,
  220. // When `true`, the calculation for the relevance score (used for sorting) will
  221. // ignore the field-length norm.
  222. // More info: https://fusejs.io/concepts/scoring-theory.html#field-length-norm
  223. ignoreFieldNorm: false
  224. };
  225. var Config = {
  226. ...BasicOptions,
  227. ...MatchOptions,
  228. ...FuzzyOptions,
  229. ...AdvancedOptions
  230. };
  231. const SPACE = /[^ ]+/g;
  232. // Field-length norm: the shorter the field, the higher the weight.
  233. // Set to 3 decimals to reduce index size.
  234. function norm(mantissa = 3) {
  235. const cache = new Map();
  236. return {
  237. get(value) {
  238. const numTokens = value.match(SPACE).length;
  239. if (cache.has(numTokens)) {
  240. return cache.get(numTokens)
  241. }
  242. const n = parseFloat((1 / Math.sqrt(numTokens)).toFixed(mantissa));
  243. cache.set(numTokens, n);
  244. return n
  245. },
  246. clear() {
  247. cache.clear();
  248. }
  249. }
  250. }
  251. class FuseIndex {
  252. constructor({ getFn = Config.getFn } = {}) {
  253. this.norm = norm(3);
  254. this.getFn = getFn;
  255. this.isCreated = false;
  256. this.setIndexRecords();
  257. }
  258. setSources(docs = []) {
  259. this.docs = docs;
  260. }
  261. setIndexRecords(records = []) {
  262. this.records = records;
  263. }
  264. setKeys(keys = []) {
  265. this.keys = keys;
  266. this._keysMap = {};
  267. keys.forEach((key, idx) => {
  268. this._keysMap[key.id] = idx;
  269. });
  270. }
  271. create() {
  272. if (this.isCreated || !this.docs.length) {
  273. return
  274. }
  275. this.isCreated = true;
  276. // List is Array<String>
  277. if (isString(this.docs[0])) {
  278. this.docs.forEach((doc, docIndex) => {
  279. this._addString(doc, docIndex);
  280. });
  281. } else {
  282. // List is Array<Object>
  283. this.docs.forEach((doc, docIndex) => {
  284. this._addObject(doc, docIndex);
  285. });
  286. }
  287. this.norm.clear();
  288. }
  289. // Adds a doc to the end of the index
  290. add(doc) {
  291. const idx = this.size();
  292. if (isString(doc)) {
  293. this._addString(doc, idx);
  294. } else {
  295. this._addObject(doc, idx);
  296. }
  297. }
  298. // Removes the doc at the specified index of the index
  299. removeAt(idx) {
  300. this.records.splice(idx, 1);
  301. // Change ref index of every subsquent doc
  302. for (let i = idx, len = this.size(); i < len; i += 1) {
  303. this.records[i].i -= 1;
  304. }
  305. }
  306. getValueForItemAtKeyId(item, keyId) {
  307. return item[this._keysMap[keyId]]
  308. }
  309. size() {
  310. return this.records.length
  311. }
  312. _addString(doc, docIndex) {
  313. if (!isDefined(doc) || isBlank(doc)) {
  314. return
  315. }
  316. let record = {
  317. v: doc,
  318. i: docIndex,
  319. n: this.norm.get(doc)
  320. };
  321. this.records.push(record);
  322. }
  323. _addObject(doc, docIndex) {
  324. let record = { i: docIndex, $: {} };
  325. // Iterate over every key (i.e, path), and fetch the value at that key
  326. this.keys.forEach((key, keyIndex) => {
  327. // console.log(key)
  328. let value = this.getFn(doc, key.path);
  329. if (!isDefined(value)) {
  330. return
  331. }
  332. if (isArray(value)) {
  333. let subRecords = [];
  334. const stack = [{ nestedArrIndex: -1, value }];
  335. while (stack.length) {
  336. const { nestedArrIndex, value } = stack.pop();
  337. if (!isDefined(value)) {
  338. continue
  339. }
  340. if (isString(value) && !isBlank(value)) {
  341. let subRecord = {
  342. v: value,
  343. i: nestedArrIndex,
  344. n: this.norm.get(value)
  345. };
  346. subRecords.push(subRecord);
  347. } else if (isArray(value)) {
  348. value.forEach((item, k) => {
  349. stack.push({
  350. nestedArrIndex: k,
  351. value: item
  352. });
  353. });
  354. }
  355. }
  356. record.$[keyIndex] = subRecords;
  357. } else if (!isBlank(value)) {
  358. let subRecord = {
  359. v: value,
  360. n: this.norm.get(value)
  361. };
  362. record.$[keyIndex] = subRecord;
  363. }
  364. });
  365. this.records.push(record);
  366. }
  367. toJSON() {
  368. return {
  369. keys: this.keys,
  370. records: this.records
  371. }
  372. }
  373. }
  374. function createIndex(keys, docs, { getFn = Config.getFn } = {}) {
  375. const myIndex = new FuseIndex({ getFn });
  376. myIndex.setKeys(keys.map(createKey));
  377. myIndex.setSources(docs);
  378. myIndex.create();
  379. return myIndex
  380. }
  381. function parseIndex(data, { getFn = Config.getFn } = {}) {
  382. const { keys, records } = data;
  383. const myIndex = new FuseIndex({ getFn });
  384. myIndex.setKeys(keys);
  385. myIndex.setIndexRecords(records);
  386. return myIndex
  387. }
  388. function transformMatches(result, data) {
  389. const matches = result.matches;
  390. data.matches = [];
  391. if (!isDefined(matches)) {
  392. return
  393. }
  394. matches.forEach((match) => {
  395. if (!isDefined(match.indices) || !match.indices.length) {
  396. return
  397. }
  398. const { indices, value } = match;
  399. let obj = {
  400. indices,
  401. value
  402. };
  403. if (match.key) {
  404. obj.key = match.key.src;
  405. }
  406. if (match.idx > -1) {
  407. obj.refIndex = match.idx;
  408. }
  409. data.matches.push(obj);
  410. });
  411. }
  412. function transformScore(result, data) {
  413. data.score = result.score;
  414. }
  415. function computeScore(
  416. pattern,
  417. {
  418. errors = 0,
  419. currentLocation = 0,
  420. expectedLocation = 0,
  421. distance = Config.distance,
  422. ignoreLocation = Config.ignoreLocation
  423. } = {}
  424. ) {
  425. const accuracy = errors / pattern.length;
  426. if (ignoreLocation) {
  427. return accuracy
  428. }
  429. const proximity = Math.abs(expectedLocation - currentLocation);
  430. if (!distance) {
  431. // Dodge divide by zero error.
  432. return proximity ? 1.0 : accuracy
  433. }
  434. return accuracy + proximity / distance
  435. }
  436. function convertMaskToIndices(
  437. matchmask = [],
  438. minMatchCharLength = Config.minMatchCharLength
  439. ) {
  440. let indices = [];
  441. let start = -1;
  442. let end = -1;
  443. let i = 0;
  444. for (let len = matchmask.length; i < len; i += 1) {
  445. let match = matchmask[i];
  446. if (match && start === -1) {
  447. start = i;
  448. } else if (!match && start !== -1) {
  449. end = i - 1;
  450. if (end - start + 1 >= minMatchCharLength) {
  451. indices.push([start, end]);
  452. }
  453. start = -1;
  454. }
  455. }
  456. // (i-1 - start) + 1 => i - start
  457. if (matchmask[i - 1] && i - start >= minMatchCharLength) {
  458. indices.push([start, i - 1]);
  459. }
  460. return indices
  461. }
  462. // Machine word size
  463. const MAX_BITS = 32;
  464. function search(
  465. text,
  466. pattern,
  467. patternAlphabet,
  468. {
  469. location = Config.location,
  470. distance = Config.distance,
  471. threshold = Config.threshold,
  472. findAllMatches = Config.findAllMatches,
  473. minMatchCharLength = Config.minMatchCharLength,
  474. includeMatches = Config.includeMatches,
  475. ignoreLocation = Config.ignoreLocation
  476. } = {}
  477. ) {
  478. if (pattern.length > MAX_BITS) {
  479. throw new Error(PATTERN_LENGTH_TOO_LARGE(MAX_BITS))
  480. }
  481. const patternLen = pattern.length;
  482. // Set starting location at beginning text and initialize the alphabet.
  483. const textLen = text.length;
  484. // Handle the case when location > text.length
  485. const expectedLocation = Math.max(0, Math.min(location, textLen));
  486. // Highest score beyond which we give up.
  487. let currentThreshold = threshold;
  488. // Is there a nearby exact match? (speedup)
  489. let bestLocation = expectedLocation;
  490. // Performance: only computer matches when the minMatchCharLength > 1
  491. // OR if `includeMatches` is true.
  492. const computeMatches = minMatchCharLength > 1 || includeMatches;
  493. // A mask of the matches, used for building the indices
  494. const matchMask = computeMatches ? Array(textLen) : [];
  495. let index;
  496. // Get all exact matches, here for speed up
  497. while ((index = text.indexOf(pattern, bestLocation)) > -1) {
  498. let score = computeScore(pattern, {
  499. currentLocation: index,
  500. expectedLocation,
  501. distance,
  502. ignoreLocation
  503. });
  504. currentThreshold = Math.min(score, currentThreshold);
  505. bestLocation = index + patternLen;
  506. if (computeMatches) {
  507. let i = 0;
  508. while (i < patternLen) {
  509. matchMask[index + i] = 1;
  510. i += 1;
  511. }
  512. }
  513. }
  514. // Reset the best location
  515. bestLocation = -1;
  516. let lastBitArr = [];
  517. let finalScore = 1;
  518. let binMax = patternLen + textLen;
  519. const mask = 1 << (patternLen - 1);
  520. for (let i = 0; i < patternLen; i += 1) {
  521. // Scan for the best match; each iteration allows for one more error.
  522. // Run a binary search to determine how far from the match location we can stray
  523. // at this error level.
  524. let binMin = 0;
  525. let binMid = binMax;
  526. while (binMin < binMid) {
  527. const score = computeScore(pattern, {
  528. errors: i,
  529. currentLocation: expectedLocation + binMid,
  530. expectedLocation,
  531. distance,
  532. ignoreLocation
  533. });
  534. if (score <= currentThreshold) {
  535. binMin = binMid;
  536. } else {
  537. binMax = binMid;
  538. }
  539. binMid = Math.floor((binMax - binMin) / 2 + binMin);
  540. }
  541. // Use the result from this iteration as the maximum for the next.
  542. binMax = binMid;
  543. let start = Math.max(1, expectedLocation - binMid + 1);
  544. let finish = findAllMatches
  545. ? textLen
  546. : Math.min(expectedLocation + binMid, textLen) + patternLen;
  547. // Initialize the bit array
  548. let bitArr = Array(finish + 2);
  549. bitArr[finish + 1] = (1 << i) - 1;
  550. for (let j = finish; j >= start; j -= 1) {
  551. let currentLocation = j - 1;
  552. let charMatch = patternAlphabet[text.charAt(currentLocation)];
  553. if (computeMatches) {
  554. // Speed up: quick bool to int conversion (i.e, `charMatch ? 1 : 0`)
  555. matchMask[currentLocation] = +!!charMatch;
  556. }
  557. // First pass: exact match
  558. bitArr[j] = ((bitArr[j + 1] << 1) | 1) & charMatch;
  559. // Subsequent passes: fuzzy match
  560. if (i) {
  561. bitArr[j] |=
  562. ((lastBitArr[j + 1] | lastBitArr[j]) << 1) | 1 | lastBitArr[j + 1];
  563. }
  564. if (bitArr[j] & mask) {
  565. finalScore = computeScore(pattern, {
  566. errors: i,
  567. currentLocation,
  568. expectedLocation,
  569. distance,
  570. ignoreLocation
  571. });
  572. // This match will almost certainly be better than any existing match.
  573. // But check anyway.
  574. if (finalScore <= currentThreshold) {
  575. // Indeed it is
  576. currentThreshold = finalScore;
  577. bestLocation = currentLocation;
  578. // Already passed `loc`, downhill from here on in.
  579. if (bestLocation <= expectedLocation) {
  580. break
  581. }
  582. // When passing `bestLocation`, don't exceed our current distance from `expectedLocation`.
  583. start = Math.max(1, 2 * expectedLocation - bestLocation);
  584. }
  585. }
  586. }
  587. // No hope for a (better) match at greater error levels.
  588. const score = computeScore(pattern, {
  589. errors: i + 1,
  590. currentLocation: expectedLocation,
  591. expectedLocation,
  592. distance,
  593. ignoreLocation
  594. });
  595. if (score > currentThreshold) {
  596. break
  597. }
  598. lastBitArr = bitArr;
  599. }
  600. const result = {
  601. isMatch: bestLocation >= 0,
  602. // Count exact matches (those with a score of 0) to be "almost" exact
  603. score: Math.max(0.001, finalScore)
  604. };
  605. if (computeMatches) {
  606. const indices = convertMaskToIndices(matchMask, minMatchCharLength);
  607. if (!indices.length) {
  608. result.isMatch = false;
  609. } else if (includeMatches) {
  610. result.indices = indices;
  611. }
  612. }
  613. return result
  614. }
  615. function createPatternAlphabet(pattern) {
  616. let mask = {};
  617. for (let i = 0, len = pattern.length; i < len; i += 1) {
  618. const char = pattern.charAt(i);
  619. mask[char] = (mask[char] || 0) | (1 << (len - i - 1));
  620. }
  621. return mask
  622. }
  623. class BitapSearch {
  624. constructor(
  625. pattern,
  626. {
  627. location = Config.location,
  628. threshold = Config.threshold,
  629. distance = Config.distance,
  630. includeMatches = Config.includeMatches,
  631. findAllMatches = Config.findAllMatches,
  632. minMatchCharLength = Config.minMatchCharLength,
  633. isCaseSensitive = Config.isCaseSensitive,
  634. ignoreLocation = Config.ignoreLocation
  635. } = {}
  636. ) {
  637. this.options = {
  638. location,
  639. threshold,
  640. distance,
  641. includeMatches,
  642. findAllMatches,
  643. minMatchCharLength,
  644. isCaseSensitive,
  645. ignoreLocation
  646. };
  647. this.pattern = isCaseSensitive ? pattern : pattern.toLowerCase();
  648. this.chunks = [];
  649. if (!this.pattern.length) {
  650. return
  651. }
  652. const addChunk = (pattern, startIndex) => {
  653. this.chunks.push({
  654. pattern,
  655. alphabet: createPatternAlphabet(pattern),
  656. startIndex
  657. });
  658. };
  659. const len = this.pattern.length;
  660. if (len > MAX_BITS) {
  661. let i = 0;
  662. const remainder = len % MAX_BITS;
  663. const end = len - remainder;
  664. while (i < end) {
  665. addChunk(this.pattern.substr(i, MAX_BITS), i);
  666. i += MAX_BITS;
  667. }
  668. if (remainder) {
  669. const startIndex = len - MAX_BITS;
  670. addChunk(this.pattern.substr(startIndex), startIndex);
  671. }
  672. } else {
  673. addChunk(this.pattern, 0);
  674. }
  675. }
  676. searchIn(text) {
  677. const { isCaseSensitive, includeMatches } = this.options;
  678. if (!isCaseSensitive) {
  679. text = text.toLowerCase();
  680. }
  681. // Exact match
  682. if (this.pattern === text) {
  683. let result = {
  684. isMatch: true,
  685. score: 0
  686. };
  687. if (includeMatches) {
  688. result.indices = [[0, text.length - 1]];
  689. }
  690. return result
  691. }
  692. // Otherwise, use Bitap algorithm
  693. const {
  694. location,
  695. distance,
  696. threshold,
  697. findAllMatches,
  698. minMatchCharLength,
  699. ignoreLocation
  700. } = this.options;
  701. let allIndices = [];
  702. let totalScore = 0;
  703. let hasMatches = false;
  704. this.chunks.forEach(({ pattern, alphabet, startIndex }) => {
  705. const { isMatch, score, indices } = search(text, pattern, alphabet, {
  706. location: location + startIndex,
  707. distance,
  708. threshold,
  709. findAllMatches,
  710. minMatchCharLength,
  711. includeMatches,
  712. ignoreLocation
  713. });
  714. if (isMatch) {
  715. hasMatches = true;
  716. }
  717. totalScore += score;
  718. if (isMatch && indices) {
  719. allIndices = [...allIndices, ...indices];
  720. }
  721. });
  722. let result = {
  723. isMatch: hasMatches,
  724. score: hasMatches ? totalScore / this.chunks.length : 1
  725. };
  726. if (hasMatches && includeMatches) {
  727. result.indices = allIndices;
  728. }
  729. return result
  730. }
  731. }
  732. const registeredSearchers = [];
  733. function createSearcher(pattern, options) {
  734. for (let i = 0, len = registeredSearchers.length; i < len; i += 1) {
  735. let searcherClass = registeredSearchers[i];
  736. if (searcherClass.condition(pattern, options)) {
  737. return new searcherClass(pattern, options)
  738. }
  739. }
  740. return new BitapSearch(pattern, options)
  741. }
  742. const LogicalOperator = {
  743. AND: '$and',
  744. OR: '$or'
  745. };
  746. const KeyType = {
  747. PATH: '$path',
  748. PATTERN: '$val'
  749. };
  750. const isExpression = (query) =>
  751. !!(query[LogicalOperator.AND] || query[LogicalOperator.OR]);
  752. const isPath = (query) => !!query[KeyType.PATH];
  753. const isLeaf = (query) =>
  754. !isArray(query) && isObject(query) && !isExpression(query);
  755. const convertToExplicit = (query) => ({
  756. [LogicalOperator.AND]: Object.keys(query).map((key) => ({
  757. [key]: query[key]
  758. }))
  759. });
  760. // When `auto` is `true`, the parse function will infer and initialize and add
  761. // the appropriate `Searcher` instance
  762. function parse(query, options, { auto = true } = {}) {
  763. const next = (query) => {
  764. let keys = Object.keys(query);
  765. const isQueryPath = isPath(query);
  766. if (!isQueryPath && keys.length > 1 && !isExpression(query)) {
  767. return next(convertToExplicit(query))
  768. }
  769. if (isLeaf(query)) {
  770. const key = isQueryPath ? query[KeyType.PATH] : keys[0];
  771. const pattern = isQueryPath ? query[KeyType.PATTERN] : query[key];
  772. if (!isString(pattern)) {
  773. throw new Error(LOGICAL_SEARCH_INVALID_QUERY_FOR_KEY(key))
  774. }
  775. const obj = {
  776. keyId: createKeyId(key),
  777. pattern
  778. };
  779. if (auto) {
  780. obj.searcher = createSearcher(pattern, options);
  781. }
  782. return obj
  783. }
  784. let node = {
  785. children: [],
  786. operator: keys[0]
  787. };
  788. keys.forEach((key) => {
  789. const value = query[key];
  790. if (isArray(value)) {
  791. value.forEach((item) => {
  792. node.children.push(next(item));
  793. });
  794. }
  795. });
  796. return node
  797. };
  798. if (!isExpression(query)) {
  799. query = convertToExplicit(query);
  800. }
  801. return next(query)
  802. }
  803. class Fuse {
  804. constructor(docs, options = {}, index) {
  805. this.options = { ...Config, ...options };
  806. if (
  807. this.options.useExtendedSearch &&
  808. !false
  809. ) {
  810. throw new Error(EXTENDED_SEARCH_UNAVAILABLE)
  811. }
  812. this._keyStore = new KeyStore(this.options.keys);
  813. this.setCollection(docs, index);
  814. }
  815. setCollection(docs, index) {
  816. this._docs = docs;
  817. if (index && !(index instanceof FuseIndex)) {
  818. throw new Error(INCORRECT_INDEX_TYPE)
  819. }
  820. this._myIndex =
  821. index ||
  822. createIndex(this.options.keys, this._docs, {
  823. getFn: this.options.getFn
  824. });
  825. }
  826. add(doc) {
  827. if (!isDefined(doc)) {
  828. return
  829. }
  830. this._docs.push(doc);
  831. this._myIndex.add(doc);
  832. }
  833. remove(predicate = (/* doc, idx */) => false) {
  834. const results = [];
  835. for (let i = 0, len = this._docs.length; i < len; i += 1) {
  836. const doc = this._docs[i];
  837. if (predicate(doc, i)) {
  838. this.removeAt(i);
  839. i -= 1;
  840. len -= 1;
  841. results.push(doc);
  842. }
  843. }
  844. return results
  845. }
  846. removeAt(idx) {
  847. this._docs.splice(idx, 1);
  848. this._myIndex.removeAt(idx);
  849. }
  850. getIndex() {
  851. return this._myIndex
  852. }
  853. search(query, { limit = -1 } = {}) {
  854. const {
  855. includeMatches,
  856. includeScore,
  857. shouldSort,
  858. sortFn,
  859. ignoreFieldNorm
  860. } = this.options;
  861. let results = isString(query)
  862. ? isString(this._docs[0])
  863. ? this._searchStringList(query)
  864. : this._searchObjectList(query)
  865. : this._searchLogical(query);
  866. computeScore$1(results, { ignoreFieldNorm });
  867. if (shouldSort) {
  868. results.sort(sortFn);
  869. }
  870. if (isNumber(limit) && limit > -1) {
  871. results = results.slice(0, limit);
  872. }
  873. return format(results, this._docs, {
  874. includeMatches,
  875. includeScore
  876. })
  877. }
  878. _searchStringList(query) {
  879. const searcher = createSearcher(query, this.options);
  880. const { records } = this._myIndex;
  881. const results = [];
  882. // Iterate over every string in the index
  883. records.forEach(({ v: text, i: idx, n: norm }) => {
  884. if (!isDefined(text)) {
  885. return
  886. }
  887. const { isMatch, score, indices } = searcher.searchIn(text);
  888. if (isMatch) {
  889. results.push({
  890. item: text,
  891. idx,
  892. matches: [{ score, value: text, norm, indices }]
  893. });
  894. }
  895. });
  896. return results
  897. }
  898. _searchLogical(query) {
  899. {
  900. throw new Error(LOGICAL_SEARCH_UNAVAILABLE)
  901. }
  902. }
  903. _searchObjectList(query) {
  904. const searcher = createSearcher(query, this.options);
  905. const { keys, records } = this._myIndex;
  906. const results = [];
  907. // List is Array<Object>
  908. records.forEach(({ $: item, i: idx }) => {
  909. if (!isDefined(item)) {
  910. return
  911. }
  912. let matches = [];
  913. // Iterate over every key (i.e, path), and fetch the value at that key
  914. keys.forEach((key, keyIndex) => {
  915. matches.push(
  916. ...this._findMatches({
  917. key,
  918. value: item[keyIndex],
  919. searcher
  920. })
  921. );
  922. });
  923. if (matches.length) {
  924. results.push({
  925. idx,
  926. item,
  927. matches
  928. });
  929. }
  930. });
  931. return results
  932. }
  933. _findMatches({ key, value, searcher }) {
  934. if (!isDefined(value)) {
  935. return []
  936. }
  937. let matches = [];
  938. if (isArray(value)) {
  939. value.forEach(({ v: text, i: idx, n: norm }) => {
  940. if (!isDefined(text)) {
  941. return
  942. }
  943. const { isMatch, score, indices } = searcher.searchIn(text);
  944. if (isMatch) {
  945. matches.push({
  946. score,
  947. key,
  948. value: text,
  949. idx,
  950. norm,
  951. indices
  952. });
  953. }
  954. });
  955. } else {
  956. const { v: text, n: norm } = value;
  957. const { isMatch, score, indices } = searcher.searchIn(text);
  958. if (isMatch) {
  959. matches.push({ score, key, value: text, norm, indices });
  960. }
  961. }
  962. return matches
  963. }
  964. }
  965. // Practical scoring function
  966. function computeScore$1(results, { ignoreFieldNorm = Config.ignoreFieldNorm }) {
  967. results.forEach((result) => {
  968. let totalScore = 1;
  969. result.matches.forEach(({ key, norm, score }) => {
  970. const weight = key ? key.weight : null;
  971. totalScore *= Math.pow(
  972. score === 0 && weight ? Number.EPSILON : score,
  973. (weight || 1) * (ignoreFieldNorm ? 1 : norm)
  974. );
  975. });
  976. result.score = totalScore;
  977. });
  978. }
  979. function format(
  980. results,
  981. docs,
  982. {
  983. includeMatches = Config.includeMatches,
  984. includeScore = Config.includeScore
  985. } = {}
  986. ) {
  987. const transformers = [];
  988. if (includeMatches) transformers.push(transformMatches);
  989. if (includeScore) transformers.push(transformScore);
  990. return results.map((result) => {
  991. const { idx } = result;
  992. const data = {
  993. item: docs[idx],
  994. refIndex: idx
  995. };
  996. if (transformers.length) {
  997. transformers.forEach((transformer) => {
  998. transformer(result, data);
  999. });
  1000. }
  1001. return data
  1002. })
  1003. }
  1004. Fuse.version = '6.4.3';
  1005. Fuse.createIndex = createIndex;
  1006. Fuse.parseIndex = parseIndex;
  1007. Fuse.config = Config;
  1008. {
  1009. Fuse.parseQuery = parse;
  1010. }
  1011. export default Fuse;