Browse Source

Merge pull request #4 from hkrishna/search

Search API
pull/7/head
Peter Johnson @insertcoffee 10 years ago
parent
commit
e038a36b2f
  1. 12
      app.js
  2. 42
      controller/search.js
  3. 719
      docs/search/success.md
  4. 2
      index.js
  5. 33
      query/search.js
  6. 0
      sanitiser/sanitise.js
  7. 14
      test/ciao/search/success.coffee
  8. 2
      test/ciao/suggest/success.coffee
  9. 64
      test/unit/controller/search.js
  10. 14
      test/unit/mock/backend.js
  11. 55
      test/unit/query/search.js
  12. 6
      test/unit/run.js
  13. 4
      test/unit/sanitiser/sanitise.js

12
app.js

@ -11,13 +11,14 @@ app.use( require('./middleware/jsonp') );
/** ----------------------- sanitisers ----------------------- **/ /** ----------------------- sanitisers ----------------------- **/
var sanitisers = {}; var sanitisers = {};
sanitisers.suggest = require('./sanitiser/suggest'); sanitisers.sanitiser = require('./sanitiser/sanitise');
/** ----------------------- controllers ----------------------- **/ /** ----------------------- controllers ----------------------- **/
var controllers = {}; var controllers = {};
controllers.index = require('./controller/index'); controllers.index = require('./controller/index');
controllers.suggest = require('./controller/suggest'); controllers.suggest = require('./controller/suggest');
controllers.search = require('./controller/search');
/** ----------------------- routes ----------------------- **/ /** ----------------------- routes ----------------------- **/
@ -25,7 +26,10 @@ controllers.suggest = require('./controller/suggest');
app.get( '/', controllers.index() ); app.get( '/', controllers.index() );
// suggest API // suggest API
app.get( '/suggest', sanitisers.suggest.middleware, controllers.suggest() ); app.get( '/suggest', sanitisers.sanitiser.middleware, controllers.suggest() );
// search API
app.get( '/search', sanitisers.sanitiser.middleware, controllers.search() );
/** ----------------------- error middleware ----------------------- **/ /** ----------------------- error middleware ----------------------- **/

42
controller/search.js

@ -0,0 +1,42 @@
function setup( backend, query ){
// allow overriding of dependencies
backend = backend || require('../src/backend');
query = query || require('../query/search');
function controller( req, res, next ){
// backend command
var cmd = {
index: 'pelias',
body: query( req.clean )
};
// query backend
backend().client.search( cmd, function( err, data ){
var docs = [];
// handle backend errors
if( err ){ return next( err ); }
if( data && data.hits && data.hits.total){
docs = data.hits.hits.map( function( hit ){
return hit._source;
});
}
// respond
return res.status(200).json({
date: new Date().getTime(),
body: docs
});
});
}
return controller;
}
module.exports = setup;

719
docs/search/success.md

