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; } }