From 1a48da8b84c474a1ac7834d50ba2e13ad0eba3df Mon Sep 17 00:00:00 2001 From: Han Date: Sat, 27 Jul 2024 13:20:44 +0100 Subject: [PATCH] TOP-244 When searching for keyword and location results aren't ordered by distance --- __tests__/unit/lib/filters.test.js | 40 --- __tests__/unit/lib/locations.test.js | 112 ++++++++ __tests__/unit/lib/queries.test.js | 35 +++ __tests__/unit/v1/services/index.test.js | 6 +- .../v1/services/routes/get-services.test.js | 1 + src/controllers/v1/services/index.js | 9 +- .../v1/services/routes/get-services.js | 244 ++++++++++++++---- src/lib/filters.js | 35 --- src/lib/locations.js | 126 +++++++++ src/lib/queries.js | 92 +++++++ 10 files changed, 572 insertions(+), 128 deletions(-) create mode 100644 __tests__/unit/lib/locations.test.js create mode 100644 __tests__/unit/lib/queries.test.js create mode 100644 src/lib/locations.js create mode 100644 src/lib/queries.js diff --git a/__tests__/unit/lib/filters.test.js b/__tests__/unit/lib/filters.test.js index 0e8c401..345b0b9 100644 --- a/__tests__/unit/lib/filters.test.js +++ b/__tests__/unit/lib/filters.test.js @@ -1,45 +1,5 @@ const filters = require("./../../../src/lib/filters") -describe("filterLocation", () => { - it("should return an empty object if lat and lng are not provided", () => { - expect(filters.filterLocation()).toEqual({}) - }) - - it("should return a query object if lat and lng are provided", () => { - const lat = "40.7128" - const lng = "-74.0060" - const expectedQuery = { - "service_at_locations.location.geometry": { - $geoWithin: { - $centerSphere: [[parseFloat(lng), parseFloat(lat)], 20 / 3963.2], - }, - }, - } - expect(filters.filterLocation(lat, lng, false)).toEqual(expectedQuery) - }) - - it("should return a different query object if lat and lng and keywordSearch are provided", () => { - const lat = "40.7128" - const lng = "-74.0060" - const expectedQuery = { - $or: [ - { - "service_at_locations.location.geometry": { - $geoWithin: { - $centerSphere: [ - [parseFloat(lng), parseFloat(lat)], - 20 / 3963.2, // miles x 1609.34 = Distance in meters - ], - }, - }, - }, - { "service_at_locations.location.geometry": { $exists: false } }, - ], - } - expect(filters.filterLocation(lat, lng, true)).toEqual(expectedQuery) - }) -}) - describe("Calling visibleNow", () => { it("should return a query that checks if a document is visible now", () => { const query = filters.visibleNow() diff --git a/__tests__/unit/lib/locations.test.js b/__tests__/unit/lib/locations.test.js new file mode 100644 index 0000000..50b99b1 --- /dev/null +++ b/__tests__/unit/lib/locations.test.js @@ -0,0 +1,112 @@ +const getServices = require("../../../src/controllers/v1/services/routes/get-services") +const locations = require("./../../../src/lib/locations") + +describe("filterLocationNearest", () => { + test("should return null when the list of locations is empty", async () => { + const lat = undefined + const lng = undefined + const { proximity } = await getServices.parseRequestParameters({}) + expect(locations.filterLocationNearest(lat, lng, proximity)).toEqual({}) + }) + + test("should return results when lat and lng is set", async () => { + const lat = parseFloat(1) + const lng = parseFloat(1.2) + const { proximity } = await getServices.parseRequestParameters({ lat, lng }) + expect(locations.filterLocationNearest(lat, lng, proximity)).toEqual({ + "service_at_locations.location.geometry": { + $nearSphere: { + $geometry: { + coordinates: [lng, lat], + type: "Point", + }, + $maxDistance: proximity, + }, + }, + }) + }) + + test("should return results when one or another lat/lng is 0", async () => { + const lat = parseFloat(0) + const lng = parseFloat(1.2) + const { proximity } = await getServices.parseRequestParameters({ lat, lng }) + expect(locations.filterLocationNearest(lat, lng, proximity)).toEqual({ + "service_at_locations.location.geometry": { + $nearSphere: { + $geometry: { + coordinates: [lng, lat], + type: "Point", + }, + $maxDistance: proximity, + }, + }, + }) + }) +}) + +describe("filterLocationKeywords", () => { + test("should return an empty object when no keywords", async () => { + const keywords = undefined + const parameters = await getServices.parseRequestParameters({}) + const result = await locations.filterLocationKeywords(keywords, parameters) + expect(result).toEqual({}) + }) + + // test("should return a list of ids", async () => { + // const keywords = "SEND peer support" + // const parameters = await getServices.parseRequestParameters({}) + // const result = await locations.filterLocationKeywords(keywords, parameters) + // expect(locations.filterLocationKeywords(keywords, parameters)).toEqual({ + // $and: [ + // { keyword: "test" }, + // { filter: "test" }, + // { + // "service_at_locations.location.geometry": { + // $exists: true, + // $ne: null, + // }, + // }, + // ], + // }) + // }) +}) + +describe("filterLocation", () => { + it("should return an empty object if lat and lng are not provided", () => { + expect(locations.filterLocation()).toEqual({}) + }) + + it("should return a query object if lat and lng are provided", () => { + const lat = "40.7128" + const lng = "-74.0060" + const expectedQuery = { + "service_at_locations.location.geometry": { + $geoWithin: { + $centerSphere: [[parseFloat(lng), parseFloat(lat)], 20 / 3963.2], + }, + }, + } + expect(locations.filterLocation(lat, lng, false)).toEqual(expectedQuery) + }) + + it("should return a different query object if lat and lng and keywordSearch are provided", () => { + const lat = "40.7128" + const lng = "-74.0060" + const expectedQuery = { + $or: [ + { + "service_at_locations.location.geometry": { + $geoWithin: { + $centerSphere: [ + [parseFloat(lng), parseFloat(lat)], + 20 / 3963.2, // miles x 1609.34 = Distance in meters + ], + }, + }, + }, + { "service_at_locations.location.geometry": { $exists: false } }, + ], + } + expect(locations.filterLocation(lat, lng, true)).toEqual(expectedQuery) + }) +}) diff --git a/__tests__/unit/lib/queries.test.js b/__tests__/unit/lib/queries.test.js new file mode 100644 index 0000000..a0e7a0a --- /dev/null +++ b/__tests__/unit/lib/queries.test.js @@ -0,0 +1,35 @@ +const queries = require("./../../../src/lib/queries") + +describe("queryType", () => { + test('should return "keyword" when only keywords are provided', () => { + const parameters = { keywords: "test" } + expect(queries.queryType(parameters)).toBe("keyword") + }) + + test('should return "location" when only lat and lng are provided', () => { + const parameters = { lat: 51.5074, lng: -0.1278 } + expect(queries.queryType(parameters)).toBe("location") + }) + + test('should return "keyword_location" when keywords, lat, and lng are all provided', () => { + const parameters = { keywords: "test", lat: 51.5074, lng: -0.1278 } + expect(queries.queryType(parameters)).toBe("keyword_location") + }) + + test("should return undefined when none of the parameters are provided", () => { + const parameters = {} + expect(queries.queryType(parameters)).toBeUndefined() + }) + + test("should return undefined when keywords is provided but lat and lng are missing", () => { + const parameters = { keywords: "test", lat: undefined, lng: undefined } + expect(queries.queryType(parameters)).toBe("keyword") + }) + + test("should return undefined when lat and lng are provided but keywords is missing", () => { + const parameters = { keywords: undefined, lat: 51.5074, lng: -0.1278 } + expect(queries.queryType(parameters)).toBe("location") + }) +}) + +describe("addFilters", () => {}) diff --git a/__tests__/unit/v1/services/index.test.js b/__tests__/unit/v1/services/index.test.js index 903b080..8bbedff 100644 --- a/__tests__/unit/v1/services/index.test.js +++ b/__tests__/unit/v1/services/index.test.js @@ -15,6 +15,7 @@ describe("index", () => { const next = jest.fn() const parameters = { perPage: 10, page: 2 } + const queryType = undefined const query = { id: 123 } const results = [{ id: 123, name: "Test Service" }] const count = 1 @@ -31,11 +32,12 @@ describe("index", () => { expect(getServices.parseRequestParameters).toHaveBeenCalledWith(req.query) - expect(getServices.buildQuery).toHaveBeenCalledWith(parameters) + expect(getServices.buildQuery).toHaveBeenCalledWith(parameters, queryType) expect(getServices.executeQuery).toHaveBeenCalledWith( query, parameters.perPage, - parameters.page + parameters.page, + queryType ) expect(getServices.buildContent).toHaveBeenCalledWith( results, diff --git a/__tests__/unit/v1/services/routes/get-services.test.js b/__tests__/unit/v1/services/routes/get-services.test.js index 60c5d8a..5a80894 100644 --- a/__tests__/unit/v1/services/routes/get-services.test.js +++ b/__tests__/unit/v1/services/routes/get-services.test.js @@ -18,6 +18,7 @@ describe("get-services", () => { it("should return thes parameters when no query parameters are provided", async () => { const expectedResults = { perPage: 50, + proximity: 8046.7, page: 1, keywords: undefined, location: undefined, diff --git a/src/controllers/v1/services/index.js b/src/controllers/v1/services/index.js index 9538944..19702c6 100644 --- a/src/controllers/v1/services/index.js +++ b/src/controllers/v1/services/index.js @@ -1,4 +1,5 @@ const logger = require("../../../../utils/logger") +const queries = require("../../../lib/queries") const { getServices, getService } = require("./routes") module.exports = { @@ -33,12 +34,16 @@ module.exports = { const parameters = await getServices.parseRequestParameters(req.query) logger.info(parameters) - const query = await getServices.buildQuery(parameters) + const queryType = queries.queryType(parameters) + logger.info(`ℹ️ Query type is "${queryType}"`) + + const query = await getServices.buildQuery(parameters, queryType) const { results, count } = await getServices.executeQuery( query, parameters.perPage, - parameters.page + parameters.page, + queryType ) const content = getServices.buildContent( results, diff --git a/src/controllers/v1/services/routes/get-services.js b/src/controllers/v1/services/routes/get-services.js index 3b1352f..3b04f42 100644 --- a/src/controllers/v1/services/routes/get-services.js +++ b/src/controllers/v1/services/routes/get-services.js @@ -1,7 +1,9 @@ const filters = require("../../../../lib/filters") +const queries = require("../../../../lib/queries") const { calculateDistance, geocode, projection } = require("../../../../lib") const { db } = require("../../../../db") const logger = require("../../../../../utils/logger") +const locations = require("../../../../lib/locations") module.exports = { /** @@ -12,6 +14,7 @@ module.exports = { parseRequestParameters: async queryParams => { const perPage = parseInt(queryParams.per_page) || 50 const page = parseInt(queryParams.page) || 1 + const proximity = parseInt(queryParams.proximity) || 5 * 1609.34 // miles x 1609.34 = Distance in meters const keywords = queryParams.keywords const location = queryParams.location let lat = queryParams?.lat ? parseFloat(queryParams.lat) : undefined @@ -68,6 +71,7 @@ module.exports = { return { perPage, page, + proximity, keywords, location, lat, @@ -84,83 +88,225 @@ module.exports = { interpreted_location, } }, + /** - * + * This builds the query based on the queryType + * There are currently 4 query types, keyword, location, keyword_location, default + * The main difference is the location queries which require a different structure * @param {*} parameters * @returns */ - buildQuery: async parameters => { + buildQuery: async (parameters, queryType) => { let query = {} query.$and = [] - const filterKeywords = await filters.filterKeywords(parameters.keywords) - query = { ...filterKeywords, ...query } - - // add filtering for ages - const ages = filters.filterAges(parameters.minAge, parameters.maxAge) - query.$and.push(...ages) - - // apply only filters - const only = filters.filterOnly(parameters.only) - query.$and.push(...only) - - // apply visibility filtering - const visibleNow = filters.visibleNow() - query.$and.push(...visibleNow) - - // add filtering - query.$and.push( - filters.filterLocation( - parameters.lat, - parameters.lng, - query?.$text ?? false - ), - filters.filterDirectories(parameters.directories), - filters.filterTaxonomies(parameters.taxonomies), - filters.filterNeeds(parameters.needs), - filters.filterSuitabilities(parameters.suitabilities), - filters.filterAccessibilities(parameters.accessibilities), - filters.filterDays(parameters.days) - ) - - // clear empty values - query.$and = query.$and.filter(obj => Object.keys(obj).length !== 0) + switch (queryType) { + case "location": + // if location but no keyword is requested + // we want to filter by location + // and we add the other standard filters as well + // http://localhost:3002/api/v1/services?location=Buckingham%2C%20MK18%2C%20UK + // { + // "service_at_locations.location.geometry": { + // "$nearSphere": { + // "$geometry": { "type": "Point", "coordinates": [-0.987645, 51.999326] }, + // "$maxDistance": 32186.8 + // } + // }, + // "$and": [ + // { + // "$or": [ + // { "visible_from": null }, + // { "visible_from": { "$lte": "2024-07-27T10:30:24.535Z" } } + // ] + // }, + // { + // "$or": [ + // { "visible_to": null }, + // { "visible_to": { "$gte": "2024-07-27T10:30:24.535Z" } } + // ] + // } + // ] + // } + + const filterLocationNearest = locations.filterLocationNearest( + parameters.lat, + parameters.lng, + parameters.proximity + ) + query = { ...filterLocationNearest, ...query } + + query = await queries.addFilters(query, parameters) + + break + case "keyword_location": + // if theres a keyword and a location then we do a search first + // for keyword to refine the location search query we also include the other + // filters and exclude those with no location set + // and we add the other standard filters as well + // { + // "service_at_locations.location.geometry": { + // "$nearSphere": { + // "$geometry": { "type": "Point", "coordinates": [-0.987645, 51.999326] }, + // "$maxDistance": 32186.8 + // } + // }, + // "_id": { + // "$in": [ + // new ObjectId('66903557ea279c1d167cdf41') + // ] + // }, + // "$and": [ + // { + // "$or": [ + // { "visible_from": null }, + // { "visible_from": { "$lte": "2024-07-27T11:08:21.487Z" } } + // ] + // }, + // { + // "$or": [ + // { "visible_to": null }, + // { "visible_to": { "$gte": "2024-07-27T11:08:21.487Z" } } + // ] + // } + // ] + // } + + const filterLocationKeywords = await locations.filterLocationKeywords( + parameters.keywords, + parameters + ) + query = { ...filterLocationKeywords, ...query } + + const filterLocationKeywordsNearest = locations.filterLocationNearest( + parameters.lat, + parameters.lng, + parameters.proximity + ) + query = { ...filterLocationKeywordsNearest, ...query } + + query = await queries.addFilters(query, parameters) + + break + default: + // if theres a keyword then its added to the query + // and we add the other standard filters as well + // This is what http://localhost:3002/api/v1/services?days=monday&keywords=send%20peer%20support looks like + // { + // "$text": { "$search": "send peer support" }, + // "$and": [ + // { + // "$or": [ + // { "visible_from": null }, + // { "visible_from": { "$lte": "2024-07-27T10:01:37.059Z" } } + // ] + // }, + // { + // "$or": [ + // { "visible_to": null }, + // { "visible_to": { "$gte": "2024-07-27T10:01:37.059Z" } } + // ] + // }, + // { "regular_schedules.weekday": { "$in": ["monday"] } } + // ] + // } + const filterKeywords = await filters.filterKeywords(parameters.keywords) + query = { ...filterKeywords, ...query } + query = await queries.addFilters(query, parameters) + break + } return query }, /** - * + * this is done because of the $nearSphere method in locationGeometry. + * This is because The $nearSphere operator cannot be used with the + * countDocuments() method in MongoDB because countDocuments() + * uses an aggregation pipeline under the hood, and $nearSphere is not + * allowed in an aggregation pipeline. + * so as a workaround if we're using nearsphere we update the count + * query to prevent errors + * the result of nearsphere will include all services with a location + * so this query is a good substitute to get the totalElements value + * http://localhost:3001/api/v1/services?lat=51.2107714&lng=0.31105&per_page=10&suitabilities=physical-disabilities + * @param {*} query + * @returns + */ + createCountQuery: query => { + // "budget deep clone" we spread $and so can modify it for countQuery only + const countQuery = { ...query, $and: [...query.$and] } + if ("service_at_locations.location.geometry" in countQuery) { + delete countQuery["service_at_locations.location.geometry"] + + countQuery["$and"].push({ + "service_at_locations.location.geometry": { + $exists: true, + $ne: null, + }, + }) + } + logger.debug("\n\nℹ️ countQuery - added due to queryType") + logger.debug(countQuery) + logger.debug(JSON.stringify(countQuery)) + return countQuery + }, + + /** + * Executes the query * @param {*} query * @param {*} perPage * @param {*} page * @returns */ - async executeQuery(query, perPage, page) { + async executeQuery(query, perPage, page, queryType) { const Service = db().collection("indexed_services") - const queryProjection = query.$text - ? { + let queryProjection + let sort + let countQuery + + switch (queryType) { + case "location": + queryProjection = { ...projection } + sort = {} + countQuery = this.createCountQuery(query) + break + case "keyword_location": + queryProjection = { ...projection } + sort = {} + countQuery = this.createCountQuery(query) + break + case "keyword": + queryProjection = { ...projection, score: { $meta: "textScore" }, } - : { - ...projection, - } - - const sort = query.$text - ? { + sort = { score: { $meta: "textScore" }, updated_at: -1, } - : { - updated_at: -1, - } + countQuery = query + break + default: + queryProjection = { ...projection } + sort = { updated_at: -1 } + countQuery = query + break + } - logger.debug("query") + logger.debug("\n\nℹ️ query") logger.debug(query) logger.debug(JSON.stringify(query)) + logger.debug("\n\nℹ️ projection") + logger.debug(projection) + logger.debug(JSON.stringify(projection)) + + logger.debug("\n\nℹ️ sort") + logger.debug(sort) + logger.debug(JSON.stringify(sort)) + const [results, count] = await Promise.all([ Service.find(query) .project(queryProjection) @@ -168,7 +314,7 @@ module.exports = { .limit(perPage) .skip((page - 1) * perPage) .toArray(), - Service.countDocuments(query), + Service.countDocuments(countQuery), ]) return { results, count } diff --git a/src/lib/filters.js b/src/lib/filters.js index 1583c64..3b43c40 100644 --- a/src/lib/filters.js +++ b/src/lib/filters.js @@ -2,41 +2,6 @@ const { db } = require("../db") const logger = require("../../utils/logger") module.exports = { - filterLocation: (lat, lng, keywordSearch) => { - if (lat !== undefined && lng !== undefined) { - logger.debug( - `Looking for services near ${parseFloat(lat)}, ${parseFloat(lng)} ` - ) - // if the query has keyword search then we need to make sure that we return services with no location still too - if (keywordSearch) { - return { - $or: [ - { - "service_at_locations.location.geometry": { - $geoWithin: { - $centerSphere: [ - [parseFloat(lng), parseFloat(lat)], - 20 / 3963.2, // miles x 1609.34 = Distance in meters - ], - }, - }, - }, - { "service_at_locations.location.geometry": { $exists: false } }, - ], - } - } else { - return { - "service_at_locations.location.geometry": { - $geoWithin: { - $centerSphere: [[parseFloat(lng), parseFloat(lat)], 20 / 3963.2], // miles x 1609.34 = Distance in meters - }, - }, - } - } - } - return {} - }, - visibleNow: () => { let query = [] query.push({ diff --git a/src/lib/locations.js b/src/lib/locations.js new file mode 100644 index 0000000..efc0dfc --- /dev/null +++ b/src/lib/locations.js @@ -0,0 +1,126 @@ +const { db } = require("../db") +const logger = require("../../utils/logger") +const filters = require("./filters") +const queries = require("./queries") + +module.exports = { + /** + * this is a filter for the location nearest to the lat and lng + * NB this can't be used as part of an aggregate pipeline so countDocuments will not work + * see executeQuery() in get-services.js + * @param {*} lat + * @param {*} lng + * @returns + */ + filterLocationNearest: (lat, lng, proximity) => { + let query = {} + if (lat !== undefined && lng !== undefined) { + query["service_at_locations.location.geometry"] = { + $nearSphere: { + $geometry: { + type: "Point", + coordinates: [parseFloat(lng), parseFloat(lat)], + }, + $maxDistance: proximity, // meters + }, + } + } + return query + }, + + /** + * if there is a location or lat or lng value then we do a search first for keyword to refine the location search query + * $text performs a text search on the content of the fields indexed with a text index. + * In this case it will search the name_text_description_text index + * @TODO test http://localhost:3001/api/v1/services + * @TODO test http://localhost:3001/api/v1/services?location=London + * @TODO test http://localhost:3001/api/v1/services?lat=51.2107714&lng=0.31105&per_page=10 + * @param {*} keywords + * @param {...any} args + * @returns + */ + filterLocationKeywords: async (keywords, parameters) => { + let query = {} + if (keywords) { + let keyword_query = {} + keyword_query.$and = [] + const Service = db().collection("indexed_services") + const filterKeywords = await filters.filterKeywords(parameters.keywords) + keyword_query = { ...filterKeywords, ...keyword_query } + keyword_query = await queries.addFilters(keyword_query, parameters) + // exclude services with no location + keyword_query.$and.push({ + "service_at_locations.location.geometry": { + $exists: true, + $ne: null, + }, + }) + + logger.debug("\n\nℹ️ filterLocationKeywords query") + logger.debug(keyword_query) + logger.debug(JSON.stringify(keyword_query)) + + const docs = await Service.find(keyword_query) + .project({ _id: 1 }) + .limit(1000) + .toArray() + query._id = { $in: docs.map(doc => doc._id) } + } + return query + }, + + /** + * @deprecated because we've gone back to nearSphere for now but leaving in as we will probably use it again + * To use this simply add to query.$and array + * query.$and.push( + * filters.filterLocation( + * parameters.lat, + * parameters.lng, + * query?.$text ?? false + * ) + * ) + * This can be used instead of the above with the following benefits: + * - can be used as part of an aggregate pipeline + * - faster + * - no need for a separate countDocuments query + * The only downside is that results are not ordered nearest to furthest + * @param {*} lat + * @param {*} lng + * @param {*} keywordSearch + * @returns + */ + filterLocation: (lat, lng, keywordSearch) => { + if (lat !== undefined && lng !== undefined) { + logger.debug( + `Looking for services near ${parseFloat(lat)}, ${parseFloat(lng)} ` + ) + // if the query has keyword search then we need to make sure that we return services with no location still too + if (keywordSearch) { + return { + $or: [ + { + "service_at_locations.location.geometry": { + $geoWithin: { + $centerSphere: [ + [parseFloat(lng), parseFloat(lat)], + 20 / 3963.2, // miles x 1609.34 = Distance in meters + ], + }, + }, + }, + { "service_at_locations.location.geometry": { $exists: false } }, + ], + } + } else { + return { + "service_at_locations.location.geometry": { + $geoWithin: { + $centerSphere: [[parseFloat(lng), parseFloat(lat)], 20 / 3963.2], // miles x 1609.34 = Distance in meters + }, + }, + } + } + } + return {} + }, +} diff --git a/src/lib/queries.js b/src/lib/queries.js new file mode 100644 index 0000000..176cf0e --- /dev/null +++ b/src/lib/queries.js @@ -0,0 +1,92 @@ +const filters = require("./filters") + +module.exports = { + /** + * Determine the query type based on the parameters + * @param {*} parameters + * @returns {string} query type keyword, location, keyword_location or undefined + */ + queryType: parameters => { + if (parameters.keywords && !parameters.lat && !parameters.lng) { + return "keyword" + } else if ( + parameters.keywords === undefined && + parameters.lat && + parameters.lng + ) { + return "location" + } else if (parameters.keywords && parameters.lat && parameters.lng) { + return "keyword_location" + } else { + return undefined + } + }, + /** + * Adds the global filters to the query.$and array + * http://localhost:3002/api/v1/services?min_age=10&max_age=20&only=free&directories=bfis&taxonomies=subcat3&needs=autism&suitabilities=chair&accessibilities=hearingloop&days=monday + * { + * "$and": [ + * { "$or": [{ "max_age": null }, { "max_age": { "$gte": 10 } }] }, + * { "$or": [{ "min_age": null }, { "min_age": { "$lte": 20 } }] }, + * { "free": true }, + * { + * "$or": [ + * { "visible_from": null }, + * { "visible_from": { "$lte": "2024-07-26T18:29:23.825Z" } } + * ] + * }, + * { + * "$or": [ + * { "visible_to": null }, + * { "visible_to": { "$gte": "2024-07-26T18:29:23.825Z" } } + * ] + * }, + * { "directories.label": { "$in": ["bfis"] } }, + * { "taxonomies.slug": { "$all": ["subcat3"] } }, + * { "send_needs.slug": { "$in": ["autism"] } }, + * { "suitabilities.slug": { "$in": ["chair"] } }, + * { + * "service_at_locations.location.accessibilities.slug": { + * "$in": ["hearingloop"] + * } + * }, + * { "regular_schedules.weekday": { "$in": ["monday"] } } + * ] + * } + * @param {*} query + * @param {*} parameters + * @returns + */ + addFilters: async (query, parameters) => { + const ages = filters.filterAges(parameters.minAge, parameters.maxAge) + query.$and.push(...ages) + + // apply only filters + const only = filters.filterOnly(parameters.only) + query.$and.push(...only) + + // apply visibility filtering + const visibleNow = filters.visibleNow() + query.$and.push(...visibleNow) + + // add filtering + query.$and.push( + // locations.filterLocation( + // parameters.lat, + // parameters.lng, + // query?.$text ?? false + // ), + filters.filterDirectories(parameters.directories), + filters.filterTaxonomies(parameters.taxonomies), + filters.filterNeeds(parameters.needs), + filters.filterSuitabilities(parameters.suitabilities), + filters.filterAccessibilities(parameters.accessibilities), + filters.filterDays(parameters.days) + ) + + // clear empty values + query.$and = query.$and.filter(obj => Object.keys(obj).length !== 0) + + return query + }, +}