@ -0,0 +1,719 @@
# valid search query
*Generated: Wed Sep 17 2014 17:51:29 GMT-0400 (EDT)*
## Request
```javascript
{
"protocol": "http:",
"host": "localhost",
"method": "GET",
"port": 3100,
"path": "/search?input=lake&lat=29.49136&lon=-82.50622"
}
```
## Response
```javascript
Status: 200
{
"x-powered-by": "mapzen",
"charset": "utf8",
"cache-control": "public,max-age=60",
"server": "Pelias/0.0.0",
"access-control-allow-origin": "*",
"access-control-allow-methods": "GET",
"access-control-allow-headers": "X-Requested-With,content-type",
"access-control-allow-credentials": "true",
"content-type": "application/json; charset=utf-8",
"content-length": "9257",
"etag": "W/\"OND3fubDpKm+gxn7v+gcfQ==\"",
"date": "Wed, 17 Sep 2014 21:51:29 GMT",
"connection": "close"
}
```
```javascript
{
"date": 1410990689582,
"body": [
{
"name": {
"default": "Bluff Lake"
},
"admin0": "United States",
"admin1": "Georgia",
"admin2": "Charlton County",
"center_point": {
"lat": "30.87884",
"lon": "-82.1479"
},
"suggest": {
"input": [
"bluff lake"
],
"payload": {
"id": "geoname/4047948",
"geo": "-82.1479,30.87884"
},
"output": "Bluff Lake, Charlton County, United States"
}
},
{
"name": {
"default": "Lake Midget"
},
"admin0": "United States",
"admin1": "Florida",
"admin2": "Polk County",
"center_point": {
"lat": "28.04302",
"lon": "-81.57802"
},
"suggest": {
"input": [
"lake midget"
],
"payload": {
"id": "geoname/4047858",
"geo": "-81.57802,28.04302"
},
"output": "Lake Midget, Polk County, United States"
}
},
{
"name": {
"default": "Seagrove Lake"
},
"admin0": "United States",
"admin1": "Georgia",
"admin2": "Ware County",
"center_point": {
"lat": "30.70884",
"lon": "-82.17151"
},
"suggest": {
"input": [
"seagrove lake"
],
"payload": {
"id": "geoname/4221821",
"geo": "-82.17151,30.70884"
},
"output": "Seagrove Lake, Ware County, United States"
}
},
{
"name": {
"default": "Wilkerson Lake"
},
"admin0": "United States",
"admin1": "Georgia",
"admin2": "Clinch County",
"center_point": {
"lat": "30.76827",
"lon": "-82.80764"
},
"suggest": {
"input": [
"wilkerson lake"
],
"payload": {
"id": "geoname/4231020",
"geo": "-82.80764,30.76827"
},
"output": "Wilkerson Lake, Clinch County, United States"
}
},
{
"name": {
"default": "Sunset Lake"
},
"admin0": "United States",
"admin1": "Georgia",
"admin2": "Lowndes County",
"center_point": {
"lat": "30.65978",
"lon": "-83.25091"
},
"suggest": {
"input": [
"sunset lake"
],
"payload": {
"id": "geoname/4225258",
"geo": "-83.25091,30.65978"
},
"output": "Sunset Lake, Lowndes County, United States"
}
},
{
"name": {
"default": "Lake Wilson"
},
"admin0": "United States",
"admin1": "Florida",
"admin2": "Osceola County",
"center_point": {
"lat": "28.3353",
"lon": "-81.59836"
},
"suggest": {
"input": [
"lake wilson"
],
"payload": {
"id": "geoname/4178484",
"geo": "-81.59836,28.3353"
},
"output": "Lake Wilson, Osceola County, United States"
}
},
{
"name": {
"default": "Lake Willisara"
},
"admin0": "United States",
"admin1": "Florida",
"admin2": "Orange County",
"center_point": {
"lat": "28.50804",
"lon": "-81.3612"
},
"suggest": {
"input": [
"lake willisara"
],
"payload": {
"id": "geoname/4178413",
"geo": "-81.3612,28.50804"
},
"output": "Lake Willisara, Orange County, United States"
}
},
{
"name": {
"default": "Church Lake"
},
"admin0": "United States",
"admin1": "Florida",
"admin2": "Putnam County",
"center_point": {
"lat": "29.65132",
"lon": "-81.86765"
},
"suggest": {
"input": [
"church lake"
],
"payload": {
"id": "geoname/4150918",
"geo": "-81.86765,29.65132"
},
"output": "Church Lake, Putnam County, United States"
}
},
{
"name": {
"default": "Lake Citrus"
},
"admin0": "United States",
"admin1": "Florida",
"admin2": "Pinellas County",
"center_point": {
"lat": "27.99792",
"lon": "-82.75207"
},
"suggest": {
"input": [
"lake citrus"
],
"payload": {
"id": "geoname/4151171",
"geo": "-82.75207,27.99792"
},
"output": "Lake Citrus, Pinellas County, United States"
}
},
{
"name": {
"default": "Clear Lake"
},
"admin0": "United States",
"admin1": "Florida",
"admin2": "Lake County",
"center_point": {
"lat": "28.60427",
"lon": "-81.78748"
},
"suggest": {
"input": [
"clear lake"
],
"payload": {
"id": "geoname/4151287",
"geo": "-81.78748,28.60427"
},
"output": "Clear Lake, Lake County, United States"
}
},
{
"name": {
"default": "Clear Lake"
},
"admin0": "United States",
"admin1": "Florida",
"admin2": "Putnam County",
"center_point": {
"lat": "29.62033",
"lon": "-81.96045"
},
"suggest": {
"input": [
"clear lake"
],
"payload": {
"id": "geoname/4151296",
"geo": "-81.96045,29.62033"
},
"output": "Clear Lake, Putnam County, United States"
}
},
{
"name": {
"default": "Clearview Lake"
},
"admin0": "United States",
"admin1": "Florida",
"admin2": "Lake County",
"center_point": {
"lat": "28.93511",
"lon": "-81.88448"
},
"suggest": {
"input": [
"clearview lake"
],
"payload": {
"id": "geoname/4151315",
"geo": "-81.88448,28.93511"
},
"output": "Clearview Lake, Lake County, United States"
}
},
{
"name": {
"default": "Clear Lake"
},
"admin0": "United States",
"admin1": "Florida",
"admin2": "Alachua County",
"center_point": {
"lat": "29.65102",
"lon": "-82.39114"
},
"suggest": {
"input": [
"clear lake"
],
"payload": {
"id": "geoname/4151324",
"geo": "-82.39114,29.65102"
},
"output": "Clear Lake, Alachua County, United States"
}
},
{
"name": {
"default": "Clearwater Lake"
},
"admin0": "United States",
"admin1": "Florida",
"admin2": "Marion County",
"center_point": {
"lat": "29.07267",
"lon": "-82.504"
},
"suggest": {
"input": [
"clearwater lake"
],
"payload": {
"id": "geoname/4151333",
"geo": "-82.504,29.07267"
},
"output": "Clearwater Lake, Marion County, United States"
}
},
{
"name": {
"default": "Lake Clifton"
},
"admin0": "United States",
"admin1": "Florida",
"admin2": "Volusia County",
"center_point": {
"lat": "29.1606",
"lon": "-81.34513"
},
"suggest": {
"input": [
"lake clifton"
],
"payload": {
"id": "geoname/4151377",
"geo": "-81.34513,29.1606"
},
"output": "Lake Clifton, Volusia County, United States"
}
},
{
"name": {
"default": "Lake Clough"
},
"admin0": "United States",
"admin1": "Florida",
"admin2": "Volusia County",
"center_point": {
"lat": "28.99362",
"lon": "-81.23336"
},
"suggest": {
"input": [
"lake clough"
],
"payload": {
"id": "geoname/4151386",
"geo": "-81.23336,28.99362"
},
"output": "Lake Clough, Volusia County, United States"
}
},
{
"name": {
"default": "Connell Lake"
},
"admin0": "United States",
"admin1": "Florida",
"admin2": "Citrus County",
"center_point": {
"lat": "28.86965",
"lon": "-82.3506"
},
"suggest": {
"input": [
"connell lake"
],
"payload": {
"id": "geoname/4151719",
"geo": "-82.3506,28.86965"
},
"output": "Connell Lake, Citrus County, United States"
}
},
{
"name": {
"default": "Cook Lake"
},
"admin0": "United States",
"admin1": "Florida",
"admin2": "Lake County",
"center_point": {
"lat": "28.83482",
"lon": "-81.78047"
},
"suggest": {
"input": [
"cook lake"
],
"payload": {
"id": "geoname/4151764",
"geo": "-81.78047,28.83482"
},
"output": "Cook Lake, Lake County, United States"
}
},
{
"name": {
"default": "Cooper Lake"
},
"admin0": "United States",
"admin1": "Florida",
"admin2": "Putnam County",
"center_point": {
"lat": "29.62934",
"lon": "-81.94266"
},
"suggest": {
"input": [
"cooper lake"
],
"payload": {
"id": "geoname/4151827",
"geo": "-81.94266,29.62934"
},
"output": "Cooper Lake, Putnam County, United States"
}
},
{
"name": {
"default": "Lake Corrine"
},
"admin0": "United States",
"admin1": "Florida",
"admin2": "Orange County",
"center_point": {
"lat": "28.57264",
"lon": "-81.32241"
},
"suggest": {
"input": [
"lake corrine"
],
"payload": {
"id": "geoname/4152000",
"geo": "-81.32241,28.57264"
},
"output": "Lake Corrine, Orange County, United States"
}
},
{
"name": {
"default": "Cove Lake"
},
"admin0": "United States",
"admin1": "Florida",
"admin2": "Dixie County",
"center_point": {
"lat": "29.81069",
"lon": "-83.00354"
},
"suggest": {
"input": [
"cove lake"
],
"payload": {
"id": "geoname/4152097",
"geo": "-83.00354,29.81069"
},
"output": "Cove Lake, Dixie County, United States"
}
},
{
"name": {
"default": "Crooked Lake"
},
"admin0": "United States",
"admin1": "Florida",
"admin2": "Orange County",
"center_point": {
"lat": "28.5929",
"lon": "-81.48204"
},
"suggest": {
"input": [
"crooked lake"
],
"payload": {
"id": "geoname/4152358",
"geo": "-81.48204,28.5929"
},
"output": "Crooked Lake, Orange County, United States"
}
},
{
"name": {
"default": "Lake Crosby"
},
"admin0": "United States",
"admin1": "Florida",
"admin2": "Bradford County",
"center_point": {
"lat": "29.94273",
"lon": "-82.15715"
},
"suggest": {
"input": [
"lake crosby"
],
"payload": {
"id": "geoname/4152376",
"geo": "-82.15715,29.94273"
},
"output": "Lake Crosby, Bradford County, United States"
}
},
{
"name": {
"default": "Lake Crowell"
},
"admin0": "United States",
"admin1": "Florida",
"admin2": "Orange County",
"center_point": {
"lat": "28.41965",
"lon": "-81.49589"
},
"suggest": {
"input": [
"lake crowell"
],
"payload": {
"id": "geoname/4152440",
"geo": "-81.49589,28.41965"
},
"output": "Lake Crowell, Orange County, United States"
}
},
{
"name": {
"default": "Crystal Lake"
},
"admin0": "United States",
"admin1": "Florida",
"admin2": "Polk County",
"center_point": {
"lat": "27.86861",
"lon": "-81.71079"
},
"suggest": {
"input": [
"crystal lake"
],
"payload": {
"id": "geoname/4152466",
"geo": "-81.71079,27.86861"
},
"output": "Crystal Lake, Polk County, United States"
}
},
{
"name": {
"default": "Crystal Lake"
},
"admin0": "United States",
"admin1": "Florida",
"admin2": "Seminole County",
"center_point": {
"lat": "28.77055",
"lon": "-81.31479"
},
"suggest": {
"input": [
"crystal lake"
],
"payload": {
"id": "geoname/4152475",
"geo": "-81.31479,28.77055"
},
"output": "Crystal Lake, Seminole County, United States"
}
},
{
"name": {
"default": "Cypress Lake"
},
"admin0": "United States",
"admin1": "Florida",
"admin2": "Lake County",
"center_point": {
"lat": "28.43075",
"lon": "-81.76247"
},
"suggest": {
"input": [
"cypress lake"
],
"payload": {
"id": "geoname/4152637",
"geo": "-81.76247,28.43075"
},
"output": "Cypress Lake, Lake County, United States"
}
},
{
"name": {
"default": "Deer Lake"
},
"admin0": "United States",
"admin1": "Florida",
"admin2": "Marion County",
"center_point": {
"lat": "29.20003",
"lon": "-81.83805"
},
"suggest": {
"input": [
"deer lake"
],
"payload": {
"id": "geoname/4153052",
"geo": "-81.83805,29.20003"
},
"output": "Deer Lake, Marion County, United States"
}
},
{
"name": {
"default": "Lake Deer"
},
"admin0": "United States",
"admin1": "Florida",
"admin2": "Polk County",
"center_point": {
"lat": "28.02571",
"lon": "-81.76317"
},
"suggest": {
"input": [
"lake deer"
],
"payload": {
"id": "geoname/4153070",
"geo": "-81.76317,28.02571"
},
"output": "Lake Deer, Polk County, United States"
}
},
{
"name": {
"default": "Dempsey Lake"
},
"admin0": "United States",
"admin1": "Florida",
"admin2": "Suwannee County",
"center_point": {
"lat": "30.37031",
"lon": "-83.18977"
},
"suggest": {
"input": [
"dempsey lake"
],
"payload": {
"id": "geoname/4153160",
"geo": "-83.18977,30.37031"
},
"output": "Dempsey Lake, Suwannee County, United States"
}
}
]
}
```
## Tests
### âś“ 200 ok
```javascript
response.statusCode.should.equal 200
```
### âś“ valid response
```javascript
now = new Date().getTime()
should.exist json
should.not.exist json.error
should.exist json.date
json.date.should.be.within now-2000, now+2000
should.exist json.body
json.body.should.be.instanceof Array
```

