Skip to content

Commit

Permalink
Distance query
Browse files Browse the repository at this point in the history
  • Loading branch information
apricot13 committed Jul 11, 2024
1 parent 7ad3383 commit 7749f82
Show file tree
Hide file tree
Showing 3 changed files with 161 additions and 29 deletions.
3 changes: 2 additions & 1 deletion src/controllers/v1/services/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,8 @@ module.exports = {
const { results, count } = await getServices.executeQuery(
query,
parameters.perPage,
parameters.page
parameters.page,
parameters
)
const content = getServices.buildContent(
results,
Expand Down
144 changes: 116 additions & 28 deletions src/controllers/v1/services/routes/get-services.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ module.exports = {
* @returns
*/
parseRequestParameters: async queryParams => {
let searchType = undefined
const perPage = parseInt(queryParams.per_page) || 50
const page = parseInt(queryParams.page) || 1
const keywords = queryParams.keywords
Expand Down Expand Up @@ -65,6 +66,14 @@ module.exports = {
}
}

if (keywords && !lat && !lng) {
searchType = "keyword"
} else if (keywords === undefined && lat && lng) {
searchType = "location"
} else if (keywords && lat && lng) {
searchType = "keyword_location"
}

return {
perPage,
page,
Expand All @@ -82,6 +91,7 @@ module.exports = {
minAge,
maxAge,
interpreted_location,
searchType,
}
},
/**
Expand All @@ -93,8 +103,28 @@ module.exports = {
let query = {}
query.$and = []

const filterKeywords = await filters.filterKeywords(parameters.keywords)
query = { ...filterKeywords, ...query }
// add in keywords
if (parameters.searchType === "keyword") {
const filterKeywords = await filters.filterKeywords(parameters.keywords)
query = { ...filterKeywords, ...query }
} else if (parameters.searchType === "keyword_location") {
const filterLocationKeywords = await filters.filterLocationKeywords(
parameters.keywords
)
query = { ...filterLocationKeywords, ...query }
}

// add in locations
if (
parameters.searchType === "location" ||
parameters.searchType === "keyword_location"
) {
const locationGeometry = filters.locationGeometry(
parameters.lat,
parameters.lng
)
query = { ...locationGeometry, ...query }
}

// add filtering for ages
const ages = filters.filterAges(parameters.minAge, parameters.maxAge)
Expand All @@ -110,11 +140,6 @@ module.exports = {

// 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),
Expand All @@ -129,49 +154,112 @@ module.exports = {
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
* @TODO test this doesn't affect query object
* 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("countQuery")
logger.debug(countQuery)
logger.debug(JSON.stringify(countQuery))
return countQuery
},

/**
*
* @param {*} query
* @param {*} perPage
* @param {*} page
* @returns
*/
async executeQuery(query, perPage, page) {
async executeQuery(query, perPage, page, parameters) {
const Service = db().collection("indexed_services")

const queryProjection = query.$text
? {
let queryProjection = {}
let sort = {}

switch (parameters.searchType) {
case "keyword":
sort = {
score: { $meta: "textScore" },
updated_at: -1,
}
queryProjection = {
...projection,
score: { $meta: "textScore" },
}
: {
break
case "location":
case "keyword_location":
sort = {}
queryProjection = {
...projection,
}

const sort = query.$text
? {
score: { $meta: "textScore" },
break
default:
sort = {
updated_at: -1,
}
: {
updated_at: -1,
queryProjection = {
...projection,
}
break
}

logger.debug("query")
logger.debug(query)
logger.debug(JSON.stringify(query))

const [results, count] = await Promise.all([
Service.find(query)
.project(queryProjection)
.sort(sort)
.limit(perPage)
.skip((page - 1) * perPage)
.toArray(),
Service.countDocuments(query),
])

return { results, count }
if (
parameters.searchType === "location" ||
parameters.searchType === "keyword_location"
) {
const countQuery = this.createCountQuery(query)
const [results, count] = await Promise.all([
Service.find(query)
.project(queryProjection)
.sort(sort)
.limit(perPage)
.skip((page - 1) * perPage)
.toArray(),
Service.countDocuments(countQuery),
])
return { results, count }
} else {
const [results, count] = await Promise.all([
Service.find(query)
.project(queryProjection)
.sort(sort)
.limit(perPage)
.skip((page - 1) * perPage)
.toArray(),
Service.countDocuments(query),
])
return { results, count }
}
},

/**
Expand Down
43 changes: 43 additions & 0 deletions src/lib/filters.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,26 @@ const { db } = require("../db")
const logger = require("../../utils/logger")

module.exports = {
locationGeometry: (lat, lng) => {
let query = {}
if (lat && lng) {
query["service_at_locations.location.geometry"] = {
$nearSphere: {
$geometry: {
type: "Point",
coordinates: [parseFloat(lng), parseFloat(lat)],
},
$maxDistance: 20 * 1609.34, // miles x 1609.34 = Distance in meters
},
}
}
return query
},

/**
* @deprecated because we've gone back to nearSphere for now
* @returns
*/
filterLocation: (lat, lng, keywordSearch) => {
if (lat !== undefined && lng !== undefined) {
logger.debug(
Expand Down Expand Up @@ -67,6 +87,29 @@ module.exports = {
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 => {
let query = {}
if (keywords) {
const Service = db().collection("indexed_services")
const docs = await Service.find({
$text: { $search: keywords },
}).toArray()
query._id = { $in: docs.map(doc => doc._id) }
}
return query
},

// This filter returns all services with an age range overlapping with the
// range supplied by the user.
// min_age=0&max_age=18
Expand Down

0 comments on commit 7749f82

Please sign in to comment.