Skip to content

Commit

Permalink
added advanced search query for elasticsearch
Browse files Browse the repository at this point in the history
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
  • Loading branch information
AmasiaNalbandian committed Jan 20, 2022
1 parent 8bb13c6 commit ff02418
Show file tree
Hide file tree
Showing 3 changed files with 155 additions and 5 deletions.
57 changes: 54 additions & 3 deletions src/api/search/src/bin/validation.js
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -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);
Expand All @@ -45,4 +96,4 @@ const validateQuery = (rules) => {
};
};

module.exports.validateQuery = validateQuery(queryValidationRules);
module.exports.validateQuery = validateQuery();
11 changes: 10 additions & 1 deletion src/api/search/src/routes/query.js
Original file line number Diff line number Diff line change
@@ -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();

Expand All @@ -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;
92 changes: 91 additions & 1 deletion src/api/search/src/search.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 };

0 comments on commit ff02418

Please sign in to comment.