2
index.js

@ -14,4 +14,4 @@ if( multicore ){
else { else {
console.log( 'listening on ' + port ); console.log( 'listening on ' + port );
app.listen( process.env.PORT || 3100 ); app.listen( process.env.PORT || 3100 );
} }

33
query/search.js

@ -0,0 +1,33 @@
var logger = require('../src/logger');
// Build pelias search query
function generate( params ){
var cmd = {
"query":{
"filtered" : {
"query" : {
"match" : {
"name.default": params.input
}
},
"filter" : {
"geo_distance" : {
"distance" : "200km",
"center_point" : {
"lat": params.lat,
"lon": params.lon
}
}
}
}
},
"size": params.size
};
logger.log( 'cmd', JSON.stringify( cmd, null, 2 ) );
return cmd;
}
module.exports = generate;

0
sanitiser/suggest.js → sanitiser/sanitise.js

14
test/ciao/search/success.coffee

@ -0,0 +1,14 @@
#> valid search query
path: '/search?input=lake&lat=29.49136&lon=-82.50622'
#? 200 ok
response.statusCode.should.equal 200
#? valid response
now = new Date().getTime()
should.exist json
should.not.exist json.error
should.exist json.date
json.date.should.be.within now-2000, now+2000
should.exist json.body
json.body.should.be.instanceof Array

2
test/ciao/suggest/success.coffee

@ -10,6 +10,6 @@ now = new Date().getTime()
should.exist json should.exist json
should.not.exist json.error should.not.exist json.error
should.exist json.date should.exist json.date
json.date.should.be.within now-1000, now+1000 json.date.should.be.within now-2000, now+2000
should.exist json.body should.exist json.body
json.body.should.be.instanceof Array json.body.should.be.instanceof Array

64
test/unit/controller/search.js

@ -0,0 +1,64 @@
var setup = require('../../../controller/search'),
mockBackend = require('../mock/backend'),
mockQuery = require('../mock/query');
module.exports.tests = {};
module.exports.tests.interface = function(test, common) {
test('valid interface', function(t) {
t.equal(typeof setup, 'function', 'setup is a function');
t.equal(typeof setup(), 'function', 'setup returns a controller');
t.end();
});
};
// functionally test controller (backend success)
module.exports.tests.functional_success = function(test, common) {
test('functional test', function(t) {
var backend = mockBackend( 'client/search/ok/1', function( cmd ){
t.deepEqual(cmd, { body: { a: 'b' }, index: 'pelias' }, 'correct backend command');
});
var controller = setup( backend, mockQuery() );
var res = {
status: function( code ){
t.equal(code, 200, 'status set');
return res;
},
json: function( json ){
t.equal(typeof json, 'object', 'returns json');
t.equal(typeof json.date, 'number', 'date set');
t.true(Array.isArray(json.body), 'body is array');
// t.deepEqual(json.body, [ { value: 1 }, { value: 2 } ], 'values correctly mapped');
t.end();
}
};
controller( { clean: { a: 'b' } }, res );
});
};
// functionally test controller (backend failure)
module.exports.tests.functional_failure = function(test, common) {
test('functional test', function(t) {
var backend = mockBackend( 'client/search/fail/1', function( cmd ){
t.deepEqual(cmd, { body: { a: 'b' }, index: 'pelias' }, 'correct backend command');
});
var controller = setup( backend, mockQuery() );
var next = function( message ){
t.equal(message,'a backend error occurred','error passed to errorHandler');
t.end();
};
controller( { clean: { a: 'b' } }, undefined, next );
});
};
module.exports.all = function (tape, common) {
function test(name, testFunction) {
return tape('GET /search ' + name, testFunction);
}
for( var testCase in module.exports.tests ){
module.exports.tests[testCase](test, common);
}
};

14
test/unit/mock/backend.js

@ -6,6 +6,12 @@ responses['client/suggest/ok/1'] = function( cmd, cb ){
responses['client/suggest/fail/1'] = function( cmd, cb ){ responses['client/suggest/fail/1'] = function( cmd, cb ){
return cb( 'a backend error occurred' ); return cb( 'a backend error occurred' );
}; };
responses['client/search/ok/1'] = function( cmd, cb ){
return cb( undefined, searchEnvelope([ { value: 1 }, { value: 2 } ]) );
};
responses['client/search/fail/1'] = function( cmd, cb ){
return cb( 'a backend error occurred' );
};
function setup( key, cmdCb ){ function setup( key, cmdCb ){
function backend( a, b ){ function backend( a, b ){
@ -14,6 +20,10 @@ function setup( key, cmdCb ){
suggest: function( cmd, cb ){ suggest: function( cmd, cb ){
if( 'function' === typeof cmdCb ){ cmdCb( cmd ); } if( 'function' === typeof cmdCb ){ cmdCb( cmd ); }
return responses[key].apply( this, arguments ); return responses[key].apply( this, arguments );
},
search: function( cmd, cb ){
if( 'function' === typeof cmdCb ){ cmdCb( cmd ); }
return responses[key].apply( this, arguments );
} }
} }
}; };
@ -25,4 +35,8 @@ function suggestEnvelope( options ){
return { pelias: [{ options: options }]}; return { pelias: [{ options: options }]};
} }
function searchEnvelope( options ){
return { pelias: [{ options: options }]};
}
module.exports = setup; module.exports = setup;

55
test/unit/query/search.js

@ -0,0 +1,55 @@
var generate = require('../../../query/search');
module.exports.tests = {};
module.exports.tests.interface = function(test, common) {
test('valid interface', function(t) {
t.equal(typeof generate, 'function', 'valid function');
t.end();
});
};
module.exports.tests.query = function(test, common) {
test('valid query', function(t) {
var query = generate({
input: 'test', size: 10,
lat: 0, lon: 0,
layers: ['test']
});
var expected = {
query: {
filtered : {
query : {
match : {
"name.default": 'test'
}
},
filter : {
geo_distance : {
distance : '200km',
center_point : {
lat: 0,
lon: 0
}
}
}
}
},
size: 10
};
t.deepEqual(query, expected, 'valid search query');
t.end();
});
};
module.exports.all = function (tape, common) {
function test(name, testFunction) {
return tape('search query ' + name, testFunction);
}
for( var testCase in module.exports.tests ){
module.exports.tests[testCase](test, common);
}
};

6
test/unit/run.js

@ -5,9 +5,11 @@ var common = {};
var tests = [ var tests = [
require('./controller/index'), require('./controller/index'),
require('./controller/suggest'), require('./controller/suggest'),
require('./sanitiser/suggest'), require('./controller/search'),
require('./sanitiser/sanitise'),
require('./query/indeces'), require('./query/indeces'),
require('./query/suggest') require('./query/suggest'),
require('./query/search')
]; ];
tests.map(function(t) { tests.map(function(t) {

4
test/unit/sanitiser/suggest.js → test/unit/sanitiser/sanitise.js

@ -1,5 +1,5 @@
var sanitize = require('../../../sanitiser/suggest'), var sanitize = require('../../../sanitiser/sanitise'),
defaultError = 'invalid param \'input\': text length, must be >0', defaultError = 'invalid param \'input\': text length, must be >0',
defaultClean = { input: 'test', lat: 0, layers: [ 'geoname', 'osmnode', 'osmway', 'admin0', 'admin1', 'admin2', 'neighborhood' ], lon: 0, size: 10, zoom: 10 }; defaultClean = { input: 'test', lat: 0, layers: [ 'geoname', 'osmnode', 'osmway', 'admin0', 'admin1', 'admin2', 'neighborhood' ], lon: 0, size: 10, zoom: 10 };
@ -194,7 +194,7 @@ module.exports.tests.middleware_success = function(test, common) {
module.exports.all = function (tape, common) { module.exports.all = function (tape, common) {
function test(name, testFunction) { function test(name, testFunction) {
return tape('SANTIZE /suggest ' + name, testFunction); return tape('SANTIZE /sanitise ' + name, testFunction);
} }
for( var testCase in module.exports.tests ){ for( var testCase in module.exports.tests ){
Loading…
Cancel
Save