Merge branch '120-solr-backend'
This commit is contained in:
@@ -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>
|
||||||
|
|||||||
@@ -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> \
|
||||||
|
– <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);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|||||||
@@ -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> – 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> – 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&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
65
src/backend.solr.js
Normal 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
85
test/backend.solr.test.js
Normal 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);
|
||||||
@@ -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>
|
||||||
|
|||||||
Reference in New Issue
Block a user