├── .gitignore ├── .travis.yml ├── History.md ├── Makefile ├── README.md ├── index.js ├── lib └── index.js ├── package.json └── test └── index.js /.gitignore: -------------------------------------------------------------------------------- 1 | *.sw* 2 | node_modules/ 3 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - 0.6 4 | - 0.8 5 | - 0.10 6 | services: 7 | - mongodb 8 | -------------------------------------------------------------------------------- /History.md: -------------------------------------------------------------------------------- 1 | 2 | 0.0.2 / 2013-03-27 3 | ================== 4 | 5 | * added; `lean` support 6 | 7 | 0.0.1 / 2013-03-25 8 | ================== 9 | 10 | * initial release 11 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | 2 | test: 3 | @./node_modules/.bin/mocha -g integration -i --reporter list $(T) 4 | 5 | test-all: 6 | @./node_modules/.bin/mocha --reporter list $(T) 7 | 8 | .PHONY: test 9 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | #mongoose-text-search 2 | ====================== 3 | 4 | Provides MongoDB 2.4 text search support for mongoose. 5 | 6 | [![Build Status](https://travis-ci.org/aheckmann/mongoose-text-search.png?branch=master)](https://travis-ci.org/aheckmann/mongoose-text-search) 7 | 8 | ## Example: 9 | 10 | ```js 11 | // modules 12 | var mongoose = require('mongoose'); 13 | var textSearch = require('mongoose-text-search'); 14 | 15 | // create our schema 16 | var gameSchema = mongoose.Schema({ 17 | name: String 18 | , tags: [String] 19 | , likes: Number 20 | , created: Date 21 | }); 22 | 23 | // give our schema text search capabilities 24 | gameSchema.plugin(textSearch); 25 | 26 | // add a text index to the tags array 27 | gameSchema.index({ tags: 'text' }); 28 | 29 | // test it out 30 | var Game = mongoose.model('Game', gameSchema); 31 | 32 | Game.create({ name: 'Super Mario 64', tags: ['nintendo', 'mario', '3d'] }, function (err) { 33 | if (err) return handleError(err); 34 | 35 | Game.textSearch('3d', function (err, output) { 36 | if (err) return handleError(err); 37 | 38 | var inspect = require('util').inspect; 39 | console.log(inspect(output, { depth: null })); 40 | 41 | // { queryDebugString: '3d||||||', 42 | // language: 'english', 43 | // results: 44 | // [ { score: 1, 45 | // obj: 46 | // { name: 'Super Mario 64', 47 | // _id: 5150993001900a0000000001, 48 | // __v: 0, 49 | // tags: [ 'nintendo', 'mario', '3d' ] } } ], 50 | // stats: 51 | // { nscanned: 1, 52 | // nscannedObjects: 0, 53 | // n: 1, 54 | // nfound: 1, 55 | // timeMicros: 77 }, 56 | // ok: 1 } 57 | }); 58 | }); 59 | ``` 60 | 61 | ### Output: 62 | 63 | The output is not limited to the found documents themselves but also the complete details of the executed command. 64 | 65 | The `results` property of the output is an array of objects containing the found document and its corresponding search ranking. `score` is the ranking, `obj` is the [mongoose document](http://mongoosejs.com/docs/documents.html). 66 | 67 | For more information about these properties, read the [MongoDB documentation](http://docs.mongodb.org/manual/reference/text-search/#text-search-output). 68 | 69 | ## Options 70 | 71 | `mongoose-text-search` supports passing an options object as the second argument. 72 | 73 | - `project`: select which [fields](http://docs.mongodb.org/manual/reference/command/text/) to return (mongoose [field selection](http://mongoosejs.com/docs/api.html#query_Query-select) syntax supported) 74 | - `filter`: declare an additional [query matcher](http://docs.mongodb.org/manual/reference/command/text/) using `find` syntax (arguments are cast according to the schema). 75 | - `limit`: [maximum number](http://docs.mongodb.org/manual/reference/command/text/) of documents (mongodb default is 100) 76 | - `language`: change the [search language](http://docs.mongodb.org/manual/reference/command/text/) 77 | - `lean`: Boolean: if true, documents are not cast to [mongoose documents](http://mongoosejs.com/docs/documents.html) (default false) 78 | 79 | Example: 80 | 81 | ```js 82 | var options = { 83 | project: '-created' // do not include the `created` property 84 | , filter: { likes: { $gt: 1000000 }} // casts queries based on schema 85 | , limit: 10 86 | , language: 'spanish' 87 | , lean: true 88 | } 89 | 90 | Game.textSearch('game -mario', options, callback); 91 | ``` 92 | 93 | ## Notes: 94 | 95 | As of mongoose 3.6.0, text indexes may be added using the [Schema.index()](http://mongoosejs.com/docs/api.html#schema_Schema-index) method. 96 | 97 | As of MongoDB 2.4.0, [text search](http://docs.mongodb.org/manual/applications/text-search/) is experimental/beta. As such, this functionality is not in mongoose core. 98 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | module.exports = exports = require('./lib'); 2 | -------------------------------------------------------------------------------- /lib/index.js: -------------------------------------------------------------------------------- 1 | // mongoose-text-search 2 | 3 | module.exports = exports = function textSearch (schema, options) { 4 | schema.statics.textSearch = function (search, o, cb) { 5 | if ('function' == typeof o) cb = o, o = {}; 6 | 7 | if ('function' != typeof cb) { 8 | throw new TypeError('textSearch: callback is required'); 9 | } 10 | 11 | var model = this; 12 | var lean = !! o.lean; 13 | 14 | // mongodb commands require property order :( 15 | // text must be first 16 | var cmd = {}; 17 | cmd.text = o.text || this.collection.name; 18 | 19 | cmd.search = search; 20 | 21 | var keys = Object.keys(o); 22 | var i = keys.length; 23 | while (i--) { 24 | var key = keys[i]; 25 | switch (key) { 26 | case 'text': 27 | // fall through 28 | case 'lean': 29 | continue; 30 | case 'filter': 31 | cmd.filter = model.find(o.filter).cast(model); 32 | break; 33 | case 'project': 34 | // cast and apply default schema field selection 35 | var query = model.find().select(o.project); 36 | query._applyPaths(); 37 | var fields = query._castFields(query._fields); 38 | if (fields instanceof Error) return cb(fields); 39 | cmd.project = fields; 40 | break; 41 | default: 42 | cmd[key] = o[key]; 43 | } 44 | } 45 | 46 | this.db.db.command(cmd, function (err, res) { 47 | if (err) return cb(err, res); 48 | if (res.errmsg) return cb(new Error(res.errmsg)); 49 | if (!lean && Array.isArray(res.results)) { 50 | // convert results to documents 51 | res.results.forEach(function (doc) { 52 | if (!doc.obj) return; 53 | var d = new model(undefined, undefined, true); 54 | d.init(doc.obj); 55 | doc.obj = d; 56 | }) 57 | } 58 | cb(err, res); 59 | }); 60 | } 61 | } 62 | 63 | exports.version = require('../package').version; 64 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "mongoose-text-search", 3 | "version": "0.0.2", 4 | "description": "MongoDB 2.4 text search support for mongoose", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "make test" 8 | }, 9 | "repository": { 10 | "type": "git", 11 | "url": "git://github.com/aheckmann/mongoose-text-search.git" 12 | }, 13 | "keywords": [ 14 | "mongoose", 15 | "text", 16 | "search", 17 | "plugin", 18 | "mongodb", 19 | "fulltext" 20 | ], 21 | "author": "Aaron Heckmann ", 22 | "license": "MIT", 23 | "devDependencies": { 24 | "mocha": "*", 25 | "mongoose": "3.8.x" 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /test/index.js: -------------------------------------------------------------------------------- 1 | var assert = require('assert') 2 | var mongoose = require('mongoose') 3 | var uri = 'mongodb://localhost/test-mongoose-text-search'; 4 | var textSearch = require('../') 5 | 6 | function getSchema () { 7 | var s = mongoose.Schema({ 8 | single: String 9 | , array: [String] 10 | }) 11 | s.index({ single: 'text', array: 'text' }) 12 | return s; 13 | } 14 | 15 | function makeDocs () { 16 | var ret = []; 17 | ret.push({ 18 | single: 'Blueberry' 19 | , array: ['array', 'of', 'strings'] 20 | }) 21 | ret.push({ 22 | single: 'elephant a string' 23 | , array: ['1'] 24 | }) 25 | ret.push({ 26 | single: 'a significant word ice cream funny' 27 | }) 28 | return ret; 29 | } 30 | 31 | describe('mongoose-text-search', function(){ 32 | it('is a function', function(done){ 33 | assert.equal('function', typeof textSearch); 34 | done(); 35 | }) 36 | 37 | it('adds a textSearch method to the schema', function(done){ 38 | var s = getSchema(); 39 | s.plugin(textSearch); 40 | assert.equal('function', typeof s.statics.textSearch); 41 | done(); 42 | }) 43 | 44 | it('has a version', function(done){ 45 | assert.equal('string', typeof textSearch.version); 46 | done(); 47 | }) 48 | }) 49 | 50 | describe('mongoose-text-search integration', function(){ 51 | var db; 52 | var schema; 53 | var modelName = 'Test'; 54 | var model, blueberry, elephant, letters; 55 | 56 | before(function(done){ 57 | var m = new mongoose.Mongoose; 58 | db = m.createConnection(uri); 59 | db.once('error', done); 60 | db.once('open', function () { 61 | schema = getSchema(); 62 | schema.plugin(textSearch); 63 | model = db.model(modelName, schema); 64 | model.on('index', function (err) { 65 | assert.ifError(err); 66 | model.create(makeDocs(), function (err, blue, eleph, zzz) { 67 | if (err) return done(err); 68 | blueberry = blue; 69 | elephant = eleph; 70 | letters = zzz; 71 | done(); 72 | }); 73 | }) 74 | }) 75 | }) 76 | after(function(done){ 77 | db.db.dropDatabase(function(){ 78 | db.close(done) 79 | }) 80 | }) 81 | 82 | it('requires a callback', function(done){ 83 | assert.throws(function(){ 84 | model.textSearch('stuff'); 85 | }) 86 | assert.throws(function(){ 87 | model.textSearch('stuff', { }); 88 | }) 89 | done(); 90 | }) 91 | it('requires a search', function(done){ 92 | model.textSearch({ }, function (err) { 93 | assert.ok(err); 94 | done(); 95 | }) 96 | }) 97 | 98 | it('casts results to mongoose documents', function(done){ 99 | model.textSearch('blueberry', function (err, res) { 100 | assert.ifError(err); 101 | assert.ok(res); 102 | assert.ok(Array.isArray(res.results)); 103 | res.results.forEach(function (result) { 104 | assert.ok(result.obj instanceof mongoose.Document); 105 | }) 106 | assert.equal(1, res.results.length); 107 | assert.equal(blueberry.id, res.results[0].obj.id); 108 | done(); 109 | }) 110 | }) 111 | 112 | it('accepts limit', function(done){ 113 | model.textSearch('strings', { limit: 1 }, function (err, res) { 114 | assert.ifError(err); 115 | assert.ok(res); 116 | assert.ok(Array.isArray(res.results)); 117 | assert.equal(1, res.results.length); 118 | assert.equal(blueberry.id, res.results[0].obj.id); 119 | done(); 120 | }) 121 | }) 122 | 123 | it('accepts filter (and casts)', function(done){ 124 | model.textSearch('strings', { filter: { array: [1] } }, function (err, res) { 125 | assert.ifError(err); 126 | assert.ok(res); 127 | assert.ok(Array.isArray(res.results)); 128 | assert.equal(1, res.results.length); 129 | assert.equal(elephant.id, res.results[0].obj.id); 130 | done(); 131 | }) 132 | }) 133 | 134 | describe('accepts project', function(){ 135 | it('with object syntax', function(done){ 136 | model.textSearch('funny', { project: {single: 0}}, function (err, res) { 137 | assert.ifError(err); 138 | assert.ok(res); 139 | assert.ok(Array.isArray(res.results)); 140 | assert.equal(1, res.results.length); 141 | assert.equal(letters.id, res.results[0].obj.id); 142 | assert.equal(undefined, res.results[0].obj.single); 143 | done(); 144 | }) 145 | }) 146 | it('with string syntax', function(done){ 147 | model.textSearch('funny', { project: '-single'}, function (err, res) { 148 | assert.ifError(err); 149 | assert.ok(res); 150 | assert.ok(Array.isArray(res.results)); 151 | assert.equal(1, res.results.length); 152 | assert.equal(letters.id, res.results[0].obj.id); 153 | assert.equal(undefined, res.results[0].obj.single); 154 | done(); 155 | }) 156 | }) 157 | }) 158 | 159 | it('accepts language', function(done){ 160 | model.textSearch('funny', { language: 'spanish'}, function (err, res) { 161 | assert.ifError(err); 162 | assert.ok(res); 163 | assert.ok(Array.isArray(res.results)); 164 | assert.equal(0, res.results.length); 165 | done(); 166 | }) 167 | }) 168 | 169 | it('supports lean', function(done){ 170 | model.textSearch('string', { lean: true }, function (err, res) { 171 | assert.ifError(err); 172 | assert.ok(res); 173 | assert.ok(Array.isArray(res.results)); 174 | assert.equal(2, res.results.length); 175 | res.results.forEach(function (result) { 176 | assert.ok(!(result.obj instanceof mongoose.Document)); 177 | }) 178 | done(); 179 | }) 180 | }) 181 | 182 | }) 183 | 184 | // if mongoose >= 3.6.1 text index type is supported 185 | 186 | --------------------------------------------------------------------------------