test(backend): add matchmaking relevance and ranking tests

This commit is contained in:
Rafał Tyński
2026-10-04 02:44:03 +02:00
parent 25ce901021
commit 182c26d9f8
5 changed files with 172 additions and 2 deletions
@@ -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));
}