API Docs for: 2.2.2
Show:

File: lib/commands/yokozuna/search.js

'use strict';

var CommandBase = require('../commandbase');
var inherits = require('util').inherits;
var Joi = require('joi');

var utils = require('../../utils');

/**
 * Provides the (Yokozuna) Search class, its builder, and its response.
 * @module YZ
 */

/**
 * Command used to perform a (Yokozuna) search.
 *
 * As a convenience, a builder class is provided:
 *
 *      var search = new Search.Builder()
 *                      .withIndexName(myIndex)
 *                      .withQuery(myQuery)
 *                      .withNumRows(10)
 *                      .withCallback(myCallback)
 *                      .build();
 *
 * See {{#crossLink "Search.Builder"}}Search.Builder{{/crossLink}}
 *
 * For more information on Riak Search (Yokozuna/Solr) see:
 * [Using Search](http://docs.basho.com/riak/latest/dev/using/search/)
 *
 * @class Search
 * @constructor
 * @param {Object} options The options for this command.
 * @param {String} options.indexName Set the index name used for this search.
 * @param {String} options.q Set the Solr query string.
 * @param {Number} [options.maxRows=10] Specify the maximum number of results to return.
 * @param {String} [options.start=0] Specify the starting result of the query.
 * @param {String} [options.sortField] Sort the results on the specified field name.
 * @param {String} [options.filterQuery] Filters the search by an additional query scoped to inline fields.
 * @param {String} [options.defaultField] Use the provided field as the default. Overrides the “default_field” setting in the schema file.
 * @param {String} [options.defaultOperation] Set the default operation. Allowed settings are either “and” or “or”.
 * @param {String[]} [options.returnFields] Only return the provided fields.
 * @param {String} [options.presort] Sorts all of the results. Either "key" or "score".
 * @param {Boolean} [options.convertDocuments] Convert Solr document values to JS types.
 * @param {Function} callback The callback to execute when the comman completes.
 * @param {String} callback.err an error message. Will be null if no error.
 * @param {Object} callback.response the response from Riak (Solr)
 * @param {Number} callback.response.numFound The number of documents found.
 * @param {Number} callback.response.maxScore The max score value.
 * @param {Object[]} callback.response.docs Array of documents returned from Solr.
 * @param {Object} callback.data additional error data. Will be null if no error.
 * @extends CommandBase
 */
function Search(options, callback)  {
    CommandBase.call(this, 'RpbSearchQueryReq', 'RpbSearchQueryResp', callback);
    this.validateOptions(options, schema);
}

inherits(Search, CommandBase);

Search.prototype.constructPbRequest = function() {

    var protobuf = this.getPbReqBuilder();
    protobuf.index = new Buffer(this.options.indexName);
    protobuf.q = new Buffer(this.options.q);
    protobuf.rows = this.options.maxRows;
    protobuf.start = this.options.start;
    if (this.options.sortField) {
        protobuf.sort = new Buffer(this.options.sortField);
    }
    if (this.options.filterQuery) {
        protobuf.filter = new Buffer(this.options.filterQuery);
    }
    if (this.options.defaultField) {
        protobuf.df = new Buffer(this.options.defaultField);
    }
    if (this.options.defaultOperation) {
        protobuf.op = new Buffer(this.options.defaultOperation);
    }
    if (this.options.presort) {
        protobuf.presort = new Buffer(this.options.presort);
    }
    for (var i = 0; i < this.options.returnFields.length; i++) {
        protobuf.fl.push(new Buffer(this.options.returnFields[i]));
    }

    return protobuf;

};

// https://github.com/basho/yokozuna/blob/develop/include/yokozuna.hrl#L340-L404
function isYzField(f) {
    return f === '_yz_id'   ||
           f === '_yz_ed'   ||
           f === '_yz_fpn'  ||
           f === '_yz_vtag' ||
           f === '_yz_pn'   ||
           f === '_yz_rk'   ||
           f === '_yz_rt'   ||
           f === '_yz_rb'   ||
           f === '_yz_err';
}

Search.prototype.onSuccess = function(rpbSearchQueryResp) {

    var docsToReturn = new Array(rpbSearchQueryResp.docs.length);
    for (var i = 0; i < rpbSearchQueryResp.docs.length; i++) {
        var doc = {};
        for (var j = 0; j < rpbSearchQueryResp.docs[i].fields.length; j++) {
            var key = rpbSearchQueryResp.docs[i].fields[j].key.toString('utf8');
            var value = rpbSearchQueryResp.docs[i].fields[j].value.toString('utf8');
            // GH-165 do not convert well-known YZ fields
            if (isYzField(key) === false && this.options.convertDocuments === true) {
                // Search and MapReduce are effectively broken with the PB API because
                // everything is returned as a string.
                var valAsNum = utils.maybeIsNumber(value);
                if (valAsNum) {
                    // it's a Long or number.
                    value = valAsNum;
                } else if (value === 'null') {
                    // It's actually null
                    value = null;
                } else {
                    // might also be a boolean.
                    value = value === 'true' || (value === 'false' ? false : value);
                }
            }
            //Support multiple values for the same key
            if (doc.hasOwnProperty(key)) {
                if (doc[key].constructor === Array) {
                    doc[key].push(value);
                } else {
                    doc[key] = [ doc[key], value ];
                }
            } else {
                doc[key] = value;
            }
        }
        docsToReturn[i] = doc;
    }
    var result = { numFound : rpbSearchQueryResp.num_found,
                   maxScore : rpbSearchQueryResp.max_score,
                   docs: docsToReturn };

    this._callback(null, result);
    return true;

};

