├── .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 |
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 | [](https://github.com/stockholmux/redredisearch/actions)
2 | [](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 |
--------------------------------------------------------------------------------