diff --git a/README.md b/README.md
index 220e8f49e78c..eedc84f7e4f6 100644
--- a/README.md
+++ b/README.md
@@ -14,6 +14,7 @@ This client supports the following Google Cloud Platform services:
* [Google Cloud Datastore](#google-cloud-datastore)
* [Google Cloud Storage](#google-cloud-storage)
* [Google Cloud Pub/Sub](#google-cloud-pubsub-beta) (Beta)
+* [Google Cloud Search](#google-cloud-search) (Alpha)
If you need support for other Google APIs, check out the [Google Node.js API Client library][googleapis].
@@ -238,6 +239,48 @@ topic.subscribe('new-subscription', function(err, subscription) {
});
```
+## Google Cloud Search (Alpha)
+> This is an *Alpha* release of Google Cloud Search. This feature is not covered by any SLA or deprecation policy and may be subject to backward-incompatible changes.
+
+[Google Cloud Search][cloud-search] ([docs][cloud-search-docs]) allows you to quickly perform full-text and geospatial searches against your data without having to spin up your own instances and without the hassle of managing and maintaining a search service.
+
+See the [gcloud-node Search API documentation][gcloud-search-docs] to learn how to store and query your indexes and documents using this library.
+
+```js
+var gcloud = require('gcloud');
+
+// Authorizing on a per-API-basis. You don't need to do this if you auth on a
+// global basis (see Authorization section above).
+
+var search = gcloud.search({
+ keyFilename: '/path/to/keyfile.json',
+ projectId: 'my-project'
+});
+
+// Create a document in a new index.
+var index = search.index('memberData');
+
+var document = index.document('member-id-34211');
+document.addField('preferredContactForm').addTextValue('phone');
+
+index.createDocument(document, function(err, document) {
+ console.log(err || document);
+});
+
+// Search an index and get the results as a readable object stream.
+var index = search.index('memberData');
+
+index.search('preferredContactForm:phone')
+ .on('error', console.error)
+ .on('data', function(document) {
+ // document.id = 'member-id-34211';
+ })
+ .on('end', function() {
+ // All results consumed.
+ });
+```
+
+
## Contributing
Contributions to this library are always welcome and highly encouraged.
@@ -253,6 +296,7 @@ Apache 2.0 - See [COPYING](COPYING) for more information.
[gcloud-bigquery-docs]: https://googlecloudplatform.github.io/gcloud-node/#/docs/bigquery
[gcloud-datastore-docs]: https://googlecloudplatform.github.io/gcloud-node/#/docs/datastore
[gcloud-pubsub-docs]: https://googlecloudplatform.github.io/gcloud-node/#/docs/pubsub
+[gcloud-search-docs]: https://googlecloudplatform.github.io/gcloud-node/#/docs/search
[gcloud-storage-docs]: https://googlecloudplatform.github.io/gcloud-node/#/docs/storage
[gcloud-todos]: https://github.com/GoogleCloudPlatform/gcloud-node-todos
[gitnpm]: https://github.com/stephenplusplus/gitnpm
@@ -273,8 +317,12 @@ Apache 2.0 - See [COPYING](COPYING) for more information.
[cloud-pubsub]: https://cloud.google.com/pubsub/
[cloud-pubsub-docs]: https://cloud.google.com/pubsub/docs
+[cloud-search]: https://cloud.google.com/search/
+[cloud-search-docs]: https://cloud.google.com/search/
+
[cloud-storage]: https://cloud.google.com/storage/
[cloud-storage-docs]: https://cloud.google.com/storage/docs/overview
[cloud-storage-create-bucket]: https://cloud.google.com/storage/docs/cloud-console#_creatingbuckets
+
[hya-wave]: https://wav.hya.io
[hya-io]: https://hya.io
diff --git a/docs/json/master/search/.gitkeep b/docs/json/master/search/.gitkeep
new file mode 100644
index 000000000000..e69de29bb2d1
diff --git a/docs/site/components/docs/docs-values.js b/docs/site/components/docs/docs-values.js
index 4767cc9c66f6..2b30f1da72ea 100644
--- a/docs/site/components/docs/docs-values.js
+++ b/docs/site/components/docs/docs-values.js
@@ -88,6 +88,25 @@ angular.module('gcloud.docs')
]
},
+ search: {
+ title: 'Search',
+ _url: '{baseUrl}/search',
+ pages: [
+ {
+ title: 'Index',
+ url: '/index'
+ },
+ {
+ title: 'Document',
+ url: '/document'
+ },
+ {
+ title: 'Field',
+ url: '/field'
+ }
+ ]
+ },
+
storage: {
title: 'Storage',
_url: '{baseUrl}/storage'
@@ -135,6 +154,10 @@ angular.module('gcloud.docs')
// introduce new storage api.
'>=0.9.0': ['storageWithFiles'],
- '>=0.10.0': ['bigquery']
+ // introduce bigquery api.
+ '>=0.10.0': ['bigquery'],
+
+ // introduce search api.
+ '>=0.16.0': ['search']
}
});
diff --git a/docs/site/components/docs/docs.html b/docs/site/components/docs/docs.html
index def79b9928e0..0b5d4c650ce3 100644
--- a/docs/site/components/docs/docs.html
+++ b/docs/site/components/docs/docs.html
@@ -44,7 +44,7 @@
-
diff --git a/docs/site/components/docs/docs.js b/docs/site/components/docs/docs.js
index b78f3c3d02fd..c166986a09d5 100644
--- a/docs/site/components/docs/docs.js
+++ b/docs/site/components/docs/docs.js
@@ -244,6 +244,9 @@ angular
path.push('index.json');
} else if (module && cl) {
path.push(module);
+ if (cl === 'index') {
+ cl = 'index-class';
+ }
path.push(cl + '.json');
}
return $http.get(path.join('/'))
diff --git a/docs/site/components/docs/search-overview.html b/docs/site/components/docs/search-overview.html
new file mode 100644
index 000000000000..8747fc62ffab
--- /dev/null
+++ b/docs/site/components/docs/search-overview.html
@@ -0,0 +1,7 @@
+Search Overview
+
+ The object returned from gcloud.search
gives you complete access to store and search your indexes and documents.
+
+
+ To learn more about Search, see What is Google Cloud Search?
+
diff --git a/lib/common/StreamRouter.js b/lib/common/StreamRouter.js
new file mode 100644
index 000000000000..a79499413578
--- /dev/null
+++ b/lib/common/StreamRouter.js
@@ -0,0 +1,82 @@
+/*!
+ * Copyright 2015 Google Inc. All Rights Reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+/*!
+ * @module common/StreamRouter
+ */
+
+'use strict';
+
+var streamEvents = require('stream-events');
+var through = require('through2');
+
+/**
+ * @type {module:common/util}
+ * @private
+ */
+var util = require('../common/util.js');
+
+var StreamRouter = {};
+
+StreamRouter.extend = function(Class, methodNames) {
+ methodNames = util.arrayize(methodNames);
+
+ methodNames.forEach(function(methodName) {
+ var originalMethod = Class.prototype[methodName];
+
+ Class.prototype[methodName] = function() {
+ return StreamRouter.router_(arguments, originalMethod.bind(this));
+ };
+ });
+};
+
+StreamRouter.router_ = function(args, originalMethod) {
+ args = util.toArray(args);
+ var callback = args[args.length - 1];
+ var isStreamMode = !util.is(callback, 'function');
+
+ if (isStreamMode) {
+ var stream = streamEvents(through.obj());
+
+ var onResultSet = function(err, results, nextQuery) {
+ if (err) {
+ stream.emit('error', err);
+ stream.end();
+ return;
+ }
+
+ results.forEach(function(result) {
+ stream.push(result);
+ });
+
+ if (nextQuery) {
+ originalMethod(nextQuery, onResultSet);
+ } else {
+ stream.end();
+ }
+ };
+
+ stream.once('reading', function() {
+ originalMethod.apply(null, args.concat(onResultSet));
+ });
+
+ return stream;
+ } else {
+ originalMethod.apply(null, args);
+ }
+};
+
+module.exports = StreamRouter;
diff --git a/lib/common/util.js b/lib/common/util.js
index 5a1d4a7030f5..dbb41e657ef2 100644
--- a/lib/common/util.js
+++ b/lib/common/util.js
@@ -23,12 +23,12 @@
var extend = require('extend');
var GoogleAuth = require('google-auth-library');
+var nodeutil = require('util');
var request = require('request').defaults({
pool: {
maxSockets: Infinity
}
});
-var nodeutil = require('util');
var uuid = require('node-uuid');
/** @const {object} gcloud-node's package.json file. */
diff --git a/lib/index.js b/lib/index.js
index ac242add2a9b..11408bb0e323 100644
--- a/lib/index.js
+++ b/lib/index.js
@@ -38,6 +38,12 @@ var Datastore = require('./datastore');
*/
var PubSub = require('./pubsub');
+/**
+ * @type {module:search}
+ * @private
+ */
+var Search = require('./search');
+
/**
* @type {module:storage}
* @private
@@ -120,6 +126,10 @@ function gcloud(config) {
options = options || {};
return new PubSub(util.extendGlobalConfig(config, options));
},
+ search: function(options) {
+ options = options || {};
+ return new Search(util.extendGlobalConfig(config, options));
+ },
storage: function(options) {
options = options || {};
return new Storage(util.extendGlobalConfig(config, options));
@@ -173,11 +183,9 @@ gcloud.datastore = Datastore;
* reliable, many-to-many, asynchronous messaging service from Google Cloud
* Platform.
*
- * Note: Google Cloud Pub/Sub API is available as a Limited Preview and the
- * client library we provide is currently experimental. The API and/or the
- * client might be changed in backward-incompatible ways. This API is not
- * subject to any SLA or deprecation policy. Request to be whitelisted to use it
- * by filling the [Limited Preview application form](http://goo.gl/sO0wTu).
+ * Note: This is a *Beta* release of Google Cloud Pub/Sub. This feature is not
+ * covered by any SLA or deprecation policy and may be subject to backward-
+ * incompatible changes.
*
* @type {module:pubsub}
*
@@ -194,6 +202,33 @@ gcloud.pubsub = function(config) {
return new PubSub(config);
};
+/**
+ * **Experimental**
+ *
+ * [Google Cloud Search](https://cloud.google.com/search/) allows you to quickly
+ * perform full-text and geospatial searches against your data without having to
+ * spin up your own instances and without the hassle of managing and maintaining
+ * a search service.
+ *
+ * Note: This is an *Alpha* release of Google Cloud Search. This feature is not
+ * covered by any SLA or deprecation policy and may be subject to backward-
+ * incompatible changes.
+ *
+ * @type {module:search}
+ *
+ * @return {module:search}
+ *
+ * @example
+ * var gcloud = require('gcloud');
+ * var seach = gcloud.search({
+ * projectId: 'project-id',
+ * keyFilename: '/path/to/keyfile.json'
+ * });
+ */
+gcloud.search = function (config) {
+ return new Search(config);
+};
+
/**
* Google Cloud Storage allows you to store data on Google infrastructure.
* Read [Google Cloud Storage API docs](https://developers.google.com/storage/)
diff --git a/lib/search/document.js b/lib/search/document.js
new file mode 100644
index 000000000000..e714c16d9c01
--- /dev/null
+++ b/lib/search/document.js
@@ -0,0 +1,213 @@
+/*!
+ * Copyright 2015 Google Inc. All Rights Reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+/*!
+ * @module search/document
+ */
+
+'use strict';
+
+/**
+ * @type {module:search/field}
+ * @private
+ */
+var Field = require('./field.js');
+
+/**
+ * @type {module:common/util}
+ * @private
+ */
+var util = require('../common/util.js');
+
+/**
+ * Create a Document object to create or manipulate a document from your index.
+ *
+ * @constructor
+ * @alias module:search/document
+ *
+ * @param {string=} id - ID of the document.
+ *
+ * @example
+ * var gcloud = require('gcloud');
+ *
+ * var search = gcloud.search({
+ * projectId: 'grape-spaceship-123'
+ * });
+ *
+ * var document = search.index('records').document('stephen');
+ */
+function Document(index, id) {
+ this.search_ = index.search_;
+ this.index_ = index;
+
+ this.id = id;
+ this.fields = {};
+}
+
+/**
+ * Return just the document detail properties of this Document instance.
+ *
+ * @example
+ * document.toJSON();
+ * // {
+ * // docId: 'this-document-id',
+ * // fields: {
+ * // // ...
+ * // },
+ * // rank: 8
+ * // }
+ */
+Document.prototype.toJSON = function() {
+ var documentObject = {
+ fields: this.fields
+ };
+
+ if (util.is(this.id, 'string')) {
+ documentObject.docId = this.id;
+ }
+
+ if (util.is(this.rank, 'number')) {
+ documentObject.rank = this.rank;
+ }
+
+ return documentObject;
+};
+
+/**
+ * Add a field to this document.
+ *
+ * @throws {error} if a name is not provided.
+ *
+ * @param {string} name - This field's name.
+ * @return {module:search/field}
+ *
+ * @example
+ * var scoreField = document.addField('score');
+ * // scoreField is a Field object.
+ */
+Document.prototype.addField = function(name) {
+ if (!util.is(name, 'string')) {
+ throw new Error('`name` is required to add a field to this document.');
+ }
+
+ this.fields[name] = new Field();
+
+ return this.fields[name];
+};
+
+/**
+ * Delete this document.
+ *
+ * @param {function=} callback - The callback function.
+ *
+ * @example
+ * document.delete(function(err, apiResponse) {});
+ */
+Document.prototype.delete = function(callback) {
+ this.makeReq_('DELETE', '', null, null, function(err, apiResponse) {
+ (callback || util.noop)(err, apiResponse);
+ });
+};
+
+/**
+ * Get the details of this document. After running, the Document instance will
+ * update the `fields` and `rank` properties to their most recent values at the
+ * time of this call.
+ *
+ * @param {function} callback - The callback function.
+ *
+ * @example
+ * document.getMetadata(function(err, doc, apiResponse) {
+ * if (err) {
+ * console.error(err);
+ * return;
+ * }
+ *
+ * // `doc` is a reference to `document`, both of which are now up to date.
+ * //
+ * // document.fields = Array of Field objects.
+ * // document.rank = Document's numeric rank.
+ * });
+ */
+Document.prototype.getMetadata = function(callback) {
+ var self = this;
+
+ callback = callback || util.noop;
+
+ this.makeReq_('GET', '/', null, null, function(err, resp) {
+ if (err) {
+ callback(err, null, resp);
+ return;
+ }
+
+ self.fields = {};
+ self.rank = null;
+
+ if (util.is(resp.fields, 'object')) {
+ Object.keys(resp.fields).forEach(function(fieldName) {
+ var fieldInstance = self.addField(fieldName);
+ fieldInstance.values = resp.fields[fieldName].values;
+ });
+ }
+
+ if (util.is(resp.rank, 'number')) {
+ self.rank = resp.rank;
+ }
+
+ callback(null, self, resp);
+ });
+};
+
+/**
+ * Set the rank for this document. The rank of a document is a positive integer
+ * which determines the default ordering of documents returned from a search. By
+ * default, the rank is set at the time the document is created to the number of
+ * seconds since January 1, 2011.
+ *
+ * @throws {error} If a rank is not a number.
+ *
+ * @param {number} rank - The rank of this document.
+ *
+ * @example
+ * document.setRank(8);
+ */
+Document.prototype.setRank = function(rank) {
+ if (!util.is(rank, 'number') || rank < 0) {
+ throw new Error('`rank` should be a positive integer.');
+ }
+
+ this.rank = rank;
+};
+
+/**
+ * Make a new request object from the provided arguments and wrap the callback
+ * to intercept non-successful responses.
+ *
+ * @private
+ *
+ * @param {string} method - Action.
+ * @param {string} path - Request path.
+ * @param {*} query - Request query object.
+ * @param {*} body - Request body contents.
+ * @param {function} callback - The callback function.
+ */
+Document.prototype.makeReq_ = function(method, path, query, body, callback) {
+ path = '/documents/' + this.id + path;
+
+ this.index_.makeReq_(method, path, query, body, callback);
+};
+
+module.exports = Document;
diff --git a/lib/search/field.js b/lib/search/field.js
new file mode 100644
index 000000000000..b3b98c856fc3
--- /dev/null
+++ b/lib/search/field.js
@@ -0,0 +1,169 @@
+/*!
+ * Copyright 2015 Google Inc. All Rights Reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+/*!
+ * @module search/field
+ */
+
+'use strict';
+
+/**
+ * @type {module:common/util}
+ * @private
+ */
+var util = require('../common/util.js');
+
+/**
+ * Create a Field object to easily format a Cloud Search index's field.
+ *
+ * @constructor
+ * @alias module:search/field
+ *
+ * @example
+ * var gcloud = require('gcloud');
+ *
+ * var search = gcloud.search({
+ * projectId: 'grape-spaceship-123'
+ * });
+ *
+ * var document = search.index('records').document('stephen');
+ * var field = document.addField('alias');
+ */
+function Field() {
+ this.values = [];
+}
+
+/**
+ * An atom value is a string value that is treated as a single token. A query
+ * will not match if it includes only a substring rather than the full field
+ * value.
+ *
+ * @param {string} atom - String value.
+ *
+ * @example
+ * field.addAtomValue('ryanseys');
+ */
+Field.prototype.addAtomValue = function(atom) {
+ this.addTextValue(atom, { format: 'ATOM' });
+};
+
+/**
+ * A geo value is a point on earth with latitude and longitude coordinates.
+ *
+ * @param {string|object} geo - Coordinate value as a string or object. String
+ * format: `'40.6894, -74.0447'`.
+ * @param {number} geo.latitude - Latitudinal value.
+ * @param {number} geo.longitude - Longitudinal value.
+ *
+ * @example
+ * var coordinates = '40.6894, -74.0447';
+ * field.addGeoValue(coordinates);
+ *
+ * //-
+ * // Use an object.
+ * //-
+ * var coordinates = {
+ * latitude: 40.6894,
+ * longitude: -74.0447
+ * };
+ *
+ * field.addGeoValue(coordinates);
+ */
+Field.prototype.addGeoValue = function(geo) {
+ if (util.is(geo, 'object')) {
+ geo = util.format('{latitude}, {longitude}', geo);
+ }
+
+ this.values.push({
+ geoValue: geo
+ });
+};
+
+/**
+ * An HTML value is an HTML-formatted string. Text out of markup tags are
+ * tokenized and markup tags are considered metadata.
+ *
+ * @param {string} html - HTML value.
+ *
+ * @example
+ * var html = 'hello, world';
+ * field.addHtmlValue(html);
+ */
+Field.prototype.addHtmlValue = function(html) {
+ this.addTextValue(html, { format: 'HTML' });
+};
+
+/**
+ * A number value is a double precision floating point value.
+ *
+ * @param {number} number - Number value.
+ *
+ * @example
+ * field.addNumberValue(8);
+ */
+Field.prototype.addNumberValue = function(number) {
+ this.values.push({
+ numberValue: number
+ });
+};
+
+/**
+ * A text value is a string that is tokenized as plain text.
+ *
+ * @param {string} text - Text value.
+ * @param {object=} options - Configuration object.
+ * @param {string} options.format - One of `ATOM`, `HTML`, or `TEXT`. (Default:
+ * `TEXT`).
+ *
+ * @example
+ * field.addTextValue('Hello!');
+ */
+Field.prototype.addTextValue = function(text, options) {
+ options = options || {};
+ var format = options.format || 'TEXT';
+
+ this.values.push({
+ stringValue: text.toLowerCase(),
+ stringFormat: format.toUpperCase()
+ });
+};
+
+/**
+ * A timestamp value is a date-time value with millisecond precision.
+ *
+ * @param {string|date} timestamp - Timestamp value.
+ *
+ * @example
+ * var timestamp = '2014-08-18T21:19:55.000Z';
+ * field.addTimestampValue(timestamp);
+ *
+ * //-
+ * // Use a Date object.
+ * //-
+ * var now = Date.now();
+ * field.addTimestampValue(now);
+ */
+Field.prototype.addTimestampValue = function(timestamp) {
+ if (timestamp instanceof Date) {
+ timestamp = timestamp.toJSON();
+ }
+
+ this.values.push({
+ timestampValue: timestamp
+ });
+};
+
+module.exports = Field;
diff --git a/lib/search/index-class.js b/lib/search/index-class.js
new file mode 100644
index 000000000000..e7e617fb8756
--- /dev/null
+++ b/lib/search/index-class.js
@@ -0,0 +1,381 @@
+/*!
+ * Copyright 2015 Google Inc. All Rights Reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+/*!
+ * @module search/index
+ */
+
+'use strict';
+
+var extend = require('extend');
+
+/**
+ * @type {module:search/document}
+ * @private
+ */
+var Document = require('./document.js');
+
+/**
+ * @type {module:common/StreamRouter}
+ * @private
+ */
+var StreamRouter = require('../common/StreamRouter.js');
+
+/**
+ * @type {module:common/util}
+ * @private
+ */
+var util = require('../common/util.js');
+
+/**
+ * Create an Index object to interact with a Google Cloud Search index.
+ *
+ * @constructor
+ * @alias module:search/index
+ *
+ * @param {string} id - ID of the index.
+ *
+ * @example
+ * var gcloud = require('gcloud');
+ *
+ * var search = gcloud.search({
+ * projectId: 'grape-spaceship-123'
+ * });
+ *
+ * var index = search.index('records');
+ */
+function Index(search, id) {
+ this.search_ = search;
+ this.id = id;
+
+ if (!this.id) {
+ throw new Error('An ID is needed to access a Google Cloud Search index');
+ }
+}
+
+/**
+ * Create a document in the index.
+ *
+ * @param {object|module:search/document} documentObj - A properly-formed
+ * Document object as outlined in the
+ * [official docs](https://goo.gl/AYhSgI).
+ * @param {function} callback - The callback function.
+ *
+ * @example
+ * //-
+ * // A document can be built using the {module:search/document} object.
+ * //-
+ * var newDocument = index.document('new-document-id');
+ * newDocument.addField('person').addTextValue('Stephen');
+ *
+ * index.createDocument(newDocument, function(err, document, apiResponse) {});
+ *
+ * //-
+ * // A document can also be created from a properly-formed object as outlined
+ * // in the official docs.
+ * //
+ * // This will create the same resulting document object as the example above.
+ * //-
+ * var newDocument = {
+ * docId: 'new-document-id',
+ * fields: {
+ * person: {
+ * values: [
+ * {
+ * stringFormat: 'TEXT',
+ * stringValue: 'Stephen'
+ * }
+ * ]
+ * }
+ * }
+ * };
+ *
+ * index.createDocument(newDocument, function(err, document, apiResponse) {});
+ *
+ * //-
+ * // Specifying an ID for your new document isn't required. In both of the
+ * // scenarios above, simply don't specify 'new-document-id' and one will be
+ * // generated for you.
+ * //-
+ */
+Index.prototype.createDocument = function(documentObj, callback) {
+ var document;
+
+ if (documentObj instanceof Document) {
+ document = documentObj;
+ documentObj = document.toJSON();
+ } else {
+ document = this.documentFromObject_(documentObj);
+ }
+
+ this.makeReq_('POST', '/documents', null, documentObj, function(err, resp) {
+ if (err) {
+ callback(err, null, resp);
+ return;
+ }
+
+ callback(null, document, resp);
+ });
+};
+
+/**
+ * Access a {module:search/document} object.
+ *
+ * @param {string} id - The id of the document.
+ * @return {module:search/document}
+ *
+ * @example
+ * var myDocument = index.document('my-document');
+ */
+Index.prototype.document = function(id) {
+ return new Document(this, id);
+};
+
+/**
+ * Get {module:search/document} objects for all of the documents in this index.
+ *
+ * @param {object=} query - Query object.
+ * @param {string} query.pageSize - The maximum number of indexes to return per
+ * page. If not specified, 100 indexes are returned per page.
+ * @param {string} query.pageToken - A previously-returned page token
+ * representing part of the larger set of results to view.
+ * @param {string} query.view - One of `INDEX_VIEW_UNSPECIFIED`, `ID_ONLY`, or
+ * `FULL`. See [this table](https://goo.gl/sY6Lpt) for more details.
+ * @param {function} callback - The callback function.
+ *
+ * @example
+ * function onApiResponse(err, documents, nextQuery, apiResponse) {
+ * if (err) {
+ * console.error(err);
+ * return;
+ * }
+ *
+ * // `documents` is an array of Document objects in this index.
+ *
+ * if (nextQuery) {
+ * index.getDocuments(nextQuery, onApiResponse);
+ * }
+ * }
+ *
+ * index.getDocuments(onApiResponse);
+ *
+ * //-
+ * // Customize the request using a query object.
+ * //-
+ * index.getDocuments({
+ * pageSize: 10
+ * }, onApiResponse);
+ *
+ * //-
+ * // Get the documents as a readable object stream.
+ * //-
+ * index.getDocuments()
+ * .on('error', console.error)
+ * .on('data', function(document) {
+ * // document is a Document object.
+ * })
+ * .on('end', function() {
+ * // All documents retrieved.
+ * });
+ */
+Index.prototype.getDocuments = function(query, callback) {
+ var self = this;
+
+ if (util.is(query, 'function')) {
+ callback = query;
+ query = {};
+ }
+
+ this.makeReq_('GET', '/documents', query, null, function(err, resp) {
+ if (err) {
+ callback(err, null, null, resp);
+ return;
+ }
+
+ var nextQuery = null;
+
+ if (resp.nextPageToken) {
+ nextQuery = extend({}, query, {
+ pageToken: resp.nextPageToken
+ });
+ }
+
+ var documents = (resp.documents || [])
+ .map(self.documentFromObject_.bind(self));
+
+ callback(null, documents, nextQuery, resp);
+ });
+};
+
+/**
+ * Run a query against the documents in this index.
+ *
+ * For a full list of supported query parameters, see the
+ * [JSON API documentation](https://goo.gl/706zrP).
+ *
+ * @param {string|object} query - A query object or simply a string query.
+ * @param {string} query.pageSize - The maximum number of indexes to return per
+ * page. If not specified, 100 indexes are returned per page.
+ * @param {string} query.pageToken - A previously-returned page token
+ * representing part of the larger set of results to view.
+ * @param {string} query.query = A query string using the syntax described by
+ * the [official docs](https://goo.gl/2SYl3S).
+ * @param {function} callback - The callback function.
+ *
+ * @example
+ * function onApiResponse(err, documents, nextQuery, apiResponse) {
+ * if (err) {
+ * console.error(err);
+ * return;
+ * }
+ *
+ * // `documents` is an array of Document objects that matched your query.
+ *
+ * if (nextQuery) {
+ * index.search(nextQuery, onApiResponse);
+ * }
+ * }
+ *
+ * //-
+ * // Run a simple query against all documents.
+ * //-
+ * var query = 'person:stephen';
+ *
+ * index.search(query, onApiResponse);
+ *
+ * //-
+ * // Configure the query.
+ * //-
+ * var query = {
+ * query: 'person:stephen',
+ * pageSize: 10
+ * };
+ *
+ * index.search(query, onApiResponse);
+ *
+ * //-
+ * // Get the documents that match your query as a readable object stream.
+ * //-
+ * index.search('person:stephen')
+ * .on('error', console.error)
+ * .on('data', function(document) {
+ * // document is a Document object.
+ * })
+ * .on('end', function() {
+ * // All search results retrieved.
+ * });
+ */
+Index.prototype.search = function(query, callback) {
+ var self = this;
+
+ if (util.is(query, 'string')) {
+ query = {
+ query: query
+ };
+ }
+
+ query = query || {};
+
+ this.makeReq_('GET', '/search', query, null, function(err, resp) {
+ if (err) {
+ callback(err, null, null, resp);
+ return;
+ }
+
+ var nextQuery = null;
+
+ if (resp.nextPageToken) {
+ nextQuery = extend({}, query, {
+ pageToken: resp.nextPageToken
+ });
+ }
+
+ var documents = (resp.results || [])
+ .map(self.documentFromObject_.bind(self));
+
+ callback(null, documents, nextQuery, resp);
+ });
+};
+
+/**
+ * Convert an object to a {module:search/document} object.
+ *
+ * @private
+ *
+ * @param {object} documentObj - Object describing a document.
+ * @param {object} documentObj.fields - Fields the document contains.
+ * @param {number=} documentObj.rank - The rank of the document.
+ * @return {module:search/document}
+ *
+ * @example
+ * var documentObject = {
+ * docId: 'new-document-id',
+ * fields: {
+ * person: {
+ * values: [
+ * {
+ * stringFormat: 'TEXT',
+ * stringValue: 'Stephen'
+ * }
+ * ]
+ * }
+ * }
+ * };
+ *
+ * var document = index.documentFromObject_(documentObject);
+ * // document is a {module:search/document} object.
+ */
+Index.prototype.documentFromObject_ = function(documentObj) {
+ var document = this.document(documentObj.docId);
+
+ if (util.is(documentObj.fields, 'object')) {
+ document.fields = documentObj.fields;
+ }
+
+ if (util.is(documentObj.rank, 'number')) {
+ document.rank = documentObj.rank;
+ }
+
+ return document;
+};
+
+/**
+ * Make a new request object from the provided arguments and wrap the callback
+ * to intercept non-successful responses.
+ *
+ * @private
+ *
+ * @param {string} method - Action.
+ * @param {string} path - Request path.
+ * @param {*} query - Request query object.
+ * @param {*} body - Request body contents.
+ * @param {function} callback - The callback function.
+ */
+Index.prototype.makeReq_ = function(method, path, query, body, callback) {
+ path = '/indexes/' + this.id + path;
+
+ this.search_.makeReq_(method, path, query, body, callback);
+};
+
+/*! Developer Documentation
+ *
+ * {module:search/index#getDocuments} and {module:search/index#search} can be
+ * used with either a callback or as a readable object stream. `StreamRouter` is
+ * used to add this dual behavior to these methods.
+ */
+StreamRouter.extend(Index, ['getDocuments', 'search']);
+
+module.exports = Index;
diff --git a/lib/search/index.js b/lib/search/index.js
new file mode 100644
index 000000000000..bfae4cf16ebe
--- /dev/null
+++ b/lib/search/index.js
@@ -0,0 +1,221 @@
+/*!
+ * Copyright 2015 Google Inc. All Rights Reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+/*!
+ * @module search
+ */
+
+'use strict';
+
+var extend = require('extend');
+
+/**
+ * @type {module:search/index}
+ * @private
+ */
+var Index = require('./index-class.js');
+
+/**
+ * @type {module:common/StreamRouter}
+ * @private
+ */
+var StreamRouter = require('../common/StreamRouter.js');
+
+/**
+ * @type {module:common/util}
+ * @private
+ */
+var util = require('../common/util.js');
+
+/**
+ * @const {array} Required scopes for the Search API.
+ * @private
+ */
+var SCOPES = [
+ 'https://www.googleapis.com/auth/cloud-platform',
+ 'https://www.googleapis.com/auth/cloudsearch',
+ 'https://www.googleapis.com/auth/userinfo.email'
+];
+
+/**
+ * @const {string} Base URL for the Search API.
+ * @private
+ */
+var SEARCH_BASE_URL = 'https://cloudsearch.googleapis.com/v1/';
+
+/**
+ * Create a Search object to Interact with the Cloud Search API. Using this
+ * object, you can access your indexes with {module:search/index} and documents
+ * with {module:search/document}.
+ *
+ * Follow along with the examples to see how to do everything from creating
+ * documents to searching indexes.
+ *
+ * @alias module:search
+ * @constructor
+ *
+ * @param {object} options - [Configuration object](#/docs/?method=gcloud).
+ *
+ * @example
+ * var gcloud = require('gcloud')({
+ * keyFilename: '/path/to/keyfile.json',
+ * projectId: 'grape-spaceship-123'
+ * });
+ *
+ * var search = gcloud.search();
+ */
+function Search(options) {
+ options = options || {};
+
+ if (!options.projectId) {
+ throw util.missingProjectIdError;
+ }
+
+ this.makeAuthorizedRequest_ = util.makeAuthorizedRequestFactory({
+ credentials: options.credentials,
+ keyFile: options.keyFilename,
+ scopes: SCOPES,
+ email: options.email
+ });
+
+ this.projectId = options.projectId;
+ this.projectName = 'projects/' + this.projectId;
+}
+
+/**
+ * Get {module:search/index} objects for all of the indexes in your project.
+ *
+ * @param {object=} query - Query object.
+ * @param {string} query.pageSize - The maximum number of indexes to return per
+ * page. If not specified, 100 indexes are returned per page.
+ * @param {string} query.pageToken - A previously-returned page token
+ * representing part of the larger set of results to view.
+ * @param {string} query.prefix - The prefix of the index name. It is used to
+ * list all indexes with names that have this prefix.
+ * @param {string} query.view - See [this table](https://goo.gl/sY6Lpt) for a
+ * list of accepted values and what each will do.
+ * @param {function} callback - The callback function.
+ *
+ * @example
+ * function onApiResponse(err, indexes, nextQuery, apiResponse) {
+ * if (err) {
+ * console.error(err);
+ * return;
+ * }
+ *
+ * if (nextQuery) {
+ * search.getIndexes(nextQuery, onApiResponse);
+ * }
+ * }
+ *
+ * search.getIndexes(onApiResponse);
+ *
+ * //-
+ * // Customize the request using a query object.
+ * //-
+ * search.getIndexes({
+ * pageSize: 10
+ * }, onApiResponse);
+ */
+Search.prototype.getIndexes = function(query, callback) {
+ var self = this;
+
+ if (util.is(query, 'function')) {
+ callback = query;
+ query = {};
+ }
+
+ if (query.prefix) {
+ query.indexNamePrefix = query.prefix;
+ delete query.prefix;
+ }
+
+ this.makeReq_('GET', '/indexes', query, null, function(err, resp) {
+ if (err) {
+ callback(err, null, null, resp);
+ return;
+ }
+
+ var nextQuery = null;
+
+ if (resp.nextPageToken) {
+ nextQuery = extend({}, query, {
+ pageToken: resp.nextPageToken
+ });
+ }
+
+ var indexes = (resp.indexes || []).map(function(indexObject) {
+ var index = self.index(indexObject.indexId);
+
+ if (util.is(resp.indexedField, 'object')) {
+ index.fields = resp.indexedField;
+ }
+
+ return index;
+ });
+
+ callback(null, indexes, nextQuery, resp);
+ });
+};
+
+/**
+ * Get a reference to a Google Cloud Search index.
+ *
+ * @param {string} id - Name of the index.
+ * @return {module:search/index}
+ */
+Search.prototype.index = function(id) {
+ return new Index(this, id);
+};
+
+/**
+ * Make a new request object from the provided arguments and wrap the callback
+ * to intercept non-successful responses.
+ *
+ * @private
+ *
+ * @param {string} method - Action.
+ * @param {string} path - Request path.
+ * @param {*} query - Request query object.
+ * @param {*} body - Request body contents.
+ * @param {function} callback - The callback function.
+ */
+Search.prototype.makeReq_ = function(method, path, query, body, callback) {
+ var reqOpts = {
+ method: method,
+ qs: query,
+ uri: util.format('{base}projects/{projectId}{path}', {
+ base: SEARCH_BASE_URL,
+ projectId: this.projectId,
+ path: path
+ })
+ };
+
+ if (body) {
+ reqOpts.json = body;
+ }
+
+ this.makeAuthorizedRequest_(reqOpts, callback);
+};
+
+/*! Developer Documentation
+ *
+ * {module:search#getIndexes} can be used with either a callback or as a
+ * readable object stream. `StreamRouter` is used to add this dual behavior.
+ */
+StreamRouter.extend(Search, 'getIndexes');
+
+module.exports = Search;
diff --git a/lib/storage/bucket.js b/lib/storage/bucket.js
index 435d98c7f08c..57489be8447c 100644
--- a/lib/storage/bucket.js
+++ b/lib/storage/bucket.js
@@ -90,7 +90,7 @@ function Bucket(storage, name) {
this.storage = storage;
if (!this.name) {
- throw Error('A bucket name is needed to use Google Cloud Storage.');
+ throw new Error('A bucket name is needed to use Google Cloud Storage.');
}
/**
diff --git a/scripts/docs.sh b/scripts/docs.sh
index b797fa86140d..24a477ad27ce 100755
--- a/scripts/docs.sh
+++ b/scripts/docs.sh
@@ -31,6 +31,11 @@
./node_modules/.bin/dox < lib/pubsub/subscription.js > docs/json/master/pubsub/subscription.json &
./node_modules/.bin/dox < lib/pubsub/topic.js > docs/json/master/pubsub/topic.json &
+./node_modules/.bin/dox < lib/search/index.js > docs/json/master/search/index.json &
+./node_modules/.bin/dox < lib/search/index-class.js > docs/json/master/search/index-class.json &
+./node_modules/.bin/dox < lib/search/document.js > docs/json/master/search/document.json &
+./node_modules/.bin/dox < lib/search/field.js > docs/json/master/search/field.json &
+
./node_modules/.bin/dox < lib/storage/acl.js > docs/json/master/storage/acl.json &
./node_modules/.bin/dox < lib/storage/bucket.js > docs/json/master/storage/bucket.json &
./node_modules/.bin/dox < lib/storage/file.js > docs/json/master/storage/file.json &
diff --git a/system-test/search.js b/system-test/search.js
new file mode 100644
index 000000000000..4d059633e1c9
--- /dev/null
+++ b/system-test/search.js
@@ -0,0 +1,301 @@
+/**
+ * Copyright 2015 Google Inc. All Rights Reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+'use strict';
+
+var assert = require('assert');
+var async = require('async');
+var uuid = require('node-uuid');
+
+var env = require('./env.js');
+var gcloud = require('../lib')(env);
+
+var TEST_DOCUMENT_JSON = require('../test/testdata/search-document.json');
+
+var MAX_PARALLEL = 10;
+
+var search = gcloud.search();
+
+function deleteDocument(document, callback) {
+ document.delete(callback);
+}
+
+function deleteIndexContents(index, callback) {
+ function handleResp(err, documents, nextQuery) {
+ if (err) {
+ callback(err);
+ return;
+ }
+
+ async.eachLimit(documents, MAX_PARALLEL, deleteDocument, function(err) {
+ if (err) {
+ callback(err);
+ return;
+ }
+
+ if (nextQuery) {
+ index.getDocuments(nextQuery, handleResp);
+ return;
+ }
+
+ callback();
+ });
+ }
+
+ index.getDocuments(handleResp);
+}
+
+function deleteAllDocuments(callback) {
+ function handleResp(err, indexes, nextQuery) {
+ if (err) {
+ callback(err);
+ return;
+ }
+
+ async.eachLimit(indexes, MAX_PARALLEL, deleteIndexContents, function(err) {
+ if (err) {
+ callback(err);
+ return;
+ }
+
+ if (nextQuery) {
+ search.getIndexes(nextQuery, handleResp);
+ return;
+ }
+
+ callback();
+ });
+ }
+
+ search.getIndexes(handleResp);
+}
+
+function generateIndexName() {
+ return 'gcloud-test-index-' + uuid.v1();
+}
+
+function generateDocumentName() {
+ return 'gcloud-test-document-' + uuid.v1();
+}
+
+describe('Search', function() {
+ var INDEX_NAME = generateIndexName();
+ var index = search.index(INDEX_NAME);
+
+ before(function(done) {
+ deleteAllDocuments(done);
+ });
+
+ after(function(done) {
+ deleteAllDocuments(done);
+ });
+
+ describe('creating an index', function() {
+ it('should create a document in a new index', function(done) {
+ var newIndexName = generateIndexName();
+ var newIndex = search.index(newIndexName);
+
+ newIndex.createDocument(TEST_DOCUMENT_JSON, function(err, document) {
+ assert.ifError(err);
+ document.delete(done);
+ });
+ });
+ });
+
+ describe('listing indexes', function() {
+ before(function(done) {
+ // Creating a new document in a new index will create the index at the
+ // same time. Immediately delete the document, as we just need the index
+ // to exist.
+ var newIndexName = generateIndexName();
+ var newIndex = search.index(newIndexName);
+
+ newIndex.createDocument(TEST_DOCUMENT_JSON, function(err, document) {
+ if (err) {
+ done(err);
+ return;
+ }
+
+ document.delete(done);
+ });
+ });
+
+ it('should get all indexes', function(done) {
+ search.getIndexes(function(err, indexes) {
+ assert.ifError(err);
+ assert(indexes.length > 0);
+ done();
+ });
+ });
+
+ it('should get all indexes in stream mode', function(done) {
+ var resultsMatched = 0;
+
+ search.getIndexes()
+ .on('error', done)
+ .on('data', function() { resultsMatched++; })
+ .on('end', function() {
+ assert(resultsMatched > 0);
+ done();
+ });
+ });
+ });
+
+ describe('listing documents', function() {
+ var document;
+
+ before(function(done) {
+ async.series([
+ deleteAllDocuments,
+
+ function(next) {
+ index.createDocument(TEST_DOCUMENT_JSON, function(err, doc) {
+ if (err) {
+ next(err);
+ return;
+ }
+
+ document = doc;
+ next();
+ });
+ }
+ ], done);
+ });
+
+ after(function(done) {
+ document.delete(done);
+ });
+
+ it('should get all documents', function(done) {
+ index.getDocuments(function(err, documents) {
+ assert.ifError(err);
+ assert.strictEqual(documents.length, 1);
+ done();
+ });
+ });
+
+ it('should get all documents in stream mode', function(done) {
+ var resultsMatched = 0;
+
+ index.getDocuments()
+ .on('error', done)
+ .on('data', function() { resultsMatched++; })
+ .on('end', function() {
+ assert.strictEqual(resultsMatched, 1);
+ done();
+ });
+ });
+ });
+
+ describe('creating documents', function() {
+ it('should create a document from the doc builder', function(done) {
+ // This recreates the test JSON file with the document builder.
+ var newDocument = index.document(TEST_DOCUMENT_JSON.docId);
+
+ newDocument.setRank(TEST_DOCUMENT_JSON.rank);
+
+ Object.keys(TEST_DOCUMENT_JSON.fields).forEach(function(fieldName) {
+ var field = newDocument.addField(fieldName);
+
+ TEST_DOCUMENT_JSON.fields[fieldName].values.forEach(function(value) {
+ if (value.geoValue) {
+ field.addGeoValue(value.geoValue);
+ }
+
+ if (value.numberValue) {
+ field.addNumberValue(value.numberValue);
+ }
+
+ if (value.stringValue) {
+ var options = {};
+ if (value.stringFormat) {
+ options.format = value.stringFormat;
+ }
+ field.addTextValue(value.stringValue, options);
+ }
+
+ if (value.timestampValue) {
+ field.addTimestampValue(value.timestampValue);
+ }
+ });
+ });
+
+ index.createDocument(newDocument, function(err, document) {
+ assert.ifError(err);
+ document.getMetadata(function(err) {
+ assert.ifError(err);
+ assert.deepEqual(document.toJSON(), TEST_DOCUMENT_JSON);
+ done();
+ });
+ });
+ });
+
+ it('should create a document from JSON', function(done) {
+ index.createDocument(TEST_DOCUMENT_JSON, function(err, document) {
+ assert.ifError(err);
+ document.getMetadata(function(err) {
+ assert.ifError(err);
+ assert.deepEqual(document.toJSON(), TEST_DOCUMENT_JSON);
+ done();
+ });
+ });
+ });
+ });
+
+ describe('search', function() {
+ var query = 'ryan';
+ var DOCUMENT_NAME = generateDocumentName();
+ var document;
+
+ before(function(done) {
+ document = index.document(DOCUMENT_NAME);
+
+ var questions = document.addField('question');
+ questions.addTextValue('Where did Ryan go?');
+ questions.addTextValue('Where did Silvano go?');
+
+ index.createDocument(document, done);
+ });
+
+ after(function(done) {
+ document.delete(done);
+ });
+
+ it('should search document', function(done) {
+ index.search(query, function(err, results) {
+ assert.ifError(err);
+ assert.equal(results.length, 1);
+ assert.equal(results[0].id, DOCUMENT_NAME);
+ done();
+ });
+ });
+
+ it('should search document in stream mode', function(done) {
+ var results = [];
+
+ index.search(query)
+ .on('error', done)
+ .on('data', function(result) {
+ results.push(result);
+ })
+ .on('end', function() {
+ assert.equal(results.length, 1);
+ assert.equal(results[0].id, DOCUMENT_NAME);
+ done();
+ });
+ });
+ });
+});
diff --git a/test/docs.js b/test/docs.js
index dc145939657a..de9b9af1d0c0 100644
--- a/test/docs.js
+++ b/test/docs.js
@@ -78,6 +78,7 @@ describe('documentation', function() {
gcloud: gcloud,
require: require,
process: process,
+ console: console,
Buffer: Buffer,
Date: Date,
Array: Array
diff --git a/test/search.index.js b/test/search.index.js
new file mode 100644
index 000000000000..e69de29bb2d1
diff --git a/test/search/index.js b/test/search/index.js
new file mode 100644
index 000000000000..8d7589f13e7c
--- /dev/null
+++ b/test/search/index.js
@@ -0,0 +1,38 @@
+/*!
+ * Copyright 2015 Google Inc. All Rights Reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+'use strict';
+
+var assert = require('assert');
+
+var Search = require('../../lib/search/index.js');
+
+describe('Search', function() {
+ var PROJECT_ID = 'project-id';
+ var search;
+
+ beforeEach(function() {
+ search = new Search({
+ projectId: PROJECT_ID
+ });
+ });
+
+ describe('instantiation', function() {
+ it('should work', function() {
+ assert(true);
+ });
+ });
+});
diff --git a/test/testdata/search-document.json b/test/testdata/search-document.json
new file mode 100644
index 000000000000..82742d8ccf46
--- /dev/null
+++ b/test/testdata/search-document.json
@@ -0,0 +1,31 @@
+{
+ "docId": "doc1",
+ "fields": {
+ "field2": {
+ "values": [
+ {
+ "stringValue": "helloworld",
+ "stringFormat": "HTML"
+ }
+ ]
+ },
+ "field1": {
+ "values": [
+ {
+ "stringValue": "helloworld",
+ "stringFormat": "TEXT"
+ },
+ {
+ "timestampValue": "2014-08-18T21:19:55.000Z"
+ },
+ {
+ "numberValue": 10
+ },
+ {
+ "geoValue": "40.6894, -74.0447"
+ }
+ ]
+ }
+ },
+ "rank": 8
+}