diff --git a/src/lib/server/db/employee.ts b/src/lib/server/db/employee.ts index 7d61355..4350607 100644 --- a/src/lib/server/db/employee.ts +++ b/src/lib/server/db/employee.ts @@ -59,8 +59,9 @@ export async function getEmployees( entryBuilding: employeeEntry.building, exitTimestamp: max(employeeExit.timestamp), leastDistance: sqlLeast([ - sqlLevenshteinDistance(sqlConcat([employee.fname], ' '), nonEmptySearchQuery), - sqlLevenshteinDistance(sqlConcat([employee.lname], ' '), nonEmptySearchQuery), + sqlLevenshteinDistance(sqlConcat([employee.identifier]), nonEmptySearchQuery), + sqlLevenshteinDistance(sqlConcat([employee.fname]), nonEmptySearchQuery), + sqlLevenshteinDistance(sqlConcat([employee.lname]), nonEmptySearchQuery), sqlLevenshteinDistance( sqlConcat([employee.fname, employee.lname], ' '), nonEmptySearchQuery @@ -69,7 +70,11 @@ export async function getEmployees( sqlConcat([employee.lname, employee.fname], ' '), nonEmptySearchQuery ) - ]).as('least_distance') + ]).as('least_distance'), + leastDistanceIdentifier: sqlLevenshteinDistance( + sqlConcat([employee.identifier]), + nonEmptySearchQuery + ).as('least_distance_identifier') }) .from(employee) .leftJoin(maxEntrySubquery, eq(maxEntrySubquery.employeeId, employee.id)) @@ -107,7 +112,11 @@ export async function getEmployees( maxEntrySubquery.maxEntryTimestamp, employeeEntry.building ) - .orderBy(({ leastDistance, identifier }) => [leastDistance, identifier]) + .orderBy(({ leastDistance, leastDistanceIdentifier, identifier }) => [ + leastDistance, + leastDistanceIdentifier, + identifier + ]) .limit(limit) .offset(offset) : await db diff --git a/src/lib/server/db/fuzzysearch.ts b/src/lib/server/db/fuzzysearch.ts index fec30dd..7b77951 100644 --- a/src/lib/server/db/fuzzysearch.ts +++ b/src/lib/server/db/fuzzysearch.ts @@ -5,6 +5,18 @@ type FuzzySearchFiltersOptions = { substr?: boolean; }; +type LevenshteinOptions = { + insertCost: number; + deleteCost: number; + substitutionCost: number; +}; + +const defaultLevenshteinOptions: LevenshteinOptions = { + insertCost: 1, + deleteCost: 3, + substitutionCost: 2 +}; + export function fuzzySearchFilters( dbFields: Column[], searchQuery: string, @@ -45,6 +57,14 @@ export function fuzzySearchFilters( * Returns the sql for concatenating multiple columns with a separator using CONCAT_WS */ export function sqlConcat(cols: Column[], separator?: string): SQL { + if (cols.length === 0) { + throw new Error('Passed columns length is 0'); + } + + if (cols.length === 1) { + return sql`${cols[0]}`; + } + const sqlCols = cols .map((col) => sql`${col}`) .reduce((prev, curr) => sql`${prev}, ${curr}`); @@ -64,13 +84,22 @@ export function sqlLeast(cols: SQL[]): SQL { /* * Returns the sql for determining if the levenshtein distance is less than or equal to the passed distance */ -export function sqlLevenshtein(col: SQL, input: string, distance: number): SQL { - return sql`LEVENSHTEIN(LOWER(${col}), LOWER(${input})) <= ${distance}`; +export function sqlLevenshtein( + col: SQL, + input: string, + distance: number, + opts: LevenshteinOptions = defaultLevenshteinOptions +): SQL { + return sql`LEVENSHTEIN(LOWER(${input}), LOWER(${col}), ${opts.insertCost}, ${opts.deleteCost}, ${opts.substitutionCost}) <= ${distance}`; } /* * Returns the sql for getting the levenshtein distance */ -export function sqlLevenshteinDistance(col: SQL, input: string): SQL { - return sql`LEVENSHTEIN(LOWER(${col}), LOWER(${input}))`; +export function sqlLevenshteinDistance( + col: SQL, + input: string, + opts: LevenshteinOptions = defaultLevenshteinOptions +): SQL { + return sql`LEVENSHTEIN(LOWER(${input}), LOWER(${col}), ${opts.insertCost}, ${opts.deleteCost}, ${opts.substitutionCost})`; } diff --git a/src/lib/server/db/student.ts b/src/lib/server/db/student.ts index a1bde45..d18766a 100644 --- a/src/lib/server/db/student.ts +++ b/src/lib/server/db/student.ts @@ -59,8 +59,9 @@ export async function getStudents( entryBuilding: studentEntry.building, exitTimestamp: max(studentExit.timestamp), leastDistance: sqlLeast([ - sqlLevenshteinDistance(sqlConcat([student.fname], ' '), nonEmptySearchQuery), - sqlLevenshteinDistance(sqlConcat([student.lname], ' '), nonEmptySearchQuery), + sqlLevenshteinDistance(sqlConcat([student.index]), nonEmptySearchQuery), + sqlLevenshteinDistance(sqlConcat([student.fname]), nonEmptySearchQuery), + sqlLevenshteinDistance(sqlConcat([student.lname]), nonEmptySearchQuery), sqlLevenshteinDistance( sqlConcat([student.fname, student.lname], ' '), nonEmptySearchQuery @@ -69,7 +70,11 @@ export async function getStudents( sqlConcat([student.lname, student.fname], ' '), nonEmptySearchQuery ) - ]).as('least_distance') + ]).as('least_distance'), + leastDistanceIdentifier: sqlLevenshteinDistance( + sqlConcat([student.index]), + nonEmptySearchQuery + ).as('least_distance_identifier') }) .from(student) .leftJoin(maxEntrySubquery, eq(maxEntrySubquery.studentId, student.id)) @@ -105,7 +110,11 @@ export async function getStudents( maxEntrySubquery.maxEntryTimestamp, studentEntry.building ) - .orderBy(({ leastDistance, index }) => [leastDistance, index]) + .orderBy(({ leastDistance, leastDistanceIdentifier, index }) => [ + leastDistance, + leastDistanceIdentifier, + index + ]) .limit(limit) .offset(offset) : await db