Skip to content
Snippets Groups Projects
Commit 8380cb44 authored by Prathviraj Suryakant Thokal's avatar Prathviraj Suryakant Thokal
Browse files

Changes in fuzzy search function

parent bd7f6dea
Branches fuzzySearch
No related tags found
No related merge requests found
......@@ -138,15 +138,17 @@ function inferModelName(relationKey: string): string {
// Customize if your Prisma model names don't match relation keys
return relationKey.charAt(0).toUpperCase() + relationKey.slice(1);
}
interface FuzzySearchArgs {
searchFields: string[];
searchTerm: string;
limit: number;
offset: number;
similariyThreshold?: number;
similarityThreshold?: number;
where?: Record<string, any>;
orderBy?: Record<string, 'asc' | 'desc'>;
select?: string[];
include?: Record<string, boolean>;
distinct?: string[];
}
export type FuzzySearchResult<T> = {
......@@ -159,7 +161,18 @@ export async function fuzzySearch<T>(
model: string,
args: FuzzySearchArgs
): Promise<FuzzySearchResult<T>> {
const { searchFields, searchTerm, limit = 10, offset = 0, similariyThreshold = 0.2, where = {} } = args;
const {
searchFields,
searchTerm,
limit = 10,
offset = 0,
similarityThreshold = 0.1,
where = {},
orderBy,
select,
include,
distinct,
} = args;
if (!searchFields?.length || !searchTerm) {
throw new Error('searchFields and searchTerm are required');
......@@ -180,45 +193,81 @@ export async function fuzzySearch<T>(
if (sanitizedFields.length === 0) {
throw new Error('No valid fields provided for fuzzy search');
}
if (!Number.isInteger(limit) || limit < 0 || limit > 1000) {
throw new Error('Invalid limit');
}
if (!Number.isInteger(offset) || offset < 0) {
throw new Error('Invalid offset');
}
const extended = createExtendedPrisma(prisma);
const fuzzyClauses = sanitizedFields
.map((field) => `similarity("${field}", $1) > ${similariyThreshold}`)
.map((field) => `similarity("${field}", $1) > ${similarityThreshold}`)
.join(' OR ');
const similarityExpressions = sanitizedFields
.map((field) => `similarity("${field}", $1)`)
.join(', ');
// Handle additional `where` clauses
const extraConditions: string[] = [];
const extraValues = [];
const extraValues: any[] = [];
let paramIndex = 2;
for (const [key, value] of Object.entries(where)) {
if (!/^[a-zA-Z0-9_]+$/.test(key)) {
throw new Error(`Invalid column name in where clause: "${key}"`);
}
extraConditions.push(`"${key}" = $${paramIndex}`);
// UUID detection + casting
const isUUID =
typeof value === "string" && /^[0-9a-fA-F-]{36}$/.test(value);
const cast = isUUID ? "::uuid" : "";
extraConditions.push(`"${key}" = $${paramIndex}${cast}`);
extraValues.push(value);
paramIndex++;
}
const fullWhereClause = [
`(${fuzzyClauses})`,
...(extraConditions.length ? [extraConditions.join(' AND ')] : [])
...(extraConditions.length ? [extraConditions.join(' AND ')] : []),
].join(' AND ');
let selectClause = '*';
if (select?.length) {
const sanitizedSelect = select.filter((s) => /^[a-zA-Z0-9_]+$/.test(s));
if (sanitizedSelect.length) {
selectClause = sanitizedSelect.map((s) => `"${s}"`).join(', ');
}
}
let orderByClause = `ORDER BY GREATEST(${similarityExpressions}) DESC`;
if (orderBy) {
const entries = Object.entries(orderBy)
.filter(([key, direction]) =>
/^[a-zA-Z0-9_]+$/.test(key) && /^(asc|desc)$/i.test(direction)
)
.map(([key, direction]) => `"${key}" ${direction.toUpperCase()}`);
if (entries.length) {
orderByClause = `ORDER BY ${entries.join(', ')}`;
}
}
let distinctClause = '';
if (distinct?.length) {
const sanitizedDistinct = distinct.filter((d) => /^[a-zA-Z0-9_]+$/.test(d));
if (sanitizedDistinct.length) {
distinctClause = `DISTINCT ON (${sanitizedDistinct.map((d) => `"${d}"`).join(', ')})`;
}
}
const dataQuery = `
SELECT *
SELECT ${distinctClause} ${selectClause}
FROM "${model}"
WHERE ${fullWhereClause}
ORDER BY GREATEST(${similarityExpressions}) DESC
${orderByClause}
LIMIT ${limit} OFFSET ${offset}
`;
......@@ -229,12 +278,29 @@ export async function fuzzySearch<T>(
`;
const [results, countResult] = await Promise.all([
prisma.$queryRawUnsafe(dataQuery, searchTerm, ...extraValues) as T[],
prisma.$queryRawUnsafe(countQuery, searchTerm, ...extraValues) as { total: bigint }[],
extended.$queryRawUnsafe(dataQuery, searchTerm, ...extraValues) as T[],
extended.$queryRawUnsafe(countQuery, searchTerm, ...extraValues) as { total: bigint }[],
]);
const total = Number(countResult[0]?.total || 0);
const data = results.map((r: any) => decryptFields(model, r));
let data = results;
if (typeof decryptFields === 'function') {
data = results.map((r: any) => decryptFields(model, r));
}
return { data, total };
if (include) {
const includedData = await Promise.all(
data.map((item: any) =>
extended[model].findUnique({
where: { id: item.id },
include,
}) as unknown as T
)
);
return { data: includedData, total: Number(countResult[0]?.total || 0) };
}
return {
data,
total: Number(countResult[0]?.total || 0),
};
}
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment