From a3b6f9460bc744d065dd5992f8e5d84c9db5b76f Mon Sep 17 00:00:00 2001 From: Brett Lawson Date: Sun, 5 Jun 2016 23:35:01 -0700 Subject: [PATCH] JSCBC-275: Implemented support for FTS querying. This is implemented at both the bucket level as well as at the cluster level using the new cluster level authentication. Change-Id: I226b6feec13b44a0834e58d0d062b2e0d5d41680 Reviewed-on: http://review.couchbase.org/64754 Reviewed-by: Mark Nunberg Tested-by: Brett Lawson --- lib/binding.js | 1 + lib/bucket.js | 201 ++++++++++++++- lib/cluster.js | 46 +++- lib/couchbase.js | 2 + lib/searchquery.js | 185 ++++++++++++++ lib/searchquery_facets.js | 52 ++++ lib/searchquery_queries.js | 483 +++++++++++++++++++++++++++++++++++++ lib/utils.js | 15 ++ src/binding.cc | 1 + src/couchbase_impl.cc | 47 ++++ src/couchbase_impl.h | 4 + src/operations.cc | 32 +++ 12 files changed, 1060 insertions(+), 9 deletions(-) create mode 100644 lib/searchquery.js create mode 100644 lib/searchquery_facets.js create mode 100644 lib/searchquery_queries.js create mode 100644 lib/utils.js diff --git a/lib/binding.js b/lib/binding.js index c110f371..e8c7b2c3 100644 --- a/lib/binding.js +++ b/lib/binding.js @@ -40,6 +40,7 @@ var fs = require('fs'); /** @name CouchbaseBinding.CouchbaseImpl#durability */ /** @name CouchbaseBinding.CouchbaseImpl#viewQuery */ /** @name CouchbaseBinding.CouchbaseImpl#n1qlQuery */ +/** @name CouchbaseBinding.CouchbaseImpl#ftsQuery */ /** @name CouchbaseBinding.CouchbaseImpl#lookupIn */ /** @name CouchbaseBinding.CouchbaseImpl#mutateIn */ /** @name CouchbaseBinding.CouchbaseImpl#_errorTest */ diff --git a/lib/bucket.js b/lib/bucket.js index f6e1f69a..14bfcee3 100644 --- a/lib/bucket.js +++ b/lib/bucket.js @@ -15,6 +15,7 @@ var connStr = require('./connstr'); var ViewQuery = require('./viewquery'); var SpatialQuery = require('./spatialquery'); var N1qlQuery = require('./n1qlquery'); +var SearchQuery = require('./searchquery'); var BucketManager = require('./bucketmgr'); var CONST = binding.Constants; @@ -739,19 +740,205 @@ Bucket.prototype._n1ql = function(query, params, callback) { return req; }; +/** + * @class Meta + * @classdesc + * The meta-information available from a view query response. + * @private + * @memberof Bucket.N1qlQueryResponse + */ +/** + * The status information for this query, includes properties + * such as total, failed and successful. + * + * @var {number} Bucket.FtsQueryResponse.Meta#status + * @since 2.1.7 + * @uncommitted + */ +/** + * Any non-fatal errors that occured during query processing. + * + * @var {number} Bucket.FtsQueryResponse.Meta#errors + * @since 2.1.7 + * @uncommitted + */ +/** + * The total number of hits that were available for this seach query. + * + * @var {number} Bucket.FtsQueryResponse.Meta#totalHits + * @since 2.1.7 + * @uncommitted + */ +/** + * The resulting facet information for any facets that were specified + * in the search query. + * + * @var {number} Bucket.FtsQueryResponse.Meta#facets + * @since 2.1.7 + * @uncommitted + */ +/** + * The time spent processing this query. + * + * @var {number} Bucket.FtsQueryResponse.Meta#took + * @since 2.1.7 + * @uncommitted + */ +/** + * The maximum score out of all the results in this query. + * + * @var {number} Bucket.FtsQueryResponse.Meta#maxScore + * @since 2.1.7 + * @uncommitted + */ + +/** + * Emitted whenever a new row is available from a queries result set. + * + * @event Bucket.FtsQueryResponse#row + * @param {Object} row + * @param {Bucket.FtsQueryResponse.Meta} meta + * + * @since 2.1.7 + * @uncommitted + */ +/** + * Emitted whenever all rows are available from a queries result set. + * + * @event Bucket.FtsQueryResponse#rows + * @param {Object[]} rows + * @param {Bucket.FtsQueryResponse.Meta} meta + * + * @since 2.1.7 + * @uncommitted + */ +/** + * Emitted once a query has completed executing and emitting all rows. + * + * @event Bucket.FtsQueryResponse#end + * @param {Bucket.FtsQueryResponse.Meta} meta + * + * @since 2.1.7 + * @uncommitted + */ +/** + * Emitted if an error occurs while executing a query. + * + * @event Bucket.FtsQueryResponse#error + * @param {Error} error + * + * @since 2.1.7 + * @uncommitted + */ + +/** + * An event emitter allowing you to bind to various query result set + * events. + * + * @constructor + * + * @private + * @memberof Bucket + * @extends events.EventEmitter + * + * @since 2.1.7 + * @uncommitted + */ +function FtsQueryResponse() { +} +util.inherits(FtsQueryResponse, events.EventEmitter); +Bucket.FtsQueryResponse = FtsQueryResponse; + +/** + * Executes a FTS http request. + * + * @param {SearchQuery} q + * @param {FtsQueryResponse} emitter + * + * @private + * @ignore + */ +Bucket.prototype._ftsReq = function(q, emitter) { + var rows = []; + this._cb.ftsQuery( + q, + function(errCode, val) { + if (errCode === -1) { // Row + var row = val; + if (rows) { + if (events.EventEmitter.listenerCount(emitter, 'rows') > 0) { + rows.push(row); + } else { + rows = null; + } + } + emitter.emit('row', row); + } else if (errCode === 0) { // Success + var meta = val; + if (meta instanceof Object) { + meta.totalHits = meta.total_hits; + meta.maxScore = meta.max_score; + delete meta.total_hits; + delete meta.max_score; + } + if (rows) { + emitter.emit('rows', rows, meta); + } + emitter.emit('end', meta); + } else { // Error + var err = new Error('An FTS error occured: ' + val); + emitter.emit('error', err); + } + }); +}; + +/** + * Executes a FTS query from a SearchQuery. + * + * @param {SearchQuery} query + * @param {function} callback + + * @private + * @ignore + */ +Bucket.prototype._fts = function(query, callback) { + var req = new FtsQueryResponse(); + + var invokeCb = callback; + if (!invokeCb) { + invokeCb = function(err) { + req.emit('error', err); + }; + } + + this._maybeInvoke(this._ftsReq.bind(this), + [query, req, invokeCb]); + + if (callback) { + req.on('rows', function(rows, meta) { + callback(null, rows, meta); + }); + req.on('error', function(err) { + callback(err, null, null); + }); + } + + return req; +}; + /** * Executes a previously prepared query object. This could be a - * {@link ViewQuery} or a {@link N1qlQuery}. + * {@link ViewQuery}, {@link N1qlQuery} or a {@link SearchQuery}. * - * Note: N1qlQuery queries are currently an uncommitted interface and may be - * subject to change in 2.0.0's final release. + * Note: SearchQuery queries are currently an uncommitted interface and may be + * subject to change in a future release. * - * @param {ViewQuery|N1qlQuery} query + * @param {ViewQuery|N1qlQuery|SearchQuery} query * The query to execute. * @param {Object|Array} [params] * A list or map to do replacements on a N1QL query. * @param {Bucket.QueryCallback} callback - * @returns {Bucket.ViewQueryResponse|Bucket.N1qlQueryResponse} + * @returns {Bucket.ViewQueryResponse|Bucket.N1qlQueryResponse|Bucket.FtsQueryResponse} * * @since 2.0.0 * @committed @@ -772,6 +959,10 @@ Bucket.prototype.query = function(query, params, callback) { return this._n1ql( query, params, callback ); + } else if (query instanceof SearchQuery) { + return this._fts( + query, callback + ); } else { throw new TypeError( 'First argument needs to be a ViewQuery, SpatialQuery or N1qlQuery.'); diff --git a/lib/cluster.js b/lib/cluster.js index 9272309e..a2ffe162 100644 --- a/lib/cluster.js +++ b/lib/cluster.js @@ -5,6 +5,7 @@ var connstr = require('./connstr'); var Bucket = require('./bucket'); var ClusterManager = require('./clustermgr'); var N1qlQuery = require('./n1qlquery'); +var SearchQuery = require('./searchquery'); function _arrayRemove(arr, element) { var newArray = []; @@ -202,20 +203,53 @@ Cluster.prototype._n1ql = function(query, params, callback) { return req; }; +Cluster.prototype._ftsReq = function(q, emitter) { + var bucket = this.connectedBuckets[0]; + bucket._ftsReq(q, emitter); +}; + +Cluster.prototype._fts = function(query, callback) { + var req = new Bucket.FtsQueryResponse(); + + var invokeCb = callback; + if (!invokeCb) { + invokeCb = function(err) { + req.emit('error', err); + }; + } + + this._maybeInvoke(this._ftsReq.bind(this), + [query, req, invokeCb]); + + if (callback) { + req.on('rows', function(rows, meta) { + callback(null, rows, meta); + }); + req.on('error', function(err) { + callback(err, null, null); + }); + } + + return req; +}; + /** - * Executes a previously prepared query object. This must be a - * {@link N1qlQuery}. + * Executes a previously prepared query object. This could be a + * {@link N1qlQuery} or a {@link SearchQuery}. * * Note: You must have at least one bucket open (this is neccessary to * have cluster mapping information), and additionally be using the new * cluster-level authentication methods. + * + * Note: SearchQuery queries are currently an uncommitted interface and may be + * subject to change in a future release. * - * @param {N1qlQuery} query + * @param {N1qlQuery|SearchQuery} query * The query to execute. * @param {Object|Array} [params] * A list or map to do replacements on a N1QL query. * @param {Bucket.QueryCallback} callback - * @returns {Bucket.N1qlQueryResponse} + * @returns {Bucket.N1qlQueryResponse|Bucket.FtsQueryResponse} * * @since 2.1.7 * @committed @@ -253,6 +287,10 @@ Cluster.prototype.query = function(query, params, callback) { return this._n1ql( query, params, callback ); + } else if (query instanceof SearchQuery) { + return this._fts( + query, callback + ); } else { throw new TypeError( 'First argument needs to be a N1qlQuery.'); diff --git a/lib/couchbase.js b/lib/couchbase.js index 2fcce878..c4024ecc 100644 --- a/lib/couchbase.js +++ b/lib/couchbase.js @@ -6,6 +6,8 @@ module.exports.Cluster = require('./cluster'); module.exports.SpatialQuery = require('./spatialquery'); module.exports.ViewQuery = require('./viewquery'); module.exports.N1qlQuery = require('./n1qlquery'); +module.exports.SearchQuery = require('./searchquery'); +module.exports.SearchFacet = require('./searchquery_facets'); module.exports.MutationState = require('./mutationstate'); module.exports.Mock = require('./mock/couchbase'); module.exports.Error = binding.Error; diff --git a/lib/searchquery.js b/lib/searchquery.js new file mode 100644 index 00000000..50b5a69d --- /dev/null +++ b/lib/searchquery.js @@ -0,0 +1,185 @@ +'use strict'; + +var util = require('util'); +var cbutils = require('./utils'); +var queryProtos = require('./searchquery_queries'); + +/** + * Class for building of FTS search queries. This class should never + * be constructed directly, instead you should use the {@link SearchQuery.new} + * static method to instantiate a {@link SearchQuery}. + * + * @constructor + * + * @since 2.1.7 + * @uncommitted + */ +function SearchQuery(indexName, query) { + this.data = { + indexName: indexName, + query: query + }; +} + +/** + * Enumeration for specifying FTS highlight styling. + * + * @readonly + * @enum {number} + */ +SearchQuery.HighlightStyle = { + /** + * This causes hits to be highlighted using HTML tags. + */ + HTML: 'html', + + /** + * This causes hits to be highlighted with ANSI character codes. + */ + ANSI: 'ansi' +}; + +/** + * Specifies how many results to skip from the beginning of the result set. + * + * @param {number} skip + * @returns {SearchQuery} + * + * @since 2.1.7 + * @uncommitted + */ +SearchQuery.prototype.skip = function(skip) { + this.data.from = skip; + return this; +}; + +/** + * Specifies the maximum number of results to return. + * + * @param {number} limit + * @returns {SearchQuery} + * + * @since 2.1.7 + * @uncommitted + */ +SearchQuery.prototype.limit = function(limit) { + this.data.size = limit; + return this; +}; + +/** + * Includes information about the internal search semantics used + * to execute your query. + * + * @param {boolean} explain + * @returns {SearchQuery} + * + * @since 2.1.7 + * @uncommitted + */ +SearchQuery.prototype.explain = function(explain) { + this.data.explain = explain; + return this; +}; + +/** + * Requests a particular highlight style and field list for this query. + * + * @param {SearchQuery.HighlightStyle} style + * @param {string[]} fields + * @returns {SearchQuery} + * + * @since 2.1.7 + * @uncommitted + */ +SearchQuery.prototype.highlight = function(style, fields) { + fields = cbutils.unpackArgs(fields, arguments, 1); + + if (!this.data.highlight) { + this.data.highlight = {}; + } + + this.data.highlight.style = style; + this.data.highlight.fields = fields; + return this; +}; + +/** + * Specifies the fields you wish to receive in the result set. + * + * @param {string[]} fields + * @returns {SearchQuery} + * + * @since 2.1.7 + * @uncommitted + */ +SearchQuery.prototype.fields = function(fields) { + fields = cbutils.unpackArgs(fields, arguments); + + this.data.fields = fields; + return this; +}; + +/** + * Adds a SearchFacet object to return information about as part + * of the execution of this query. + * + * @param {string} name + * @param {SeachFacet} facet + * @returns {SearchQuery} + * + * @since 2.1.7 + * @uncommitted + */ +SearchQuery.prototype.addFacet = function(name, facet) { + if (!this.data.facets) { + this.data.facets = {}; + } + this.data.facets[name] = facet; + return this; +}; + +/** + * Specifies the maximum time to wait (in millseconds) for this + * query to complete. + * + * @param {number} timeout + * @returns {SearchQuery} + * + * @since 2.1.7 + * @uncommitted + */ +SearchQuery.prototype.timeout = function(timeout) { + if (!this.data.ctl) { + this.data.ctl = {}; + } + + this.data.ctl.timeout = timeout; + return this; +}; + +SearchQuery.prototype.toJSON = function() { + return this.data; +}; + +/** + * Creates a new search query from an index name and search query definition. + * + * @param {string} indexName + * @param {SearchQuery.Query} query + * @returns {SearchQuery} + * + * @since 2.1.7 + * @uncommitted + */ +SearchQuery.new = function(indexName, query) { + return new SearchQuery(indexName, query); +}; + +for (var i in queryProtos) { + if (queryProtos.hasOwnProperty(i)) { + SearchQuery[i] = queryProtos[i]; + } +} + +module.exports = SearchQuery; diff --git a/lib/searchquery_facets.js b/lib/searchquery_facets.js new file mode 100644 index 00000000..103c83ea --- /dev/null +++ b/lib/searchquery_facets.js @@ -0,0 +1,52 @@ +'use strict'; + +function TermFacet(field, size) { + this.field = field; + this.size = size; +} +module.exports.TermFacet = TermFacet; + +module.exports.term = function(field, size) { + return new TermFacet(field, size); +}; + +function NumericFacet(field, size) { + this.field = field; + this.size = size; + this.numeric_ranges = []; +} +module.exports.NumericFacet = NumericFacet; + +module.exports.numeric = function(field, size) { + return new NumericFacet(field, size); +}; + +NumericFacet.prototype.addRange = function(name, start, end) { + this.numeric_ranges.push({ + name: name, + start: start, + end: end + }); + return this; +}; + +function DateFacet(field, size) { + this.field = field; + this.size = size; + this.date_ranges = []; +} +module.exports.DateFacet = DateFacet; + +module.exports.date = function(field, size) { + return new DateFacet(field, size); +}; + +DateFacet.prototype.addRange = function(name, start, end) { + this.date_ranges.push({ + name: name, + start: start, + end: end + }); + return this; +}; + diff --git a/lib/searchquery_queries.js b/lib/searchquery_queries.js new file mode 100644 index 00000000..e0daf0fd --- /dev/null +++ b/lib/searchquery_queries.js @@ -0,0 +1,483 @@ +'use strict'; + +var cbutils = require('./utils'); + +/** + * @constructor + * @private + * @ignore + */ +function QueryBase() { + this.data = {}; +} + +QueryBase.prototype.field = function(field) { + this.data.field = field; + return this; +}; + +QueryBase.prototype.analyzer = function(analyzer) { + this.data.analyzer = analyzer; + return this; +}; + +QueryBase.prototype.prefixLength = function(prefixLength) { + this.data.prefix_length = prefixLength; + return this; +}; + +QueryBase.prototype.fuzziness = function(fuzziness) { + this.data.fuzziness = fuzziness; + return this; +}; + +QueryBase.prototype.boost = function(boost) { + this.data.boost = boost; + return this; +}; + +QueryBase.prototype.dateTimeParser = function(dateTimeParser) { + this.data.datetime_parser = dateTimeParser; + return ths; +}; + +QueryBase.prototype.toJSON = function() { + return this.data; +}; + +var SearchQuery = {}; +module.exports = SearchQuery; + +function MatchQuery(match) { + this.data = { + match: match + }; +} +SearchQuery.MatchQuery = MatchQuery; + +/** + * match creates a Query MatchQuery matching text. + * + * @param {string} match + * @returns {MatchQuery} + */ +SearchQuery.match = function(match) { + return new MatchQuery(match); +}; + +MatchQuery.prototype.field = QueryBase.prototype.field; +MatchQuery.prototype.analyzer = QueryBase.prototype.analyzer; +MatchQuery.prototype.prefixLength = QueryBase.prototype.prefixLength; +MatchQuery.prototype.fuzziness = QueryBase.prototype.fuzziness; +MatchQuery.prototype.boost = QueryBase.prototype.boost; +MatchQuery.prototype.toJSON = QueryBase.prototype.toJSON; + + +function MatchPhraseQuery(phrase) { + this.data = { + match_phrase: phrase + }; +} +SearchQuery.MatchPhraseQuery = MatchPhraseQuery; + +/** + * matchPhase creates a new MatchPhraseQuery object for matching + * phrases in the index. + * + * @param {string} phrase + * @returns {MatchPhraseQuery} + */ +SearchQuery.matchPhrase = function(phrase) { + return new MatchPhraseQuery(phrase); +}; + +MatchPhraseQuery.prototype.field = QueryBase.prototype.field; +MatchPhraseQuery.prototype.analyzer = QueryBase.prototype.analyzer; +MatchPhraseQuery.prototype.boost = QueryBase.prototype.boost; +MatchPhraseQuery.prototype.toJSON = QueryBase.prototype.toJSON; + + +function RegexpQuery(regexp) { + this.data = { + regexp: regexp + }; +} +SearchQuery.RegexpQuery = RegexpQuery; + +/** + * regexp creates a new RegexpQuery object for matching against a + * regexp query in the index. + * + * @param {string} regexp + * @returns {RegexpQuery} + */ +SearchQuery.regexp = function(regexp) { + return new RegexpQuery(regexp); +}; + +RegexpQuery.prototype.field = QueryBase.prototype.field; +RegexpQuery.prototype.boost = QueryBase.prototype.boost; +RegexpQuery.prototype.toJSON = QueryBase.prototype.toJSON; + + +function StringQuery(query) { + this.data = { + query: query + }; +} +SearchQuery.StringQuery = StringQuery; + +/** + * string creates a StringQuery for matching strings. + * + * @param {string} query + * @returns {StringQuery} + */ +SearchQuery.string = function(query) { + return new StringQuery(query); +}; + +StringQuery.prototype.boost = QueryBase.prototype.boost; +StringQuery.prototype.toJSON = QueryBase.prototype.toJSON; + + +function NumericRangeQuery() { + this.data = {}; +} +SearchQuery.NumericRangeQuery = NumericRangeQuery; + +/** + * numericRange creates a NumericRangeQuery for matching numeric + * ranges in an index. + * + * @returns {NumericRangeQuery} + */ +SearchQuery.numericRange = function() { + return new NumericRangeQuery(); +}; + +NumericRangeQuery.prototype.boost = QueryBase.prototype.boost; +NumericRangeQuery.prototype.toJSON = QueryBase.prototype.toJSON; + + +function DateRangeQuery() { + this.data = {}; +} +SearchQuery.DateRangeQuery = DateRangeQuery; + +/** + * dateRange creates a DateRangeQuery for matching date ranges in an index. + * + * @returns {DateRangeQuery} + */ +SearchQuery.dateRange = function() { + return new DateRangeQuery(); +}; + +DateRangeQuery.prototype.dateTimeParser = QueryBase.prototype.dateTimeParser; +DateRangeQuery.prototype.boost = QueryBase.prototype.boost; +DateRangeQuery.prototype.toJSON = QueryBase.prototype.toJSON; + + +function ConjunctionQuery(queries) { + queries = cbutils.unpackArgs(queries, arguments); + this.data = { + conjuncts: [] + }; + this.and(queries); +} +SearchQuery.ConjunctionQuery = ConjunctionQuery; + +/** + * conjuncts creates a ConjunctionQuery for matching all of a list of + * subqueries in an index. + * + * @param {SearchQuery.Query[]} queries + * @returns {ConjunctionQuery} + */ +SearchQuery.conjuncts = function(queries) { + queries = cbutils.unpackArgs(queries, arguments); + return new ConjunctionQuery(queries); +}; + +ConjunctionQuery.prototype.and = function(queries) { + queries = cbutils.unpackArgs(queries, arguments); + for (var i = 0; i < queries.length; ++i) { + this.data.conjuncts.push(queries[i]); + } +}; +ConjunctionQuery.prototype.boost = QueryBase.prototype.boost; +ConjunctionQuery.prototype.toJSON = QueryBase.prototype.toJSON; + + +function DisjunctionQuery(queries) { + queries = cbutils.unpackArgs(queries); + this.data = { + disjuncts: [] + }; + this.or(queries); +} +SearchQuery.DisjunctionQuery = DisjunctionQuery; + +/** + * disjuncts creates a DisjunctionQuery for matching any of a list of + * subqueries in an index. + * + * @param {SearchQuery.Query[]} queries + * @returns {ConjunctionQuery} + */ +SearchQuery.disjuncts = function(queries) { + queries = cbutils.unpackArgs(queries, arguments); + return new DisjunctionQuery(queries); +}; + +DisjunctionQuery.prototype.or = function(queries) { + queries = cbutils.unpackArgs(queries, arguments); + for (var i = 0; i < queries.length; ++i) { + this.data.disjuncts.push(queries[i]); + } +}; +DisjunctionQuery.prototype.boost = QueryBase.prototype.boost; +DisjunctionQuery.prototype.toJSON = QueryBase.prototype.toJSON; + + +function BooleanQuery() { + this.data = {}; + this.shouldMin = undefined; +} +SearchQuery.BooleanQuery = BooleanQuery; + +/** + * boolean creates a compound BooleanQuery composed of several + * other Query objects. + * + * @returns {BooleanQuery} + */ +SearchQuery.boolean = function() { + return new BooleanQuery(); +}; + +BooleanQuery.prototype.must = function(query) { + if (!(query instanceof ConjunctionQuery)) { + query = new ConjunctionQuery([query]); + } + this.data.must = query; + return this; +}; +BooleanQuery.prototype.should = function(query) { + if (!(query instanceof DisjunctionQuery)) { + query = new DisjunctionQuery([query]); + } + this.data.should = query; + return this; +}; +BooleanQuery.prototype.mustNot = function(query) { + if (!(query instanceof DisjunctionQuery)) { + query = new DisjunctionQuery([query]); + } + this.data.must_not = query; + return this; +}; +BooleanQuery.prototype.shouldMin = function(shouldMin) { + this.shouldMin = shouldMin; + return this; +}; +BooleanQuery.prototype.boost = QueryBase.prototype.boost; + +BooleanQuery.prototype.toJSON = function() { + var out = {}; + if (this.data.must) { + out.must = this.data.must; + } + if (this.data.should) { + var shouldData = this.data.should.toJSON(); + shouldData.min = this.shouldMin; + out.should = shouldData; + out.should = {}; + } + if (this.data.must_not) { + out.must_not = this.data.must_not; + } + return out; +}; + + +function WildcardQuery(wildcard) { + this.data = { + wildcard: wildcard + }; +} +SearchQuery.WildcardQuery = WildcardQuery; + +/** + * wildcard creates a WildcardQuery which allows you to match a + * string with wildcards in an index. + * + * @param {string} wildcard + * @returns {WildcardQuery} + */ +SearchQuery.wildcard = function(wildcard) { + return new WildcardQuery(wildcard); +}; + +WildcardQuery.prototype.field = QueryBase.prototype.field; +WildcardQuery.prototype.boost = QueryBase.prototype.boost; +WildcardQuery.prototype.toJSON = QueryBase.prototype.toJSON; + + +function DocIdQuery(ids) { + ids = cbutils.unpackArgs(ids, arguments); + this.data = { + ids: [] + }; + this.addDocIds(ids); +} +SearchQuery.DocIdQuery = DocIdQuery; + +/** + * docIds creates a DocIdQuery which allows you to match a list of + * document ids in an index. + * + * @param {string[]} ids + * @returns {DocIdQuery} + */ +SearchQuery.docIds = function(ids) { + ids = cbutils.unpackArgs(ids, arguments); + return new DocIdQuery(ids); +}; + +DocIdQuery.prototype.addDocIds = function(ids) { + ids = cbutils.unpackArgs(ids, arguments); + for (var i = 0; i < ids.length; ++i) { + this.data.ids.push(ids); + } +}; +DocIdQuery.prototype.field = QueryBase.prototype.field; +DocIdQuery.prototype.boost = QueryBase.prototype.boost; +DocIdQuery.prototype.toJSON = QueryBase.prototype.toJSON; + + +function BooleanFieldQuery(val) { + this.data = { + bool: val + }; +} +SearchQuery.BooleanFieldQuery = BooleanFieldQuery; + +/** + * booleanField creates a BooleanFieldQuery for searching boolean fields + * in an index. + * + * @param {boolean} val + * @returns {BooleanFieldQuery} + */ +SearchQuery.booleanField = function(val) { + return new BooleanFieldQuery(val); +}; + +DocIdQuery.prototype.field = QueryBase.prototype.field; +DocIdQuery.prototype.boost = QueryBase.prototype.boost; +DocIdQuery.prototype.toJSON = QueryBase.prototype.toJSON; + + +function TermQuery(term) { + this.data = { + term: term + }; +} +SearchQuery.TermQuery = TermQuery; + +/** + * term creates a TermQuery for searching terms in an index. + * + * @param {string} term + * @returns {TermQuery} + */ +SearchQuery.term = function(term) { + return new TermQuery(term); +}; + +TermQuery.prototype.field = QueryBase.prototype.field; +TermQuery.prototype.prefixLength = QueryBase.prototype.prefixLength; +TermQuery.prototype.fuzziness = QueryBase.prototype.fuzziness; +TermQuery.prototype.boost = QueryBase.prototype.boost; +TermQuery.prototype.toJSON = QueryBase.prototype.toJSON; + + +function PhraseQuery(terms) { + + this.data = { + terms: terms + }; +} +SearchQuery.PhraseQuery = PhraseQuery; + +/** + * phrase creates a new PhraseQuery for searching a phrase in an index. + * + * @param {string[]} terms + * @returns {PhraseQuery} + */ +SearchQuery.phrase = function(terms) { + return new PhraseQuery(terms); +}; + +PhraseQuery.prototype.field = QueryBase.prototype.field; +PhraseQuery.prototype.boost = QueryBase.prototype.boost; +PhraseQuery.prototype.toJSON = QueryBase.prototype.toJSON; + + +function PrefixQuery(prefix) { + this.data = { + prefix: prefix + }; +} +SearchQuery.PrefixQuery = PrefixQuery; + +/** + * prefix creates a new MatchQuery for searching for a prefix in an index. + * + * @param {string} prefix + * @returns {PrefixQuery} + */ +SearchQuery.prefix = function(prefix) { + return new PrefixQuery(prefix); +}; + +PrefixQuery.prototype.field = QueryBase.prototype.field; +PrefixQuery.prototype.boost = QueryBase.prototype.boost; +PrefixQuery.prototype.toJSON = QueryBase.prototype.toJSON; + + +function MatchAllQuery() { + this.data = {}; +} +SearchQuery.MatchAllQuery = MatchAllQuery; + +/** + * matchAll creates a MatchAllQuery which matches anything. + * + * @returns {MatchAllQuery} + */ +SearchQuery.matchAll = function() { + return new MatchAllQuery(); +}; + +MatchAllQuery.prototype.toJSON = QueryBase.prototype.toJSON; + + +function MatchNoneQuery() { + this.data = {}; +} +SearchQuery.MatchNoneQuery = MatchNoneQuery; + +/** + * matchNone creates a MatchNoneQuery which matches nothing. + * + * @returns {MatchNoneQuery} + */ +SearchQuery.matchNone = function() { + return new MatchNoneQuery(); +}; + +MatchNoneQuery.prototype.toJSON = QueryBase.prototype.toJSON; diff --git a/lib/utils.js b/lib/utils.js new file mode 100644 index 00000000..d76d4f9d --- /dev/null +++ b/lib/utils.js @@ -0,0 +1,15 @@ +'use strict'; + +function unpackArgs(first, args, first_index) { + if (!first_index) { + first_index = 0; + } + if (!Array.isArray(first)) { + first = [first]; + for (var i = first_index + 1; i < args.length; ++i) { + first.push(args[i]); + } + } + return first; +} +module.exports.unpackArgs = unpackArgs; diff --git a/src/binding.cc b/src/binding.cc index 457517dc..24641666 100644 --- a/src/binding.cc +++ b/src/binding.cc @@ -84,6 +84,7 @@ NAN_MODULE_INIT(CouchbaseImpl::Init) Nan::SetPrototypeMethod(t, "durability", fnDurability); Nan::SetPrototypeMethod(t, "viewQuery", fnViewQuery); Nan::SetPrototypeMethod(t, "n1qlQuery", fnN1qlQuery); + Nan::SetPrototypeMethod(t, "ftsQuery", fnFtsQuery); Nan::SetPrototypeMethod(t, "lookupIn", fnLookupIn); Nan::SetPrototypeMethod(t, "mutateIn", fnMutateIn); diff --git a/src/couchbase_impl.cc b/src/couchbase_impl.cc index 0d134831..aef9dfc5 100644 --- a/src/couchbase_impl.cc +++ b/src/couchbase_impl.cc @@ -455,6 +455,53 @@ void n1qlrow_callback(lcb_t instance, int ignoreme, callback->Call(2, args); } +void ftsrow_callback(lcb_t instance, int ignoreme, + const lcb_RESPFTS *resp) +{ + Nan::Callback *callback = (Nan::Callback*)resp->cookie; + Nan::HandleScope scope; + + Local jsonParseLcl = Nan::New(CouchbaseImpl::jsonParse); + + if (resp->rflags & LCB_RESP_F_FINAL) { + Local dataRes; + if (resp->rc != LCB_SUCCESS) { + if (resp->row) { + dataRes = Nan::New((const char*)resp->row, (int)resp->nrow).ToLocalChecked(); + } else { + dataRes = Nan::Null(); + } + } else { + Handle metaStr = + Nan::New((const char*)resp->row, (int)resp->nrow).ToLocalChecked(); + dataRes = jsonParseLcl->Call(Nan::GetCurrentContext()->Global(), 1, &metaStr); + Local metaObj = dataRes->ToObject(); + if (!metaObj.IsEmpty()) { + metaObj->Delete(Nan::New(CouchbaseImpl::resultsKey)); + } + } + + Local args[] = { + Nan::New(resp->rc), + dataRes + }; + callback->Call(2, args); + + delete callback; + return; + } + + Handle rowStr = + Nan::New((const char*)resp->row, (int)resp->nrow).ToLocalChecked(); + Local rowObj = + jsonParseLcl->Call(Nan::GetCurrentContext()->Global(), 1, &rowStr); + Local args[] = { + Nan::New(-1), + rowObj + }; + callback->Call(2, args); +} + static void subdoc_callback(lcb_t instance, int cbtype, const lcb_RESPBASE *respbase) { diff --git a/src/couchbase_impl.h b/src/couchbase_impl.h index 2055a6e0..9c410b1e 100644 --- a/src/couchbase_impl.h +++ b/src/couchbase_impl.h @@ -54,6 +54,7 @@ #include #include #include +#include #include "cas.h" #include "token.h" @@ -124,6 +125,7 @@ class CouchbaseImpl: public Nan::ObjectWrap static NAN_METHOD(fnDurability); static NAN_METHOD(fnViewQuery); static NAN_METHOD(fnN1qlQuery); + static NAN_METHOD(fnFtsQuery); static NAN_METHOD(fnLookupIn); static NAN_METHOD(fnMutateIn); @@ -178,6 +180,8 @@ void viewrow_callback(lcb_t instance, int ignoreme, const lcb_RESPVIEWQUERY *resp); void n1qlrow_callback(lcb_t instance, int ignoreme, const lcb_RESPN1QL *resp); +void ftsrow_callback(lcb_t instance, int ignoreme, + const lcb_RESPFTS *resp); } #endif diff --git a/src/operations.cc b/src/operations.cc index edfa8f05..3ea64e36 100644 --- a/src/operations.cc +++ b/src/operations.cc @@ -418,6 +418,38 @@ NAN_METHOD(CouchbaseImpl::fnN1qlQuery) { return info.GetReturnValue().Set(true); } +NAN_METHOD(CouchbaseImpl::fnFtsQuery) { + CouchbaseImpl *me = ObjectWrap::Unwrap(info.This()); + lcb_CMDFTS cmd; + void *cookie; + Nan::HandleScope scope; + CommandEncoder enc; + + Local jsonStringifyLcl = Nan::New(CouchbaseImpl::jsonStringify); + + memset(&cmd, 0, sizeof(cmd)); + cmd.callback = ftsrow_callback; + + Handle optsinfo[] = { info[0] }; + Local optsVal = + jsonStringifyLcl->Call(Nan::GetCurrentContext()->Global(), 1, optsinfo); + Local optsStr = optsVal.As(); + if (!enc.parseString(&cmd.query, &cmd.nquery, optsStr)) { + return Nan::ThrowError(Error::create("bad opts passed")); + } + + if (!enc.parseCookie(&cookie, info[1])) { + return Nan::ThrowError(Error::create("bad callback passed")); + } + + lcb_error_t err = lcb_fts_query(me->getLcbHandle(), cookie, &cmd); + if (err) { + return Nan::ThrowError(Error::create(err)); + } + + return info.GetReturnValue().Set(true); +} + NAN_METHOD(CouchbaseImpl::fnLookupIn) { CouchbaseImpl *me = ObjectWrap::Unwrap(info.This()); lcb_CMDSUBDOC cmd;