From ff02418ca8e15e62e9e1fcd3c4c8c4e04f713d83 Mon Sep 17 00:00:00 2001 From: AmasiaNalbandian Date: Tue, 14 Dec 2021 21:52:00 -0500 Subject: [PATCH] added advanced search query for elasticsearch added advanced route with params separated query validation based on route pathname cleaned up the files added validation cleaned up validator added oneof in declarations fixed validation changed query fields and added if statements for options passed changed description for advanced search fixed date format to correct format --- src/api/search/src/bin/validation.js | 57 ++++++++++++++++- src/api/search/src/routes/query.js | 11 +++- src/api/search/src/search.js | 92 +++++++++++++++++++++++++++- 3 files changed, 155 insertions(+), 5 deletions(-) diff --git a/src/api/search/src/bin/validation.js b/src/api/search/src/bin/validation.js index 882fe2b270..f34cef732c 100644 --- a/src/api/search/src/bin/validation.js +++ b/src/api/search/src/bin/validation.js @@ -1,4 +1,4 @@ -const { check, validationResult } = require('express-validator'); +const { oneOf, check, validationResult } = require('express-validator'); const queryValidationRules = [ // text must be between 1 and 256 and not empty @@ -31,8 +31,59 @@ const queryValidationRules = [ .bail(), ]; -const validateQuery = (rules) => { +/** + * Advanced search is more flexible, only needs at least ONE field, but can run without any too. + * Date formats must be YYYY-MM-DD + */ +const advancedQueryValidationRules = [ + oneOf([ + check('post') + .exists({ checkFalsy: true }) + .withMessage('post should not be empty') + .bail() + .isLength({ max: 256, min: 1 }) + .withMessage('post should be between 1 to 256 characters') + .bail(), + check('author') + .exists({ checkFalsy: true }) + .withMessage('author should exist') + .bail() + .isLength({ max: 100, min: 2 }) + .withMessage('invalid author value') + .bail(), + check('title') + .exists({ checkFalsy: true }) + .withMessage('title should exist') + .bail() + .isLength({ max: 100, min: 2 }) + .withMessage('invalid title value') + .bail(), + ]), + check('to').optional().isISO8601().withMessage('invalid date format').bail(), + + check('from').optional().isISO8601().withMessage('invalid date format').bail(), + check('perPage') + .optional() + .isInt({ min: 1, max: 10 }) + .withMessage('perPage should be empty or a number between 1 to 10') + .bail(), + + check('page') + .optional() + .isInt({ min: 0, max: 999 }) + .withMessage('page should be empty or a number between 0 to 999') + .bail(), +]; + +/** + * Validates query by passing rules. The rules are different based on the pathname + * of the request. If the pathname is '/' it is the basic route. + * Otherwise, if '/advanced/' it is the advanced search + */ +const validateQuery = () => { return async (req, res, next) => { + const rules = req.baseUrl === '/' ? queryValidationRules : advancedQueryValidationRules; + await Promise.all(rules.map((rule) => rule.run(req))); const result = validationResult(req); @@ -45,4 +96,4 @@ const validateQuery = (rules) => { }; }; -module.exports.validateQuery = validateQuery(queryValidationRules); +module.exports.validateQuery = validateQuery(); diff --git a/src/api/search/src/routes/query.js b/src/api/search/src/routes/query.js index 85a6309cad..c713afaca8 100644 --- a/src/api/search/src/routes/query.js +++ b/src/api/search/src/routes/query.js @@ -1,6 +1,6 @@ const { Router, createError } = require('@senecacdot/satellite'); const { validateQuery } = require('../bin/validation'); -const search = require('../search'); +const { search, advancedSearch } = require('../search'); const router = Router(); @@ -13,4 +13,13 @@ router.get('/', validateQuery, async (req, res, next) => { } }); +// route for advanced +router.get('/advanced', validateQuery, async (req, res, next) => { + try { + res.send(await advancedSearch(req.query)); + } catch (error) { + next(createError(503, error)); + } +}); + module.exports = router; diff --git a/src/api/search/src/search.js b/src/api/search/src/search.js index b918f3a4e3..9d49a6a7b2 100644 --- a/src/api/search/src/search.js +++ b/src/api/search/src/search.js @@ -76,4 +76,94 @@ const search = async ( }; }; -module.exports = search; +/** + * Advanced search allows you to look up multiple or single fields based on the input provided + * @param options.post - text to search in post field + * @param options.author - text to search in author field + * @param options.title - text to search in title field + * @param options.from - published after this date + * @param options.to - published before this date + * @return all the results matching the fields text + * Range queries: https://www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl-query-string-query.html#_ranges + * Match field queries: https://www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl-ppmatch-query.html#query-dsl-match-query-zero + */ +const advancedSearch = async (options) => { + const results = { + query: { + bool: { + must: [], + }, + }, + }; + + const { must } = results.query.bool; + + if (options.author) { + must.push({ + match: { + author: { + query: options.author, + zero_terms_query: 'all', + }, + }, + }); + } + + if (options.post) { + must.push({ + match: { + text: { + query: options.post, + zero_terms_query: 'all', + }, + }, + }); + } + + if (options.title) { + must.push({ + match: { + title: { + query: options.title, + zero_terms_query: 'all', + }, + }, + }); + } + + if (options.from || options.to) { + must.push({ + range: { + published: { + gte: options.from || '2000-01-01', + lte: options.to || new Date().toISOString().split('T')[0], + }, + }, + }); + } + + if (!options.perPage) { + options.perPage = ELASTIC_MAX_RESULTS_PER_PAGE; + } + + if (!options.page) { + options.page = 0; + } + + const { + body: { hits }, + } = await client.search({ + from: calculateFrom(options.page, options.perPage), + size: options.perPage, + _source: ['id'], + index, + type, + body: results, + }); + + return { + results: hits.total.value, + values: hits.hits.map(({ _id }) => ({ id: _id, url: `${POSTS_URL}/${_id}` })), + }; +}; +module.exports = { search, advancedSearch };