UNPKG

@windingtree/wt-search-api

Version:

NodeJS app that enables quick search over data from Winding Tree platform

167 lines (153 loc) 4.81 kB
const byLocation = require('./indices/by-location'); const byCategory = require('./indices/by-category'); const byDefaultLocale = require('./indices/by-default-locale'); const { db } = require('../../config'); const INDEXERS = [ byCategory, byLocation, byDefaultLocale, ]; /** * Implements hotel indexing and subsequent retrieval by * filtering and sorting criteria. * * Right now the indexer is based on plain portable SQL without * commitment to a specific backend. */ class Indexer { /** * Index / deindex a single hotel. * * @param {Object} hotel as returned from Hotel.getHotelData. * @return {Promise<void>} * */ async indexHotel (hotel) { for (const indexer of INDEXERS) { await indexer.indexHotel(hotel); } } /** * Get a list of hotel addresses based on the index filters, * ordered by the sorting specification. * * Note that if a hotel doesn't specify the attributes necessary * for sorting (e.g. location when sorting by distance), it is * omitted from the results altogether. * * @param {Number} limit * @param {String} startWith (optional) * @param {Array} filters (optional) * @param {Object} sorting (optional) * @return {Promise<Array>} * */ async _getHotelAddresses (limit, startWith, filters, sorting) { if (!filters && !sorting) { throw new Error('At least one of `filters`, `sorting` must be provided.'); } // 1. Assemble all tables. let allTables = new Set(); if (filters) { for (const filter of filters) { allTables.add(filter.table); } } if (sorting) { allTables.add(sorting.table); } // 2. Join all tables on hotel_address. allTables = Array.from(allTables); const firstTable = allTables[0], tablesRest = allTables.slice(1); let query = db(firstTable); for (const table of tablesRest) { query = query.innerJoin(table, `${firstTable}.hotel_address`, '=', `${table}.hotel_address`); } // 3. Apply filtering conditions. if (filters) { const head = filters[0], tail = filters.slice(1); query = query.where(head.condition); for (const filter of tail) { query = query.andWhere(filter.condition); } } // 4. Apply sorting. if (sorting) { query = query.select(sorting.select).orderBy(sorting.columnName); } // 5. Apply pagination criteria. query = query.orderBy(`${firstTable}.hotel_address`).limit(limit); if (startWith) { if (sorting) { // Retrieve the sorting score of the first item. let startWithScore = (await db(sorting.table) .select(sorting.select) .where(`${sorting.table}.hotel_address`, startWith))[0]; startWithScore = startWithScore && startWithScore[sorting.columnName]; if (startWithScore) { query = query .where(`${sorting.columnName}`, '>=', startWithScore) .orWhere(function () { this.where(`${sorting.columnName}`, '=', startWithScore) .andWhere(`${firstTable}.hotel_address`, '>=', startWith); }); } // If there's no such score, ignore the startWith parameter. } else { query = query.where(`${firstTable}.hotel_address`, '>=', startWith); } } const data = await query.select(`${firstTable}.hotel_address`); return data.map((item) => { const x = { address: item.hotel_address }; if (sorting) { x.score = { name: sorting.name, value: sorting.computeScore(item[sorting.columnName]), }; } return x; }); } /** * Get a list of hotel addresses based on the input query. * * Query is an object like this: * * { * filters: [ * { * type: 'location', * condition: { lat: 10, lng: 10, distance: 20 }, * }, * ], * sorting: { * type: 'distance', * data: { lat: 10, lng: 10 }, * }, * } * * (This example filters hotels that are at max 20 kilometers * away from the [10, 10] coordinates and sorts the list by * distance from the same location.) * * @param {Object} query * @param {Number} limit * @param {String} startWith (optional) * @return {Promise<Array>} * */ async lookup (query, limit, startWith) { let filtering = INDEXERS .map((indexer) => indexer.getFiltering(query)) .filter(Boolean) .reduce((prev, curr) => prev.concat(curr), []); filtering = (filtering.length === 0) ? undefined : filtering; const sorting = INDEXERS .map((indexer) => indexer.getSorting(query)) .filter(Boolean) .reduce((prev, curr) => prev || curr, undefined); return this._getHotelAddresses(limit, startWith, filtering, sorting); } } module.exports = Indexer;