Skip to content
This repository was archived by the owner on Jan 19, 2024. It is now read-only.

Commit c913ed8

Browse files
committed
add hydratation feature
1 parent e81c704 commit c913ed8

File tree

4 files changed

+281
-7
lines changed

4 files changed

+281
-7
lines changed

README.md

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ mongoose-elasticsearch-xp is a [mongoose](http://mongoosejs.com/) plugin that ca
1515
- [Indexing on demand](#indexing-on-demand)
1616
- [Mapping](#mapping)
1717
- [Creating mappings on-demand](#creating-mappings-on-demand)
18+
- [Hydration](#hydration)
1819

1920
## Why this plugin?
2021

@@ -44,6 +45,7 @@ Options are:
4445
* `port` - the port Elasticsearch is running on
4546
* `auth` - the authentication needed to reach Elasticsearch server. In the standard format of 'username:password'
4647
* `protocol` - the protocol the Elasticsearch server uses. Defaults to http
48+
* `hydrate` - whether or not to replace ES source by mongo document
4749

4850

4951
To have a model indexed into Elasticsearch simply add the plugin.
@@ -295,6 +297,43 @@ User
295297
You'll have to manage whether or not you need to create the mapping, mongoose-elasticsearch-xp will make no assumptions and simply attempt to create the mapping.
296298
If the mapping already exists, an Exception detailing such will be populated in the `err` argument.
297299
300+
## Hydration
301+
By default objects returned from performing a search will be the objects as is in Elasticsearch.
302+
This is useful in cases where only what was indexed needs to be displayed (think a list of results) while the actual mongoose object contains the full data when viewing one of the results.
303+
304+
However, if you want the results to be actual mongoose objects you can provide {hydrate:true} as the second argument to a search call.
305+
306+
```javascript
307+
User
308+
.esSearch({query_string: {query: "john"}}, {hydrate:true})
309+
.then(function (results) {
310+
// results here
311+
});
312+
```
313+
314+
To modify default hydratation, provide an object to `hydrate` instead of "true".
315+
`hydrate` accept {select: string, options: object, docsOnly: boolean}
316+
317+
```javascript
318+
User
319+
.esSearch({query_string: {query: "john"}}, {hydrate: {select: 'name age', options: {lean: true}}})
320+
.then(function (results) {
321+
// results here
322+
});
323+
```
324+
325+
When using hydration, `hits._source` is replaced by `hits.doc`.
326+
327+
If you only want the models, instead of the complete ES results, use the option "docsOnly".
328+
329+
```javascript
330+
User
331+
.esSearch({query_string: {query: "john"}}, {hydrate: {select: 'name age', docsOnly; true}})
332+
.then(function (users) {
333+
// users is an array of User
334+
});
335+
```
336+
298337
299338
[npm-url]: https://npmjs.org/package/mongoose-elasticsearch-xp
300339
[npm-image]: https://badge.fury.io/js/mongoose-elasticsearch-xp.svg

index.js

Lines changed: 52 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ var generateMapping = require('./lib/mapping').generate;
22
var client = require('./lib/client');
33
var utils = require('./lib/utils');
44
var Bulker = require('./lib/bulker');
5+
var mongoose = require('mongoose');
56

67

78
module.exports = function (schema, options) {
@@ -146,7 +147,8 @@ function search(query, options, callback) {
146147
query = query || {};
147148
options = options || {};
148149

149-
var esOptions = this.esOptions();
150+
var self = this;
151+
var esOptions = self.esOptions();
150152
var params = {
151153
index: esOptions.index,
152154
type: esOptions.type
@@ -158,7 +160,51 @@ function search(query, options, callback) {
158160
} else {
159161
params.body = query.query ? query : {query: query};
160162
}
161-
esOptions.client.search(params, defer.callback);
163+
if (options.hydrate) {
164+
params._source = false;
165+
}
166+
esOptions.client.search(params, function (err, result) {
167+
if (err) {
168+
return defer.reject(err);
169+
}
170+
if (!options.hydrate) {
171+
return defer.resolve(result);
172+
}
173+
if (!result.hits.total) {
174+
return defer.resolve(result);
175+
}
176+
177+
var ids = result.hits.hits.map(function (hit) {
178+
return mongoose.Types.ObjectId(hit._id);
179+
});
180+
181+
var hydrate = options.hydrate || {};
182+
var select = hydrate.select || null;
183+
var opts = hydrate.options || null;
184+
var docsOnly = hydrate.docsOnly || false;
185+
186+
187+
self.find({_id: {$in: ids}}, select, opts, function (err, users) {
188+
if (err) {
189+
return defer.reject(err);
190+
}
191+
var userByIds = {};
192+
users.forEach(function (user) {
193+
userByIds[user._id] = user;
194+
});
195+
if (docsOnly) {
196+
result = ids.map(function (id) {
197+
return userByIds[id];
198+
});
199+
} else {
200+
result.hits.hits.forEach(function (hit) {
201+
hit.doc = userByIds[hit._id];
202+
});
203+
}
204+
return defer.resolve(result);
205+
});
206+
207+
});
162208

163209
return defer.promise;
164210
}
@@ -187,7 +233,7 @@ function synchronize(conditions, projection, options, callback) {
187233
options = null;
188234
}
189235

190-
var schema = this;
236+
var model = this;
191237
var defer = utils.defer(callback);
192238
var esOptions = this.esOptions();
193239
var batch = esOptions.bulk && esOptions.bulk.batch ? esOptions.bulk.batch : 50;
@@ -202,7 +248,7 @@ function synchronize(conditions, projection, options, callback) {
202248
}
203249

204250
function onError(err) {
205-
schema.emit('es-bulk-error', err);
251+
model.emit('es-bulk-error', err);
206252
if (streamClosed) {
207253
finalize();
208254
} else {
@@ -211,7 +257,7 @@ function synchronize(conditions, projection, options, callback) {
211257
}
212258

213259
function onSent(len) {
214-
schema.emit('es-bulk-sent', len);
260+
model.emit('es-bulk-sent', len);
215261
if (streamClosed) {
216262
finalize();
217263
} else {
@@ -228,7 +274,7 @@ function synchronize(conditions, projection, options, callback) {
228274
{index: {_index: esOptions.index, _type: esOptions.type, _id: doc._id.toString()}},
229275
utils.serialize(doc, esOptions.mapping)
230276
);
231-
schema.emit('es-bulk-data', doc);
277+
model.emit('es-bulk-data', doc);
232278
if (!sending) {
233279
stream.resume();
234280
}

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "mongoose-elasticsearch-xp",
3-
"version": "1.2.0",
3+
"version": "1.3.0",
44
"description": "A mongoose plugin that indexes models into elastic search (an alternative to mongoosastic)",
55
"tags": [
66
"mongodb",

test/hydratation.js

Lines changed: 189 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,189 @@
1+
var utils = require('./utils');
2+
var mongoose = require('mongoose');
3+
var plugin = require('../');
4+
5+
describe("hydratation", function () {
6+
7+
utils.setup();
8+
9+
beforeEach(function (done) {
10+
11+
var UserSchema = new mongoose.Schema({
12+
name: String,
13+
age: Number
14+
});
15+
16+
UserSchema.plugin(plugin);
17+
18+
var UserModel = mongoose.model('User', UserSchema);
19+
20+
var john = new UserModel({name: 'John', age: 35});
21+
var jane = new UserModel({name: 'Jane', age: 34});
22+
var bob = new UserModel({name: 'Bob', age: 36});
23+
24+
this.model = UserModel;
25+
this.users = {
26+
john: john,
27+
jane: jane,
28+
bob: bob
29+
};
30+
31+
utils.deleteModelIndexes(UserModel)
32+
.then(function () {
33+
return UserModel.esCreateMapping();
34+
})
35+
.then(function () {
36+
return utils.Promise.all([john, jane, bob].map(function (user) {
37+
return new utils.Promise(function (resolve) {
38+
user.on('es-indexed', resolve);
39+
user.save();
40+
});
41+
}));
42+
})
43+
.then(function () {
44+
return UserModel.esRefresh();
45+
})
46+
.then(function () {
47+
done();
48+
});
49+
});
50+
51+
it('should hydrate results', function (done) {
52+
53+
var UserModel = this.model;
54+
var john = this.users.john;
55+
var bob = this.users.bob;
56+
57+
UserModel
58+
.esSearch(
59+
{
60+
query: {match_all: {}},
61+
sort: [
62+
{age: {order: "desc"}}
63+
],
64+
filter: {range: {age: {gte: 35}}}
65+
},
66+
{hydrate: true}
67+
)
68+
.then(function (result) {
69+
var hit;
70+
expect(result.hits.total).to.eql(2);
71+
72+
hit = result.hits.hits[0];
73+
expect(hit._source).to.be.undefined;
74+
expect(hit.doc).to.be.an.instanceof(UserModel);
75+
expect(hit.doc._id.toString()).to.eql(bob._id.toString());
76+
expect(hit.doc.name).to.eql(bob.name);
77+
expect(hit.doc.age).to.eql(bob.age);
78+
79+
hit = result.hits.hits[1];
80+
expect(hit._source).to.be.undefined;
81+
expect(hit.doc).to.be.an.instanceof(UserModel);
82+
expect(hit.doc._id.toString()).to.eql(john._id.toString());
83+
expect(hit.doc.name).to.eql(john.name);
84+
expect(hit.doc.age).to.eql(john.age);
85+
86+
done();
87+
})
88+
.catch(function (err) {
89+
done(err);
90+
});
91+
});
92+
93+
it('should hydrate returning only models', function (done) {
94+
95+
var UserModel = this.model;
96+
var john = this.users.john;
97+
var bob = this.users.bob;
98+
99+
UserModel
100+
.esSearch(
101+
{
102+
query: {match_all: {}},
103+
sort: [
104+
{age: {order: "desc"}}
105+
],
106+
filter: {range: {age: {gte: 35}}}
107+
},
108+
{hydrate: {docsOnly: true}}
109+
)
110+
.then(function (users) {
111+
var user;
112+
expect(users.length).to.eql(2);
113+
114+
user = users[0];
115+
expect(user._id.toString()).to.eql(bob._id.toString());
116+
expect(user.name).to.eql(bob.name);
117+
expect(user.age).to.eql(bob.age);
118+
119+
user = users[1];
120+
expect(user._id.toString()).to.eql(john._id.toString());
121+
expect(user.name).to.eql(john.name);
122+
expect(user.age).to.eql(john.age);
123+
124+
done();
125+
})
126+
.catch(function (err) {
127+
done(err);
128+
});
129+
});
130+
131+
it('should hydrate using projection', function (done) {
132+
133+
var UserModel = this.model;
134+
var jane = this.users.jane;
135+
136+
return UserModel
137+
138+
.esSearch(
139+
'name:jane',
140+
{hydrate: {select: 'name'}}
141+
)
142+
.then(function (result) {
143+
var hit;
144+
expect(result.hits.total).to.eql(1);
145+
146+
hit = result.hits.hits[0];
147+
expect(hit._source).to.be.undefined;
148+
expect(hit.doc).to.be.an.instanceof(UserModel);
149+
expect(hit.doc._id.toString()).to.eql(jane._id.toString());
150+
expect(hit.doc.name).to.eql(jane.name);
151+
expect(hit.doc.age).to.be.undefined;
152+
153+
done();
154+
})
155+
.catch(function (err) {
156+
done(err);
157+
});
158+
});
159+
160+
it('should hydrate using options', function (done) {
161+
162+
var UserModel = this.model;
163+
var jane = this.users.jane;
164+
165+
return UserModel
166+
167+
.esSearch(
168+
'name:jane',
169+
{hydrate: {options: {lean: true}}}
170+
)
171+
.then(function (result) {
172+
var hit;
173+
expect(result.hits.total).to.eql(1);
174+
175+
hit = result.hits.hits[0];
176+
expect(hit._source).to.be.undefined;
177+
expect(hit.doc).not.to.be.an.instanceof(UserModel);
178+
expect(hit.doc._id.toString()).to.eql(jane._id.toString());
179+
expect(hit.doc.name).to.eql(jane.name);
180+
expect(hit.doc.age).to.eql(jane.age);
181+
182+
done();
183+
})
184+
.catch(function (err) {
185+
done(err);
186+
});
187+
});
188+
189+
});

0 commit comments

Comments
 (0)