Jump To …

model.js

Recline Backbone Models

this.recline = this.recline || {};
this.recline.Model = this.recline.Model || {};

(function($, my) {

A Dataset model

A model has the following (non-Backbone) attributes:

@property {FieldList} fields: (aka columns) is a FieldList listing all the fields on this Dataset (this can be set explicitly, or, will be set by Dataset.fetch() or Dataset.query()

@property {DocumentList} currentDocuments: a DocumentList containing the Documents we have currently loaded for viewing (updated by calling query method)

@property {number} docCount: total number of documents in this dataset

@property {Backend} backend: the Backend (instance) for this Dataset

@property {Query} queryState: Query object which stores current queryState. queryState may be edited by other components (e.g. a query editor view) changes will trigger a Dataset query.

@property {FacetList} facets: FacetList object containing all current Facets.

my.Dataset = Backbone.Model.extend({
  __type__: 'Dataset',

initialize

Sets up instance properties (see above)

  initialize: function(model, backend) {
    _.bindAll(this, 'query');
    this.backend = backend;
    if (backend && backend.constructor == String) {
      this.backend = my.backends[backend];
    }
    this.fields = new my.FieldList();
    this.currentDocuments = new my.DocumentList();
    this.facets = new my.FacetList();
    this.docCount = null;
    this.queryState = new my.Query();
    this.queryState.bind('change', this.query);
    this.queryState.bind('facet:add', this.query);
  },

query

AJAX method with promise API to get documents from the backend.

It will query based on current query state (given by this.queryState) updated by queryObj (if provided).

Resulting DocumentList are used to reset this.currentDocuments and are also returned.

  query: function(queryObj) {
    var self = this;
    this.trigger('query:start');
    var actualQuery = self._prepareQuery(queryObj);
    var dfd = $.Deferred();
    this.backend.query(this, actualQuery).done(function(queryResult) {
      self.docCount = queryResult.total;
      var docs = _.map(queryResult.hits, function(hit) {
        var _doc = new my.Document(hit._source);
        _doc.backend = self.backend;
        _doc.dataset = self;
        return _doc;
      });
      self.currentDocuments.reset(docs);
      if (queryResult.facets) {
        var facets = _.map(queryResult.facets, function(facetResult, facetId) {
          facetResult.id = facetId;
          return new my.Facet(facetResult);
        });
        self.facets.reset(facets);
      }
      self.trigger('query:done');
      dfd.resolve(self.currentDocuments);
    })
    .fail(function(arguments) {
      self.trigger('query:fail', arguments);
      dfd.reject(arguments);
    });
    return dfd.promise();
  },

  _prepareQuery: function(newQueryObj) {
    if (newQueryObj) {
      this.queryState.set(newQueryObj);
    }
    var out = this.queryState.toJSON();
    return out;
  },

  toTemplateJSON: function() {
    var data = this.toJSON();
    data.docCount = this.docCount;
    data.fields = this.fields.toJSON();
    return data;
  }
});

A Document (aka Row)

A single entry or row in the dataset

my.Document = Backbone.Model.extend({
  __type__: 'Document',
  initialize: function() {
    _.bindAll(this, 'getFieldValue');
  },

getFieldValue

For the provided Field get the corresponding rendered computed data value for this document.

  getFieldValue: function(field) {
    var val = this.get(field.id);
    if (field.deriver) {
      val = field.deriver(val, field, this);
    }
    if (field.renderer) {
      val = field.renderer(val, field, this);
    }
    return val;
  }
});

A Backbone collection of Documents

my.DocumentList = Backbone.Collection.extend({
  __type__: 'DocumentList',
  model: my.Document
});

A Field (aka Column) on a Dataset

Following (Backbone) attributes as standard:

  • id: a unique identifer for this field- usually this should match the key in the documents hash
  • label: (optional: defaults to id) the visible label used for this field
  • type: (optional: defaults to string) the type of the data in this field. Should be a string as per type names defined by ElasticSearch - see Types list on http://www.elasticsearch.org/guide/reference/mapping/
  • format: (optional) used to indicate how the data should be formatted. For example:
    • type=date, format=yyyy-mm-dd
    • type=float, format=percentage
    • type=float, format='###,###.##'
  • is_derived: (default: false) attribute indicating this field has no backend data but is just derived from other fields (see below).

Following additional instance properties:

@property {Function} renderer: a function to render the data for this field. Signature: function(value, field, doc) where value is the value of this cell, field is corresponding field object and document is the document object. Note that implementing functions can ignore arguments (e.g. function(value) would be a valid formatter function).

@property {Function} deriver: a function to derive/compute the value of data in this field as a function of this field's value (if any) and the current document, its signature and behaviour is the same as for renderer. Use of this function allows you to define an entirely new value for data in this field. This provides support for a) 'derived/computed' fields: i.e. fields whose data are functions of the data in other fields b) transforming the value of this field prior to rendering.

my.Field = Backbone.Model.extend({

defaults - define default values

  defaults: {
    label: null,
    type: 'string',
    format: null,
    is_derived: false
  },

initialize

@param {Object} data: standard Backbone model attributes

@param {Object} options: renderer and/or deriver functions.

  initialize: function(data, options) {

if a hash not passed in the first argument throw error

    if ('0' in data) {
      throw new Error('Looks like you did not pass a proper hash with id to Field constructor');
    }
    if (this.attributes.label == null) {
      this.set({label: this.id});
    }
    if (options) {
      this.renderer = options.renderer;
      this.deriver = options.deriver;
    }
  }
});

my.FieldList = Backbone.Collection.extend({
  model: my.Field
});

Query

Query instances encapsulate a query to the backend (see query method on backend). Useful both for creating queries and for storing and manipulating query state - e.g. from a query editor).

Query Structure and format

Query structure should follow that of ElasticSearch query language.

NB: It is up to specific backends how to implement and support this query structure. Different backends might choose to implement things differently or not support certain features. Please check your backend for details.

Query object has the following key attributes:

Additions:

  • q: either straight text or a hash will map directly onto a query_string query in backend

    • Of course this can be re-interpreted by different backends. E.g. some may just pass this straight through e.g. for an SQL backend this could be the full SQL query
  • filters: dict of ElasticSearch filters. These will be and-ed together for execution.

Examples

{
   q: 'quick brown fox',
   filters: [
     { term: { 'owner': 'jones' } }
   ]
}
my.Query = Backbone.Model.extend({
  defaults: function() {
    return {
      size: 100
      , from: 0
      , facets: {}

http://www.elasticsearch.org/guide/reference/query-dsl/and-filter.html , filter: {}

      , filters: []
    }
  },

addTermFilter

Set (update or add) a terms filter to filters

See http://www.elasticsearch.org/guide/reference/query-dsl/terms-filter.html

  addTermFilter: function(fieldId, value) {
    var filters = this.get('filters');
    var filter = { term: {} };
    filter.term[fieldId] = value;
    filters.push(filter);
    this.set({filters: filters});

change does not seem to be triggered automatically

    if (value) {
      this.trigger('change');
    } else {

adding a new blank filter and do not want to trigger a new query

      this.trigger('change:filters:new-blank');
    }
  },

removeFilter

Remove a filter from filters at index filterIndex

  removeFilter: function(filterIndex) {
    var filters = this.get('filters');
    filters.splice(filterIndex, 1);
    this.set({filters: filters});
    this.trigger('change');
  },

addFacet

Add a Facet to this query

See http://www.elasticsearch.org/guide/reference/api/search/facets/

  addFacet: function(fieldId) {
    var facets = this.get('facets');

Assume id and fieldId should be the same (TODO: this need not be true if we want to add two different type of facets on same field)

    if (_.contains(_.keys(facets), fieldId)) {
      return;
    }
    facets[fieldId] = {
      terms: { field: fieldId }
    };
    this.set({facets: facets}, {silent: true});
    this.trigger('facet:add', this);
  }
});

A Facet (Result)

Object to store Facet information, that is summary information (e.g. values and counts) about a field obtained by some faceting method on the backend.

Structure of a facet follows that of Facet results in ElasticSearch, see: http://www.elasticsearch.org/guide/reference/api/search/facets/

Specifically the object structure of a facet looks like (there is one addition compared to ElasticSearch: the "id" field which corresponds to the key used to specify this facet in the facet query):

{
  "id": "id-of-facet",
  // type of this facet (terms, range, histogram etc)
  "_type" : "terms",
  // total number of tokens in the facet
  "total": 5,
  // @property {number} number of documents which have no value for the field
  "missing" : 0,
  // number of facet values not included in the returned facets
  "other": 0,
  // term object ({term: , count: ...})
  "terms" : [ {
      "term" : "foo",
      "count" : 2
    }, {
      "term" : "bar",
      "count" : 2
    }, {
      "term" : "baz",
      "count" : 1
    }
  ]
}
my.Facet = Backbone.Model.extend({
  defaults: function() {
    return {
      _type: 'terms',
      total: 0,
      other: 0,
      missing: 0,
      terms: []
    }
  }
});

A Collection/List of Facets

my.FacetList = Backbone.Collection.extend({
  model: my.Facet
});

Backend registry

Backends will register themselves by id into this registry

my.backends = {};

}(jQuery, this.recline.Model));