test(backend): add matchmaking relevance and ranking tests
This commit is contained in:
@@ -4,6 +4,7 @@ using Microsoft.AspNetCore.Mvc;
|
||||
using TarninaHub.Application.Abstractions;
|
||||
using TarninaHub.Application.Dtos;
|
||||
using TarninaHub.Controllers;
|
||||
using TarninaHub.Tests.Matchmaking;
|
||||
|
||||
namespace TarninaHub.Tests.Controllers;
|
||||
|
||||
@@ -11,7 +12,7 @@ public class InnovationsControllerTests
|
||||
{
|
||||
private static InnovationsController CreateController(IInnovationService service)
|
||||
{
|
||||
var controller = new InnovationsController(service);
|
||||
var controller = new InnovationsController(service, new StubMatchmakingService());
|
||||
var identity = new ClaimsIdentity(
|
||||
[new Claim("sub", "user:tester"), new Claim("role", "admin")],
|
||||
authenticationType: "test");
|
||||
|
||||
@@ -4,6 +4,7 @@ using Microsoft.AspNetCore.Mvc;
|
||||
using TarninaHub.Application.Abstractions;
|
||||
using TarninaHub.Application.Dtos;
|
||||
using TarninaHub.Controllers;
|
||||
using TarninaHub.Tests.Matchmaking;
|
||||
|
||||
namespace TarninaHub.Tests.Controllers;
|
||||
|
||||
@@ -11,7 +12,7 @@ public class SubmissionsControllerTests
|
||||
{
|
||||
private static SubmissionsController CreateController(ISubmissionService service, string role = "operator")
|
||||
{
|
||||
var controller = new SubmissionsController(service);
|
||||
var controller = new SubmissionsController(service, new StubMatchmakingService());
|
||||
var identity = new ClaimsIdentity(
|
||||
[new Claim("sub", "user:tester"), new Claim("role", role)],
|
||||
authenticationType: "test");
|
||||
|
||||
@@ -0,0 +1,70 @@
|
||||
using TarninaHub.Application.Matchmaking;
|
||||
|
||||
namespace TarninaHub.Tests.Matchmaking;
|
||||
|
||||
public class EmbeddingAndRankingTests
|
||||
{
|
||||
private static double Magnitude(float[] vector) =>
|
||||
Math.Sqrt(vector.Sum(value => (double)value * value));
|
||||
|
||||
[Fact]
|
||||
public void HashingEmbeddingProvider_ProducesNormalizedVectorOfConfiguredDimension()
|
||||
{
|
||||
var provider = new HashingEmbeddingProvider();
|
||||
|
||||
var vector = provider.Embed("klub sąsiedzki dla seniorów");
|
||||
|
||||
Assert.Equal(EmbeddingConstants.Dimension, vector.Length);
|
||||
Assert.Equal(1.0, Magnitude(vector), precision: 5);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void HashingEmbeddingProvider_IsDeterministic()
|
||||
{
|
||||
var provider = new HashingEmbeddingProvider();
|
||||
|
||||
var first = provider.Embed("praca wspierana niepełnosprawność");
|
||||
var second = provider.Embed("praca wspierana niepełnosprawność");
|
||||
|
||||
Assert.Equal(first, second);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CosineSimilarity_IsHigherForSharedVocabulary()
|
||||
{
|
||||
var provider = new HashingEmbeddingProvider();
|
||||
|
||||
var query = provider.Embed("seniorzy spotkania samotność");
|
||||
var related = provider.Embed("klub sąsiedzki seniorzy spotkania samotność");
|
||||
var unrelated = provider.Embed("hostel młodzież bezdomność");
|
||||
|
||||
var relatedScore = VectorMath.CosineSimilarity(query, related);
|
||||
var unrelatedScore = VectorMath.CosineSimilarity(query, unrelated);
|
||||
|
||||
Assert.True(relatedScore > unrelatedScore);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SemanticRanker_OrdersByScoreAndRespectsTopK()
|
||||
{
|
||||
var provider = new HashingEmbeddingProvider();
|
||||
var query = provider.Embed("cyfryzacja seniorzy internet");
|
||||
var candidates = new (string Id, IReadOnlyList<float> Vector)[]
|
||||
{
|
||||
("cyfryzacja", provider.Embed("cyfryzacja seniorzy internet szkolenie")),
|
||||
("praca", provider.Embed("praca wspierana doradca")),
|
||||
("hostel", provider.Embed("hostel młodzież bezdomność")),
|
||||
};
|
||||
|
||||
var ranked = SemanticRanker.Rank(query, candidates, topK: 2);
|
||||
|
||||
Assert.Equal(2, ranked.Count);
|
||||
Assert.Equal("cyfryzacja", ranked[0].Item);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CosineSimilarity_ReturnsZeroForMismatchedDimensions()
|
||||
{
|
||||
Assert.Equal(0, VectorMath.CosineSimilarity([1f, 0f], [1f]));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
using TarninaHub.Application.Matchmaking;
|
||||
|
||||
namespace TarninaHub.Tests.Matchmaking;
|
||||
|
||||
/// <summary>
|
||||
/// Relevance test set for semantic matchmaking. Measures top-1 accuracy,
|
||||
/// precision@3 and recall@3 over a small synthetic corpus (matches the
|
||||
/// demo dataset vocabulary).
|
||||
/// </summary>
|
||||
public class MatchmakingQualityTests
|
||||
{
|
||||
private static readonly (string Id, string Text)[] Documents =
|
||||
[
|
||||
("klub", "klub sąsiedzki seniorzy spotkania samotność izolacja"),
|
||||
("praca", "praca wspierana niepełnosprawność zatrudnienie doradca"),
|
||||
("hostel", "hostel aktywizacyjny młodzież bezdomność usamodzielnianie"),
|
||||
("psych", "wsparcie psychiczne młodzież szkoła profilaktyka"),
|
||||
("ubostwo", "ubóstwo energetyczne żywność spizarnia seniorzy"),
|
||||
("integracja", "asysta kulturowa cudzoziemcy integracja język"),
|
||||
("rodzina", "rodzina zastępcza rodzeństwo piecza wsparcie"),
|
||||
("cyfryzacja", "cyfryzacja seniorzy internet szkolenie obsługa"),
|
||||
];
|
||||
|
||||
private static readonly (string Query, string Expected)[] Queries =
|
||||
[
|
||||
("seniorzy spotkania samotność", "klub"),
|
||||
("praca niepełnosprawność doradca", "praca"),
|
||||
("hostel młodzież bezdomność", "hostel"),
|
||||
("psychiczne młodzież szkoła", "psych"),
|
||||
("ubóstwo energetyczne spizarnia", "ubostwo"),
|
||||
("asysta kulturowa integracja", "integracja"),
|
||||
("rodzina zastępcza rodzeństwo", "rodzina"),
|
||||
("cyfryzacja seniorzy internet", "cyfryzacja"),
|
||||
];
|
||||
|
||||
[Fact]
|
||||
public void Matchmaking_MeetsRelevanceThresholds()
|
||||
{
|
||||
var provider = new HashingEmbeddingProvider();
|
||||
var vectors = Documents
|
||||
.Select(document => (document.Id, Vector: (IReadOnlyList<float>)provider.Embed(document.Text)))
|
||||
.ToList();
|
||||
|
||||
var top1Correct = 0;
|
||||
var recallAt3 = 0;
|
||||
var precisionSum = 0.0;
|
||||
|
||||
foreach (var (query, expected) in Queries)
|
||||
{
|
||||
var ranked = SemanticRanker.Rank(provider.Embed(query), vectors, topK: 3);
|
||||
|
||||
if (ranked.Count > 0 && ranked[0].Item == expected)
|
||||
{
|
||||
top1Correct++;
|
||||
}
|
||||
|
||||
if (ranked.Any(item => item.Item == expected))
|
||||
{
|
||||
recallAt3++;
|
||||
}
|
||||
|
||||
precisionSum += ranked.Count(item => item.Item == expected) / 3.0;
|
||||
}
|
||||
|
||||
var top1Accuracy = (double)top1Correct / Queries.Length;
|
||||
var recall = (double)recallAt3 / Queries.Length;
|
||||
var meanPrecision = precisionSum / Queries.Length;
|
||||
|
||||
Assert.True(top1Accuracy >= 0.75, $"top-1 accuracy was {top1Accuracy:P0}");
|
||||
Assert.True(recall >= 0.9, $"recall@3 was {recall:P0}");
|
||||
Assert.True(meanPrecision >= 0.3, $"precision@3 was {meanPrecision:P0}");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
using TarninaHub.Application.Abstractions;
|
||||
using TarninaHub.Application.Dtos;
|
||||
|
||||
namespace TarninaHub.Tests.Matchmaking;
|
||||
|
||||
public sealed class StubMatchmakingService : IMatchmakingService
|
||||
{
|
||||
public Task IndexInnovationAsync(string innovationId, CancellationToken cancellationToken) =>
|
||||
Task.CompletedTask;
|
||||
|
||||
public Task IndexProblemAsync(string problemId, CancellationToken cancellationToken) =>
|
||||
Task.CompletedTask;
|
||||
|
||||
public Task IndexSubmissionAsync(string submissionId, CancellationToken cancellationToken) =>
|
||||
Task.CompletedTask;
|
||||
|
||||
public Task<int> ReindexAsync(CancellationToken cancellationToken) =>
|
||||
Task.FromResult(0);
|
||||
|
||||
public Task<MatchResponse?> MatchAsync(
|
||||
string submissionId,
|
||||
MatchRequest request,
|
||||
CancellationToken cancellationToken) =>
|
||||
Task.FromResult<MatchResponse?>(new MatchResponse([], [], true));
|
||||
}
|
||||
Reference in New Issue
Block a user