feat: implement geocoding evaluation scripts and enhance routing service with traffic-aware path processing
This commit is contained in:
@@ -0,0 +1,141 @@
|
||||
import * as fs from 'fs';
|
||||
import * as path from 'path';
|
||||
import axios from 'axios';
|
||||
|
||||
// Constants
|
||||
const API_URL = process.env.TEST_API_URL || 'https://map-saas.intaleqapp.com/api/geocoding/search';
|
||||
const API_KEY = process.env.TEST_API_KEY || 'zP9vL5mK2nQ8xR7jT4wS1yB6hG3fV0cX';
|
||||
let DATA_FILE = path.join(__dirname, '../../../data/golden_tests/queries_syria.json');
|
||||
if (!fs.existsSync(DATA_FILE) && fs.existsSync('/data/data/golden_tests/queries_syria.json')) {
|
||||
DATA_FILE = '/data/data/golden_tests/queries_syria.json'; // Docker environment fallback
|
||||
}
|
||||
|
||||
// Interface for Test Query
|
||||
interface TestQuery {
|
||||
query: string;
|
||||
expected_location: {
|
||||
lat: number;
|
||||
lon: number;
|
||||
};
|
||||
expected_category?: string;
|
||||
tolerance_meters: number;
|
||||
}
|
||||
|
||||
// Interface for API Result
|
||||
interface SearchResult {
|
||||
place_id: number;
|
||||
name: string;
|
||||
latitude: number | string;
|
||||
longitude: number | string;
|
||||
category: string;
|
||||
type: string;
|
||||
distance?: number;
|
||||
}
|
||||
|
||||
// Haversine Formula to calculate distance in meters
|
||||
function calculateDistance(lat1: number, lon1: number, lat2: number, lon2: number): number {
|
||||
const R = 6371e3; // Earth radius in meters
|
||||
const φ1 = (lat1 * Math.PI) / 180;
|
||||
const φ2 = (lat2 * Math.PI) / 180;
|
||||
const Δφ = ((lat2 - lat1) * Math.PI) / 180;
|
||||
const Δλ = ((lon2 - lon1) * Math.PI) / 180;
|
||||
|
||||
const a =
|
||||
Math.sin(Δφ / 2) * Math.sin(Δφ / 2) +
|
||||
Math.cos(φ1) * Math.cos(φ2) * Math.sin(Δλ / 2) * Math.sin(Δλ / 2);
|
||||
const c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));
|
||||
|
||||
return R * c;
|
||||
}
|
||||
|
||||
async function runEvaluation() {
|
||||
console.log('🚀 Starting Geocoding Evaluation (Golden Test Set)...\n');
|
||||
|
||||
if (!fs.existsSync(DATA_FILE)) {
|
||||
console.error(`❌ Data file not found at ${DATA_FILE}`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const rawData = fs.readFileSync(DATA_FILE, 'utf8');
|
||||
const queries: TestQuery[] = JSON.parse(rawData);
|
||||
|
||||
let top1Hits = 0;
|
||||
let top3Hits = 0;
|
||||
let zeroResults = 0;
|
||||
let latencies: number[] = [];
|
||||
|
||||
for (const test of queries) {
|
||||
console.log(`Testing query: "${test.query}"`);
|
||||
|
||||
const startTime = Date.now();
|
||||
try {
|
||||
const response = await axios.get(API_URL, {
|
||||
headers: { 'x-api-key': API_KEY },
|
||||
params: { q: test.query, country: 'syria' }
|
||||
});
|
||||
|
||||
const latency = Date.now() - startTime;
|
||||
latencies.push(latency);
|
||||
|
||||
const results: SearchResult[] = response.data.results || response.data;
|
||||
|
||||
if (!results || results.length === 0) {
|
||||
console.log(` ❌ Zero results returned.`);
|
||||
zeroResults++;
|
||||
continue;
|
||||
}
|
||||
|
||||
let foundInTop1 = false;
|
||||
let foundInTop3 = false;
|
||||
|
||||
for (let i = 0; i < Math.min(results.length, 3); i++) {
|
||||
const res = results[i];
|
||||
const resLat = Number(res.latitude || (res as any).lat);
|
||||
const resLon = Number(res.longitude || (res as any).lon);
|
||||
|
||||
const dist = calculateDistance(test.expected_location.lat, test.expected_location.lon, resLat, resLon);
|
||||
const isMatch = dist <= test.tolerance_meters;
|
||||
|
||||
console.log(` [Rank ${i + 1}] Name: "${res.name_ar || res.name}" | Dist: ${Math.round(dist)}m | Source: ${res.source} | Category: ${res.category}`);
|
||||
|
||||
if (isMatch) {
|
||||
if (i === 0) foundInTop1 = true;
|
||||
foundInTop3 = true;
|
||||
console.log(` ✅ Match found at rank ${i + 1} (Distance: ${Math.round(dist)}m)`);
|
||||
// We found the match, but we still print the rest for debugging
|
||||
}
|
||||
}
|
||||
|
||||
if (!foundInTop3) {
|
||||
const topDist = calculateDistance(test.expected_location.lat, test.expected_location.lon, Number(results[0].latitude || (results[0] as any).lat), Number(results[0].longitude || (results[0] as any).lon));
|
||||
console.log(` ❌ No match in Top-3. Top result distance: ${Math.round(topDist)}m`);
|
||||
}
|
||||
|
||||
if (foundInTop1) top1Hits++;
|
||||
if (foundInTop3) top3Hits++;
|
||||
|
||||
} catch (error: any) {
|
||||
console.error(` ❌ API Error: ${error.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
// Calculate metrics
|
||||
latencies.sort((a, b) => a - b);
|
||||
const p50 = latencies[Math.floor(latencies.length * 0.5)] || 0;
|
||||
const p95 = latencies[Math.floor(latencies.length * 0.95)] || 0;
|
||||
|
||||
const total = queries.length;
|
||||
|
||||
console.log('\n=======================================');
|
||||
console.log('📊 EVALUATION RESULTS');
|
||||
console.log('=======================================');
|
||||
console.log(`Total Queries : ${total}`);
|
||||
console.log(`Top-1 Accuracy : ${((top1Hits / total) * 100).toFixed(1)}% (${top1Hits}/${total})`);
|
||||
console.log(`Top-3 Accuracy : ${((top3Hits / total) * 100).toFixed(1)}% (${top3Hits}/${total})`);
|
||||
console.log(`Zero Results : ${((zeroResults / total) * 100).toFixed(1)}% (${zeroResults}/${total})`);
|
||||
console.log(`p50 Latency : ${p50} ms`);
|
||||
console.log(`p95 Latency : ${p95} ms`);
|
||||
console.log('=======================================\n');
|
||||
}
|
||||
|
||||
runEvaluation().catch(console.error);
|
||||
@@ -0,0 +1,91 @@
|
||||
import axios from 'axios';
|
||||
|
||||
const API_URL = process.env.TEST_API_URL || 'http://localhost:3200/api/geocoding/search';
|
||||
const API_KEY = process.env.TEST_API_KEY || 'zP9vL5mK2nQ8xR7jT4wS1yB6hG3fV0cX';
|
||||
const TOTAL_REQUESTS = 5000;
|
||||
const CONCURRENCY = 50; // Number of parallel requests
|
||||
|
||||
const keywords = ["مستشفى", "شارع", "مدرسة", "جامعة", "مطعم", "فندق", "مسجد", "مقهى", "صيدلية", "مول", "دوار", "عيادة", "بنك", "مخبز"];
|
||||
const names = ["السلام", "التخصصي", "المدينة", "النور", "اليرموك", "العربي", "الحديث", "الوطني", "المركزي", "الواحة", "الاموي", "المواساة"];
|
||||
|
||||
function generateRandomQuery(): string {
|
||||
const keyword = keywords[Math.floor(Math.random() * keywords.length)];
|
||||
const name = names[Math.floor(Math.random() * names.length)];
|
||||
const randomSuffix = Math.random() > 0.5 ? ` ${Math.floor(Math.random() * 100)}` : '';
|
||||
return `${keyword} ${name}${randomSuffix}`;
|
||||
}
|
||||
|
||||
async function runLoadTest() {
|
||||
console.log(`🚀 Starting Load Test: ${TOTAL_REQUESTS} requests with concurrency of ${CONCURRENCY}...`);
|
||||
console.log(`📡 Target API: ${API_URL}\n`);
|
||||
|
||||
let completed = 0;
|
||||
let successful = 0;
|
||||
let failed = 0;
|
||||
const latencies: number[] = [];
|
||||
|
||||
const startTime = Date.now();
|
||||
|
||||
// Worker function to process a chunk of requests
|
||||
async function worker(requestsCount: number) {
|
||||
for (let i = 0; i < requestsCount; i++) {
|
||||
const query = generateRandomQuery();
|
||||
const reqStart = Date.now();
|
||||
try {
|
||||
await axios.get(API_URL, {
|
||||
headers: { 'x-api-key': API_KEY },
|
||||
params: { q: query, country: 'syria' } // targeting syria since data exists there
|
||||
});
|
||||
successful++;
|
||||
} catch (error: any) {
|
||||
if (failed === 0) {
|
||||
console.error('First failure reason:', error.response?.data || error.message);
|
||||
}
|
||||
failed++;
|
||||
} finally {
|
||||
latencies.push(Date.now() - reqStart);
|
||||
completed++;
|
||||
|
||||
if (completed % 500 === 0) {
|
||||
console.log(`⏳ Progress: ${completed}/${TOTAL_REQUESTS} requests completed...`);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Split the total requests among the concurrent workers
|
||||
const requestsPerWorker = Math.floor(TOTAL_REQUESTS / CONCURRENCY);
|
||||
const workers: Promise<void>[] = [];
|
||||
|
||||
for (let i = 0; i < CONCURRENCY; i++) {
|
||||
// The last worker takes any remainder
|
||||
const count = (i === CONCURRENCY - 1) ? requestsPerWorker + (TOTAL_REQUESTS % CONCURRENCY) : requestsPerWorker;
|
||||
workers.push(worker(count));
|
||||
}
|
||||
|
||||
await Promise.all(workers);
|
||||
|
||||
const totalTimeSec = (Date.now() - startTime) / 1000;
|
||||
const rps = TOTAL_REQUESTS / totalTimeSec;
|
||||
|
||||
latencies.sort((a, b) => a - b);
|
||||
const p50 = latencies[Math.floor(latencies.length * 0.50)] || 0;
|
||||
const p95 = latencies[Math.floor(latencies.length * 0.95)] || 0;
|
||||
const p99 = latencies[Math.floor(latencies.length * 0.99)] || 0;
|
||||
|
||||
console.log('\n=======================================');
|
||||
console.log('📊 LOAD TEST RESULTS');
|
||||
console.log('=======================================');
|
||||
console.log(`Total Requests : ${TOTAL_REQUESTS}`);
|
||||
console.log(`Successful : ${successful} (${((successful/TOTAL_REQUESTS)*100).toFixed(1)}%)`);
|
||||
console.log(`Failed : ${failed} (${((failed/TOTAL_REQUESTS)*100).toFixed(1)}%)`);
|
||||
console.log(`Total Time : ${totalTimeSec.toFixed(2)} seconds`);
|
||||
console.log(`Throughput (RPS): ${rps.toFixed(2)} req/sec`);
|
||||
console.log('---------------------------------------');
|
||||
console.log(`p50 Latency : ${p50} ms`);
|
||||
console.log(`p95 Latency : ${p95} ms`);
|
||||
console.log(`p99 Latency : ${p99} ms`);
|
||||
console.log('=======================================\n');
|
||||
}
|
||||
|
||||
runLoadTest().catch(console.error);
|
||||
@@ -0,0 +1,36 @@
|
||||
import axios from 'axios';
|
||||
|
||||
const API_URL = process.env.TEST_API_URL || 'http://localhost:3200/api/geocoding/autocomplete';
|
||||
const API_KEY = process.env.TEST_API_KEY || 'zP9vL5mK2nQ8xR7jT4wS1yB6hG3fV0cX';
|
||||
|
||||
const testQueries = [
|
||||
"مستش",
|
||||
"شارع ال",
|
||||
"جامعة",
|
||||
"بنك ا",
|
||||
"صيد"
|
||||
];
|
||||
|
||||
async function testAutocomplete() {
|
||||
console.log(`🚀 Testing Autocomplete Latency`);
|
||||
|
||||
for (const q of testQueries) {
|
||||
const start = Date.now();
|
||||
try {
|
||||
const res = await axios.get(API_URL, {
|
||||
headers: { 'x-api-key': API_KEY },
|
||||
params: { q, country: 'syria' }
|
||||
});
|
||||
const time = Date.now() - start;
|
||||
const count = res.data.results?.length || 0;
|
||||
console.log(`✅ [${time}ms] Query: "${q}" -> ${count} results`);
|
||||
if (count > 0) {
|
||||
console.log(` First result: ${res.data.results[0].name} (${res.data.results[0].category})`);
|
||||
}
|
||||
} catch (e: any) {
|
||||
console.log(`❌ Query: "${q}" failed:`, e.message);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
testAutocomplete().catch(console.error);
|
||||
@@ -0,0 +1,38 @@
|
||||
import axios from 'axios';
|
||||
|
||||
const API_URL = process.env.TEST_API_URL || 'http://localhost:3200/api/geocoding/search';
|
||||
const API_KEY = process.env.TEST_API_KEY || 'zP9vL5mK2nQ8xR7jT4wS1yB6hG3fV0cX';
|
||||
|
||||
const testQueries = [
|
||||
"السمساني", // Should suggest "الشميساني"
|
||||
"جبل اللويبضة", // Wait, misspelled: "جبل اللوبيدة" ?
|
||||
"الاستفلال", // Should suggest "الاستقلال"
|
||||
"مستشفا الاوردن" // Should suggest "مستشفى الأردن"
|
||||
];
|
||||
|
||||
async function testDidYouMean() {
|
||||
console.log(`\n🚑 Testing 'Did You Mean?' (Spell Checker)\n`);
|
||||
|
||||
for (const q of testQueries) {
|
||||
try {
|
||||
const searchRes = await axios.get(API_URL, {
|
||||
headers: { 'x-api-key': API_KEY },
|
||||
params: { q, country: 'jordan' }
|
||||
});
|
||||
console.log(`🔍 Search for misspelled: "${q}"`);
|
||||
|
||||
if (searchRes.data.results?.length > 0) {
|
||||
console.log(` ✅ Found results: ${searchRes.data.results[0].name}`);
|
||||
} else if (searchRes.data.did_you_mean) {
|
||||
console.log(` ⚠️ 0 Results. Did you mean: ✨ "${searchRes.data.did_you_mean}" ✨ ?`);
|
||||
} else {
|
||||
console.log(` ❌ 0 Results. No suggestions found.`);
|
||||
}
|
||||
console.log('--------------------------------------------------');
|
||||
} catch (e: any) {
|
||||
console.log(`❌ Query "${q}" failed:`, e.response?.data || e.message);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
testDidYouMean().catch(console.error);
|
||||
@@ -0,0 +1,39 @@
|
||||
import axios from 'axios';
|
||||
import { Client } from 'pg';
|
||||
|
||||
const API_URL = process.env.TEST_API_URL || 'http://localhost:3200/api/geocoding/search';
|
||||
const API_KEY = process.env.TEST_API_KEY || 'zP9vL5mK2nQ8xR7jT4wS1yB6hG3fV0cX';
|
||||
const DB_URL = process.env.DATABASE_URL || 'postgresql://mapuser:TestMapPass123!@localhost:5432/mapdb';
|
||||
|
||||
async function testFailedLogging() {
|
||||
console.log(`🚀 Testing Failed Searches Logging`);
|
||||
const fakeQuery = 'مكان_غير_موجود_في_العالم_ابدا';
|
||||
|
||||
// 1. Trigger the search (should return 0 results)
|
||||
try {
|
||||
const res = await axios.get(API_URL, {
|
||||
headers: { 'x-api-key': API_KEY },
|
||||
params: { q: fakeQuery, country: 'syria' }
|
||||
});
|
||||
console.log(`✅ Search triggered. Results count: ${res.data.results?.length}`);
|
||||
} catch (e: any) {
|
||||
console.log(`❌ Search failed:`, e.message);
|
||||
}
|
||||
|
||||
// Wait a second for async DB insert
|
||||
await new Promise(r => setTimeout(r, 1000));
|
||||
|
||||
// 2. Check the database directly
|
||||
const client = new Client({ connectionString: DB_URL });
|
||||
await client.connect();
|
||||
const dbRes = await client.query('SELECT * FROM failed_searches WHERE query_text = $1', [fakeQuery]);
|
||||
await client.end();
|
||||
|
||||
if (dbRes.rows.length > 0) {
|
||||
console.log(`✅ SUCCESS! Log found in DB:`, dbRes.rows[0].query_text, `(Count: ${dbRes.rows[0].search_count})`);
|
||||
} else {
|
||||
console.log(`❌ FAILURE! No log found in DB for the query.`);
|
||||
}
|
||||
}
|
||||
|
||||
testFailedLogging().catch(console.error);
|
||||
@@ -0,0 +1,45 @@
|
||||
import axios from 'axios';
|
||||
|
||||
const API_URL = process.env.TEST_API_URL || 'http://localhost:3200/api/geocoding/search';
|
||||
const API_KEY = process.env.TEST_API_KEY || 'zP9vL5mK2nQ8xR7jT4wS1yB6hG3fV0cX';
|
||||
|
||||
const testQueries = [
|
||||
"سيتي مول",
|
||||
"مستشفى التخصصي"
|
||||
];
|
||||
|
||||
async function testPoiGates() {
|
||||
console.log(`\n🏢 Testing POI Gates (Clustering Integration)\n`);
|
||||
|
||||
for (const q of testQueries) {
|
||||
try {
|
||||
const searchRes = await axios.get(API_URL, {
|
||||
headers: { 'x-api-key': API_KEY },
|
||||
params: { q, country: 'jordan' }
|
||||
});
|
||||
console.log(`🔍 Search for "${q}" -> ${searchRes.data.results?.length} results`);
|
||||
|
||||
if (searchRes.data.results?.length > 0) {
|
||||
const topResult = searchRes.data.results[0];
|
||||
console.log(` 🏆 Top Result: ${topResult.name}`);
|
||||
console.log(` Coordinates: ${topResult.latitude}, ${topResult.longitude} (Centroid)`);
|
||||
|
||||
if (topResult.gates && topResult.gates.length > 0) {
|
||||
console.log(` ✅ Found ${topResult.gates.length} Gates!`);
|
||||
topResult.gates.forEach((gate: any, idx: number) => {
|
||||
const mainMarker = gate.is_main_gate ? '⭐ MAIN GATE' : '🚪 SIDE GATE';
|
||||
console.log(` ${idx + 1}. [${mainMarker}] ${gate.name_ar} (${gate.name_en})`);
|
||||
console.log(` -> Navigate to: ${gate.latitude}, ${gate.longitude}`);
|
||||
});
|
||||
} else {
|
||||
console.log(` ⚠️ No gates found. Sero drops will default to centroid.`);
|
||||
}
|
||||
}
|
||||
console.log('--------------------------------------------------');
|
||||
} catch (e: any) {
|
||||
console.log(`❌ Query "${q}" failed:`, e.response?.data || e.message);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
testPoiGates().catch(console.error);
|
||||
@@ -0,0 +1,44 @@
|
||||
import axios from 'axios';
|
||||
|
||||
const API_URL = process.env.TEST_API_URL || 'http://localhost:3200/api/geocoding/search';
|
||||
const AUTO_URL = process.env.TEST_AUTO_URL || 'http://localhost:3200/api/geocoding/autocomplete';
|
||||
const API_KEY = process.env.TEST_API_KEY || 'zP9vL5mK2nQ8xR7jT4wS1yB6hG3fV0cX';
|
||||
|
||||
const testQueries = [
|
||||
"قرب مستشفى",
|
||||
"بجانب بنك",
|
||||
"مقابل جامعة",
|
||||
];
|
||||
|
||||
async function testRelativeQueries() {
|
||||
console.log(`🚀 Testing Relative Queries (Search & Autocomplete)`);
|
||||
|
||||
for (const q of testQueries) {
|
||||
try {
|
||||
// Test Search
|
||||
const searchRes = await axios.get(API_URL, {
|
||||
headers: { 'x-api-key': API_KEY },
|
||||
params: { q, country: 'syria' }
|
||||
});
|
||||
console.log(`\n🔍 Search for "${q}" -> ${searchRes.data.results?.length} results`);
|
||||
if (searchRes.data.results?.length > 0) {
|
||||
console.log(` Top result name: ${searchRes.data.results[0].name}`);
|
||||
console.log(` Top result name_ar: ${searchRes.data.results[0].name_ar}`);
|
||||
}
|
||||
|
||||
// Test Autocomplete
|
||||
const autoRes = await axios.get(AUTO_URL, {
|
||||
headers: { 'x-api-key': API_KEY },
|
||||
params: { q, country: 'syria' }
|
||||
});
|
||||
console.log(`⚡ Autocomplete for "${q}" -> ${autoRes.data.results?.length} results`);
|
||||
if (autoRes.data.results?.length > 0) {
|
||||
console.log(` Top auto name: ${autoRes.data.results[0].name}`);
|
||||
}
|
||||
} catch (e: any) {
|
||||
console.log(`❌ Query "${q}" failed:`, e.response?.data || e.message);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
testRelativeQueries().catch(console.error);
|
||||
Reference in New Issue
Block a user