142 lines
4.8 KiB
TypeScript
142 lines
4.8 KiB
TypeScript
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);
|