score.js 2.2 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667
  1. /**
  2. * Scoring functions for the QMD benchmark harness.
  3. *
  4. * Computes precision@k, recall, MRR, and F1 for search results
  5. * against ground-truth expected files.
  6. */
  7. /**
  8. * Normalize a file path for comparison.
  9. * Strips qmd:// prefix, lowercases, removes leading/trailing slashes.
  10. */
  11. export function normalizePath(p) {
  12. if (p.startsWith("qmd://")) {
  13. // qmd://collection/path/to/file → path/to/file
  14. const withoutScheme = p.slice("qmd://".length);
  15. const slashIdx = withoutScheme.indexOf("/");
  16. p = slashIdx >= 0 ? withoutScheme.slice(slashIdx + 1) : withoutScheme;
  17. }
  18. return p.toLowerCase().replace(/^\/+|\/+$/g, "");
  19. }
  20. /**
  21. * Check if two paths refer to the same file.
  22. * Handles different path formats by comparing normalized suffixes.
  23. */
  24. export function pathsMatch(result, expected) {
  25. const nr = normalizePath(result);
  26. const ne = normalizePath(expected);
  27. if (nr === ne)
  28. return true;
  29. if (nr.endsWith(ne) || ne.endsWith(nr))
  30. return true;
  31. return false;
  32. }
  33. /**
  34. * Score a set of search results against expected files.
  35. */
  36. export function scoreResults(resultFiles, expectedFiles, topK) {
  37. // Count hits in top-k
  38. const topKResults = resultFiles.slice(0, topK);
  39. let hitsAtK = 0;
  40. for (const expected of expectedFiles) {
  41. if (topKResults.some(r => pathsMatch(r, expected))) {
  42. hitsAtK++;
  43. }
  44. }
  45. // Count total hits anywhere
  46. let totalHits = 0;
  47. for (const expected of expectedFiles) {
  48. if (resultFiles.some(r => pathsMatch(r, expected))) {
  49. totalHits++;
  50. }
  51. }
  52. // MRR: reciprocal rank of first relevant result
  53. let mrr = 0;
  54. for (let i = 0; i < resultFiles.length; i++) {
  55. if (expectedFiles.some(e => pathsMatch(resultFiles[i], e))) {
  56. mrr = 1 / (i + 1);
  57. break;
  58. }
  59. }
  60. const denominator = Math.min(topK, expectedFiles.length);
  61. const precision_at_k = denominator > 0 ? hitsAtK / denominator : 0;
  62. const recall = expectedFiles.length > 0 ? totalHits / expectedFiles.length : 0;
  63. const f1 = precision_at_k + recall > 0
  64. ? 2 * (precision_at_k * recall) / (precision_at_k + recall)
  65. : 0;
  66. return { precision_at_k, recall, mrr, f1, hits_at_k: hitsAtK };
  67. }