Public Access
1
0
Files
leads4less/server/src/search/run-search.ts
T

169 lines
4.8 KiB
TypeScript

import type { Pool } from 'pg';
import { getEnv } from '../config/env.js';
import { buildBusinessPayload, collectPlaces, geocodeLocation } from './google-places.js';
import { completeSearchJob, createSearchJob, createSearchJobForCoordinates, failSearchJob, updateSearchJobCenter, upsertBusiness, upsertSearchJobResult } from './repository.js';
import type { RunSearchInput, RunSearchResult } from './types.js';
function buildMatchedKeywords(keywords?: string) {
return keywords
? keywords
.split(',')
.map((keyword) => keyword.trim())
.filter(Boolean)
: [];
}
async function executeSearchJobAtCoordinates(
db: Pool,
input: {
jobId: string;
userId: string;
lat: number;
lng: number;
radiusKm: number;
businessType: string;
keywords?: string;
queryContextTerms?: string[];
},
) {
const env = getEnv();
if (!env.GOOGLE_MAPS_SERVER_KEY) {
throw new Error('GOOGLE_MAPS_SERVER_KEY is required for running research.');
}
const places = await collectPlaces({
apiKey: env.GOOGLE_MAPS_SERVER_KEY,
textQuery: [input.businessType, input.keywords, ...(input.queryContextTerms ?? [])].filter(Boolean).join(' '),
lat: input.lat,
lng: input.lng,
radiusKm: input.radiusKm,
});
const matchedKeywords = buildMatchedKeywords(input.keywords);
const capturedAt = new Date().toISOString();
let totalResults = 0;
for (const [index, place] of places.entries()) {
if (!place.id || !place.displayName?.text) {
continue;
}
const businessId = await upsertBusiness(db, input.userId, buildBusinessPayload(place, input.businessType));
await upsertSearchJobResult(db, {
userId: input.userId,
searchJobId: input.jobId,
businessId,
matchedKeywords: matchedKeywords.length > 0 ? matchedKeywords : null,
rank: index + 1,
capturedAt,
});
totalResults += 1;
}
return completeSearchJob(db, input.jobId, totalResults);
}
export async function runSearchForUser(db: Pool, userId: string, payload: RunSearchInput): Promise<RunSearchResult> {
const hasProvidedCoordinates = typeof payload.lat === 'number' && typeof payload.lng === 'number';
const job = hasProvidedCoordinates
? await createSearchJobForCoordinates(db, userId, {
name: payload.name || `${payload.businessType} in ${payload.location}`,
city: payload.location,
address: payload.location,
radiusKm: payload.radiusKm,
businessType: payload.businessType,
keywords: payload.keywords,
lat: payload.lat!,
lng: payload.lng!,
})
: await createSearchJob(db, userId, payload);
const jobId = job.id;
try {
const env = getEnv();
if (!env.GOOGLE_MAPS_SERVER_KEY) {
throw new Error('GOOGLE_MAPS_SERVER_KEY is required for running research.');
}
const resolvedCenter = hasProvidedCoordinates
? { lat: payload.lat!, lng: payload.lng! }
: await geocodeLocation(payload.location, env.GOOGLE_MAPS_SERVER_KEY);
if (!hasProvidedCoordinates) {
await updateSearchJobCenter(db, jobId, resolvedCenter.lat, resolvedCenter.lng);
}
const completedJob = await executeSearchJobAtCoordinates(db, {
jobId,
userId,
lat: resolvedCenter.lat,
lng: resolvedCenter.lng,
radiusKm: payload.radiusKm,
businessType: payload.businessType,
keywords: payload.keywords,
});
return {
job: completedJob,
totalResults: completedJob.totalResults,
};
} catch (error) {
await failSearchJob(db, jobId);
throw error;
}
}
export async function runSearchForPostalArea(db: Pool, userId: string, input: {
name: string;
city?: string | null;
address?: string | null;
postalCode?: string | null;
countryCode?: string | null;
radiusKm: number;
businessType: string;
keywords?: string;
lat: number;
lng: number;
deepResearchBatchId: string;
postalAreaId: string;
queryContextTerms?: string[];
}): Promise<RunSearchResult> {
const job = await createSearchJobForCoordinates(db, userId, {
name: input.name,
city: input.city,
address: input.address,
postalCode: input.postalCode,
countryCode: input.countryCode,
radiusKm: input.radiusKm,
businessType: input.businessType,
keywords: input.keywords,
lat: input.lat,
lng: input.lng,
deepResearchBatchId: input.deepResearchBatchId,
postalAreaId: input.postalAreaId,
});
try {
const completedJob = await executeSearchJobAtCoordinates(db, {
jobId: job.id,
userId,
lat: input.lat,
lng: input.lng,
radiusKm: input.radiusKm,
businessType: input.businessType,
keywords: input.keywords,
queryContextTerms: input.queryContextTerms,
});
return {
job: completedJob,
totalResults: completedJob.totalResults,
};
} catch (error) {
await failSearchJob(db, job.id);
throw error;
}
}