Add support for partial PIS matches by skipping first n stops

Signed-off-by: Fred Boniface <fred@fjla.uk>
This commit is contained in:
Fred Boniface 2023-11-29 22:58:08 +00:00
parent 820c8b0dd4
commit e01cdd2f7e
5 changed files with 180 additions and 71 deletions

View File

@ -33,7 +33,7 @@
"zlib": "^1.0.5" "zlib": "^1.0.5"
}, },
"devDependencies": { "devDependencies": {
"@owlboard/ts-types": "^0.1.0", "@owlboard/ts-types": "^0.1.2",
"@types/jest": "^29.5.3", "@types/jest": "^29.5.3",
"eslint": "^8.39.0", "eslint": "^8.39.0",
"jest": "^29.6.2", "jest": "^29.6.2",

View File

@ -48,13 +48,13 @@ async function random(req, res, next) {
} }
async function testingTiplocArray(req, res, next) { async function testingTiplocArray(req, res, next) {
let array = JSON.parse(req.params.array) let array = JSON.parse(req.params.array);
return pis.findByTiplocArray(array) res.json(await pis.findByTiplocArray(array));
} }
module.exports = { module.exports = {
byStartEndCRS, byStartEndCRS,
byCode, byCode,
random, random,
testingTiplocArray testingTiplocArray,
}; };

View File

@ -5,9 +5,9 @@ module.exports = async function authCheck(req, res, next) {
//log.out("authMiddlewares: Checking authentication", "dbug"); //log.out("authMiddlewares: Checking authentication", "dbug");
logger.logger.debug("Auth check starting"); logger.logger.debug("Auth check starting");
if (process.env.NODE_ENV === "development") { if (process.env.NODE_ENV === "development") {
logger.logger.warn("DEVELOPMENT MODE - AUTHENTICATION DISABLED") logger.logger.warn("DEVELOPMENT MODE - AUTHENTICATION DISABLED");
res.isAuthed = true; res.isAuthed = true;
return next() return next();
} }
try { try {
var uuid = req.headers.uuid; var uuid = req.headers.uuid;

View File

@ -3,8 +3,15 @@
const db = require("../services/dbAccess.services"); const db = require("../services/dbAccess.services");
const clean = require("../utils/sanitizer.utils"); const clean = require("../utils/sanitizer.utils");
import type { OB_Pis_SimpleObject } from "@owlboard/ts-types";
import { logger } from "../utils/logger.utils"; import { logger } from "../utils/logger.utils";
import { queryAggregate } from "./dbAccess.services"; import { queryAggregate } from "./dbAccess.services";
import {
getPartialEndTiplocMatchPipeline,
getFullTiplocMatchPipeline,
} from "../utils/pis.utils";
import { Document } from "mongodb";
const supported = ["GW", "UK"]; const supported = ["GW", "UK"];
@ -33,7 +40,9 @@ async function findPisByOrigDest(start: string, end: string) {
return search; return search;
} }
async function findPisByCode(code: string) { async function findPisByCode(
code: string
): Promise<OB_Pis_SimpleObject | null> {
logger.debug(`pisServices.findPisByCode: Searching for PIS code: ${code}`); logger.debug(`pisServices.findPisByCode: Searching for PIS code: ${code}`);
const cleanCode = clean.removeNonNumeric(code); const cleanCode = clean.removeNonNumeric(code);
const query = { const query = {
@ -43,75 +52,73 @@ async function findPisByCode(code: string) {
return await search; return await search;
} }
export async function findByTiplocArray(tiplocArray: string[]) { // Queries the database for PIS codes that match the given TIPLOC array
// If any of the TIPLOCS in an array is RDG4AB - Replace it with READING. export async function findByTiplocArray(
// ^^ Double check those though before doing it. tiplocArray: string[]
const query = { ): Promise<OB_Pis_SimpleObject | null> {
tiplocs: tiplocArray, // Firstly fix errant TIPLOCS such as RDNG4AB which will never return a result
}; // currently only works with RDNG4AB - checks for presence of RDNG4AB first to
const res = await db.query("pis", query); // avoid iterating over every array ever searched for.
// If res is empty then try to find partial match if (tiplocArray.includes("RDNG4AB")) {
if (res.length) { fixErrantTiplocs(tiplocArray);
return res; }
// PERFORMANCE NOTE:
// The majority of queries will return a full match,
// this means that a more performant pipeline is used
// to find a full match and only then are more
// complicated pipelines used to find partial matches
// if the first pipeline returns nothing.
try {
const exactMatch = await findExactMatchByTiploc(tiplocArray);
if (exactMatch) {
return convertDocument(exactMatch, "none");
} else { } else {
let partial = await findPartialMatchByTiploc(tiplocArray); const partialEnd = await findPartialEndMatchByTiploc(tiplocArray);
return partial; if (partialEnd) {
return convertDocument(partialEnd, "first");
} else {
// Here, I should search for a partialStart match. For now return null.
return null;
}
}
} catch (err) {
logger.error(err, "Error in findByTiplocArray");
return null;
} }
} }
async function findPartialMatchByTiploc(tiplocArray: string[]) { // Uses a pipeline to find an exact match
// Do some magic here, similar to findPisByOrigDest but async function findExactMatchByTiploc(array: string[]): Promise<Document> {
// ensuring that the stops in the array match the last const pipeline = getFullTiplocMatchPipeline(array);
// x number of items in the array. const res = await queryAggregate("pis", pipeline);
return res[0];
const pipeline = [
{
$addFields: {
reversedTiplocs: {
$reverseArray: "$tiplocs",
},
reversedQueryArray: {
$reverseArray: tiplocArray,
},
},
},
{
$match: {
$expr: {
$gt: [
{
$indexOfArray: [
"$reversedTiplocs",
{
$arrayElemAt: ["$reversedQueryArray", 0],
} }
],
},
-1,
],
},
},
},
{
$project: {
code: 1,
skipStops: {
$subtract: [
{
$size: "$tiplocs",
},
{
$size: tiplocArray,
},
],
},
},
},
];
const res = await queryAggregate('pis', pipeline) // Uses a pipeline to find a partial match - only supporting
console.log(JSON.stringify(res)) // codes ending with the correct stops for now.
return res async function findPartialEndMatchByTiploc(array: string[]): Promise<Document> {
const pipeline = getPartialEndTiplocMatchPipeline(array);
const res = await queryAggregate("pis", pipeline);
return res[0];
}
function convertDocument(doc: Document, skipType: string): OB_Pis_SimpleObject {
return {
code: doc.code,
toc: doc.toc,
skipCount: doc.skipStops,
skipType: skipType,
};
}
// Changes any instance of 'RDNG4AB' to 'RDNGSTN'
function fixErrantTiplocs(input: string[]): void {
input.forEach((value, index, array) => {
if (value === "RDNG4AB") {
array[index] = "RDNGSTN";
}
});
} }
module.exports = { module.exports = {

102
src/utils/pis.utils.ts Normal file
View File

@ -0,0 +1,102 @@
export function getPartialEndTiplocMatchPipeline(query: string[]) {
return [
{
$match: {
tiplocs: {
$all: query,
},
},
},
{
$addFields: {
reversedTiplocs: {
$reverseArray: "$tiplocs",
},
query: {
$literal: query,
},
},
},
{
$addFields: {
reversedQuery: {
$reverseArray: "$query",
},
},
},
{
$match: {
$expr: {
$eq: [
{
$slice: [
"$reversedTiplocs",
0,
{
$size: "$reversedQuery",
},
],
},
"$reversedQuery",
],
},
},
},
{
$addFields: {
skipStops: {
$subtract: [
{
$size: "$tiplocs",
},
{
$size: "$reversedQuery",
},
],
},
},
},
{
$sort: {
skipStops: 1,
},
},
{
$limit: 1,
},
{
$project: {
code: 1,
skipStops: 1,
toc: 1,
_id: 0,
},
},
];
}
export function getFullTiplocMatchPipeline(query: string[]) {
return [
{
$match: {
tiplocs: query,
},
},
{
$limit: 1,
},
{
$addFields: {
skipStops: 0,
},
},
{
$project: {
code: 1,
toc: 1,
skipStops: 1,
_id: 0,
},
},
];
}