UNPKG

13.6 kBJavaScriptView Raw
1// This query is useful for specifying all the combinations of inputs starting
2// from most granular to least granular. For example, given the input:
3// `30 w 26th street, new york, ny, usa`
4// the following pseudo-query would be generated:
5//
6// - (housenumber=30 && street=w 26th street && city=new york && state=ny && country=usa) ||
7// - (city=new york && state=ny && country=usa) ||
8// - (state=ny && country=usa) ||
9// - (country=usa)
10//
11// That is, it specifies as much as possible from the analyzed form, then falling
12// back in decreasing granularity.
13//
14// In the event of an input like `Socorro, Pennsylvania, Canada` where there is
15// no city named Socorro in Pennsylvania and there is no region named Pennsylvania
16// in Canada, the single result would be for country=Canada
17//
18// In the case that a full street+city+state+country is correct and found, all
19// OR'd queries will return results so only the most specific result should be
20// retained
21//
22// If the analyzed input contains both a query and housenumber+street, then
23// the constructured query searches for the query term in the context of the coarse
24// inputs, then falls back to the housenumber+street. It's important to not
25// search for the query term and housenumber+street at the same time because
26// only venues matching the query term at that exact housenumber+street would
27// be returned.
28
29var _ = require('lodash');
30var baseQuery = require('./baseQuery');
31
32function Layout(){
33 this._score = [];
34 this._filter = [];
35}
36
37Layout.prototype.score = function( view ){
38 this._score.push( view );
39 return this;
40};
41
42Layout.prototype.filter = function( view ){
43 this._filter.push( view );
44 return this;
45};
46
47function addPrimary(value, layer, fields, likely_to_have_abbreviation) {
48 // base primary query should match on layer and one of the admin fields via
49 // multi_match
50 var o = {
51 bool: {
52 _name: 'fallback.' + layer,
53 must: [
54 {
55 multi_match: {
56 query: value,
57 type: 'phrase',
58 fields: fields
59 }
60 }
61 ],
62 filter: {
63 term: {
64 layer: layer
65 }
66 }
67 }
68 };
69
70 // When the value is likely to have abbreviations, as in the case of regions
71 // and countries, don't add the must match on phrase.default. For example,
72 // when the input 'Socorro, PA' falls back to just 'PA' (because there's no
73 // place in PA called Socorro), forcing a phrase match on 'PA' would fail
74 // since the indexed value is 'Pennsylvania', not 'PA'. Having this conditional
75 // here allows primary matches in regions and countries, where there is less
76 // danger of analysis ambiguity, to only have to match on region/region_a or
77 // country/country_a. Commented out to show intent.
78 //
79 // if (!likely_to_have_abbreviation) {
80 // o.bool.must.push(
81 // {
82 // match_phrase: {
83 // 'phrase.default': {
84 // query: value
85 // }
86 // }
87 // }
88 // );
89 // }
90
91 return o;
92
93}
94
95// Secondary matches are for less granular administrative areas that have been
96// specified. For example, in "Socorro, NM", "Socorro" is primary, whereas
97// "NM" is secondary.
98function addSecondary(value, fields) {
99 return {
100 multi_match: {
101 query: value,
102 type: 'phrase',
103 fields: fields
104 }
105 };
106
107}
108
109// add the postal code if supplied
110function addSecPostCode(vs, o) {
111 // add postcode if specified
112 if (vs.isset('input:postcode')) {
113 o.bool.should.push({
114 match_phrase: {
115 'address_parts.zip': {
116 query: vs.var('input:postcode').toString()
117 }
118 }
119 });
120 }
121}
122
123function addSecNeighbourhood(vs, o) {
124 // add neighbourhood if specified
125 if (vs.isset('input:neighbourhood')) {
126 o.bool.must.push(addSecondary(
127 vs.var('input:neighbourhood').toString(),
128 [
129 'parent.neighbourhood',
130 'parent.neighbourhood_a'
131 ]
132 ));
133 }
134}
135
136function addSecBorough(vs, o) {
137 // add borough if specified
138 if (vs.isset('input:borough')) {
139 o.bool.must.push(addSecondary(
140 vs.var('input:borough').toString(),
141 [
142 'parent.borough',
143 'parent.borough_a'
144 ]
145 ));
146 }
147}
148
149function addSecLocality(vs, o) {
150 // add locality if specified
151 if (vs.isset('input:locality')) {
152 o.bool.must.push(addSecondary(
153 vs.var('input:locality').toString(),
154 [
155 'parent.locality',
156 'parent.locality_a',
157 'parent.localadmin',
158 'parent.localadmin_a'
159 ]
160 ));
161 }
162}
163
164function addSecCounty(vs, o) {
165 // add county if specified
166 if (vs.isset('input:county')) {
167 o.bool.must.push(addSecondary(
168 vs.var('input:county').toString(),
169 [
170 'parent.county',
171 'parent.county_a',
172 'parent.macrocounty',
173 'parent.macrocounty_a'
174 ]
175 ));
176 }
177}
178
179function addSecRegion(vs, o) {
180 // add region if specified
181 if (vs.isset('input:region')) {
182 o.bool.must.push(addSecondary(
183 vs.var('input:region').toString(),
184 [
185 'parent.region',
186 'parent.region_a',
187 'parent.macroregion',
188 'parent.macroregion_a'
189 ]
190 ));
191 }
192}
193
194function addSecCountry(vs, o) {
195 // add country if specified
196 if (vs.isset('input:country')) {
197 o.bool.must.push(addSecondary(
198 vs.var('input:country').toString(),
199 [
200 'parent.country',
201 'parent.country_a',
202 'parent.dependency',
203 'parent.dependency_a'
204 ]
205 ));
206 }
207}
208
209
210function addQuery(vs) {
211 var o = addPrimary(
212 vs.var('input:query').toString(),
213 'venue',
214 [
215 'phrase.default'
216 ],
217 false
218 );
219
220 addSecNeighbourhood(vs, o);
221 addSecBorough(vs, o);
222 addSecLocality(vs, o);
223 addSecCounty(vs, o);
224 addSecRegion(vs, o);
225 addSecCountry(vs, o);
226
227 return o;
228
229}
230
231function addUnitAndHouseNumberAndStreet(vs) {
232 var o = {
233 bool: {
234 _name: 'fallback.address',
235 must: [
236 {
237 match_phrase: {
238 'address_parts.unit': {
239 query: vs.var('input:unit').toString()
240 }
241 }
242 },
243 {
244 match_phrase: {
245 'address_parts.number': {
246 query: vs.var('input:housenumber').toString()
247 }
248 }
249 },
250 {
251 match_phrase: {
252 'address_parts.street': {
253 query: vs.var('input:street').toString()
254 }
255 }
256 }
257 ],
258 should: [],
259 filter: {
260 term: {
261 layer: 'address'
262 }
263 }
264 }
265 };
266
267 if (vs.isset('boost:address')) {
268 o.bool.boost = vs.var('boost:address');
269 }
270
271 addSecPostCode(vs, o);
272 addSecNeighbourhood(vs, o);
273 addSecBorough(vs, o);
274 addSecLocality(vs, o);
275 addSecCounty(vs, o);
276 addSecRegion(vs, o);
277 addSecCountry(vs, o);
278
279 return o;
280
281}
282
283function addHouseNumberAndStreet(vs) {
284 var o = {
285 bool: {
286 _name: 'fallback.address',
287 must: [
288 {
289 match_phrase: {
290 'address_parts.number': {
291 query: vs.var('input:housenumber').toString()
292 }
293 }
294 },
295 {
296 match_phrase: {
297 'address_parts.street': {
298 query: vs.var('input:street').toString()
299 }
300 }
301 }
302 ],
303 should: [],
304 filter: {
305 term: {
306 layer: 'address'
307 }
308 }
309 }
310 };
311
312 if (vs.isset('boost:address')) {
313 o.bool.boost = vs.var('boost:address');
314 }
315
316 addSecPostCode(vs, o);
317 addSecNeighbourhood(vs, o);
318 addSecBorough(vs, o);
319 addSecLocality(vs, o);
320 addSecCounty(vs, o);
321 addSecRegion(vs, o);
322 addSecCountry(vs, o);
323
324 return o;
325
326}
327
328function addStreet(vs) {
329 var o = {
330 bool: {
331 _name: 'fallback.street',
332 must: [
333 {
334 match_phrase: {
335 'address_parts.street': {
336 query: vs.var('input:street').toString()
337 }
338 }
339 }
340 ],
341 should: [],
342 filter: {
343 term: {
344 layer: 'street'
345 }
346 }
347 }
348 };
349
350 if (vs.isset('boost:street')) {
351 o.bool.boost = vs.var('boost:street');
352 }
353
354 addSecPostCode(vs, o);
355 addSecNeighbourhood(vs, o);
356 addSecBorough(vs, o);
357 addSecLocality(vs, o);
358 addSecCounty(vs, o);
359 addSecRegion(vs, o);
360 addSecCountry(vs, o);
361
362 return o;
363
364}
365
366function addNeighbourhood(vs) {
367 var o = addPrimary(
368 vs.var('input:neighbourhood').toString(),
369 'neighbourhood',
370 [
371 'parent.neighbourhood',
372 'parent.neighbourhood_a'
373 ],
374 false
375 );
376
377 addSecBorough(vs, o);
378 addSecLocality(vs, o);
379 addSecCounty(vs, o);
380 addSecRegion(vs, o);
381 addSecCountry(vs, o);
382
383 return o;
384
385}
386
387function addBorough(vs) {
388 var o = addPrimary(
389 vs.var('input:borough').toString(),
390 'borough',
391 [
392 'parent.borough',
393 'parent.borough_a'
394 ],
395 false
396 );
397
398 addSecLocality(vs, o);
399 addSecCounty(vs, o);
400 addSecRegion(vs, o);
401 addSecCountry(vs, o);
402
403 return o;
404
405}
406
407function addLocality(vs) {
408 var o = addPrimary(vs.var('input:locality').toString(),
409 'locality', ['parent.locality', 'parent.locality_a'], false);
410
411 addSecCounty(vs, o);
412 addSecRegion(vs, o);
413 addSecCountry(vs, o);
414
415 return o;
416
417}
418
419function addLocalAdmin(vs) {
420 var o = addPrimary(vs.var('input:locality').toString(),
421 'localadmin', ['parent.localadmin', 'parent.localadmin_a'], false);
422
423 addSecCounty(vs, o);
424 addSecRegion(vs, o);
425 addSecCountry(vs, o);
426
427 return o;
428
429}
430
431function addCounty(vs) {
432 var o = addPrimary(
433 vs.var('input:county').toString(),
434 'county',
435 [
436 'parent.county',
437 'parent.county_a'
438 ],
439 false
440 );
441
442 addSecRegion(vs, o);
443 addSecCountry(vs, o);
444
445 return o;
446
447}
448
449function addMacroCounty(vs) {
450 var o = addPrimary(
451 vs.var('input:county').toString(),
452 'macrocounty',
453 [
454 'parent.macrocounty',
455 'parent.macrocounty_a'
456 ],
457 false
458 );
459
460 addSecRegion(vs, o);
461 addSecCountry(vs, o);
462
463 return o;
464
465}
466
467function addRegion(vs) {
468 var o = addPrimary(
469 vs.var('input:region').toString(),
470 'region',
471 [
472 'parent.region',
473 'parent.region_a'
474 ],
475 true
476 );
477
478 addSecCountry(vs, o);
479
480 return o;
481
482}
483
484function addMacroRegion(vs) {
485 var o = addPrimary(
486 vs.var('input:region').toString(),
487 'macroregion',
488 [
489 'parent.macroregion',
490 'parent.macroregion_a'
491 ],
492 true
493 );
494
495 addSecCountry(vs, o);
496
497 return o;
498
499}
500
501function addDependency(vs) {
502 var o = addPrimary(
503 vs.var('input:country').toString(),
504 'dependency',
505 [
506 'parent.dependency',
507 'parent.dependency_a'
508 ],
509 true
510 );
511
512 return o;
513
514}
515
516function addCountry(vs) {
517 var o = addPrimary(
518 vs.var('input:country').toString(),
519 'country',
520 [
521 'parent.country',
522 'parent.country_a'
523 ],
524 true
525 );
526
527 return o;
528
529}
530
531function addPostCode(vs) {
532 var o = addPrimary(
533 vs.var('input:postcode').toString(),
534 'postalcode',
535 [
536 'parent.postalcode'
537 ],
538 false
539 );
540
541 // same position in hierarchy as borough according to WOF
542 // https://github.com/whosonfirst/whosonfirst-placetypes#here-is-a-pretty-picture
543 addSecLocality(vs, o);
544 addSecCounty(vs, o);
545 addSecRegion(vs, o);
546 addSecCountry(vs, o);
547
548 return o;
549
550}
551
552
553Layout.prototype.render = function( vs ){
554 var q = Layout.base( vs );
555
556 var funcScoreShould = q.query.function_score.query.bool.should;
557
558 if (vs.isset('input:query')) {
559 funcScoreShould.push(addQuery(vs));
560 }
561 if (vs.isset('input:unit') && vs.isset('input:housenumber') && vs.isset('input:street')) {
562 funcScoreShould.push(addUnitAndHouseNumberAndStreet(vs));
563 }
564 else if (vs.isset('input:housenumber') && vs.isset('input:street')) {
565 funcScoreShould.push(addHouseNumberAndStreet(vs));
566 }
567 if (vs.isset('input:postcode')) {
568 funcScoreShould.push(addPostCode(vs));
569 }
570 if (vs.isset('input:street')) {
571 funcScoreShould.push(addStreet(vs));
572 }
573 if (vs.isset('input:neighbourhood')) {
574 funcScoreShould.push(addNeighbourhood(vs));
575 }
576 if (vs.isset('input:borough')) {
577 funcScoreShould.push(addBorough(vs));
578 }
579 if (vs.isset('input:locality')) {
580 funcScoreShould.push(addLocality(vs));
581 funcScoreShould.push(addLocalAdmin(vs));
582 }
583 if (vs.isset('input:county')) {
584 funcScoreShould.push(addCounty(vs));
585 funcScoreShould.push(addMacroCounty(vs));
586 }
587 if (vs.isset('input:region')) {
588 funcScoreShould.push(addRegion(vs));
589 funcScoreShould.push(addMacroRegion(vs));
590 }
591 if (vs.isset('input:country')) {
592 funcScoreShould.push(addDependency(vs));
593 funcScoreShould.push(addCountry(vs));
594 }
595
596 // handle scoring views under 'query' section (both 'must' & 'should')
597 if( this._score.length ){
598 this._score.forEach( function( view ){
599 var rendered = view( vs );
600 if( rendered ){
601 q.query.function_score.functions.push( rendered );
602 }
603 });
604 }
605
606 // handle filter views under 'filter' section (only 'must' is allowed here)
607 if( this._filter.length ){
608 this._filter.forEach( function( view ){
609 var rendered = view( vs );
610 if( rendered ){
611 if( !q.query.function_score.query.bool.hasOwnProperty( 'filter' ) ){
612 q.query.function_score.query.bool.filter = {
613 bool: {
614 must: []
615 }
616 };
617 }
618 q.query.function_score.query.bool.filter.bool.must.push( rendered );
619 }
620 });
621 }
622
623 return q;
624};
625
626Layout.base = function( vs ){
627 var baseQueryCopy = _.cloneDeep(baseQuery);
628
629 baseQueryCopy.size = vs.var('size');
630 baseQueryCopy.track_scores = vs.var('track_scores');
631
632 return baseQueryCopy;
633
634};
635
636module.exports = Layout;