├── LICENSE ├── README.md ├── examples └── vegetables.js ├── index.js ├── lib ├── autocomplete.js └── trie.js ├── package.json └── test ├── common.js ├── run ├── test.basic.js ├── test.large.js └── words.txt /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License 2 | 3 | Copyright (c) 2010-2015 Google, Inc. http://angularjs.org 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 13 | all 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 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Node Autocomplete 2 | 3 | [Node Autocomplete](http://www.github.com/marccampbell/node-autocomplete) is an autocomplete library for [node.js](http://nodejs.org). 4 | 5 | ## Installation 6 | 7 | ```bash 8 | $ npm install autocomplete 9 | ``` 10 | 11 | ## Features 12 | 13 | - in memory, in process, not redis dependent 14 | - internal [trie](http://en.wikipedia.org/wiki/Trie) data structure to store the strings 15 | - super fast for adding, removing and lookups 16 | - performance tested for string lists of 500,000 words 17 | - high level of tests 18 | 19 | ## Running Tests 20 | 21 | Install development dependencies: 22 | 23 | ```bash 24 | $ npm install 25 | ``` 26 | 27 | Then: 28 | 29 | ```bash 30 | $ test/run 31 | ``` 32 | 33 | Actively tested with node: 34 | 35 | - 0.4.9 36 | 37 | ## Authors 38 | 39 | * Marc Campbell 40 | 41 | ## License 42 | 43 | (The MIT License) 44 | 45 | Copyright (c) 2011 Marc Campbell <marc.e.campbell@gmail.com> 46 | 47 | Permission is hereby granted, free of charge, to any person obtaining 48 | a copy of this software and associated documentation files (the 49 | 'Software'), to deal in the Software without restriction, including 50 | without limitation the rights to use, copy, modify, merge, publish, 51 | distribute, sublicense, and/or sell copies of the Software, and to 52 | permit persons to whom the Software is furnished to do so, subject to 53 | the following conditions: 54 | 55 | The above copyright notice and this permission notice shall be 56 | included in all copies or substantial portions of the Software. 57 | 58 | THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, 59 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 60 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 61 | IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY 62 | CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, 63 | TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 64 | SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 65 | 66 | -------------------------------------------------------------------------------- /examples/vegetables.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Module dependencies. 3 | */ 4 | 5 | var Autocomplete = require('../lib/autocomplete'); 6 | 7 | var VEGETABLES = ['arugula', 'beet', 'broccoli', 'cauliflower', 'corn', 'cabbage', 'carrot']; 8 | 9 | // Create the autocomplete object 10 | var autocomplete = Autocomplete.connectAutocomplete(); 11 | 12 | // Initialize the autocomplete object and define a 13 | // callback to populate it with data 14 | autocomplete.initialize(function(onReady) { 15 | onReady(VEGETABLES); 16 | }); 17 | 18 | // Later... When it's time to search: 19 | var matches = autocomplete.search('ca'); 20 | console.log(matches); 21 | 22 | // this will print: 23 | // ['cabbage', 'carrot'] 24 | 25 | 26 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | module.exports = require('./lib/autocomplete'); 2 | -------------------------------------------------------------------------------- /lib/autocomplete.js: -------------------------------------------------------------------------------- 1 | var EventEmitter = require('events').EventEmitter, 2 | util = require('util'), 3 | Trie = require('./trie').Trie; 4 | 5 | exports = module.exports = Autocomplete; 6 | exports.version = '0.0.1'; 7 | 8 | 9 | exports.connectAutocomplete = function(onReady) { 10 | Autocomplete.singleton = new Autocomplete(); 11 | if(onReady) 12 | onReady(Autocomplete.singleton); 13 | return Autocomplete.singleton; 14 | }; 15 | 16 | function Autocomplete(name) { 17 | this.trie = new Trie() 18 | EventEmitter.call(this); 19 | } 20 | util.inherits(Autocomplete, EventEmitter); 21 | 22 | Autocomplete.prototype.close = function() { 23 | this.emit('close'); 24 | }; 25 | 26 | Autocomplete.prototype.initialize = function(getInitialElements) { 27 | getInitialElements(function(elements) { 28 | elements.forEach(function(element) { 29 | if(typeof element === 'object') { 30 | var item = new Object; 31 | item.key = element[0]; 32 | item.value = element[1]; 33 | Autocomplete.singleton.addElement(item); 34 | } 35 | else { 36 | Autocomplete.singleton.addElement(element); 37 | } 38 | }); 39 | Autocomplete.singleton.emit('loaded'); 40 | }); 41 | }; 42 | 43 | Autocomplete.prototype.addElement = function(element) { 44 | this.trie.addValue(element); 45 | }; 46 | 47 | 48 | Autocomplete.prototype.removeElement = function(element) { 49 | this.trie.removeValue(element); 50 | }; 51 | 52 | Autocomplete.prototype.search = function(prefix) { 53 | return this.trie.autoComplete(prefix); 54 | }; 55 | 56 | 57 | -------------------------------------------------------------------------------- /lib/trie.js: -------------------------------------------------------------------------------- 1 | var sys = require('sys'); 2 | 3 | Trie = function() { 4 | this.words = 0; 5 | this.prefixes = 0; 6 | this.value = ""; 7 | this.children = []; 8 | }; 9 | 10 | 11 | /** 12 | * Add a value to the trie 13 | * 14 | * @param {String} value 15 | * @param {Number} index (optional) 16 | */ 17 | Trie.prototype.addValue = function(item, index) { 18 | if (!index) { 19 | index = 0; 20 | } 21 | if(item === null) { 22 | return; 23 | } 24 | 25 | var isObject = false; 26 | 27 | if(typeof item === 'object') { 28 | isObject = true; 29 | } 30 | 31 | if (isObject && item.key.length === 0) { 32 | return; 33 | } 34 | else if(!isObject && item.length === 0) { 35 | return; 36 | } 37 | 38 | if ((isObject && index === item.key.length) || (!isObject && index === item.length)) { 39 | this.words++; 40 | this.value = isObject ? item.value : item; 41 | return; 42 | } 43 | 44 | this.prefixes++; 45 | var key = isObject ? item.key[index] : item[index]; 46 | if (this.children[key] === undefined) { 47 | this.children[key] = new Trie(); 48 | } 49 | var child = this.children[key]; 50 | child.addValue(item, index + 1); 51 | }; 52 | 53 | /** 54 | * Remove a value form the trie 55 | * 56 | * @param {String} value 57 | * @param {Number} index (optional) 58 | */ 59 | Trie.prototype.removeValue = function(item, index) { 60 | if (!index) { 61 | index = 0; 62 | } 63 | 64 | if (item.length === 0) { 65 | return; 66 | } 67 | 68 | if (index === item.length) { 69 | this.words--; 70 | this.value=""; 71 | } 72 | else { 73 | this.prefixes--; 74 | var key = item[index]; 75 | var child = this.children[key]; 76 | if(child) child.removeValue(item, index + 1); 77 | // to remove a node, we need remove it from parent's children array 78 | if(index === (item.length -1)) { 79 | if(Object.keys(child.children).length === 0) {// only remove when there is no children 80 | delete this.children[key]; 81 | } 82 | } 83 | } 84 | }; 85 | 86 | /** Get the count of instances of a word in the entire trie 87 | * 88 | * @param {String} word 89 | * @param {Number} index (optional) 90 | */ 91 | Trie.prototype.wordCount = function(value, index) { 92 | if (!index) { 93 | index = 0; 94 | } 95 | 96 | if (value.length === 0) { 97 | return 0; 98 | } 99 | 100 | if (index === value.length) { 101 | return this.words; 102 | } else { 103 | var key = value[index]; 104 | var child = this.children[key]; 105 | if (child) { 106 | return child.wordCount(value, index + 1); 107 | } else { 108 | return 0; 109 | } 110 | } 111 | }; 112 | 113 | /** Get the count of instances of a prefix in the enture trie 114 | * 115 | * @param {String} prefix 116 | * @param {Number} index 117 | */ 118 | Trie.prototype.prefixCount = function(prefix, index) { 119 | if (!index) { 120 | index = 0; 121 | } 122 | 123 | if (prefix.length === 0) { 124 | return 0; 125 | } 126 | 127 | if (index === prefix.length) { 128 | return this.prefixes; 129 | } else { 130 | var key = prefix[index]; 131 | var child = this.children[key]; 132 | if (child) { 133 | return child.prefixCount(prefix, index + 1); 134 | } else { 135 | return 0; 136 | } 137 | } 138 | }; 139 | 140 | /** 141 | * Check if a word exists in the trie 142 | * 143 | * @param {String} value 144 | */ 145 | Trie.prototype.wordExists = function(value) { 146 | if (value.length === 0) { 147 | return false; 148 | } 149 | 150 | return this.wordCount(value) > 0; 151 | }; 152 | 153 | /** 154 | * Return all words with a prefix 155 | * 156 | * @param {String} prefix 157 | */ 158 | Trie.prototype.allChildWords = function(prefix) { 159 | if (!prefix) { 160 | prefix = ''; 161 | } 162 | 163 | var words = []; 164 | if (this.words > 0) { 165 | if(this.value.lenth === 0) { 166 | var tmp = new Object(); 167 | tmp.key = prefix; 168 | tmp.value = prefix 169 | words.push(tmp); 170 | } 171 | else { 172 | var tmp = new Object(); 173 | tmp.key = prefix; 174 | tmp.value = this.value; 175 | words.push(tmp); 176 | } 177 | } 178 | 179 | for (key in this.children) { 180 | var child = this.children[key]; 181 | words = words.concat(child.allChildWords(prefix + key)); 182 | } 183 | 184 | return words; 185 | } 186 | 187 | /** 188 | * Perform an autocomplete match 189 | * 190 | * @param {String} prefix 191 | * @param {Number} index 192 | */ 193 | Trie.prototype.autoComplete = function(prefix, index) { 194 | if (!index) { 195 | index = 0; 196 | } 197 | 198 | if (prefix.length === 0) { 199 | return []; 200 | } 201 | 202 | var key = prefix[index]; 203 | var child = this.children[key]; 204 | if (!child) { 205 | return []; 206 | } else { 207 | if (index === prefix.length - 1) { 208 | return child.allChildWords(prefix); 209 | } else { 210 | return child.autoComplete(prefix, index + 1); 211 | } 212 | } 213 | }; 214 | 215 | exports.Trie = Trie; 216 | 217 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "autocomplete" 3 | , "description": "An in-memory autocomplete package based on the trie data structure" 4 | , "keywords": ["autocomplete", "trie", "search"] 5 | , "version": "0.0.2" 6 | , "homepage": "http://www.github.com/marccampbell/node-autocomplete" 7 | , "repository": "git://github.com/marccampbell/node-autocomplete.git" 8 | , "author": "Marc Campbell (http://twitter.com/mccode)" 9 | , "main": "index" 10 | , "dependencies": { 11 | } 12 | , "devDependencies": { "should": "0.2.x"} 13 | , "engines": { "node": "*" } 14 | } 15 | 16 | -------------------------------------------------------------------------------- /test/common.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Module dependencies. 3 | */ 4 | 5 | var should = require('should'); 6 | 7 | 8 | -------------------------------------------------------------------------------- /test/run: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | clean() { 4 | killall -KILL node &> /dev/null 5 | } 6 | 7 | clean 8 | echo 9 | 10 | files=test/test.*.js 11 | for file in $files; do 12 | printf "\033[90m ${file#test/}\033[0m " 13 | node $@ $file && echo "\033[36m✓\033[0m" 14 | test $? -eq 0 || exit $? 15 | done 16 | echo 17 | 18 | -------------------------------------------------------------------------------- /test/test.basic.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Module dependencies. 3 | */ 4 | 5 | var autocomplete = require('../lib/autocomplete'), 6 | sys = require('sys'); 7 | 8 | require('./common'); 9 | 10 | process.cwd().should.include.string('autocomplete'); 11 | 12 | var a = autocomplete.connectAutocomplete(onReady); 13 | 14 | a.on('close', function() { 15 | process.exit(0); 16 | }); 17 | 18 | a.on('loaded', function() { 19 | // Search for a prefix with multiple matches 20 | var result = a.search('ap'); 21 | console.log('\n'); 22 | console.log('result for searching ap:\n'); 23 | console.log(result); 24 | var ap = a.search('ap').length.should.eql(3); 25 | // Search for a prefix with a single match 26 | a.search('bana').length.should.eql(1); 27 | 28 | // Search for a prefix with 0 matches 29 | a.search('cheese').length.should.eql(0); 30 | 31 | // Add a result 32 | a.addElement('cheeseburger'); 33 | a.search('cheeseburger').length.should.eql(1); 34 | 35 | // Remove a result 36 | a.removeElement('apple'); 37 | var result = a.search('ap'); 38 | console.log('\n'); 39 | console.log('result for searching ap after remove apple:\n'); 40 | console.log(result); 41 | a.search('app').length.should.eql(2); 42 | a.removeElement('banana'); 43 | a.search('banana').length.should.eql(0); 44 | 45 | a.search('apple pie').length.should.eql(1); 46 | 47 | matches = a.search('apple'); 48 | a.search('apple').length.should.eql('2'); 49 | a.close(); 50 | }); 51 | 52 | function onReady(autoComplete) { 53 | autoComplete.initialize(function(addItem) { 54 | addItem(['fruit', ['apple', 'red'], 'banana', 'orange', ['apples', 'yumyum'], ['apple pie', 'tasty'], 55 | 'kiwi', 'orange juice']); 56 | }); 57 | } 58 | 59 | -------------------------------------------------------------------------------- /test/test.large.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Module dependencies. 3 | */ 4 | 5 | var autocomplete = require('../lib/autocomplete'), 6 | sys = require('sys'), 7 | fs = require('fs'); 8 | 9 | require('./common'); 10 | 11 | process.cwd().should.include.string('autocomplete'); 12 | 13 | 14 | // Read the really big data file into memory 15 | function readLines(input, onReady) { 16 | var words = []; 17 | 18 | input.on('data', function(data) { 19 | words = words.concat(data.toString().split('\n')); 20 | }); 21 | 22 | input.on('end', function() { 23 | onReady(words); 24 | }); 25 | 26 | }; 27 | 28 | var input = fs.createReadStream('./test/words.txt'); 29 | readLines(input, function(words) { 30 | var a = autocomplete.connectAutocomplete(); 31 | 32 | a.on('close', function() { 33 | process.exit(0); 34 | }); 35 | 36 | a.on('loaded', function() { 37 | var start = new Date(); 38 | matches = a.search('mon'); 39 | var duration = new Date() - start; 40 | console.log('Elapsed time to search dictionary for 3 character prefix: ' + duration + 'ms'); 41 | 42 | matches.length.should.eql(663); 43 | }); 44 | 45 | a.initialize(function(onReady) { 46 | console.log('\nLoading large dictionary of words (~500,000 words)'); 47 | var start = new Date(); 48 | onReady(words); 49 | var duration = new Date() - start; 50 | console.log('Elapsed time to load dictionary: ' + duration + 'ms'); 51 | }); 52 | }); 53 | 54 | --------------------------------------------------------------------------------