├── .npmignore ├── index.js ├── History.md ├── Makefile ├── examples ├── urls.json ├── static │ └── form.html ├── simple.js ├── web.demo.node.js └── web-index.js ├── package.json ├── .github └── workflows │ └── nodejs.yml ├── .gitignore ├── benchmarks ├── benchmark.reds.node.js └── index.js ├── Readme.md └── lib └── redredisearch.js /.npmignore: -------------------------------------------------------------------------------- 1 | support 2 | test 3 | examples 4 | *.sock 5 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | 2 | module.exports = require('./lib/redredisearch.js'); -------------------------------------------------------------------------------- /History.md: -------------------------------------------------------------------------------- 1 | 0.0.1 / July 2017 2 | ================== 3 | * Forked from Reds 4 | 5 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | 2 | test: 3 | @node test 4 | 5 | bench: 6 | @./node_modules/.bin/matcha benchmarks 7 | 8 | .PHONY: test bench -------------------------------------------------------------------------------- /examples/urls.json: -------------------------------------------------------------------------------- 1 | [ 2 | "http://learnboost.com", 3 | "http://nodejs.org", 4 | "http://expressjs.com", 5 | "http://expressjs.com/guide.html", 6 | "http://expressjs.com/applications.html", 7 | "http://pugjs.com", 8 | "http://google.com", 9 | "http://ign.com", 10 | "http://1up.com", 11 | "http://gamespot.com", 12 | "http://yahoo.com", 13 | "http://purevolume.com", 14 | "http://icefilms.info", 15 | "http://mongoosejs.com/", 16 | "http://mongoosejs.com/docs/api.html", 17 | "http://news.ycombinator.com", 18 | "http://redis.io", 19 | "http://redislabs.com" 20 | ] -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "redredisearch", 3 | "version": "0.0.1", 4 | "description": "Redis search for node.js powered by the RediSearch module", 5 | "keywords": [ 6 | "redis", 7 | "search", 8 | "redisearch" 9 | ], 10 | "author": "Kyle Davis", 11 | "dependencies": { 12 | "redis": "^2.7.1" 13 | }, 14 | "devDependencies": { 15 | "yargs": "^7.0.2", 16 | "async": "^2.3.0", 17 | "matcha": "^0.6.0", 18 | "should": "^3.3.2", 19 | "request": "^2.81.0", 20 | "express": "^4.15.3" 21 | }, 22 | "main": "index", 23 | "repository": { 24 | "type": "git", 25 | "url": "https://github.com/stockholmux/redredisearch" 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /.github/workflows/nodejs.yml: -------------------------------------------------------------------------------- 1 | name: Node CI 2 | 3 | on: [push] 4 | 5 | jobs: 6 | build: 7 | 8 | runs-on: ubuntu-latest 9 | 10 | strategy: 11 | matrix: 12 | node-version: [8.x, 10.x, 12.x] 13 | 14 | services: 15 | redisgraph: 16 | image: redislabs/redisearch:latest 17 | ports: 18 | - 6379:6379 19 | 20 | steps: 21 | - uses: actions/checkout@v1 22 | - name: Use Node.js ${{ matrix.node-version }} 23 | uses: actions/setup-node@v1 24 | with: 25 | node-version: ${{ matrix.node-version }} 26 | - name: npm install, build, and test 27 | run: | 28 | npm ci 29 | npm run build --if-present 30 | npm test 31 | env: 32 | CI: true 33 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | 8 | # Runtime data 9 | pids 10 | *.pid 11 | *.seed 12 | *.pid.lock 13 | 14 | # Directory for instrumented libs generated by jscoverage/JSCover 15 | lib-cov 16 | 17 | # Coverage directory used by tools like istanbul 18 | coverage 19 | 20 | # nyc test coverage 21 | .nyc_output 22 | 23 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 24 | .grunt 25 | 26 | # Bower dependency directory (https://bower.io/) 27 | bower_components 28 | 29 | # node-waf configuration 30 | .lock-wscript 31 | 32 | # Compiled binary addons (http://nodejs.org/api/addons.html) 33 | build/Release 34 | 35 | # Dependency directories 36 | node_modules/ 37 | jspm_packages/ 38 | 39 | # Typescript v1 declaration files 40 | typings/ 41 | 42 | # Optional npm cache directory 43 | .npm 44 | 45 | # Optional eslint cache 46 | .eslintcache 47 | 48 | # Optional REPL history 49 | .node_repl_history 50 | 51 | # Output of 'npm pack' 52 | *.tgz 53 | 54 | # Yarn Integrity file 55 | .yarn-integrity 56 | 57 | # dotenv environment variables file 58 | .env 59 | 60 | .DS_Store 61 | *.sock 62 | testing.js 63 | 64 | # misc 65 | 66 | -------------------------------------------------------------------------------- /examples/static/form.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | Realtime Search 4 | 5 | 29 | 30 | 46 | 47 | 48 |
49 | 50 |

Try "express", "redis", "hacker" ...

51 |
52 | 53 | 54 | -------------------------------------------------------------------------------- /examples/simple.js: -------------------------------------------------------------------------------- 1 | const 2 | argv = require('yargs') // command line handling 3 | .demand('connection') // require the 'connection' argument 4 | .demand('query') // the query we'll run against the indexed values 5 | .argv, 6 | redsearch = require('../'), // RedRediSearch, syntax compatible with Reds 7 | redis = require('redis'), // node_redis module 8 | creds = require(argv.connection), // load the JSON specified in the argument 9 | client = redis.createClient(creds); // create a Redis client with the Node_redis connection object 10 | 11 | redsearch.setClient(client); // associate the correct client. 12 | 13 | redsearch.createSearch('pets', {}, function(err,search) { 14 | // $ node examples/simple --connection /path/to/connection/object/json --query tobi 15 | // $ node examples/simple --connection /path/to/connection/object/json --query tobi 16 | // $ node examples/simple --connection /path/to/connection/object/json --query cat 17 | // $ node examples/simple --connection /path/to/connection/object/json --query fun 18 | // $ node examples/simple --connection /path/to/connection/object/json --query "funny ferret" 19 | 20 | var strs = []; 21 | strs.push('Manny is a cat'); 22 | strs.push('Luna is a cat'); 23 | strs.push('Tobi is a ferret'); 24 | strs.push('Loki is a ferret'); 25 | strs.push('Jane is a ferret'); 26 | strs.push('Jane is funny ferret'); 27 | 28 | // index them 29 | 30 | strs.forEach(function(str, i){ 31 | search.index(str, i); 32 | }); 33 | 34 | // query 35 | 36 | search.query(argv.query).end(function(err, ids){ 37 | if (err) throw err; 38 | var res = ids.map(function(i){ return strs[i]; }); 39 | console.log(); 40 | console.log(' Search results for "%s"', argv.query); 41 | res.forEach(function(str){ 42 | console.log(' - %s', str); 43 | }); 44 | console.log(); 45 | process.exit(); 46 | }); 47 | }); 48 | -------------------------------------------------------------------------------- /examples/web.demo.node.js: -------------------------------------------------------------------------------- 1 | const 2 | argv = require('yargs') // command line handling 3 | .demand('connection') // require the 'connection' argument (this is a node_redis connection object in JSON Format) 4 | .argv, 5 | redsearch = require('../'), // RedRediSearch, syntax compatible with Reds 6 | redis = require('redis'), // node_redis module 7 | creds = require(argv.connection), // load the JSON specified in the argument 8 | client = redis.createClient(creds), // create a Redis client with the Node_redis connection object 9 | express = require('express'), // simple web server module 10 | urls = require('./urls.json'), // load the URLs from a JSON file 11 | app = express(), // server instance 12 | port = 3000; // load demo on http://localhost:3000/ 13 | 14 | redsearch.setClient(client); // associate the correct client. 15 | 16 | redsearch.createSearch('web',{},function(err,search) { // create the search with at the "web" key 17 | app.get( // HTTP Get 18 | '/search', // route for /search 19 | function(req,res,next) { 20 | search 21 | .query(req.query.q) // /search?q=[search query] 22 | .end(function(err, ids){ 23 | if (err) { next(err); } else { // error handling 24 | res.json( // return JSON 25 | ids.map(function(id){ return urls[id]; }) // this will return all the URLs that match the results 26 | ); 27 | } 28 | }); 29 | } 30 | ); 31 | 32 | app 33 | .use(express.static('static')) // server out static files (the form) 34 | .listen(port,function() { // start at `port` 35 | console.log('Listening at',port); // we're loaded - let the console know 36 | }); 37 | }); 38 | -------------------------------------------------------------------------------- /examples/web-index.js: -------------------------------------------------------------------------------- 1 | const 2 | argv = require('yargs') // command line handling 3 | .demand('connection') // require the 'connection' argument (this is a node_redis connection object in JSON Format) 4 | .argv, 5 | redsearch = require('../'), // RedRediSearch, syntax compatible with Reds 6 | redis = require('redis'), // node_redis module 7 | request = require('request'), // Get a remote URL 8 | creds = require(argv.connection), // load the JSON specified in the argument 9 | client = redis.createClient(creds), // create a Redis client with the Node_redis connection object 10 | urls = require('./urls.json'), // load the URLs from a JSON file 11 | start = new Date; // for calculating time of index 12 | 13 | function striptags(html) { // quick and dirty, don't reuse ("Lame" according to TJ) 14 | return String(html).replace(/<\/?([^>]+)>/g, ''); 15 | } 16 | 17 | redsearch.setClient(client); // associate the correct client. 18 | 19 | redsearch.createSearch('web',{},function(err,search) { // create the search with at the "web" key 20 | var pending = urls.length; 21 | 22 | urls.forEach(function(url, i){ // over each URL 23 | function log(msg) { // logging for this specific URL 24 | console.log( 25 | ' \033[90m%s \033[36m%s\033[0m', msg, url 26 | ); 27 | } 28 | log('fetching'); 29 | 30 | request(url, function(err, res, body){ 31 | if (err) throw err; // error 'handling' 32 | var words = striptags(body); // strip html tags 33 | 34 | log('indexing'); 35 | search.index(words, i, function(err){ // words are being indexed and the ID is just a number here 36 | if (err) throw err; 37 | log('completed'); 38 | --pending || done(); // if pending drops to 0 then call done. 39 | }); 40 | }); 41 | }); 42 | 43 | // all done 44 | 45 | function done() { // wrap up 46 | console.log(' indexed %d pages in %ds' 47 | , urls.length 48 | , ((new Date - start) / 1000).toFixed(2)); 49 | client.quit(); 50 | } 51 | }); -------------------------------------------------------------------------------- /benchmarks/benchmark.reds.node.js: -------------------------------------------------------------------------------- 1 | // this will benchmark reds. The other benchmark (benchmark/index.js) will actually benchmark the package. This is included for comparison purposes. 2 | // to run this you'll need to install the reds module with npm - not included becuase it's not really a dependency 3 | var argv = require('yargs') 4 | .demand('connection') 5 | .argv; 6 | var redis = require('redis'); 7 | var connectionObj = require(argv.connection); 8 | var reds; 9 | var fs = require('fs'); 10 | 11 | 12 | reds = require('reds'); 13 | 14 | reds.setClient(redis.createClient(connectionObj)); 15 | 16 | reds = reds.createSearch('reds'); 17 | // test data 18 | 19 | var tiny = fs.readFileSync('./node_modules/reds/package.json', 'utf8'); 20 | tiny = Array(5).join(tiny); 21 | var small = fs.readFileSync('./node_modules/reds/Readme.md', 'utf8'); 22 | var medium = Array(10).join(small); 23 | var large = Array(30).join(medium); 24 | 25 | // benchmarks 26 | 27 | suite('indexing', function(){ 28 | bench('tiny index', function(done){ 29 | reds.index(tiny, 'reds1234', done); 30 | }); 31 | 32 | bench('small index', function(done){ 33 | reds.index(small, 'reds1234', done); 34 | }); 35 | 36 | bench('medium index', function(done){ 37 | reds.index(medium, 'reds1234', done); 38 | }); 39 | 40 | bench('large', function(done){ 41 | reds.index(large, 'reds1234', done); 42 | }); 43 | 44 | bench('query - one term', function(done){ 45 | reds 46 | .query('one') 47 | .end(done); 48 | }); 49 | 50 | bench('query - two terms (and)', function(done){ 51 | reds 52 | .query('one two') 53 | .end(done); 54 | }); 55 | 56 | bench('query - two terms (or)', function(done){ 57 | reds 58 | .query('one two') 59 | .type('or') 60 | .end(done); 61 | }); 62 | 63 | bench('query - three terms (and)', function(done){ 64 | reds 65 | .query('one two three') 66 | .end(done); 67 | }); 68 | 69 | bench('query - three terms (or)', function(done){ 70 | reds 71 | .query('one two three') 72 | .type('or') 73 | .end(done); 74 | }); 75 | 76 | let rightsAndFreedoms = 'Everyone has the following fundamental freedoms: (a) freedom of conscience and religion; (b) freedom of thought, belief, opinion and expression, including freedom of the press and other media of communication; (c) freedom of peaceful assembly; and (d) freedom of association.'; 77 | bench('query - long (and)', function(done){ 78 | reds 79 | .query(rightsAndFreedoms) 80 | .end(done); 81 | }); 82 | 83 | bench('query - long (or)', function(done){ 84 | reds 85 | .query(rightsAndFreedoms) 86 | .type('or') 87 | .end(done); 88 | }); 89 | 90 | }); -------------------------------------------------------------------------------- /benchmarks/index.js: -------------------------------------------------------------------------------- 1 | // this will benchmark redredisearch. Also included is the comparision benchmark for reds (benchmarks/benchmark.reds.node.js) as to compare the two packages 2 | var argv = require('yargs') 3 | .demand('connection') // you need to provide the --connection in the cmd line arguments, it's a path to a JSON file of node_redis connection information 4 | .argv; 5 | var redis = require('redis'); 6 | var connectionObj = require(argv.connection); 7 | var reds; 8 | var client = redis.createClient(connectionObj); 9 | var fs = require('fs'); 10 | 11 | 12 | reds = require('../'); 13 | 14 | reds.setClient(client); 15 | 16 | // test data 17 | 18 | var tiny = fs.readFileSync('../package.json', 'utf8'); 19 | tiny = Array(5).join(tiny); 20 | var small = fs.readFileSync('../Readme.md', 'utf8'); 21 | var medium = Array(10).join(small); 22 | var large = Array(30).join(medium); 23 | 24 | 25 | suite('indexing', function(){ 26 | var 27 | search; 28 | 29 | before(function(next) { 30 | reds.createSearch('redisearch', {}, function(err,redisearch) { 31 | if (err) { throw err; } 32 | search = redisearch; 33 | next(); 34 | }); 35 | }); 36 | 37 | bench('tiny index', function(done){ 38 | search.index(tiny, 'redisearch1234', done); 39 | }); 40 | 41 | bench('small index', function(done){ 42 | search.index(small, 'redisearch1234', done); 43 | }); 44 | 45 | bench('medium index', function(done){ 46 | search.index(medium, 'redisearch1234', done); 47 | }); 48 | 49 | bench('large index', function(done){ 50 | search.index(large, 'redisearch1234', done); 51 | }); 52 | 53 | bench('query - one term', function(done){ 54 | search 55 | .query('one') 56 | .end(done); 57 | }); 58 | 59 | bench('query - two terms (and)', function(done){ 60 | search 61 | .query('one two') 62 | .end(done); 63 | }); 64 | 65 | bench('query - two terms (or)', function(done){ 66 | search 67 | .query('one two') 68 | .type('or') 69 | .end(done); 70 | }); 71 | 72 | bench('query - three terms (and)', function(done){ 73 | search 74 | .query('one two three') 75 | .end(done); 76 | }); 77 | 78 | bench('query - three terms (or)', function(done){ 79 | search 80 | .query('one two three') 81 | .type('or') 82 | .end(done); 83 | }); 84 | 85 | let rightsAndFreedoms = 'Everyone has the following fundamental freedoms: (a) freedom of conscience and religion; (b) freedom of thought, belief, opinion and expression, including freedom of the press and other media of communication; (c) freedom of peaceful assembly; and (d) freedom of association.'; 86 | bench('query - long (and)', function(done){ 87 | search 88 | .query(rightsAndFreedoms) 89 | .end(done); 90 | }); 91 | 92 | bench('query - long (or)', function(done){ 93 | search 94 | .query(rightsAndFreedoms) 95 | .type('or') 96 | .end(done); 97 | }); 98 | 99 | bench('query - direct / complex', function(done){ 100 | search 101 | .query('(dog|cat) (lassie|garfield)') 102 | .type('direct') 103 | .end(done); 104 | }); 105 | 106 | after(function() { 107 | client.quit(); 108 | }); 109 | }); 110 | 111 | 112 | -------------------------------------------------------------------------------- /Readme.md: -------------------------------------------------------------------------------- 1 | [![Actions Status](https://github.com/stockholmux/redredisearch/workflows/Node%20CI/badge.svg)](https://github.com/stockholmux/redredisearch/actions) 2 | [![npm version](https://badge.fury.io/js/redredisearch.svg)](https://badge.fury.io/js/redredisearch) 3 | 4 | # RedRediSearch 5 | 6 | RedRediSearch is a Node.js wrapper library for the [RediSearch](http://redisearch.io/) Redis module. It is more-or-less syntactically compatible with [Reds](https://github.com/tj/reds), another Node.js search library. RedRediSearch and RediSearch can provide full-text searching that is much faster than the original Reds library (see Benchmarks). 7 | 8 | 9 | ## Upgrading 10 | 11 | If you are upgrading from Reds, you'll need to make your `createSearch` asynchronous and re-index your data. Otherwise, your app-level logic and code should be compatible. 12 | 13 | ## Installation 14 | 15 | $ npm install redredisearch 16 | 17 | ## Example 18 | 19 | The first thing you'll want to do is create a `Search` instance, which allows you to pass a `key`, used for namespacing within RediSearch so that you may have several searches in the same Redis database. You may specify your own [node_redis](https://github.com/NodeRedis/node_redis) instance with the `redredisearch.setClient` function. 20 | 21 | ```js 22 | redredisearch.createSearch('pets',{}, function(err, search) { 23 | /* ... */ 24 | }); 25 | ``` 26 | 27 | You can then add items to the index with the `Search#index` function. 28 | 29 | ```js 30 | var strs = []; 31 | strs.push('Tobi wants four dollars'); 32 | strs.push('Tobi only wants $4'); 33 | strs.push('Loki is really fat'); 34 | strs.push('Loki, Jane, and Tobi are ferrets'); 35 | strs.push('Manny is a cat'); 36 | strs.push('Luna is a cat'); 37 | strs.push('Mustachio is a cat'); 38 | 39 | redredisearch.createSearch('pets',{}, function(err,search) { 40 | strs.forEach(function(str, i){ search.index(str, i); }); 41 | }); 42 | ``` 43 | 44 | To perform a query against the index simply invoke `Search#query()` with a string, and pass a callback, which receives an array of ids when present, or an empty array otherwise. 45 | 46 | ```js 47 | search 48 | .query('Tobi dollars') 49 | .end(function(err, ids){ 50 | if (err) throw err; 51 | console.log('Search results for "%s":', query); 52 | ids.forEach(function(id){ 53 | console.log(' - %s', strs[id]); 54 | }); 55 | }); 56 | ``` 57 | 58 | By default, queries are an intersection of the search words. The previous example would yield the following output since only one string contains both "Tobi" _and_ "dollars": 59 | 60 | ``` 61 | Search results for "Tobi dollars": 62 | - Tobi wants four dollars 63 | ``` 64 | 65 | We can tweak the query to perform a union by passing either "union" or "or" to `Search#type()` in `redredisearch.search()` between `Search#query()` and `Search#end()`, indicating that _any_ of the constants computed may be present for the `id` to match. 66 | 67 | ```js 68 | search 69 | .query('tobi dollars') 70 | .type('or') 71 | .end(function(err, ids){ 72 | if (err) throw err; 73 | console.log('Search results for "%s":', query); 74 | ids.forEach(function(id){ 75 | console.log(' - %s', strs[id]); 76 | }); 77 | }); 78 | ``` 79 | 80 | The union search would yield the following since three strings contain either "Tobi" _or_ "dollars": 81 | 82 | ``` 83 | Search results for "tobi dollars": 84 | - Tobi wants four dollars 85 | - Tobi only wants $4 86 | - Loki, Jane, and Tobi are ferrets 87 | ``` 88 | 89 | RediSearch has an advanced query syntax that can be used by using the 'direct' search type. See the [RediSearch documentation](http://redisearch.io/Query_Syntax/) for this syntax. 90 | 91 | ```js 92 | search 93 | .query('(hello|hella) (world|werld)') 94 | .type('direct') 95 | .end(function(err, ids){ 96 | /* ... */ 97 | }); 98 | ``` 99 | 100 | Also included in the package is the RediSearch Suggestion API. This has no corollary in the Reds module. The Suggestion API is ideal for auto-complete type situations and is entirely separate from the Search API. 101 | 102 | ```js 103 | var suggestions = redredisearch.suggestion('my-suggestion-list'); 104 | 105 | suggestions.add( 106 | 'redis', // add 'redis' 107 | 2, // with a 'score' of 2, this affects the position in the results, higher = higher up in results 108 | function(err,sizeOfSuggestionList) { /* ... */ } // callback 109 | ); 110 | suggestions.add( 111 | 'redisearch', 112 | 5, 113 | function(err,sizeOfSuggestionList) { /* ... */ } 114 | ); 115 | suggestions.add( 116 | 'reds', 117 | 1, 118 | function(err,sizeOfSuggestionList) { /* ... */ } 119 | ); 120 | 121 | /* ... */ 122 | 123 | sugggestions.get( 124 | 're', // prefix - will find anything starting with "re" 125 | function(err, returnedSuggestions) { 126 | /* returnedSuggestions is set to [ "redisearch", "redis", "reds" ] */ 127 | } 128 | ); 129 | 130 | sugggestions.get( 131 | 'redis', // prefix - will find anything starting with "redis", so not "reds" 132 | function(err, returnedSuggestions) { 133 | /* returnedSuggestions is set to [ "redisearch", "redis" ] */ 134 | } 135 | ) 136 | ``` 137 | 138 | There is also a `fuzzy` opt and `maxResults` that can either be set by chaining or by passing an object in the second argument in the constructor. 139 | 140 | 141 | ## API 142 | 143 | ```js 144 | redredisearch.createSearch(key, options, fn) : Search 145 | redredisearch.setClient(inClient) 146 | redredisearch.createClient() 147 | redredisearch.confirmModule(cb) 148 | redredisearch.words(str) : Array 149 | redredisearch.suggestionList(key,opts) : Suggestion 150 | Search#index(text, id[, fn]) 151 | Search#remove(id[, fn]); 152 | Search#query(text, fn[, type]) : Query 153 | Query#type(type) 154 | Query#between(str) 155 | Query#end(fn) 156 | Suggestion#fuzzy(isFuzzy) 157 | Suggestion#maxResults(maxResults) 158 | Suggestion#add(str,score,fn) 159 | Suggestion#get(prefix,fn) 160 | Suggestion#del(str,fn) 161 | 162 | ``` 163 | 164 | Examples: 165 | 166 | ```js 167 | var search = redredisearch.createSearch('misc'); 168 | search.index('Foo bar baz', 'abc'); 169 | search.index('Foo bar', 'bcd'); 170 | search.remove('bcd'); 171 | search.query('foo bar').end(function(err, ids){}); 172 | ``` 173 | 174 | 175 | ## Benchmarks 176 | 177 | When compared to Reds, RedRediSearch is much faster at indexing and somewhat faster at query: 178 | 179 | _Indexing - documents / second_ 180 | 181 | | Module | Tiny | Small | Medium | Large | 182 | |----------------|------|-------|--------|-------| 183 | | Reds | 122 | 75 | 10 | 0 | 184 | | RediRediSearch | 1,256| 501 | 132 | 5 | 185 | 186 | _Query - queries / second_ 187 | 188 | | Module | 1 term | 2 terms / AND | 2 terms / OR | 3 terms / AND | 3 terms / OR | Long* / AND | Long* / OR | 189 | |----------------|--------|---------------|--------------|---------------|--------------|------------|----------| 190 | | Reds | 8,754 | 8,765 | 8,389 | 7,622 | 7,193 | 1,649 | 1,647 | 191 | | RedRediSearch | 10,955 | 12,945 | 10,054 | 12,769 | 8,389 | 6,456 | 12,311 | 192 | 193 | The "Long" query string is taken from the Canadian Charter of Rights and Freedoms: "Everyone has the following fundamental freedoms: (a) freedom of conscience and religion; (b) freedom of thought, belief, opinion and expression, including freedom of the press and other media of communication; (c) freedom of peaceful assembly; and (d) freedom of association." (Used because I just had it open in another tab...) 194 | 195 | ## Next steps 196 | 197 | - More coverage of RediSearch features 198 | - Tests 199 | - Better examples 200 | 201 | 202 | ## License 203 | 204 | (The MIT License) 205 | 206 | Copyright (c) 2011 TJ Holowaychuk <tj@vision-media.ca> 207 | 208 | Modified work Copyright (c) 2017 Kyle Davis 209 | 210 | Permission is hereby granted, free of charge, to any person obtaining 211 | a copy of this software and associated documentation files (the 212 | 'Software'), to deal in the Software without restriction, including 213 | without limitation the rights to use, copy, modify, merge, publish, 214 | distribute, sublicense, and/or sell copies of the Software, and to 215 | permit persons to whom the Software is furnished to do so, subject to 216 | the following conditions: 217 | 218 | The above copyright notice and this permission notice shall be 219 | included in all copies or substantial portions of the Software. 220 | 221 | THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, 222 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 223 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 224 | IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY 225 | CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, 226 | TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 227 | SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 228 | -------------------------------------------------------------------------------- /lib/redredisearch.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * redredisearch 3 | * 4 | * Forked from tj/reds 5 | * Original work Copyright(c) 2011 TJ Holowaychuk 6 | * Modified work Copyright(c) 2017 Kyle Davis 7 | * MIT Licensed 8 | */ 9 | 10 | /** 11 | * Module dependencies. 12 | */ 13 | 14 | 15 | var redis = require('redis'); 16 | function noop(){}; 17 | 18 | /** 19 | * Library version. 20 | */ 21 | 22 | exports.version = '0.0.1'; 23 | 24 | /** 25 | * Expose `Search`. 26 | */ 27 | 28 | exports.Search = Search; 29 | 30 | /** 31 | * Expose `Query`. 32 | */ 33 | 34 | exports.Query = Query; 35 | 36 | /** 37 | * Search types. 38 | */ 39 | 40 | var types = { 41 | intersect: 'and', 42 | union: 'or', 43 | and: 'and', 44 | or: 'or' 45 | }; 46 | 47 | /** 48 | * Alternate way to set client 49 | * provide your own behaviour. 50 | * 51 | * @param {RedisClient} inClient 52 | * @return {RedisClient} 53 | * @api public 54 | */ 55 | 56 | exports.setClient = function(inClient) { 57 | return exports.client = inClient; 58 | } 59 | 60 | /** 61 | * Create a redis client, override to 62 | * provide your own behaviour. 63 | * 64 | * @return {RedisClient} 65 | * @api public 66 | */ 67 | 68 | exports.createClient = function(){ 69 | return exports.client 70 | || (exports.client = redis.createClient()); 71 | }; 72 | 73 | /** 74 | * Confirm the existence of the RediSearch Redis module 75 | * 76 | * @api public 77 | */ 78 | 79 | exports.confirmModule = function(cb) { 80 | exports.client.send_command('ft.create',[], function(err) { 81 | let strMsg = String(err); 82 | if (strMsg.indexOf('ERR wrong number of arguments') > 0) { 83 | cb(null); 84 | } else { 85 | cb(err); 86 | } 87 | }); 88 | } 89 | 90 | /** 91 | * Return a new reds `Search` with the given `key`. 92 | * @param {String} key 93 | * @param {Object} opts 94 | * @return {Search} 95 | * @api public 96 | */ 97 | 98 | exports.createSearch = function(key,opts,cb){ 99 | const 100 | searchObj = function(err,info) { 101 | if (err) { cb(err); } else { 102 | cb(err,new Search(key,info,opts)); 103 | } 104 | }; 105 | 106 | opts = !opts ? {} : opts; 107 | opts.payloadField = opts.payloadField ? opts.payloadField : 'payload'; 108 | 109 | if (!key) throw new Error('createSearch() requires a redis key for namespacing'); 110 | 111 | exports.client.send_command('FT.INFO',[key],function(err,info) { 112 | if (err) { 113 | //if the index is not found, we need to make it. 114 | if (String(err).indexOf('Unknown Index name') > 0 ){ 115 | let args = [ 116 | key, 117 | 'SCHEMA', opts.payloadField, 'text' 118 | ]; 119 | exports.client.send_command( 120 | 'FT.CREATE', 121 | args, 122 | function(err) { 123 | if (err) { cb(err); } else { 124 | exports.client.send_command('FT.INFO',[key],searchObj); 125 | } 126 | } 127 | ); 128 | } 129 | 130 | } else { searchObj(err,info); } 131 | }); 132 | }; 133 | 134 | /** 135 | * Return the words in `str`. This is for compatability reasons (convert OR queries to pipes) 136 | * 137 | * @param {String} str 138 | * @return {Array} 139 | * @api private 140 | */ 141 | 142 | exports.words = function(str){ 143 | return String(str).match(/\w+/g); 144 | }; 145 | 146 | 147 | /** 148 | * Initialize a new `Query` with the given `str` 149 | * and `search` instance. 150 | * 151 | * @param {String} str 152 | * @param {Search} search 153 | * @api public 154 | */ 155 | 156 | function Query(str, search) { 157 | this.str = str; 158 | this.type('and'); 159 | this.search = search; 160 | } 161 | 162 | /** 163 | * Set `type` to "union" or "intersect", aliased as 164 | * "or" and "and". 165 | * 166 | * @param {String} type 167 | * @return {Query} for chaining 168 | * @api public 169 | */ 170 | 171 | Query.prototype.type = function(type){ 172 | if (type === 'direct') { 173 | this._directQuery = true; 174 | } else { 175 | this._direct = false; 176 | this._type = types[type]; 177 | } 178 | return this; 179 | }; 180 | 181 | /** 182 | * Limit search to the specified range of elements. 183 | * 184 | * @param {String} start 185 | * @param {String} stop 186 | * @return {Query} for chaining 187 | * @api public 188 | */ 189 | Query.prototype.between = function(start, stop){ 190 | this._start = start; 191 | this._stop = stop; 192 | return this; 193 | }; 194 | 195 | /** 196 | * Perform the query and callback `fn(err, ids)`. 197 | * 198 | * @param {Function} fn 199 | * @return {Query} for chaining 200 | * @api public 201 | */ 202 | 203 | Query.prototype.end = function(fn){ 204 | var 205 | key = this.search.key, 206 | db = this.search.client, 207 | query = this.str, 208 | direct = this._directQuery, 209 | args = [], 210 | joiner = ' ', 211 | rediSearchQuery; 212 | 213 | if (direct) { 214 | rediSearchQuery = query; 215 | } else { 216 | rediSearchQuery = exports.words(query); 217 | if (this._type === 'or') { 218 | joiner = '|' 219 | } 220 | rediSearchQuery = rediSearchQuery.join(joiner); 221 | } 222 | args = [ 223 | key, 224 | rediSearchQuery, 225 | 'NOCONTENT' 226 | ]; 227 | if (this._start !== undefined) { 228 | args.push('LIMIT',this._start,this._stop); 229 | } 230 | 231 | db.send_command( 232 | 'FT.SEARCH', 233 | args, 234 | function(err,resp) { 235 | if (err) { fn(err); } else { 236 | fn(err,resp.slice(1)); 237 | } 238 | } 239 | ); 240 | 241 | return this; 242 | }; 243 | 244 | /** 245 | * Initialize a new `Suggestion` with the given `key`. 246 | * 247 | * @param {String} key 248 | * @param {Object} opts 249 | * @api public 250 | */ 251 | var Suggestion = function(key,opts) { 252 | this.key = key; 253 | this.client = exports.createClient(); 254 | this.opts = opts || {}; 255 | if (this.opts.fuzzy) { 256 | this.fuzzy = opts.fuzzy; 257 | } 258 | if (this.opts.maxResults) { 259 | this.maxResults = opts.maxResults; 260 | } 261 | if (this.opts.incr) { 262 | this.incr = opts.incr; 263 | } 264 | if (this.opts.withPayloads) { 265 | this.withPayloads = true; 266 | } 267 | } 268 | 269 | /** 270 | * Create a new Suggestion object 271 | * 272 | * @param {String} key 273 | * @param {Object} opts 274 | * @api public 275 | */ 276 | exports.suggestionList = function(key,opts) { 277 | return new Suggestion(key,opts); 278 | } 279 | 280 | /** 281 | * Set `fuzzy` on suggestion get. Can also be set via opts in the constructor 282 | * 283 | * @param {Boolean} isFuzzy 284 | * @return {Suggestion} for chaining 285 | * @api public 286 | */ 287 | 288 | Suggestion.prototype.fuzzy = function(isFuzzy){ 289 | this.fuzzy = isFuzzy; 290 | return this; 291 | }; 292 | 293 | /** 294 | * Set the max number of returned suggestions. Can also be set via opts in the constructor 295 | * 296 | * @param {Number} maxResults 297 | * @return {Suggestion} for chaining 298 | * @api public 299 | */ 300 | 301 | Suggestion.prototype.maxResults = function(maxResults){ 302 | this.maxResults = maxResults; 303 | return this; 304 | }; 305 | 306 | Suggestion.prototype.add = function(str,score,payload,fn) { 307 | if((typeof fn === 'undefined' || fn === null) && typeof payload === "function"){ 308 | if(typeof fn !== 'undefined'){ 309 | fn = payload; 310 | } else { 311 | var fn = payload; 312 | } 313 | payload = null; 314 | }; 315 | 316 | var key = this.key; 317 | var db = this.client; 318 | var args = [ 319 | key, 320 | str, 321 | score, 322 | ]; 323 | if (this.incr) { 324 | args.push('INCR'); 325 | } 326 | if(payload !== null){ 327 | args.push('PAYLOAD', (typeof payload === 'object' ? JSON.stringify(payload) : payload.toString())); 328 | } 329 | db.send_command( 330 | 'FT.SUGADD', 331 | args, 332 | fn || noop 333 | ); 334 | return this; 335 | } 336 | 337 | Suggestion.prototype.get = function(prefix,fn) { 338 | var key = this.key; 339 | var db = this.client; 340 | var args = [ 341 | key, 342 | prefix 343 | ]; 344 | if (this.fuzzy) { 345 | args.push('FUZZY'); 346 | } 347 | if (this.maxResults) { 348 | args.push('MAX',this.maxResults); 349 | } 350 | if (this.withPayloads) { 351 | args.push('WITHPAYLOADS'); 352 | } 353 | 354 | db.send_command( 355 | 'FT.SUGGET', 356 | args, 357 | fn 358 | ); 359 | 360 | return this; 361 | } 362 | 363 | Suggestion.prototype.del = function(str,fn) { 364 | var key = this.key; 365 | var db = this.client; 366 | 367 | db.send_command( 368 | 'FT.SUGDEL', 369 | [ 370 | key, 371 | str 372 | ], 373 | fn 374 | ); 375 | 376 | return this; 377 | } 378 | 379 | /** 380 | * Initialize a new `Search` with the given `key`. 381 | * 382 | * @param {String} key 383 | * @api public 384 | */ 385 | 386 | function Search(key,info,opts) { 387 | this.key = key; 388 | this.client = exports.createClient(); 389 | this.opts = opts; 390 | } 391 | 392 | /** 393 | * Index the given `str` mapped to `id`. 394 | * 395 | * @param {String} str 396 | * @param {Number|String} id 397 | * @param {Function} fn 398 | * @api public 399 | */ 400 | 401 | Search.prototype.index = function(str, id, fn){ 402 | var key = this.key; 403 | var db = this.client; 404 | var opts = this.opts; 405 | 406 | db.send_command( 407 | 'FT.ADD', 408 | [ 409 | key, 410 | id, 411 | 1, //default - this should be to be set in future versions 412 | 'NOSAVE', //emulating Reds original behaviour 413 | 'REPLACE', //emulating Reds original behaviour 414 | 'FIELDS', 415 | opts.payloadField, 416 | str 417 | ], 418 | fn || noop 419 | ); 420 | 421 | return this; 422 | }; 423 | 424 | /** 425 | * Remove occurrences of `id` from the index. 426 | * 427 | * @param {Number|String} id 428 | * @api public 429 | */ 430 | 431 | Search.prototype.remove = function(id, fn){ 432 | fn = fn || noop; 433 | var key = this.key; 434 | var db = this.client; 435 | 436 | //this.removeIndex(db, id, key, fn); 437 | db.send_command( 438 | 'FT.DEL', 439 | [ 440 | key, 441 | id 442 | ], 443 | fn 444 | ) 445 | 446 | return this; 447 | }; 448 | 449 | /** 450 | * Perform a search on the given `query` returning 451 | * a `Query` instance. 452 | * 453 | * @param {String} query 454 | * @param {Query} 455 | * @api public 456 | */ 457 | 458 | Search.prototype.query = function(query){ 459 | return new Query(query, this); 460 | }; 461 | --------------------------------------------------------------------------------