1 | /*!
|
2 | * solr client
|
3 | * Copyright(c) 2011-2012 HipSnip Limited
|
4 | * Author Rémy Loubradou <remyloubradou@gmail.com>
|
5 | * MIT Licensed
|
6 | */
|
7 |
|
8 | /**
|
9 | * Load dependencies
|
10 | */
|
11 |
|
12 | var http = require('http'),
|
13 | Query = require('./query'),
|
14 | querystring = require('querystring'),
|
15 | format = require('./utils/format'),
|
16 | SolrError = require('./error/solr-error'),
|
17 | JSONStream = require('JSONStream'),
|
18 | duplexer = require('duplexer'),
|
19 | request = require('request');
|
20 |
|
21 | /**
|
22 | * Expose `createClient()`.
|
23 | */
|
24 |
|
25 | exports.createClient = createClient;
|
26 |
|
27 | /**
|
28 | * Create an instance of `Client`
|
29 | *
|
30 | * @param {String|Object} [host='127.0.0.1'] - IP address or host address of the Solr server
|
31 | * @param {Number|String} [port='8983'] - port of the Solr server
|
32 | * @param {String} [core=''] - name of the Solr core requested
|
33 | * @param {String} [path='/solr'] - root path of all requests
|
34 | *
|
35 | * @return {Client}
|
36 | * @api public
|
37 | */
|
38 |
|
39 | function createClient(host, port, core, path){
|
40 | var options = (typeof host === 'object')? host : {
|
41 | host : host,
|
42 | port : port,
|
43 | core : core,
|
44 | path : path
|
45 | };
|
46 | return new Client(options);
|
47 | }
|
48 |
|
49 | /**
|
50 | * Create a new `Client`
|
51 | * @constructor
|
52 | *
|
53 | * @param {Object} options - set of options used to request the Solr server
|
54 | * @param {String} options.host - IP address or host address of the Solr server
|
55 | * @param {Number|String} options.port - port of the Solr server
|
56 | * @param {String} options.core - name of the Solr core requested
|
57 | * @param {String} options.path - root path of all requests
|
58 | *
|
59 | * @return {Client}
|
60 | * @api private
|
61 | */
|
62 |
|
63 | var Client = function(options){
|
64 | this.options = {
|
65 | host : options.host || '127.0.0.1',
|
66 | port : options.port || '8983',
|
67 | core : options.core || '',
|
68 | path : options.path || '/solr'
|
69 | };
|
70 | this.data = {};
|
71 | this.autoCommit = false;
|
72 | }
|
73 |
|
74 | /**
|
75 | * Create credential using the basic access authentication method
|
76 | *
|
77 | * @param {String} username
|
78 | * @param {String} password
|
79 | *
|
80 | * @return {Client}
|
81 | * @api public
|
82 | */
|
83 |
|
84 | Client.prototype.basicAuth = function(username,password){
|
85 | var self = this;
|
86 | this.options.authorization = 'Basic ' + new Buffer(username + ':' + password).toString('base64');
|
87 | return self;
|
88 | }
|
89 |
|
90 | /**
|
91 | * Remove authorization header
|
92 | *
|
93 | * @return {Client}
|
94 | * @api public
|
95 | */
|
96 |
|
97 | Client.prototype.unauth = function(){
|
98 | var self = this;
|
99 | delete this.options.authorization;
|
100 | return self;
|
101 | }
|
102 |
|
103 | /**
|
104 | * Add a document or a list of documents
|
105 | *
|
106 | * @param {Object|Array} doc - document or list of documents to add into the Solr database
|
107 | * @param {Function} callback(err,obj) - a function executed when the Solr server responds or an error occurs
|
108 | * @param {Error} callback().err
|
109 | * @param {Object} callback().obj - JSON response sent by the Solr server deserialized
|
110 | *
|
111 | * @return {Client}
|
112 | * @api public
|
113 | */
|
114 |
|
115 | Client.prototype.add = function(docs,callback){
|
116 | var self = this;
|
117 | docs = format.dateISOify(docs); // format `Date` object into string understable for Solr as a date.
|
118 | docs = Array.isArray(docs) ? docs : [docs];
|
119 | this.update(docs,callback);
|
120 | return self;
|
121 | }
|
122 |
|
123 | /**
|
124 | * Add the remote resource located at the given path `options.path` into the Solr database.
|
125 | *
|
126 | * @param {Object} options -
|
127 | * @param {String} options.path - path of the file. HTTP URL, the full path or a path relative to the CWD of the running solr server must be used.
|
128 | * @param {String} [options.format='xml'] - format of the resource. XML, CSV or JSON formats must be used.
|
129 | * @param {String} [options.contentType='text/plain;charset=utf-8'] - content type of the resource
|
130 | * @param {Object} [options.parameters] - set of extras parameters pass along in the query.
|
131 | * @param {Function} callback(err,obj) - a function executed when the Solr server responds or an error occurs
|
132 | * @param {Error} callback().err
|
133 | * @param {Object} callback().obj - JSON response sent by the Solr server deserialized
|
134 | *
|
135 | * @return {Client}
|
136 | * @api public
|
137 | */
|
138 |
|
139 | Client.prototype.addRemoteResource = function(options,callback){
|
140 | var self = this;
|
141 | options.parameters = options.parameters || {};
|
142 | options.format = (options.format === 'xml' ? '' : options.format || ''); // reason: the default route of the XmlUpdateRequestHandle is /update and not /update/xml.
|
143 | options.parameters.commit = (options.parameters.commit === undefined ? this.autoCommit : options.parameters.commit);
|
144 | options.parameters['stream.contentType'] = options.contentType || 'text/plain;charset=utf-8';
|
145 | if(options.path.match(/^https?:\/\//)){
|
146 | options.parameters['stream.url'] = options.path;
|
147 | }else{
|
148 | options.parameters['stream.file'] = options.path;
|
149 | }
|
150 | var path = 'update/' + options.format.toLowerCase() + '?' + querystring.stringify(options.parameters) + '&wt=json';
|
151 | this.options.fullPath = [this.options.path, this.options.core, path]
|
152 | .filter(function(element){
|
153 | if(element) return true;
|
154 | return false;
|
155 | })
|
156 | .join('/');
|
157 | queryRequest(this.options,callback);
|
158 | return self;
|
159 | }
|
160 |
|
161 | /**
|
162 | * Create a writable/readable `Stream` to add documents into the Solr database
|
163 | *
|
164 | * return {Stream}
|
165 | * @api public
|
166 | */
|
167 |
|
168 | Client.prototype.createAddStream = function(){
|
169 | var path = [this.options.path,this.options.core,'update/json?commit='+ this.autoCommit +'&wt=json']
|
170 | .filter(function(element){
|
171 | if(element) return true;
|
172 | return false;
|
173 | })
|
174 | .join('/');
|
175 | var headers = {
|
176 | 'content-type' : 'application/json',
|
177 | 'charset' : 'utf-8'
|
178 | };
|
179 | if(this.options.authorization){
|
180 | headers['authorization'] = this.options.authorization;
|
181 | }
|
182 | var optionsRequest = {
|
183 | url : 'http://' + this.options.host +':' + this.options.port + path ,
|
184 | method : 'POST',
|
185 | headers : headers
|
186 | };
|
187 | var jsonStreamStringify = JSONStream.stringify();
|
188 | var postRequest = request(optionsRequest);
|
189 | jsonStreamStringify.pipe(postRequest);
|
190 | var duplex = duplexer(jsonStreamStringify,postRequest);
|
191 | return duplex ;
|
192 | }
|
193 |
|
194 | /**
|
195 | * Commit last added and removed documents, that means your documents are now indexed.
|
196 | *
|
197 | * @param {Object} options
|
198 | * @param {Function} callback(err,obj) - a function executed when the Solr server responds or an error occurs
|
199 | * @param {Error} callback().err
|
200 | * @param {Object} callback().obj - JSON response sent by the Solr server deserialized
|
201 | *
|
202 | * @return {Client}
|
203 | * @api private
|
204 | */
|
205 |
|
206 | Client.prototype.commit = function(options,callback){
|
207 | if(callback === undefined){
|
208 | callback = options;
|
209 | options = undefined;
|
210 | }
|
211 | this.data = {};
|
212 | this.data['commit'] = {};
|
213 |
|
214 | if( options !== undefined ){
|
215 | var availableAttributes = ['waitFlush','waitSeacher'];
|
216 | for ( var i = 0; i < availableAttributes.length ; i++){
|
217 | if ( options[availableAttributes [i]] !== undefined ){
|
218 | this.data['commit'][availableAttributes[i]] = options[availableAttributes[i]];
|
219 | }
|
220 | }
|
221 | }
|
222 | this.update(this.data,callback);
|
223 | }
|
224 |
|
225 | /**
|
226 | * Delete documents based on the given `field` and `text`.
|
227 | *
|
228 | * @param {String} field
|
229 | * @param {String} text
|
230 | * @param {Function} callback(err,obj) - a function executed when the Solr server responds or an error occurs
|
231 | * @param {Error} callback().err
|
232 | * @param {Object} callback().obj - JSON response sent by the Solr server deserialized
|
233 | *
|
234 | * @return {Client}
|
235 | * @api public
|
236 | */
|
237 |
|
238 | Client.prototype.delete = function(field,text,callback) {
|
239 | var self = this;
|
240 | text = format.dateISOify(text);
|
241 | this.data = {};
|
242 | this.data['delete'] = {query : field + ':' + text};
|
243 | this.update(this.data,callback);
|
244 | return self;
|
245 | }
|
246 |
|
247 | /**
|
248 | * Delete a range of documents based on the given `field`, `start` and `stop` arguments.
|
249 | *
|
250 | * @param {String} field
|
251 | * @param {String|Date} start
|
252 | * @param {String|Date} stop
|
253 | * @param {Function} callback(err,obj) - a function executed when the Solr server responds or an error occurs
|
254 | * @param {Error} callback().err
|
255 | * @param {Object} callback().obj - JSON response sent by the Solr server deserialized
|
256 | *
|
257 | * @return {Client}
|
258 | * @api public
|
259 | */
|
260 |
|
261 | Client.prototype.deleteByRange = function(field,start,stop,callback){
|
262 | var self = this;
|
263 | start = format.dateISOify(start);
|
264 | stop = format.dateISOify(stop);
|
265 | this.data = {};
|
266 | this.data['delete'] = { query : field + ':[' + start + ' TO ' + stop + ']' };
|
267 | this.update(this.data,callback);
|
268 | return self;
|
269 | }
|
270 |
|
271 | /**
|
272 | * Delete the document with the given `id`
|
273 | *
|
274 | * @param {String|Number} id - id of the document you want to delete
|
275 | * @param {Function} callback(err,obj) - a function executed when the Solr server responds or an error occurs
|
276 | * @param {Error} callback().err
|
277 | * @param {Object} callback().obj - JSON response sent by the Solr server deserialized
|
278 | *
|
279 | * @return {Client}
|
280 | * @api public
|
281 | */
|
282 |
|
283 | Client.prototype.deleteByID = function(id,callback){
|
284 | var self = this;
|
285 | this.data = {};
|
286 | this.data['delete'] = {id : id.toString()};
|
287 | this.update(this.data,callback);
|
288 | return self;
|
289 | }
|
290 |
|
291 | /**
|
292 | * Delete documents matching the given `query`
|
293 | *
|
294 | * @param {String} query -
|
295 | * @param {Function} callback(err,obj) - a function executed when the Solr server responds or an error occurs
|
296 | * @param {Error} callback().err
|
297 | * @param {Object} callback().obj - JSON response sent by the Solr server deserialized
|
298 | *
|
299 | * @return {Client}
|
300 | * @api public
|
301 | */
|
302 |
|
303 | Client.prototype.deleteByQuery = function(query,callback){
|
304 | var self = this;
|
305 | this.data = {};
|
306 | this.data['delete'] = {query : query};
|
307 | this.update(this.data,callback);
|
308 | return self;
|
309 | }
|
310 |
|
311 | /**
|
312 | * Optimize the index
|
313 | *
|
314 | * @param {Object} options -
|
315 | * @param {Function} callback(err,obj) - a function executed when the Solr server responds or an error occurs
|
316 | * @param {Error} callback().err
|
317 | * @param {Object} callback().obj - JSON response sent by the Solr server deserialized
|
318 | *
|
319 | * @return {Client}
|
320 | * @api public
|
321 | */
|
322 |
|
323 | Client.prototype.optimize = function(options,callback){
|
324 | if(callback === undefined){
|
325 | callback = options;
|
326 | options = undefined;
|
327 | }
|
328 | this.data = {};
|
329 | this.data['optimize'] = {};
|
330 | if( options !== undefined ){
|
331 | var availableAttributes = ['waitFlush','waitSearcher'];
|
332 | for ( var i = 0; i < availableAttributes.length ; i++){
|
333 | if ( options[availableAttributes [i]] !== undefined ){
|
334 | this.data['optimize'][availableAttributes[i]] = options[availableAttributes[i]];
|
335 | }
|
336 | }
|
337 | }
|
338 | this.update(this.data,callback);
|
339 | }
|
340 |
|
341 | /**
|
342 | * Rollback all add/delete commands made since the last commit.
|
343 | *
|
344 | * @param {Function} callback(err,obj) - a function executed when the Solr server responds or an error occurs
|
345 | * @param {Error} callback().err
|
346 | * @param {Object} callback().obj - JSON response sent by the Solr server deserialized
|
347 | *
|
348 | * @return {Client}
|
349 | * @api public
|
350 | */
|
351 |
|
352 | Client.prototype.rollback = function(callback){
|
353 | this.data = {};
|
354 | this.data['rollback'] = {};
|
355 | this.update(this.data,callback);
|
356 | }
|
357 |
|
358 | /**
|
359 | * Send an update command to the Solr server with the given `data` stringified in the body.
|
360 | *
|
361 | * @param {Object} data - data sent to the Solr server
|
362 | * @param {Function} callback(err,obj) - a function executed when the Solr server responds or an error occurs
|
363 | * @param {Error} callback().err
|
364 | * @param {Object} callback().obj - JSON response sent by the Solr server deserialized
|
365 | *
|
366 | * @return {Client}
|
367 | * @api private
|
368 | */
|
369 |
|
370 | Client.prototype.update = function(data,callback){
|
371 | var self = this;
|
372 | this.options.json = JSON.stringify(data);
|
373 | this.options.fullPath = [this.options.path,this.options.core,'update/json?commit='+ this.autoCommit +'&wt=json']
|
374 | .filter(function(element){
|
375 | if(element) return true;
|
376 | return false;
|
377 | })
|
378 | .join('/');
|
379 | updateRequest(this.options,callback);
|
380 | return self;
|
381 | }
|
382 |
|
383 | /**
|
384 | * Search documents matching the `query`
|
385 | *
|
386 | * @param {Query|String} query
|
387 | * @param {Function} callback(err,obj) - a function executed when the Solr server responds or an error occurs
|
388 | * @param {Error} callback().err
|
389 | * @param {Object} callback().obj - JSON response sent by the Solr server deserialized
|
390 | *
|
391 | * @return {Client}
|
392 | * @api public
|
393 | */
|
394 |
|
395 | Client.prototype.search = function(query,callback){
|
396 | var self = this;
|
397 | // Allow to be more flexible allow query to be a string and not only a Query object
|
398 | var parameters = query.build ? query.build() : query;
|
399 | this.options.fullPath = [this.options.path,this.options.core,'select?' + parameters + '&wt=json']
|
400 | .filter(function(element){
|
401 | if(element) return true;
|
402 | return false;
|
403 | })
|
404 | .join('/'); ;
|
405 | queryRequest(this.options,callback);
|
406 | return self;
|
407 | }
|
408 |
|
409 | /**
|
410 | * Create an instance of `Query`
|
411 | *
|
412 | * @return {Query}
|
413 | * @api public
|
414 | */
|
415 |
|
416 | Client.prototype.createQuery = function(){
|
417 | return new Query();
|
418 | }
|
419 |
|
420 |
|
421 | /**
|
422 | * Ping the Solr server
|
423 | *
|
424 | * @param {Function} callback(err,obj) - a function executed when the Solr server responds or an error occurs
|
425 | * @param {Error} callback().err
|
426 | * @param {Object} callback().obj - JSON response sent by the Solr server deserialized
|
427 | *
|
428 | * @return {Client}
|
429 | * @api public
|
430 | */
|
431 |
|
432 | Client.prototype.ping = function(callback){
|
433 | var self = this;
|
434 | this.options.fullPath = [this.options.path,this.options.core,'admin/ping?wt=json']
|
435 | .filter(function(element){
|
436 | if(element) return true;
|
437 | return false;
|
438 | })
|
439 | .join('/');
|
440 | queryRequest(this.options,callback);
|
441 | return self;
|
442 | }
|
443 |
|
444 | /**
|
445 | * HTTP POST request. Send update commands to the Solr server (commit, add, delete, optimize)
|
446 | *
|
447 | * @param {Object} params
|
448 | * @param {String} params.host - IP address or host address of the Solr server
|
449 | * @param {Number|String} params.port - port of the Solr server
|
450 | * @param {String} params.core - name of the Solr core requested
|
451 | * @param {String} params.authorization - value of the authorization header
|
452 | * @param {String} params.fullPath - full path of the request
|
453 | * @param {String} params.json
|
454 | * @param {Function} callback(err,obj) - a function executed when the Solr server responds or an error occurs
|
455 | * @param {Error} callback().err
|
456 | * @param {Object} callback().obj - JSON response sent by the Solr server deserialized
|
457 | *
|
458 | * @api private
|
459 | */
|
460 |
|
461 | var updateRequest = function(params,callback){
|
462 | var headers = {
|
463 | 'content-type' : 'application/json',
|
464 | 'charset' : 'utf-8',
|
465 | 'content-length': Buffer.byteLength(params.json),
|
466 | };
|
467 | if(params.authorization){
|
468 | headers['authorization'] = params.authorization;
|
469 | }
|
470 | var options = {
|
471 | host : params.host,
|
472 | port : params.port,
|
473 | method : 'POST',
|
474 | headers : headers,
|
475 | path : params.fullPath
|
476 | };
|
477 |
|
478 | var callbackResponse = function(res){
|
479 | var buffer = '';
|
480 | var err = null;
|
481 | res.on('data',function(chunk){
|
482 | buffer += chunk;
|
483 | });
|
484 |
|
485 | res.on('end',function(){
|
486 | if(res.statusCode !== 200){
|
487 | err = new SolrError(res.statusCode,buffer);
|
488 | if (callback) callback(err,null);
|
489 | }else{
|
490 | try{
|
491 | var data = JSON.parse(buffer);
|
492 | }catch(error){
|
493 | err = error;
|
494 | }finally{
|
495 | if (callback) callback(err,data);
|
496 | }
|
497 | }
|
498 | });
|
499 | }
|
500 |
|
501 | var request = http.request(options,callbackResponse);
|
502 |
|
503 | request.on('error',function(err){
|
504 | if (callback) callback(err,null);
|
505 | });
|
506 | request.write(params.json);
|
507 | request.end();
|
508 | }
|
509 |
|
510 | /**
|
511 | * HTTP GET request. Send a query command to the Solr server (query)
|
512 | *
|
513 | * @param {Object} params
|
514 | * @param {String} params.host - IP address or host address of the Solr server
|
515 | * @param {Number|String} params.port - port of the Solr server
|
516 | * @param {String} params.core - name of the Solr core requested
|
517 | * @param {String} params.authorization - value of the authorization header
|
518 | * @param {String} params.fullPath - full path of the request, contains query parameters
|
519 | * @param {Function} callback(err,obj) - a function executed when the Solr server responds or an error occurs
|
520 | * @param {Error} callback().err
|
521 | * @param {Object} callback().obj - JSON response sent by the Solr server deserialized
|
522 | *
|
523 | * @api private
|
524 | */
|
525 |
|
526 | var queryRequest = function(params,callback){
|
527 | var options = {
|
528 | host : params.host,
|
529 | port : params.port,
|
530 | path : params.fullPath
|
531 | };
|
532 |
|
533 | if(params.authorization){
|
534 | var headers = {
|
535 | 'authorization' : params.authorization
|
536 | };
|
537 | options.headers = headers;
|
538 | }
|
539 |
|
540 | var callbackResponse = function(res){
|
541 | var buffer = '';
|
542 | var err = null;
|
543 | res.on('data',function(chunk){
|
544 | buffer += chunk;
|
545 | });
|
546 |
|
547 | res.on('end',function(){
|
548 | if(res.statusCode !== 200){
|
549 | err = new SolrError(res.statusCode,buffer);
|
550 | if (callback) callback(err,null);
|
551 | }else{
|
552 | try{
|
553 | var data = JSON.parse(buffer);
|
554 | }catch(error){
|
555 | err = error;
|
556 | }finally{
|
557 | if (callback) callback(err,data);
|
558 | }
|
559 | }
|
560 | });
|
561 | }
|
562 |
|
563 | var request = http.get(options,callbackResponse);
|
564 |
|
565 | request.on('error',function(err){
|
566 | if (callback) callback(err,null);
|
567 | });
|
568 | }
|