diff --git a/src/backend/elasticsearch.js b/src/backend/elasticsearch.js
new file mode 100644
index 00000000..36a7c1a4
--- /dev/null
+++ b/src/backend/elasticsearch.js
@@ -0,0 +1,75 @@
+this.recline = this.recline || {};
+this.recline.Backend = this.recline.Backend || {};
+
+(function($, my) {
+ // ## ElasticSearch Backend
+ //
+ // Connecting to [ElasticSearch](http://www.elasticsearch.org/)
+ //
+ // To use this backend ensure your Dataset has a elasticsearch_url,
+ // webstore_url or url attribute (used in that order)
+ my.ElasticSearch = Backbone.Model.extend({
+ _getESUrl: function(dataset) {
+ var out = dataset.get('elasticsearch_url');
+ if (out) return out;
+ out = dataset.get('webstore_url');
+ if (out) return out;
+ out = dataset.get('url');
+ return out;
+ },
+ sync: function(method, model, options) {
+ var self = this;
+ if (method === "read") {
+ if (model.__type__ == 'Dataset') {
+ var base = self._getESUrl(model);
+ var schemaUrl = base + '/_mapping';
+ var jqxhr = $.ajax({
+ url: schemaUrl,
+ dataType: 'jsonp'
+ });
+ var dfd = $.Deferred();
+ my.wrapInTimeout(jqxhr).done(function(schema) {
+ // only one top level key in ES = the type so we can ignore it
+ var key = _.keys(schema)[0];
+ var fieldData = _.map(schema[key].properties, function(dict, fieldName) {
+ dict.id = fieldName;
+ return dict;
+ });
+ model.fields.reset(fieldData);
+ dfd.resolve(model, jqxhr);
+ })
+ .fail(function(arguments) {
+ dfd.reject(arguments);
+ });
+ return dfd.promise();
+ }
+ } else {
+ alert('This backend currently only supports read operations');
+ }
+ },
+ query: function(model, queryObj) {
+ var base = this._getESUrl(model);
+ var data = _.extend({}, queryObj);
+ var jqxhr = $.ajax({
+ url: base + '/_search',
+ data: data,
+ dataType: 'jsonp'
+ });
+ var dfd = $.Deferred();
+ // TODO: fail case
+ jqxhr.done(function(results) {
+ model.docCount = results.hits.total;
+ var docs = _.map(results.hits.hits, function(result) {
+ var _out = result._source;
+ _out.id = result._id;
+ return _out;
+ });
+ dfd.resolve(docs);
+ });
+ return dfd.promise();
+ }
+ });
+ recline.Model.backends['elasticsearch'] = new my.ElasticSearch();
+
+}(jQuery, this.recline.Backend));
+
diff --git a/test/backend.elasticsearch.test.js b/test/backend.elasticsearch.test.js
new file mode 100644
index 00000000..d3822c16
--- /dev/null
+++ b/test/backend.elasticsearch.test.js
@@ -0,0 +1,124 @@
+(function ($) {
+module("Backend ElasticSearch");
+
+var mapping_data = {
+ "note": {
+ "properties": {
+ "_created": {
+ "format": "dateOptionalTime",
+ "type": "date"
+ },
+ "_last_modified": {
+ "format": "dateOptionalTime",
+ "type": "date"
+ },
+ "end": {
+ "type": "string"
+ },
+ "owner": {
+ "type": "string"
+ },
+ "start": {
+ "type": "string"
+ },
+ "title": {
+ "type": "string"
+ }
+ }
+ }
+};
+
+var sample_data = {
+ "_shards": {
+ "failed": 0,
+ "successful": 5,
+ "total": 5
+ },
+ "hits": {
+ "hits": [
+ {
+ "_id": "u3rpLyuFS3yLNXrtxWkMwg",
+ "_index": "hypernotes",
+ "_score": 1.0,
+ "_source": {
+ "_created": "2012-02-24T17:53:57.286Z",
+ "_last_modified": "2012-02-24T17:53:57.286Z",
+ "owner": "tester",
+ "title": "Note 1"
+ },
+ "_type": "note"
+ },
+ {
+ "_id": "n7JMkFOHSASJCVTXgcpqkA",
+ "_index": "hypernotes",
+ "_score": 1.0,
+ "_source": {
+ "_created": "2012-02-24T17:53:57.290Z",
+ "_last_modified": "2012-02-24T17:53:57.290Z",
+ "owner": "tester",
+ "title": "Note 3"
+ },
+ "_type": "note"
+ },
+ {
+ "_id": "g7UMA55gTJijvsB3dFitzw",
+ "_index": "hypernotes",
+ "_score": 1.0,
+ "_source": {
+ "_created": "2012-02-24T17:53:57.289Z",
+ "_last_modified": "2012-02-24T17:53:57.289Z",
+ "owner": "tester",
+ "title": "Note 2"
+ },
+ "_type": "note"
+ }
+ ],
+ "max_score": 1.0,
+ "total": 3
+ },
+ "timed_out": false,
+ "took": 2
+};
+
+test("ElasticSearch", function() {
+ var dataset = new recline.Model.Dataset({
+ url: 'https://localhost:9200/my-es-db/my-es-type'
+ },
+ 'elasticsearch'
+ );
+
+ var stub = sinon.stub($, 'ajax', function(options) {
+ if (options.url.indexOf('_mapping') != -1) {
+ return {
+ done: function(callback) {
+ callback(mapping_data);
+ return this;
+ },
+ fail: function() {
+ return this;
+ }
+ }
+ } else {
+ return {
+ done: function(callback) {
+ callback(sample_data);
+ },
+ fail: function() {
+ }
+ }
+ }
+ });
+
+ dataset.fetch().then(function(dataset) {
+ deepEqual(['_created', '_last_modified', 'end', 'owner', 'start', 'title'], _.pluck(dataset.fields.toJSON(), 'id'));
+ dataset.query().then(function(docList) {
+ equal(3, dataset.docCount);
+ equal(3, docList.length);
+ equal('Note 1', docList.models[0].get('title'));
+ start();
+ });
+ });
+ $.ajax.restore();
+});
+
+})(this.jQuery);
diff --git a/test/index.html b/test/index.html
index 0f16894d..fb96f9cf 100644
--- a/test/index.html
+++ b/test/index.html
@@ -23,7 +23,9 @@
+
+