matcher with only db selection

This commit is contained in:
sbriat 2023-09-06 15:17:51 +02:00
parent e0030aba73
commit 98530af14a
13 changed files with 354 additions and 61 deletions

View File

@ -54,7 +54,7 @@ export class MatchQueryHandler implements IQueryHandler {
maxDetourDurationRatio: this._defaultParams.MAX_DETOUR_DURATION_RATIO,
})
.setDatesAndSchedule(this.datetimeTransformer);
await query.setRoutes(this.routeProvider);
await query.setRoute(this.routeProvider);
let algorithm: Algorithm;
switch (query.algorithmType) {

View File

@ -112,9 +112,10 @@ export class MatchQuery extends QueryBase {
setDatesAndSchedule = (
datetimeTransformer: DateTimeTransformerPort,
): MatchQuery => {
const initialFromDate: string = this.fromDate;
this.fromDate = datetimeTransformer.fromDate(
{
date: this.fromDate,
date: initialFromDate,
time: this.schedule[0].time,
coordinates: {
lon: this.waypoints[0].lon,
@ -126,7 +127,7 @@ export class MatchQuery extends QueryBase {
this.toDate = datetimeTransformer.toDate(
this.toDate,
{
date: this.fromDate,
date: initialFromDate,
time: this.schedule[0].time,
coordinates: {
lon: this.waypoints[0].lon,
@ -164,7 +165,7 @@ export class MatchQuery extends QueryBase {
return this;
};
setRoutes = async (routeProvider: RouteProviderPort): Promise<MatchQuery> => {
setRoute = async (routeProvider: RouteProviderPort): Promise<MatchQuery> => {
const roles: Role[] = [];
if (this.driver) roles.push(Role.DRIVER);
if (this.passenger) roles.push(Role.PASSENGER);
@ -177,7 +178,7 @@ export class MatchQuery extends QueryBase {
};
}
type ScheduleItem = {
export type ScheduleItem = {
day?: number;
time: string;
margin?: number;

View File

@ -2,22 +2,24 @@ import { Frequency, Role } from '@modules/ad/core/domain/ad.types';
import { Candidate } from '../../../types/algorithm.types';
import { Selector } from '../algorithm.abstract';
import { AdReadModel } from '@modules/ad/infrastructure/ad.repository';
import { ScheduleItem } from '../match.query';
import { Waypoint } from '../../../types/waypoint.type';
import { Coordinates } from '../../../types/coordinates.type';
export class PassengerOrientedSelector extends Selector {
select = async (): Promise<Candidate[]> => {
const queryStringRoles: QueryStringRole[] = [];
if (this.query.driver)
queryStringRoles.push({
query: this.createQueryString(Role.DRIVER),
query: this._createQueryString(Role.DRIVER),
role: Role.DRIVER,
});
if (this.query.passenger)
queryStringRoles.push({
query: this.createQueryString(Role.PASSENGER),
query: this._createQueryString(Role.PASSENGER),
role: Role.PASSENGER,
});
console.log(queryStringRoles);
return (
await Promise.all(
queryStringRoles.map<Promise<AdsRole>>(
@ -43,66 +45,251 @@ export class PassengerOrientedSelector extends Selector {
.flat();
};
private createQueryString = (role: Role): string =>
private _createQueryString = (role: Role): string =>
[
this.createSelect(role),
this.createFrom(),
this._createSelect(role),
this._createFrom(),
'WHERE',
this.createWhere(role),
].join(' ');
this._createWhere(role),
]
.join(' ')
.replace(/\s+/g, ' '); // remove duplicate spaces for easy debug !
private createSelect = (role: Role): string =>
private _createSelect = (role: Role): string =>
[
`SELECT
ad.uuid,driver,passenger,frequency,public.st_astext(matcher.ad.waypoints) as waypoints,
"fromDate","toDate",
"seatsProposed","seatsRequested",
strict,
"fwdAzimuth","backAzimuth",
`SELECT \
ad.uuid,driver,passenger,frequency,public.st_astext(ad.waypoints) as waypoints,\
"fromDate","toDate",\
"seatsProposed","seatsRequested",\
strict,\
"fwdAzimuth","backAzimuth",\
si.day,si.time,si.margin`,
role == Role.DRIVER ? this.selectAsDriver() : this.selectAsPassenger(),
role == Role.DRIVER ? this._selectAsDriver() : this._selectAsPassenger(),
].join();
private selectAsDriver = (): string =>
private _selectAsDriver = (): string =>
`${this.query.route?.driverDuration} as duration,${this.query.route?.driverDistance} as distance`;
private selectAsPassenger = (): string =>
private _selectAsPassenger = (): string =>
`"driverDuration" as duration,"driverDistance" as distance`;
private createFrom = (): string =>
private _createFrom = (): string =>
'FROM ad LEFT JOIN schedule_item si ON ad.uuid = si."adUuid"';
private createWhere = (role: Role): string =>
[this.whereRole(role), this.whereStrict(), this.whereDate()].join(' AND ');
private _createWhere = (role: Role): string =>
[
this._whereRole(role),
this._whereStrict(),
this._whereDate(),
this._whereSchedule(role),
this._whereAzimuth(),
this._whereProportion(role),
this._whereRemoteness(role),
]
.filter((where: string) => where != '')
.join(' AND ');
private whereRole = (role: Role): string =>
private _whereRole = (role: Role): string =>
role == Role.PASSENGER ? 'driver=True' : 'passenger=True';
private whereStrict = (): string =>
private _whereStrict = (): string =>
this.query.strict
? this.query.frequency == Frequency.PUNCTUAL
? `frequency='${Frequency.PUNCTUAL}'`
: `frequency='${Frequency.RECURRENT}'`
: '';
private whereDate = (): string => {
const whereDate = `(
(
"fromDate" <= '${this.query.fromDate}' and "fromDate" <= '${this.query.toDate}' and
"toDate" >= '${this.query.toDate}' and "toDate" >= '${this.query.fromDate}'
) OR (
"fromDate" >= '${this.query.fromDate}' and "fromDate" <= '${this.query.toDate}' and
"toDate" <= '${this.query.toDate}' and "toDate" >= '${this.query.fromDate}'
) OR (
"fromDate" <= '${this.query.fromDate}' and "fromDate" <= '${this.query.toDate}' and
"toDate" <= '${this.query.toDate}' and "toDate" >= '${this.query.fromDate}'
) OR (
"fromDate" >= '${this.query.fromDate}' and "fromDate" <= '${this.query.toDate}' and
"toDate" >= '${this.query.toDate}' and "toDate" >= '${this.query.fromDate}'
)
private _whereDate = (): string =>
`(\
(\
"fromDate" <= '${this.query.fromDate}' AND "fromDate" <= '${this.query.toDate}' AND\
"toDate" >= '${this.query.toDate}' AND "toDate" >= '${this.query.fromDate}'\
) OR (\
"fromDate" >= '${this.query.fromDate}' AND "fromDate" <= '${this.query.toDate}' AND\
"toDate" <= '${this.query.toDate}' AND "toDate" >= '${this.query.fromDate}'\
) OR (\
"fromDate" <= '${this.query.fromDate}' AND "fromDate" <= '${this.query.toDate}' AND\
"toDate" <= '${this.query.toDate}' AND "toDate" >= '${this.query.fromDate}'\
) OR (\
"fromDate" >= '${this.query.fromDate}' AND "fromDate" <= '${this.query.toDate}' AND\
"toDate" >= '${this.query.toDate}' AND "toDate" >= '${this.query.fromDate}'\
)\
)`;
return whereDate;
private _whereSchedule = (role: Role): string => {
const schedule: string[] = [];
// we need full dates to compare times, because margins can lead to compare on previous or next day
// -first we establish a base calendar (up to a week)
const scheduleDates: Date[] = this._datesBetweenBoundaries(
this.query.fromDate,
this.query.toDate,
);
// - then we compare each resulting day of the schedule with each day of calendar,
// adding / removing margin depending on the role
scheduleDates.map((date: Date) => {
this.query.schedule
.filter(
(scheduleItem: ScheduleItem) => date.getDay() == scheduleItem.day,
)
.map((scheduleItem: ScheduleItem) => {
switch (role) {
case Role.PASSENGER:
schedule.push(this._wherePassengerSchedule(date, scheduleItem));
break;
case Role.DRIVER:
schedule.push(this._whereDriverSchedule(date, scheduleItem));
break;
}
});
});
if (schedule.length > 0) {
return ['(', schedule.join(' OR '), ')'].join('');
}
return '';
};
private _wherePassengerSchedule = (
date: Date,
scheduleItem: ScheduleItem,
): string => {
let maxDepartureDatetime: Date = new Date(date);
maxDepartureDatetime.setHours(parseInt(scheduleItem.time.split(':')[0]));
maxDepartureDatetime.setMinutes(parseInt(scheduleItem.time.split(':')[1]));
maxDepartureDatetime = this._addMargin(
maxDepartureDatetime,
scheduleItem.margin as number,
);
// we want the min departure time of the driver to be before the max departure time of the passenger
return `make_timestamp(\
${maxDepartureDatetime.getFullYear()},\
${maxDepartureDatetime.getMonth() + 1},\
${maxDepartureDatetime.getDate()},\
CAST(EXTRACT(hour from time) as integer),\
CAST(EXTRACT(minute from time) as integer),0) - interval '1 second' * margin <=\
make_timestamp(\
${maxDepartureDatetime.getFullYear()},\
${maxDepartureDatetime.getMonth() + 1},\
${maxDepartureDatetime.getDate()},${maxDepartureDatetime.getHours()},${maxDepartureDatetime.getMinutes()},0)`;
};
private _whereDriverSchedule = (
date: Date,
scheduleItem: ScheduleItem,
): string => {
let minDepartureDatetime: Date = new Date(date);
minDepartureDatetime.setHours(parseInt(scheduleItem.time.split(':')[0]));
minDepartureDatetime.setMinutes(parseInt(scheduleItem.time.split(':')[1]));
minDepartureDatetime = this._addMargin(
minDepartureDatetime,
-(scheduleItem.margin as number),
);
// we want the max departure time of the passenger to be after the min departure time of the driver
return `make_timestamp(\
${minDepartureDatetime.getFullYear()},
${minDepartureDatetime.getMonth() + 1},
${minDepartureDatetime.getDate()},\
CAST(EXTRACT(hour from time) as integer),\
CAST(EXTRACT(minute from time) as integer),0) + interval '1 second' * margin >=\
make_timestamp(\
${minDepartureDatetime.getFullYear()},
${minDepartureDatetime.getMonth() + 1},
${minDepartureDatetime.getDate()},${minDepartureDatetime.getHours()},${minDepartureDatetime.getMinutes()},0)`;
};
private _whereAzimuth = (): string => {
if (!this.query.useAzimuth) return '';
const { minAzimuth, maxAzimuth } = this._azimuthRange(
this.query.route?.backAzimuth as number,
this.query.azimuthMargin as number,
);
if (minAzimuth <= maxAzimuth)
return `("fwdAzimuth" <= ${minAzimuth} OR "fwdAzimuth" >= ${maxAzimuth})`;
return `("fwdAzimuth" <= ${minAzimuth} AND "fwdAzimuth" >= ${maxAzimuth})`;
};
private _whereProportion = (role: Role): string => {
if (!this.query.useProportion) return '';
switch (role) {
case Role.PASSENGER:
return `(${this.query.route?.passengerDistance}>(${this.query.proportion}*"driverDistance"))`;
case Role.DRIVER:
return `("passengerDistance">(${this.query.proportion}*${this.query.route?.driverDistance}))`;
}
};
private _whereRemoteness = (role: Role): string => {
this.query.waypoints.sort(
(firstWaypoint: Waypoint, secondWaypoint: Waypoint) =>
firstWaypoint.position - secondWaypoint.position,
);
switch (role) {
case Role.PASSENGER:
return `\
public.st_distance('POINT(${this.query.waypoints[0].lon} ${
this.query.waypoints[0].lat
})'::public.geography,direction)<\
${this.query.remoteness} AND \
public.st_distance('POINT(${
this.query.waypoints[this.query.waypoints.length - 1].lon
} ${
this.query.waypoints[this.query.waypoints.length - 1].lat
})'::public.geography,direction)<\
${this.query.remoteness}`;
case Role.DRIVER:
const lineStringPoints: string[] = [];
this.query.route?.points.forEach((point: Coordinates) =>
lineStringPoints.push(
`public.st_makepoint(${point.lon},${point.lat})`,
),
);
const lineString = [
'public.st_makeline( ARRAY[ ',
lineStringPoints.join(','),
'] )::public.geography',
].join('');
return `\
public.st_distance( public.st_startpoint(waypoints::public.geometry), ${lineString})<\
${this.query.remoteness} AND \
public.st_distance( public.st_endpoint(waypoints::public.geometry), ${lineString})<\
${this.query.remoteness}`;
}
};
private _datesBetweenBoundaries = (
firstDate: string,
lastDate: string,
max = 7,
): Date[] => {
const fromDate: Date = new Date(firstDate);
const toDate: Date = new Date(lastDate);
const dates: Date[] = [];
let count = 0;
for (
let date = fromDate;
date <= toDate;
date.setDate(date.getDate() + 1)
) {
dates.push(new Date(date));
count++;
if (count == max) break;
}
return dates;
};
private _addMargin = (date: Date, marginInSeconds: number): Date => {
date.setTime(date.getTime() + marginInSeconds * 1000);
return date;
};
private _azimuthRange = (
azimuth: number,
margin: number,
): { minAzimuth: number; maxAzimuth: number } => ({
minAzimuth:
azimuth - margin < 0 ? azimuth - margin + 360 : azimuth - margin,
maxAzimuth:
azimuth + margin > 360 ? azimuth + margin - 360 : azimuth + margin,
});
}
export type QueryStringRole = {

View File

@ -100,10 +100,12 @@ export class AdRepository
);
}
getCandidates = async (queryString: string): Promise<AdReadModel[]> =>
this.toAdReadModels(
getCandidates = async (queryString: string): Promise<AdReadModel[]> => {
// console.log(queryString);
return this.toAdReadModels(
(await this.prismaRaw.$queryRawUnsafe(queryString)) as UngroupedAdModel[],
);
};
private toAdReadModels = (
ungroupedAds: UngroupedAdModel[],

View File

@ -20,7 +20,7 @@ export class TimeConverter implements TimeConverterPort {
date: string,
time: string,
timezone: string,
dst = true,
dst = false,
): Date =>
new Date(
new DateTime(

View File

@ -1,3 +1,5 @@
export class MatchResponseDto {
import { ResponseBase } from '@mobicoop/ddd-library';
export class MatchResponseDto extends ResponseBase {
adId: string;
}

View File

@ -1,11 +1,12 @@
import { Controller, UsePipes } from '@nestjs/common';
import { GrpcMethod, RpcException } from '@nestjs/microservices';
import { RpcValidationPipe } from '@mobicoop/ddd-library';
import { ResponseBase, RpcValidationPipe } from '@mobicoop/ddd-library';
import { RpcExceptionCode } from '@mobicoop/ddd-library';
import { MatchPaginatedResponseDto } from '../dtos/match.paginated.response.dto';
import { QueryBus } from '@nestjs/cqrs';
import { MatchRequestDto } from './dtos/match.request.dto';
import { MatchQuery } from '@modules/ad/core/application/queries/match/match.query';
import { MatchEntity } from '@modules/ad/core/domain/match.entity';
@UsePipes(
new RpcValidationPipe({
@ -20,13 +21,18 @@ export class MatchGrpcController {
@GrpcMethod('MatcherService', 'Match')
async match(data: MatchRequestDto): Promise<MatchPaginatedResponseDto> {
try {
const matches = await this.queryBus.execute(new MatchQuery(data));
return {
data: matches,
const matches: MatchEntity[] = await this.queryBus.execute(
new MatchQuery(data),
);
return new MatchPaginatedResponseDto({
data: matches.map((match: MatchEntity) => ({
...new ResponseBase(match),
adId: match.getProps().adId,
})),
page: 1,
perPage: 5,
total: matches.length,
};
});
} catch (e) {
throw new RpcException({
code: RpcExceptionCode.UNKNOWN,

View File

@ -55,6 +55,7 @@ enum AlgorithmType {
message Match {
string id = 1;
string adId = 2;
}
message Matches {

View File

@ -1,10 +1,12 @@
import {
AD_REPOSITORY,
AD_ROUTE_PROVIDER,
INPUT_DATETIME_TRANSFORMER,
PARAMS_PROVIDER,
} from '@modules/ad/ad.di-tokens';
import { DateTimeTransformerPort } from '@modules/ad/core/application/ports/datetime-transformer.port';
import { DefaultParamsProviderPort } from '@modules/ad/core/application/ports/default-params-provider.port';
import { RouteProviderPort } from '@modules/ad/core/application/ports/route-provider.port';
import { MatchQuery } from '@modules/ad/core/application/queries/match/match.query';
import { MatchQueryHandler } from '@modules/ad/core/application/queries/match/match.query-handler';
import { AlgorithmType } from '@modules/ad/core/application/types/algorithm.types';
@ -72,6 +74,10 @@ const mockInputDateTimeTransformer: DateTimeTransformerPort = {
time: jest.fn(),
};
const mockRouteProvider: RouteProviderPort = {
getBasic: jest.fn(),
};
describe('Match Query Handler', () => {
let matchQueryHandler: MatchQueryHandler;
@ -91,6 +97,10 @@ describe('Match Query Handler', () => {
provide: INPUT_DATETIME_TRANSFORMER,
useValue: mockInputDateTimeTransformer,
},
{
provide: AD_ROUTE_PROVIDER,
useValue: mockRouteProvider,
},
],
}).compile();

View File

@ -1,5 +1,6 @@
import { DateTimeTransformerPort } from '@modules/ad/core/application/ports/datetime-transformer.port';
import { DefaultParams } from '@modules/ad/core/application/ports/default-params.type';
import { RouteProviderPort } from '@modules/ad/core/application/ports/route-provider.port';
import { MatchQuery } from '@modules/ad/core/application/queries/match/match.query';
import { AlgorithmType } from '@modules/ad/core/application/types/algorithm.types';
import { Waypoint } from '@modules/ad/core/application/types/waypoint.type';
@ -49,6 +50,23 @@ const mockInputDateTimeTransformer: DateTimeTransformerPort = {
time: jest.fn().mockImplementation(() => '23:05'),
};
const mockRouteProvider: RouteProviderPort = {
getBasic: jest
.fn()
.mockImplementationOnce(() => ({
driverDistance: undefined,
driverDuration: undefined,
passengerDistance: 150120,
passengerDuration: 6540,
fwdAzimuth: 276,
backAzimuth: 96,
points: [],
}))
.mockImplementationOnce(() => {
throw new Error();
}),
};
describe('Match Query', () => {
it('should set default values', async () => {
const matchQuery = new MatchQuery({
@ -124,4 +142,40 @@ describe('Match Query', () => {
expect(matchQuery.seatsProposed).toBe(3);
expect(matchQuery.seatsRequested).toBe(1);
});
it('should set route', async () => {
const matchQuery = new MatchQuery({
frequency: Frequency.PUNCTUAL,
fromDate: '2023-08-28',
toDate: '2023-08-28',
schedule: [
{
time: '01:05',
},
],
waypoints: [originWaypoint, destinationWaypoint],
});
await matchQuery.setRoute(mockRouteProvider);
expect(matchQuery.route?.driverDistance).toBeUndefined();
expect(matchQuery.route?.passengerDistance).toBe(150120);
});
it('should throw an exception if route is not found', async () => {
const matchQuery = new MatchQuery({
driver: true,
passenger: false,
frequency: Frequency.PUNCTUAL,
fromDate: '2023-08-28',
toDate: '2023-08-28',
schedule: [
{
time: '01:05',
},
],
waypoints: [originWaypoint, destinationWaypoint],
});
await expect(matchQuery.setRoute(mockRouteProvider)).rejects.toBeInstanceOf(
Error,
);
});
});

View File

@ -32,16 +32,44 @@ const matchQuery = new MatchQuery({
driver: true,
passenger: true,
frequency: Frequency.PUNCTUAL,
fromDate: '2023-08-28',
toDate: '2023-08-28',
fromDate: '2023-06-21',
toDate: '2023-06-21',
useAzimuth: true,
azimuthMargin: 10,
useProportion: true,
proportion: 0.3,
schedule: [
{
day: 3,
time: '07:05',
margin: 900,
},
],
strict: false,
waypoints: [originWaypoint, destinationWaypoint],
});
matchQuery.route = {
driverDistance: 150120,
driverDuration: 6540,
passengerDistance: 150120,
passengerDuration: 6540,
fwdAzimuth: 276,
backAzimuth: 96,
points: [
{
lat: 48.689445,
lon: 6.17651,
},
{
lat: 48.7566,
lon: 4.3522,
},
{
lat: 48.8566,
lon: 2.3522,
},
],
};
const mockMatcherRepository: AdRepositoryPort = {
insertExtra: jest.fn(),

View File

@ -60,6 +60,7 @@ describe('Time Converter', () => {
parisDate,
parisTime,
'Europe/Paris',
true,
);
expect(utcDate.toISOString()).toBe('2023-06-22T10:00:00.000Z');
});

View File

@ -1,6 +1,7 @@
import { RpcExceptionCode } from '@mobicoop/ddd-library';
import { AlgorithmType } from '@modules/ad/core/application/types/algorithm.types';
import { Frequency } from '@modules/ad/core/domain/ad.types';
import { MatchEntity } from '@modules/ad/core/domain/match.entity';
import { MatchRequestDto } from '@modules/ad/interface/grpc-controllers/dtos/match.request.dto';
import { WaypointDto } from '@modules/ad/interface/grpc-controllers/dtos/waypoint.dto';
import { MatchGrpcController } from '@modules/ad/interface/grpc-controllers/match.grpc-controller';
@ -49,12 +50,12 @@ const mockQueryBus = {
execute: jest
.fn()
.mockImplementationOnce(() => [
{
adId: 1,
},
{
adId: 2,
},
MatchEntity.create({
adId: '0cc87f3b-7a27-4eff-9850-a5d642c2a0c3',
}),
MatchEntity.create({
adId: 'e4cc156f-aaa5-4270-bf6f-82f5a230d748',
}),
])
.mockImplementationOnce(() => {
throw new Error();