var schema = Joi.object().keys({
    indexName: Joi.string().required(),
    q: Joi.string().required(),
    maxRows: Joi.number().default(null).optional(),
    start: Joi.number().default(null).optional(),
    sortField: Joi.string().default(null).optional(),
    filterQuery: Joi.string().default(null).optional(),
    defaultField: Joi.string().default(null).optional(),
    defaultOperation: Joi.string().default(null).optional(),
    returnFields: Joi.array().default([]).optional(),
    presort: Joi.string().default(null).optional(),
    convertDocuments: Joi.boolean().default(true).optional()
});

/**
 * A builder for constructing Search instances.
 *
 * Rather than having to manually construct the __options__ and instantiating
 * a Search directly, this builder may be used.
 *
 *     var search = new Search.Builder()
 *                    .withIndexName(myIndex)
 *                    .withQuery(myQuery)
 *                    .withNumRows(10)
 *                    .withCallback(myCallback)
 *                    .build();
 *
 * @class Search.Builder
 * @constructor
 */
function Builder() {}

Builder.prototype = {

    /**
     * Set the index name used for this search.
     * @method withIndexName
     * @param {String} indexName the name of the yokozuna index
     * @chainable
     */
    withIndexName : function(indexName) {
        this.indexName = indexName;
        return this;
    },
    /**
     * Set the Solr query string.
     * All distributed Solr queries are supported, which actually
     * includes most of the single-node Solr queries.
     * @method withQuery
     * @param {String} queryString the query
     * @chainable
     */
    withQuery : function(queryString) {
        this.q = queryString;
        return this;
    },
    /**
    * Specify the maximum number of results to return.
    * Riak defaults to 10 if this is not set.
    * @method withNumRows
    * @param {Number} maxRows the maximum number of results to return.
    * @chainable
    */
    withNumRows : function(maxRows) {
        this.maxRows = maxRows;
        return this;
    },
    /**
    * Specify the starting result of the query.
    * Useful for pagination. The default is 0.
    * @method withStart
    * @param {Number} start the index of the starting result.
    * @chainable
    */
   withStart : function(start) {
       this.start = start;
       return this;
   },
   /**
    * Sort the results on the specified field name.
    * Default is “none”, which causes the results to be sorted in descending order by score.
    * @method withSortField
    * @param {String} sortField the fieldname to sort the results on.
    * @chainable
    */
   withSortField : function(sortField) {
       this.sortField = sortField;
       return this;
   },
   /**
    * Filters the search by an additional query scoped to inline fields.
    * @method withFilterQuery
    * @param {String} filterQuery the filter query.
    * @chainable
    */
   withFilterQuery : function(filterQuery) {
       this.filterQuery = filterQuery;
       return this;
   },
   /**
    * Use the provided field as the default.
    * Overrides the “default_field” setting in the schema file.
    * @method withDefaultField
    * @param {String} fieldName the name of the field.
    * @chainable
    */
   withDefaultField : function(fieldName) {
       this.defaultField = fieldName;
       return this;
   },
   /**
    * Set the default operation.
    * Allowed settings are either “and” or “or”.
    * Overrides the “default_op” setting in the schema file.
    * @method withDefaultOperation
    * @param {String} op A string containing either "and" or "or".
    * @chainable
    */
   withDefaultOperation : function(op) {
       this.defaultOperation = op;
       return this;
   },
   /**
    * Only return the provided fields.
    * Filters the results to only contain the provided fields.
    * @method withReturnFields
    * @param {String[]} fields an array of field names.
    * @chainable
    */
   withReturnFields : function(fields) {
       this.returnFields = fields;
       return this;
   },
   /**
    * Sorts all of the results by bucket key, or the search score, before the given rows are chosen.
    * This is useful when paginating to ensure the results are returned in a consistent order.
    * @method withPresort
    * @param {String} presort a string containing either "key" or "score".
    * @chainable
    */
   withPresort : function(presort) {
       this.presort = presort;
       return this;
   },
   /**
    * Convert values in documents returned by Solr to corresponding Javascript types.
    * @method convertDocuments
    * @param {Boolean} convert Solr document values to JS types.
    * @chainable
    */
   withConvertDocuments : function(convertDocuments) {
       this.convertDocuments = convertDocuments;
       return this;
   },
   /**
     * Set the callback to be executed when the operation completes.
     * @method withCallback
     * @param {Function} callback The callback to execute when the comman completes.
     * @param {String} callback.err an error message. Will be null if no error.
     * @param {Object} callback.response the response from Riak (Solr)
     * @param {Number} callback.response.numFound The number of documents found.
     * @param {Number} callback.response.maxScore The max score value.
     * @param {Object[]} callback.response.docs Array of documents returned from Solr.
     * @chainable
     */
   withCallback : function(callback) {
       this.callback = callback;
       return this;
   },
   /**
    * Construct a new Search instance.
    * @method build
    * @return {Search}
    */
   build : function() {
        var cb = this.callback;
        delete this.callback;
        return new Search(this, cb);
   }

};

module.exports = Search;
module.exports.Builder = Builder;