Merge branch '120-solr-backend'

This commit is contained in:
Rufus Pollock
2012-10-19 09:55:18 +01:00
6 changed files with 296 additions and 39 deletions

View File

@@ -50,6 +50,7 @@
<script type="text/javascript" src="{{page.root}}src/backend.elasticsearch.js"></script> <script type="text/javascript" src="{{page.root}}src/backend.elasticsearch.js"></script>
<script type="text/javascript" src="{{page.root}}src/backend.csv.js"></script> <script type="text/javascript" src="{{page.root}}src/backend.csv.js"></script>
<script type="text/javascript" src="{{page.root}}src/backend.ckan.js"></script> <script type="text/javascript" src="{{page.root}}src/backend.ckan.js"></script>
<script type="text/javascript" src="{{page.root}}src/backend.solr.js"></script>
<!-- views --> <!-- views -->
<script type="text/javascript" src="{{page.root}}src/view.grid.js"></script> <script type="text/javascript" src="{{page.root}}src/view.grid.js"></script>

View File

@@ -1,41 +1,66 @@
jQuery(function($) { jQuery(function($) {
var $el = $('.search-here'); var $el = $('.search-here');
// var url = 'http://openspending.org/api/search'; // Check for config from url query string
// var url = 'http://localhost:9200/tmp/sfpd-last-month'; // (this allows us to point to specific data sources backends)
var config = recline.View.parseQueryString(decodeURIComponent(window.location.search));
if (config.backend) {
setupMoreComplexExample(config);
return;
}
// Crate our Recline Dataset // the simple example case
// Here we are just using the local data
// Create our Recline Dataset
// We'll just use some sample local data (see end of this file)
var dataset = new recline.Model.Dataset({ var dataset = new recline.Model.Dataset({
records: simpleData records: sampleData
}); });
// Optional // Set up the search View
// Let's configure the initial query a bit and set up facets
dataset.queryState.set({
size: 10
},
{silent: true}
);
dataset.queryState.addFacet('Author');
dataset.query();
// The search view allows us to customize the template used to render the // We give it a custom template for rendering the example records
// list of results var template = ' \
var template = getTemplate(); <div class="record"> \
<h3> \
{{title}} <em>by {{Author}}</em> \
</h3> \
<p>{{description}}</p> \
<p><code>${{price}}</code></p> \
</div> \
';
var searchView = new SearchView({ var searchView = new SearchView({
el: $el, el: $el,
model: dataset, model: dataset,
template: template template: template
}); });
searchView.render(); searchView.render();
// Optional - we configure the initial query a bit and set up facets
dataset.queryState.set({
size: 10
},
{silent: true}
);
dataset.queryState.addFacet('Author');
// Now do the first query
// After this point the Search View will take over handling queries
dataset.query();
}); });
// Simple Search View // Simple Search View
// //
// Pulls together various Recline UI components and the central Dataset and Query (state) object // Pulls together various Recline UI components and the central Dataset and Query (state) object
// //
// Plus support for customization e.g. of template for list of results // Plus support for customization e.g. of template for list of results
//
//
// var view = new SearchView({
// el: $('some-element'),
// model: dataset
// template: mustache-template-or-function
// });
var SearchView = Backbone.View.extend({ var SearchView = Backbone.View.extend({
initialize: function(options) { initialize: function(options) {
this.el = $(this.el); this.el = $(this.el);
@@ -59,9 +84,16 @@ var SearchView = Backbone.View.extend({
', ',
render: function() { render: function() {
var results = Mustache.render(this.templateResults, { var results = '';
records: this.model.records.toJSON() if (_.isFunction(this.templateResults)) {
}); var results = _.map(this.model.records.toJSON(), this.templateResults).join('\n');
} else {
// templateResults is just for one result ...
var tmpl = '{{#records}}' + this.templateResults + '{{/records}}';
var results = Mustache.render(tmpl, {
records: this.model.records.toJSON()
});
}
var html = Mustache.render(this.template, { var html = Mustache.render(this.template, {
results: results results: results
}); });
@@ -86,24 +118,79 @@ var SearchView = Backbone.View.extend({
}); });
// -------------------------------------------------------- // --------------------------------------------------------
// Stuff specific to this demo // Stuff very specific to this demo
function getTemplate() { function setupMoreComplexExample(config) {
template = ' \ var $el = $('.search-here');
{{#records}} \ var dataset = new recline.Model.Dataset(config);
<div class="record"> \ // async as may be fetching remote
<h3> \ dataset.fetch().done(function() {
{{title}} <em>by {{Author}}</em> \ if (dataset.get('url').indexOf('openspending') === -1) {
</h3> \ // generic template function
<p>{{description}}</p> \ var template = function(record) {
<p><code>${{price}}</code></p> \ var template = '<div class="record"> \
</div> \ <ul> \
{{/records}} \ {{#data}} \
'; <li>{{key}}: {{value}}</li> \
return template; {{/data}} \
} </div> \
';
var data = _.map(_.keys(record), function(key) {
return { key: key, value: record[key] };
});
return Mustache.render(template, {
data: data
});
}
} else {
// generic template function
var template = function(record) {
record['time'] = record['time.label_facet']
var template = '<div class="record"> \
<h3> \
<a href="http://openspending.org/{{record.dataset}}/entries/{{record.id}}">{{record.dataset}} {{record.time}}</a> \
&ndash; <img src="http://openspending.org/static/img/icons/cd_16x16.png" /> {{amount_formatted}} \
</h3> \
<ul> \
{{#data}} \
<li>{{key}}: {{value}}</li> \
{{/data}} \
</div> \
';
var data = [];
_.each(_.keys(record), function(key) {
if (key !='_id' && key != 'id') {
data.push({ key: key, value: record[key] });
}
});
return Mustache.render(template, {
record: record,
amount_formatted: formatAmount(record['amount']),
data: data
});
}
}
var simpleData = [ var searchView = new SearchView({
el: $el,
model: dataset,
template: template
});
searchView.render();
dataset.queryState.set({
size: 10
},
{silent: true}
);
if (dataset.get('url').indexOf('openspending') != -1) {
dataset.queryState.addFacet('dataset');
}
dataset.query();
});
};
var sampleData = [
{ {
title: 'War and Peace', title: 'War and Peace',
description: 'The epic tale of love, war and history', description: 'The epic tale of love, war and history',
@@ -124,3 +211,20 @@ var simpleData = [
} }
]; ];
var formatAmount = function (num) {
var billion = 1000000000;
var million = 1000000;
var thousand = 1000;
var numabs = Math.abs(num);
if (numabs > billion) {
return (num / billion).toFixed(0) + 'bn';
}
if (numabs > (million / 2)) {
return (num / million).toFixed(0) + 'm';
}
if (numabs > thousand) {
return (num / thousand).toFixed(0) + 'k';
} else {
return num.toFixed(0);
}
};

View File

@@ -81,7 +81,7 @@ ul.facet-items {
<div class="info"> <div class="info">
<p>This demo shows how Recline can be used to build a search app. It includes faceting as well as seearch. You can find the <a href="app.js">source javascript here</a> &ndash; please feel free to reuse!</p> <p>This demo shows how Recline can be used to build a search app. It includes faceting as well as seearch. You can find the <a href="app.js">source javascript here</a> &ndash; please feel free to reuse!</p>
<p>The default version uses some local example data but you can also connect directly to any other backend supported by Recline, in particular SOLR or ElasticSearch.</p> <p>The default version uses some local example data but you can also connect directly to any other backend supported by Recline, in particular SOLR or ElasticSearch. For example, here's an <a href="?backend=solr&amp;url=http://openspending.org/api/search">example of this app running</a> off the OpenSpending SOLR-style search API.</p>
</div> </div>
<hr /> <hr />

65
src/backend.solr.js Normal file
View File

@@ -0,0 +1,65 @@
this.recline = this.recline || {};
this.recline.Backend = this.recline.Backend || {};
this.recline.Backend.Solr = this.recline.Backend.Solr || {};
(function($, my) {
my.__type__ = 'solr';
// ### fetch
//
// dataset must have a solr or url attribute pointing to solr endpoint
my.fetch = function(dataset) {
var jqxhr = $.ajax({
url: dataset.solr || dataset.url,
data: {
rows: 1,
wt: 'json'
},
dataType: 'jsonp',
jsonp: 'json.wrf'
});
var dfd = $.Deferred();
jqxhr.done(function(results) {
// if we get 0 results we cannot get fields
var fields = []
if (results.response.numFound > 0) {
fields = _.map(_.keys(results.response.docs[0]), function(fieldName) {
return { id: fieldName };
});
}
var out = {
fields: fields,
useMemoryStore: false
};
dfd.resolve(out);
});
return dfd.promise();
}
// TODO - much work on proper query support is needed!!
my.query = function(queryObj, dataset) {
var q = queryObj.q || '*:*';
var data = {
q: q,
rows: queryObj.size,
start: queryObj.from,
wt: 'json'
};
var jqxhr = $.ajax({
url: dataset.solr || dataset.url,
data: data,
dataType: 'jsonp',
jsonp: 'json.wrf'
});
var dfd = $.Deferred();
jqxhr.done(function(results) {
var out = {
total: results.response.numFound,
hits: results.response.docs
};
dfd.resolve(out);
});
return dfd.promise();
};
}(jQuery, this.recline.Backend.Solr));

85
test/backend.solr.test.js Normal file
View File

@@ -0,0 +1,85 @@
(function ($) {
module("Backend SOLR");
test("fetch", function() {
var dataset = new recline.Model.Dataset({
url: 'http://openspending.org/api/search',
backend: 'solr'
});
// stop();
var stub = sinon.stub($, 'ajax', function(options) {
return {
done: function(callback) {
callback(sample_data);
return this;
},
fail: function() {
}
};
});
dataset.fetch().done(function(dataset) {
var exp = [
"_id",
"amount",
"category.label_facet",
"dataset",
"from.label_facet",
"id",
"subcategory.label_facet",
"time.label_facet",
"to.label_facet"
];
deepEqual(
exp,
_.pluck(dataset.fields.toJSON(), 'id')
);
// check we've mapped types correctly
equal(dataset.fields.get('amount').get('type'), 'string');
// fetch does a query so we can check for records
equal(dataset.recordCount, 10342132);
equal(dataset.records.length, 2);
equal(dataset.records.at(0).get('id'), '3e3e25d7737634127b76d5ee4a7df280987013c7');
// start();
});
$.ajax.restore();
});
var sample_data = {
"response": {
"docs": [
{
"_id": "south-african-national-gov-budget-2012-13::3e3e25d7737634127b76d5ee4a7df280987013c7",
"amount": 30905738200000.0,
"category.label_facet": "General public services",
"dataset": "south-african-national-gov-budget-2012-13",
"from.label_facet": "National Treasury",
"id": "3e3e25d7737634127b76d5ee4a7df280987013c7",
"subcategory.label_facet": "Transfers of a general character between different levels of government",
"time.label_facet": "01. April 2012",
"to.label_facet": "Provincial Equitable Share"
},
{
"_id": "south-african-national-gov-budget-2012-13::738849e28e6b3c45e5b0001e142b51479b3a3e41",
"amount": 8938807300000.0,
"category.label_facet": "General public services",
"dataset": "south-african-national-gov-budget-2012-13",
"from.label_facet": "National Treasury",
"id": "738849e28e6b3c45e5b0001e142b51479b3a3e41",
"subcategory.label_facet": "Public debt transactions",
"time.label_facet": "01. April 2012",
"to.label_facet": "State Debt Costs"
}
],
"numFound": 10342132,
"start": 0
},
"responseHeader": {
"QTime": 578,
"status": 0
}
};
})(this.jQuery);

View File

@@ -35,6 +35,7 @@
<script type="text/javascript" src="../src/backend.elasticsearch.js"></script> <script type="text/javascript" src="../src/backend.elasticsearch.js"></script>
<script type="text/javascript" src="../src/backend.csv.js"></script> <script type="text/javascript" src="../src/backend.csv.js"></script>
<script type="text/javascript" src="../src/backend.ckan.js"></script> <script type="text/javascript" src="../src/backend.ckan.js"></script>
<script type="text/javascript" src="../src/backend.solr.js"></script>
<script type="text/javascript" src="model.test.js"></script> <script type="text/javascript" src="model.test.js"></script>
<script type="text/javascript" src="backend.memory.test.js"></script> <script type="text/javascript" src="backend.memory.test.js"></script>
@@ -43,6 +44,7 @@
<script type="text/javascript" src="backend.elasticsearch.test.js"></script> <script type="text/javascript" src="backend.elasticsearch.test.js"></script>
<script type="text/javascript" src="backend.csv.test.js"></script> <script type="text/javascript" src="backend.csv.test.js"></script>
<script type="text/javascript" src="backend.ckan.test.js"></script> <script type="text/javascript" src="backend.ckan.test.js"></script>
<script type="text/javascript" src="backend.solr.test.js"></script>
<!-- views and view tests --> <!-- views and view tests -->
<script type="text/javascript" src="../src/view.grid.js"></script> <script type="text/javascript" src="../src/view.grid.js"></script>