using System.Text;
namespace TarninaHub.Application.Matchmaking;
///
/// Deterministic, offline embedding provider (feature hashing). Produces
/// L2-normalized vectors so cosine similarity reflects shared vocabulary.
/// Interchangeable with a real LLM provider behind
/// (see ADR-0005). Used as the default until an external provider is wired.
///
public sealed class HashingEmbeddingProvider : IEmbeddingProvider
{
public int Dimension => EmbeddingConstants.Dimension;
public Task EmbedAsync(string text, CancellationToken cancellationToken)
{
return Task.FromResult(Embed(text));
}
public Task> EmbedBatchAsync(
IReadOnlyList texts,
CancellationToken cancellationToken)
{
IReadOnlyList result = texts.Select(Embed).ToList();
return Task.FromResult(result);
}
public float[] Embed(string text)
{
var vector = new float[Dimension];
if (string.IsNullOrWhiteSpace(text))
{
return vector;
}
foreach (var token in Tokenize(text))
{
var index = (int)(Fnv1a(token) % Dimension);
vector[index] += 1f;
}
Normalize(vector);
return vector;
}
private static IEnumerable Tokenize(string text)
{
var buffer = new StringBuilder();
foreach (var character in text)
{
if (char.IsLetterOrDigit(character))
{
buffer.Append(char.ToLowerInvariant(character));
}
else if (buffer.Length > 0)
{
yield return buffer.ToString();
buffer.Clear();
}
}
if (buffer.Length > 0)
{
yield return buffer.ToString();
}
}
private static void Normalize(float[] vector)
{
double sumOfSquares = vector.Sum(value => (double)value * value);
var magnitude = Math.Sqrt(sumOfSquares);
if (magnitude <= 0)
{
return;
}
for (var i = 0; i < vector.Length; i++)
{
vector[i] = (float)(vector[i] / magnitude);
}
}
private static uint Fnv1a(string token)
{
const uint offset = 2166136261;
const uint prime = 16777619;
var hash = offset;
foreach (var character in token)
{
hash ^= character;
hash *= prime;
}
return hash;
}
}