├── .travis.yml ├── LICENSE ├── README.md ├── Wade.pdf ├── dist ├── wade.js └── wade.min.js ├── gulpfile.js ├── package.json ├── src ├── index.js └── wrapper.js └── test ├── index └── index.js └── search ├── create.js ├── process.js └── search.js /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | script: npm run test 3 | node_js: 4 | - "node" 5 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017-2018 Kabir Shah 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Wade 2 | 3 | Blazing fast 1kb search 4 | 5 | [![Build Status](https://travis-ci.org/kbrsh/wade.svg?branch=master)](https://travis-ci.org/kbrsh/wade) 6 | 7 | ### Installation 8 | 9 | NPM 10 | 11 | ```sh 12 | npm install wade 13 | ``` 14 | 15 | CDN 16 | 17 | ```html 18 | 19 | ``` 20 | 21 | ### Usage 22 | 23 | Initialize Wade with an array of data. 24 | 25 | ```js 26 | const search = Wade(["Apple", "Lemon", "Orange", "Tomato"]); 27 | ``` 28 | 29 | Now you can search for a query within the data, and Wade will return results. Each result will include the index of the item in the data it corresponds to along with a score depending on the relevance of the query to the result. 30 | 31 | ```js 32 | search("App"); 33 | 34 | /* 35 | [{ 36 | index: 0, 37 | score: 1.25 38 | }] 39 | */ 40 | ``` 41 | 42 | Combined with JavaScript libraries like [Moon](https://kbrsh.github.io/moon), you can create a real-time search. 43 | 44 | ### Loading/Saving Index 45 | 46 | To save an index as a String, use `Wade.save` on a search function. 47 | 48 | For example: 49 | 50 | ```js 51 | // Create the search function 52 | const search = Wade(["Apple", "Lemon", "Orange", "Tomato"]); 53 | const index = Wade.save(search); 54 | 55 | // Save `index` 56 | ``` 57 | 58 | Later, you can get the same search function without having Wade recreate an index every time by doing: 59 | 60 | ```js 61 | // Retrieve `index`, then 62 | const search = Wade(index); 63 | ``` 64 | 65 | `index` is a String and can be saved to a file. 66 | 67 | ### Processors 68 | 69 | Wade uses a set of processors to preprocess data and search queries. By default, these will: 70 | 71 | * Make everything lowercase 72 | * Remove punctuation 73 | * Remove stop words 74 | 75 | A process consists of different functions that process a string and modify it in some way, and return the transformed string. 76 | 77 | You can easily modify the processors as they are available in `Wade.config.processors`, for example: 78 | 79 | ```js 80 | // Don't preprocess at all 81 | Wade.config.processors = []; 82 | 83 | // Add custom processor to remove periods 84 | Wade.config.processors.push(function(str) { 85 | return str.replace(/\./g, ""); 86 | }); 87 | ``` 88 | 89 | All functions will be executed in the order of the array (0-n) and they will be used on each document in the data. 90 | 91 | The stop words can be configured to include any words you like, and you can access the array of stop words by using: 92 | 93 | ```js 94 | Wade.config.stopWords = [/* array of stop words */]; 95 | ``` 96 | 97 | The punctuation regular expression used to remove punctuation can be configured with: 98 | 99 | ```js 100 | Wade.config.punctuationRE = /[.!]/g; // should contain punctuation to remove 101 | ``` 102 | 103 | ### Algorithm 104 | 105 | First, an index is generated from the data. When performing a search, the following happens: 106 | 107 | * The search query is processed. 108 | * The search query is tokenized into terms. 109 | * Each term except the last is searched for exactly and scores for each item in the data are updated according to the relevance of the term to the data. 110 | * The last keyword is treated as a prefix, and Wade performs a depth-first search and updates the score for all data prefixed with this term using the relevance weight for the term. This allows for searching as a user types. 111 | 112 | In-depth explanations of the algorithm are available on the [blog post](https://blog.kabir.sh/posts/inside-wade.html) and [pdf](https://github.com/kbrsh/wade/blob/master/Wade.pdf). 113 | 114 | ### Support 115 | 116 | Support Wade [on Patreon](https://patreon.com/kbrsh) to help sustain the development of the project. The maker of the project works on open source for free. If you or your company depend on this project, then it makes sense to donate to ensure that the project is maintained. 117 | 118 | ### License 119 | 120 | Licensed under the [MIT License](https://kbrsh.github.io/license) by [Kabir Shah](https://kabir.sh) 121 | -------------------------------------------------------------------------------- /Wade.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kbrsh/wade/3781faac4c625b351beac7959f891b260433ad35/Wade.pdf -------------------------------------------------------------------------------- /dist/wade.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Wade v0.3.3 3 | * Copyright 2017-2018 Kabir Shah 4 | * Released under the MIT License 5 | * https://github.com/kbrsh/wade 6 | */ 7 | 8 | (function(root, factory) { 9 | /* ======= Global Wade ======= */ 10 | if(typeof module === "undefined") { 11 | root.Wade = factory(); 12 | } else { 13 | module.exports = factory(); 14 | } 15 | }(this, function() { 16 | var whitespaceRE = /\s+/g; 17 | 18 | var config = { 19 | stopWords: ["about", "after", "all", "also", "am", "an", "and", "another", "any", "are", "as", "at", "be", "because", "been", "before", "being", "between", "both", "but", "by", "came", "can", "come", "could", "did", "do", "each", "for", "from", "get", "got", "has", "had", "he", "have", "her", "here", "him", "himself", "his", "how", "if", "in", "into", "is", "it", "like", "make", "many", "me", "might", "more", "most", "much", "must", "my", "never", "now", "of", "on", "only", "or", "other", "our", "out", "over", "said", "same", "see", "should", "since", "some", "still", "such", "take", "than", "that", "the", "their", "them", "then", "there", "these", "they", "this", "those", "through", "to", "too", "under", "up", "very", "was", "way", "we", "well", "were", "what", "where", "which", "while", "who", "with", "would", "you", "your", "a", "i"], 20 | punctuationRE: /[@!"',.:;?()[\]]/g, 21 | processors: [ 22 | function(entry) { 23 | return entry.toLowerCase(); 24 | }, 25 | function(entry) { 26 | return entry.replace(config.punctuationRE, ''); 27 | }, 28 | function(entry) { 29 | var stopWords = config.stopWords; 30 | var terms = getTerms(entry); 31 | var i = terms.length; 32 | 33 | while((i--) !== 0) { 34 | if(stopWords.indexOf(terms[i]) !== -1) { 35 | terms.splice(i, 1); 36 | } 37 | } 38 | 39 | return terms.join(' '); 40 | } 41 | ] 42 | }; 43 | 44 | var stringify = function(arr) { 45 | var output = '['; 46 | var separator = ''; 47 | var empty = 0; 48 | 49 | for(var i = 0; i < arr.length; i++) { 50 | var element = arr[i]; 51 | 52 | if(element === undefined) { 53 | empty++; 54 | } else { 55 | if(typeof element !== "number") { 56 | element = stringify(element); 57 | } 58 | 59 | if(empty > 0) { 60 | output += separator + '@' + empty; 61 | empty = 0; 62 | separator = ','; 63 | } 64 | 65 | output += separator + element; 66 | separator = ','; 67 | } 68 | } 69 | 70 | return output + ']'; 71 | } 72 | 73 | var parse = function(str) { 74 | var arr = []; 75 | var stack = [arr]; 76 | var currentIndex = 1; 77 | 78 | while(stack.length !== 0) { 79 | var currentArr = stack[stack.length - 1]; 80 | var element = ''; 81 | 82 | for(; currentIndex < str.length; currentIndex++) { 83 | var char = str[currentIndex]; 84 | if(char === ',') { 85 | if(element.length !== 0) { 86 | if(element[0] === '@') { 87 | var elementInt = parseInt(element.substring(1)); 88 | for(var i = 0; i < elementInt; i++) { 89 | currentArr.push(undefined); 90 | } 91 | } else { 92 | currentArr.push(parseFloat(element)); 93 | } 94 | element = ''; 95 | } 96 | } else if(char === '[') { 97 | var childArr = []; 98 | currentArr.push(childArr); 99 | stack.push(childArr); 100 | currentIndex++; 101 | break; 102 | } else if(char === ']') { 103 | stack.pop(); 104 | currentIndex++; 105 | break; 106 | } else { 107 | element += char; 108 | } 109 | } 110 | 111 | if(element.length !== 0) { 112 | currentArr.push(parseInt(element)); 113 | } 114 | } 115 | 116 | return arr; 117 | } 118 | 119 | var getTerms = function(entry) { 120 | var terms = entry.split(whitespaceRE); 121 | 122 | if(terms[0].length === 0) { 123 | terms.shift(); 124 | } 125 | 126 | if(terms[terms.length - 1].length === 0) { 127 | terms.pop(); 128 | } 129 | 130 | return terms; 131 | } 132 | 133 | var processEntry = function(entry) { 134 | if(entry.length === 0) { 135 | return entry; 136 | } else { 137 | var processors = config.processors; 138 | 139 | for(var i = 0; i < processors.length; i++) { 140 | entry = processors[i](entry); 141 | } 142 | 143 | return entry; 144 | } 145 | } 146 | 147 | var update = function(results, resultIndexes, increment, data) { 148 | var relevance = data[1]; 149 | for(var i = 2; i < data.length; i++) { 150 | var index = data[i]; 151 | var resultIndex = resultIndexes[index]; 152 | if(resultIndex === undefined) { 153 | var lastIndex = results.length; 154 | resultIndexes[index] = lastIndex; 155 | results[lastIndex] = { 156 | index: index, 157 | score: relevance * increment 158 | }; 159 | } else { 160 | results[resultIndex].score += relevance * increment; 161 | } 162 | } 163 | } 164 | 165 | var Wade = function(data) { 166 | var search = function(query) { 167 | var index = search.index; 168 | var processed = processEntry(query); 169 | var results = []; 170 | var resultIndexes = {}; 171 | 172 | if(processed.length === 0) { 173 | return results; 174 | } else { 175 | var terms = getTerms(processed); 176 | var termsLength = terms.length; 177 | var exactTermsLength = termsLength - 1; 178 | var increment = 1 / termsLength; 179 | 180 | exactOuter: for(var i = 0; i < exactTermsLength; i++) { 181 | var term = terms[i]; 182 | var termLength = term.length - 1; 183 | var node = index; 184 | 185 | for(var j = 0; j <= termLength; j++) { 186 | var termOffset = node[0][0]; 187 | var termIndex = term.charCodeAt(j) + termOffset; 188 | 189 | if(termIndex < 1 || (termOffset === undefined && j === termLength) || node[termIndex] === undefined) { 190 | continue exactOuter; 191 | } 192 | 193 | node = node[termIndex]; 194 | } 195 | 196 | var nodeData = node[0]; 197 | if(nodeData.length !== 1) { 198 | update(results, resultIndexes, increment, nodeData); 199 | } 200 | } 201 | 202 | var lastTerm = terms[exactTermsLength]; 203 | var lastTermLength = lastTerm.length - 1; 204 | var node$1 = index; 205 | 206 | for(var i$1 = 0; i$1 <= lastTermLength; i$1++) { 207 | var lastTermOffset = node$1[0][0]; 208 | var lastTermIndex = lastTerm.charCodeAt(i$1) + lastTermOffset; 209 | 210 | if(lastTermIndex < 1 || (lastTermOffset === undefined && i$1 === lastTermLength) || node$1[lastTermIndex] === undefined) { 211 | break; 212 | } 213 | 214 | node$1 = node$1[lastTermIndex]; 215 | } 216 | 217 | if(node$1 !== undefined) { 218 | var nodes = [node$1]; 219 | for(var i$2 = 0; i$2 < nodes.length; i$2++) { 220 | var childNode = nodes[i$2]; 221 | var childNodeData = childNode[0]; 222 | 223 | if(childNodeData.length !== 1) { 224 | update(results, resultIndexes, increment, childNodeData); 225 | } 226 | 227 | for(var j$1 = 1; j$1 < childNode.length; j$1++) { 228 | var grandChildNode = childNode[j$1]; 229 | if(grandChildNode !== undefined) { 230 | nodes.push(grandChildNode); 231 | } 232 | } 233 | } 234 | } 235 | 236 | return results; 237 | } 238 | } 239 | 240 | if(Array.isArray(data)) { 241 | search.index = Wade.index(data); 242 | } else { 243 | search.index = parse(data); 244 | } 245 | 246 | return search; 247 | } 248 | 249 | Wade.index = function(data) { 250 | var dataLength = 0; 251 | var ranges = {}; 252 | var processed = []; 253 | 254 | for(var i = 0; i < data.length; i++) { 255 | var entry = processEntry(data[i]); 256 | 257 | if(entry.length !== 0) { 258 | var terms = getTerms(entry); 259 | var termsLength = terms.length; 260 | 261 | for(var j = 0; j < termsLength; j++) { 262 | var term = terms[j]; 263 | var processedTerm = []; 264 | var currentRanges = ranges; 265 | 266 | for(var n = 0; n < term.length; n++) { 267 | var char = term.charCodeAt(n); 268 | var highByte = char >>> 8; 269 | var lowByte = char & 0xFF; 270 | 271 | if(highByte !== 0) { 272 | if(currentRanges.minimum === undefined || highByte < currentRanges.minimum) { 273 | currentRanges.minimum = highByte; 274 | } 275 | 276 | if(currentRanges.maximum === undefined || highByte > currentRanges.maximum) { 277 | currentRanges.maximum = highByte; 278 | } 279 | 280 | var nextRanges = currentRanges[highByte]; 281 | if(nextRanges === undefined) { 282 | currentRanges = currentRanges[highByte] = {}; 283 | } else { 284 | currentRanges = nextRanges; 285 | } 286 | 287 | processedTerm.push(highByte); 288 | } 289 | 290 | if(currentRanges.minimum === undefined || lowByte < currentRanges.minimum) { 291 | currentRanges.minimum = lowByte; 292 | } 293 | 294 | if(currentRanges.maximum === undefined || lowByte > currentRanges.maximum) { 295 | currentRanges.maximum = lowByte; 296 | } 297 | 298 | var nextRanges$1 = currentRanges[lowByte]; 299 | if(nextRanges$1 === undefined) { 300 | currentRanges = currentRanges[lowByte] = {}; 301 | } else { 302 | currentRanges = nextRanges$1; 303 | } 304 | 305 | processedTerm.push(lowByte); 306 | } 307 | 308 | processed.push(i); 309 | processed.push(termsLength); 310 | processed.push(processedTerm); 311 | } 312 | } 313 | 314 | dataLength++; 315 | } 316 | 317 | var indexMinimum = ranges.minimum; 318 | var indexMaximum = ranges.maximum; 319 | var indexSize = 1; 320 | var indexOffset; 321 | 322 | if(indexMinimum !== undefined && indexMaximum !== undefined) { 323 | indexSize = indexMaximum - indexMinimum + 2; 324 | indexOffset = 1 - indexMinimum; 325 | } 326 | 327 | var nodeDataSets = []; 328 | var index = new Array(indexSize); 329 | index[0] = [indexOffset]; 330 | 331 | for(var i$1 = 0; i$1 < processed.length; i$1 += 3) { 332 | var dataIndex = processed[i$1]; 333 | var termsLength$1 = processed[i$1 + 1]; 334 | var processedTerm$1 = processed[i$1 + 2]; 335 | var processedTermLength = processedTerm$1.length - 1; 336 | var node = index; 337 | var termRanges = ranges; 338 | 339 | for(var j$1 = 0; j$1 < processedTermLength; j$1++) { 340 | var char$1 = processedTerm$1[j$1]; 341 | var charIndex = char$1 + node[0][0]; 342 | var termNode = node[charIndex]; 343 | termRanges = termRanges[char$1]; 344 | 345 | if(termNode === undefined) { 346 | var termMinimum = termRanges.minimum; 347 | var termMaximum = termRanges.maximum; 348 | termNode = node[charIndex] = new Array(termMaximum - termMinimum + 2); 349 | termNode[0] = [1 - termMinimum]; 350 | } 351 | 352 | node = termNode; 353 | } 354 | 355 | var lastChar = processedTerm$1[processedTermLength]; 356 | var lastCharIndex = lastChar + node[0][0] 357 | var lastTermNode = node[lastCharIndex]; 358 | termRanges = termRanges[lastChar]; 359 | 360 | if(lastTermNode === undefined) { 361 | var lastTermMinimum = termRanges.minimum; 362 | var lastTermMaximum = termRanges.maximum; 363 | var lastTermSize = 1; 364 | var lastTermOffset = (void 0); 365 | 366 | if(lastTermMinimum !== undefined && lastTermMaximum !== undefined) { 367 | lastTermSize = lastTermMaximum - lastTermMinimum + 2; 368 | lastTermOffset = 1 - lastTermMinimum; 369 | } 370 | 371 | lastTermNode = node[lastCharIndex] = new Array(lastTermSize); 372 | nodeDataSets.push(lastTermNode[0] = [lastTermOffset, 1 / termsLength$1, dataIndex]); 373 | } else { 374 | var nodeData = lastTermNode[0]; 375 | 376 | if(nodeData.length === 1) { 377 | nodeData.push(1 / termsLength$1); 378 | nodeData.push(dataIndex); 379 | nodeDataSets.push(nodeData); 380 | } else { 381 | nodeData[1] += 1 / termsLength$1; 382 | nodeData.push(dataIndex); 383 | } 384 | } 385 | } 386 | 387 | for(var i$2 = 0; i$2 < nodeDataSets.length; i$2++) { 388 | var nodeData$1 = nodeDataSets[i$2]; 389 | nodeData$1[1] = 1.5 - (nodeData$1[1] / dataLength); 390 | } 391 | 392 | return index; 393 | } 394 | 395 | Wade.save = function(search) { 396 | return stringify(search.index); 397 | } 398 | 399 | Wade.config = config; 400 | 401 | Wade.version = "0.3.3"; 402 | 403 | return Wade; 404 | })); 405 | -------------------------------------------------------------------------------- /dist/wade.min.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Wade v0.3.3 3 | * Copyright 2017-2018 Kabir Shah 4 | * Released under the MIT License 5 | * https://github.com/kbrsh/wade 6 | */ 7 | !function(e,r){"undefined"==typeof module?e.Wade=r():module.exports=r()}(this,function(){var e=/\s+/g,r={stopWords:["about","after","all","also","am","an","and","another","any","are","as","at","be","because","been","before","being","between","both","but","by","came","can","come","could","did","do","each","for","from","get","got","has","had","he","have","her","here","him","himself","his","how","if","in","into","is","it","like","make","many","me","might","more","most","much","must","my","never","now","of","on","only","or","other","our","out","over","said","same","see","should","since","some","still","such","take","than","that","the","their","them","then","there","these","they","this","those","through","to","too","under","up","very","was","way","we","well","were","what","where","which","while","who","with","would","you","your","a","i"],punctuationRE:/[@!"',.:;?()[\]]/g,processors:[function(e){return e.toLowerCase()},function(e){return e.replace(r.punctuationRE,"")},function(e){for(var n=r.stopWords,i=o(e),t=i.length;0!=t--;)-1!==n.indexOf(i[t])&&i.splice(t,1);return i.join(" ")}]},n=function(e){for(var r="[",i="",o=0,t=0;t0&&(r+=i+"@"+o,o=0,i=","),r+=i+a,i=",")}return r+"]"},i=function(e){for(var r=[],n=[r],i=1;0!==n.length;){for(var o=n[n.length-1],t="";i>>8,p=255&g;if(0!==c){(void 0===l.minimum||cl.maximum)&&(l.maximum=c);var w=l[c];l=void 0===w?l[c]={}:w,f.push(c)}(void 0===l.minimum||pl.maximum)&&(l.maximum=p);var x=l[p];l=void 0===x?l[p]={}:x,f.push(p)}i.push(a),i.push(m),i.push(f)}r++}var y,b=n.minimum,A=n.maximum,k=1;void 0!==b&&void 0!==A&&(k=A-b+2,y=1-b);var C=[],W=new Array(k);W[0]=[y];for(var E=0;E 0) { 45 | output += separator + '@' + empty; 46 | empty = 0; 47 | separator = ','; 48 | } 49 | 50 | output += separator + element; 51 | separator = ','; 52 | } 53 | } 54 | 55 | return output + ']'; 56 | } 57 | 58 | const parse = function(str) { 59 | let arr = []; 60 | let stack = [arr]; 61 | let currentIndex = 1; 62 | 63 | while(stack.length !== 0) { 64 | let currentArr = stack[stack.length - 1]; 65 | let element = ''; 66 | 67 | for(; currentIndex < str.length; currentIndex++) { 68 | const char = str[currentIndex]; 69 | if(char === ',') { 70 | if(element.length !== 0) { 71 | if(element[0] === '@') { 72 | const elementInt = parseInt(element.substring(1)); 73 | for(let i = 0; i < elementInt; i++) { 74 | currentArr.push(undefined); 75 | } 76 | } else { 77 | currentArr.push(parseFloat(element)); 78 | } 79 | element = ''; 80 | } 81 | } else if(char === '[') { 82 | let childArr = []; 83 | currentArr.push(childArr); 84 | stack.push(childArr); 85 | currentIndex++; 86 | break; 87 | } else if(char === ']') { 88 | stack.pop(); 89 | currentIndex++; 90 | break; 91 | } else { 92 | element += char; 93 | } 94 | } 95 | 96 | if(element.length !== 0) { 97 | currentArr.push(parseInt(element)); 98 | } 99 | } 100 | 101 | return arr; 102 | } 103 | 104 | const getTerms = function(entry) { 105 | let terms = entry.split(whitespaceRE); 106 | 107 | if(terms[0].length === 0) { 108 | terms.shift(); 109 | } 110 | 111 | if(terms[terms.length - 1].length === 0) { 112 | terms.pop(); 113 | } 114 | 115 | return terms; 116 | } 117 | 118 | const processEntry = function(entry) { 119 | if(entry.length === 0) { 120 | return entry; 121 | } else { 122 | const processors = config.processors; 123 | 124 | for(let i = 0; i < processors.length; i++) { 125 | entry = processors[i](entry); 126 | } 127 | 128 | return entry; 129 | } 130 | } 131 | 132 | const update = function(results, resultIndexes, increment, data) { 133 | const relevance = data[1]; 134 | for(let i = 2; i < data.length; i++) { 135 | const index = data[i]; 136 | const resultIndex = resultIndexes[index]; 137 | if(resultIndex === undefined) { 138 | const lastIndex = results.length; 139 | resultIndexes[index] = lastIndex; 140 | results[lastIndex] = { 141 | index: index, 142 | score: relevance * increment 143 | }; 144 | } else { 145 | results[resultIndex].score += relevance * increment; 146 | } 147 | } 148 | } 149 | 150 | const Wade = function(data) { 151 | const search = function(query) { 152 | const index = search.index; 153 | const processed = processEntry(query); 154 | let results = []; 155 | let resultIndexes = {}; 156 | 157 | if(processed.length === 0) { 158 | return results; 159 | } else { 160 | const terms = getTerms(processed); 161 | const termsLength = terms.length; 162 | const exactTermsLength = termsLength - 1; 163 | const increment = 1 / termsLength; 164 | 165 | exactOuter: for(let i = 0; i < exactTermsLength; i++) { 166 | const term = terms[i]; 167 | const termLength = term.length - 1; 168 | let node = index; 169 | 170 | for(let j = 0; j <= termLength; j++) { 171 | const termOffset = node[0][0]; 172 | const termIndex = term.charCodeAt(j) + termOffset; 173 | 174 | if(termIndex < 1 || (termOffset === undefined && j === termLength) || node[termIndex] === undefined) { 175 | continue exactOuter; 176 | } 177 | 178 | node = node[termIndex]; 179 | } 180 | 181 | const nodeData = node[0]; 182 | if(nodeData.length !== 1) { 183 | update(results, resultIndexes, increment, nodeData); 184 | } 185 | } 186 | 187 | const lastTerm = terms[exactTermsLength]; 188 | const lastTermLength = lastTerm.length - 1; 189 | let node = index; 190 | 191 | for(let i = 0; i <= lastTermLength; i++) { 192 | const lastTermOffset = node[0][0]; 193 | const lastTermIndex = lastTerm.charCodeAt(i) + lastTermOffset; 194 | 195 | if(lastTermIndex < 1 || (lastTermOffset === undefined && i === lastTermLength) || node[lastTermIndex] === undefined) { 196 | break; 197 | } 198 | 199 | node = node[lastTermIndex]; 200 | } 201 | 202 | if(node !== undefined) { 203 | let nodes = [node]; 204 | for(let i = 0; i < nodes.length; i++) { 205 | let childNode = nodes[i]; 206 | const childNodeData = childNode[0]; 207 | 208 | if(childNodeData.length !== 1) { 209 | update(results, resultIndexes, increment, childNodeData); 210 | } 211 | 212 | for(let j = 1; j < childNode.length; j++) { 213 | const grandChildNode = childNode[j]; 214 | if(grandChildNode !== undefined) { 215 | nodes.push(grandChildNode); 216 | } 217 | } 218 | } 219 | } 220 | 221 | return results; 222 | } 223 | } 224 | 225 | if(Array.isArray(data)) { 226 | search.index = Wade.index(data); 227 | } else { 228 | search.index = parse(data); 229 | } 230 | 231 | return search; 232 | } 233 | 234 | Wade.index = function(data) { 235 | let dataLength = 0; 236 | let ranges = {}; 237 | let processed = []; 238 | 239 | for(let i = 0; i < data.length; i++) { 240 | const entry = processEntry(data[i]); 241 | 242 | if(entry.length !== 0) { 243 | const terms = getTerms(entry); 244 | const termsLength = terms.length; 245 | 246 | for(let j = 0; j < termsLength; j++) { 247 | const term = terms[j]; 248 | let processedTerm = []; 249 | let currentRanges = ranges; 250 | 251 | for(let n = 0; n < term.length; n++) { 252 | const char = term.charCodeAt(n); 253 | const highByte = char >>> 8; 254 | const lowByte = char & 0xFF; 255 | 256 | if(highByte !== 0) { 257 | if(currentRanges.minimum === undefined || highByte < currentRanges.minimum) { 258 | currentRanges.minimum = highByte; 259 | } 260 | 261 | if(currentRanges.maximum === undefined || highByte > currentRanges.maximum) { 262 | currentRanges.maximum = highByte; 263 | } 264 | 265 | let nextRanges = currentRanges[highByte]; 266 | if(nextRanges === undefined) { 267 | currentRanges = currentRanges[highByte] = {}; 268 | } else { 269 | currentRanges = nextRanges; 270 | } 271 | 272 | processedTerm.push(highByte); 273 | } 274 | 275 | if(currentRanges.minimum === undefined || lowByte < currentRanges.minimum) { 276 | currentRanges.minimum = lowByte; 277 | } 278 | 279 | if(currentRanges.maximum === undefined || lowByte > currentRanges.maximum) { 280 | currentRanges.maximum = lowByte; 281 | } 282 | 283 | let nextRanges = currentRanges[lowByte]; 284 | if(nextRanges === undefined) { 285 | currentRanges = currentRanges[lowByte] = {}; 286 | } else { 287 | currentRanges = nextRanges; 288 | } 289 | 290 | processedTerm.push(lowByte); 291 | } 292 | 293 | processed.push(i); 294 | processed.push(termsLength); 295 | processed.push(processedTerm); 296 | } 297 | } 298 | 299 | dataLength++; 300 | } 301 | 302 | const indexMinimum = ranges.minimum; 303 | const indexMaximum = ranges.maximum; 304 | let indexSize = 1; 305 | let indexOffset; 306 | 307 | if(indexMinimum !== undefined && indexMaximum !== undefined) { 308 | indexSize = indexMaximum - indexMinimum + 2; 309 | indexOffset = 1 - indexMinimum; 310 | } 311 | 312 | let nodeDataSets = []; 313 | let index = new Array(indexSize); 314 | index[0] = [indexOffset]; 315 | 316 | for(let i = 0; i < processed.length; i += 3) { 317 | const dataIndex = processed[i]; 318 | const termsLength = processed[i + 1]; 319 | const processedTerm = processed[i + 2]; 320 | const processedTermLength = processedTerm.length - 1; 321 | let node = index; 322 | let termRanges = ranges; 323 | 324 | for(let j = 0; j < processedTermLength; j++) { 325 | const char = processedTerm[j]; 326 | const charIndex = char + node[0][0]; 327 | let termNode = node[charIndex]; 328 | termRanges = termRanges[char]; 329 | 330 | if(termNode === undefined) { 331 | const termMinimum = termRanges.minimum; 332 | const termMaximum = termRanges.maximum; 333 | termNode = node[charIndex] = new Array(termMaximum - termMinimum + 2); 334 | termNode[0] = [1 - termMinimum]; 335 | } 336 | 337 | node = termNode; 338 | } 339 | 340 | const lastChar = processedTerm[processedTermLength]; 341 | const lastCharIndex = lastChar + node[0][0] 342 | let lastTermNode = node[lastCharIndex]; 343 | termRanges = termRanges[lastChar]; 344 | 345 | if(lastTermNode === undefined) { 346 | const lastTermMinimum = termRanges.minimum; 347 | const lastTermMaximum = termRanges.maximum; 348 | let lastTermSize = 1; 349 | let lastTermOffset; 350 | 351 | if(lastTermMinimum !== undefined && lastTermMaximum !== undefined) { 352 | lastTermSize = lastTermMaximum - lastTermMinimum + 2; 353 | lastTermOffset = 1 - lastTermMinimum; 354 | } 355 | 356 | lastTermNode = node[lastCharIndex] = new Array(lastTermSize); 357 | nodeDataSets.push(lastTermNode[0] = [lastTermOffset, 1 / termsLength, dataIndex]); 358 | } else { 359 | let nodeData = lastTermNode[0]; 360 | 361 | if(nodeData.length === 1) { 362 | nodeData.push(1 / termsLength); 363 | nodeData.push(dataIndex); 364 | nodeDataSets.push(nodeData); 365 | } else { 366 | nodeData[1] += 1 / termsLength; 367 | nodeData.push(dataIndex); 368 | } 369 | } 370 | } 371 | 372 | for(let i = 0; i < nodeDataSets.length; i++) { 373 | let nodeData = nodeDataSets[i]; 374 | nodeData[1] = 1.5 - (nodeData[1] / dataLength); 375 | } 376 | 377 | return index; 378 | } 379 | 380 | Wade.save = function(search) { 381 | return stringify(search.index); 382 | } 383 | 384 | Wade.config = config; 385 | 386 | Wade.version = "__VERSION__"; 387 | -------------------------------------------------------------------------------- /src/wrapper.js: -------------------------------------------------------------------------------- 1 | (function(root, factory) { 2 | /* ======= Global Wade ======= */ 3 | if(typeof module === "undefined") { 4 | root.Wade = factory(); 5 | } else { 6 | module.exports = factory(); 7 | } 8 | }(this, function() { 9 | //=require ../dist/wade.js 10 | return Wade; 11 | })); 12 | -------------------------------------------------------------------------------- /test/index/index.js: -------------------------------------------------------------------------------- 1 | const Wade = require("../../dist/wade.js"); 2 | const expect = require("chai").expect; 3 | 4 | describe("Search Index", function() { 5 | const search = Wade(["Hey", "Hello", "Branch"]); 6 | const index = search.index; 7 | 8 | it("should create a search index", function() { 9 | expect(index).to.deep.equal([ 10 | [-97], 11 | [ 12 | [-113], 13 | [ 14 | [-96], 15 | [[-109], [[-98], [[-103], [[undefined, 1.1666666666666667, 2]]]]] 16 | ] 17 | ], 18 | undefined, 19 | undefined, 20 | undefined, 21 | undefined, 22 | undefined, 23 | [ 24 | [-100], 25 | [ 26 | [-107], 27 | [[-107], [[-110], [[undefined, 1.1666666666666667, 1]]]], 28 | undefined, 29 | undefined, 30 | undefined, 31 | undefined, 32 | undefined, 33 | undefined, 34 | undefined, 35 | undefined, 36 | undefined, 37 | undefined, 38 | undefined, 39 | undefined, 40 | [[undefined, 1.1666666666666667, 0]] 41 | ] 42 | ] 43 | ]); 44 | }); 45 | 46 | it("should load a saved index", function() { 47 | expect(index).to.deep.equal(Wade("[[-97],[[-113],[[-96],[[-109],[[-98],[[-103],[[@1,1.1666666666666667,2]]]]]]],@5,[[-100],[[-107],[[-107],[[-110],[[@1,1.1666666666666667,1]]]],@12,[[@1,1.1666666666666667,0]]]]]").index); 48 | }); 49 | }); 50 | -------------------------------------------------------------------------------- /test/search/create.js: -------------------------------------------------------------------------------- 1 | const Wade = require("../../dist/wade.js"); 2 | const expect = require("chai").expect; 3 | 4 | describe("Create Search", function() { 5 | it("should create a search function", function() { 6 | const search = Wade([]); 7 | expect(search).to.be.a('function'); 8 | }); 9 | }); 10 | -------------------------------------------------------------------------------- /test/search/process.js: -------------------------------------------------------------------------------- 1 | const Wade = require("../../dist/wade.js"); 2 | const expect = require("chai").expect; 3 | 4 | const processEntry = function(entry) { 5 | if(entry.length === 0) { 6 | return entry; 7 | } else { 8 | const processors = Wade.config.processors; 9 | 10 | for(let i = 0; i < processors.length; i++) { 11 | entry = processors[i](entry); 12 | } 13 | 14 | return entry; 15 | } 16 | } 17 | 18 | describe("Processing", function() { 19 | it("should process data", function() { 20 | expect(processEntry("ALL UPPERCASE!!")).to.equal("uppercase"); 21 | expect(processEntry("This. is wade")).to.equal("wade"); 22 | expect(processEntry("")).to.equal(""); 23 | }); 24 | }); 25 | -------------------------------------------------------------------------------- /test/search/search.js: -------------------------------------------------------------------------------- 1 | const Wade = require("../../dist/wade.js"); 2 | const expect = require("chai").expect; 3 | 4 | describe("Search", function() { 5 | const data = ["I have to get that suit.", "He's the worst, earth.", "what do you want?", "it is the man.", "the man doesn't have the money."]; 6 | const search = Wade(data); 7 | 8 | describe("single term", function() { 9 | const results = search("man"); 10 | 11 | it("should have two results", function() { 12 | expect(results.length).to.equal(2); 13 | }); 14 | 15 | it("should have relevant results", function() { 16 | expect(results[0].index).to.equal(3); 17 | expect(results[1].index).to.equal(4); 18 | }); 19 | }); 20 | 21 | describe("single term with prefix", function() { 22 | const results = search("wor"); 23 | 24 | it("should have one result", function() { 25 | expect(results.length).to.equal(1); 26 | }); 27 | 28 | it("should have relevant results", function() { 29 | expect(results[0].index).to.equal(1); 30 | }); 31 | }); 32 | 33 | describe("multiple terms for one result", function() { 34 | const results = search("worst earth"); 35 | 36 | it("should have one result", function() { 37 | expect(results.length).to.equal(1); 38 | }); 39 | 40 | it("should have relevant results", function() { 41 | expect(results[0].index).to.equal(1); 42 | }); 43 | }); 44 | 45 | describe("multiple terms for one result with prefix", function() { 46 | const results = search("worst ear"); 47 | 48 | it("should have one result", function() { 49 | expect(results.length).to.equal(1); 50 | }); 51 | 52 | it("should have relevant results", function() { 53 | expect(results[0].index).to.equal(1); 54 | }); 55 | }); 56 | 57 | describe("multiple terms for multiple results", function() { 58 | const results = search("get that suit worst earth"); 59 | 60 | it("should have one result", function() { 61 | expect(results.length).to.equal(2); 62 | }); 63 | 64 | it("should have relevant results", function() { 65 | expect(results[0].index).to.equal(0); 66 | expect(results[1].index).to.equal(1); 67 | }); 68 | }); 69 | 70 | describe("multiple terms for multiple results with prefix", function() { 71 | const results = search("get that suit worst ear"); 72 | 73 | it("should have one result", function() { 74 | expect(results.length).to.equal(2); 75 | }); 76 | 77 | it("should have relevant results", function() { 78 | expect(results[0].index).to.equal(0); 79 | expect(results[1].index).to.equal(1); 80 | }); 81 | }); 82 | }); 83 | --------------------------------------------------------------------------------