AuthorBuddy/Services/RagService.cs
2026-05-20 06:12:54 +02:00

145 lines
4.6 KiB
C#

using AuthorBuddy.Web.Data;
using LiteDB;
using System.Security.Cryptography;
using System.Text;
namespace AuthorBuddy.Web.Services;
public class RagService : IRagService
{
private readonly ILiteDatabase _db;
private readonly IOllamaService _ollama;
public RagService(ILiteDatabase db, IOllamaService ollama)
{
_db = db;
_ollama = ollama;
}
public async Task IndexProjectAsync(int projectId, string rootPath, IProgress<string> progress)
{
var files = Directory.GetFiles(rootPath, "*.md", SearchOption.AllDirectories)
.Where(f => f.Contains("00_Drafts") || f.Contains("01_Fakten"));
var metaCol = _db.GetCollection<DocumentMetaEntity>("documentMeta");
var vecCol = _db.GetCollection<DocumentVector>("vectors");
foreach (var file in files)
{
progress?.Report($"Indexiere: {Path.GetFileName(file)}...");
string content = await File.ReadAllTextAsync(file);
string hash = ComputeHash(content);
var meta = metaCol.FindOne(m => m.ProjectId == projectId && m.FilePath == file);
if (meta != null && meta.Hash == hash)
{
var existingVec = vecCol.FindOne(v => v.DocumentMetaId == meta.Id);
if (existingVec != null)
{
progress?.Report($" ↳ unverändert, übersprungen");
continue;
}
}
var embedding = await _ollama.GetEmbeddingAsync(content);
if (embedding.Length == 0)
{
progress?.Report($" ↳ Fehler beim Embedding, übersprungen");
continue;
}
if (meta == null)
{
meta = new DocumentMetaEntity
{
ProjectId = projectId,
FilePath = file,
LastModified = File.GetLastWriteTimeUtc(file),
Hash = hash,
FileType = ".md"
};
metaCol.Insert(meta);
}
else
{
meta.LastModified = File.GetLastWriteTimeUtc(file);
meta.Hash = hash;
metaCol.Update(meta);
}
var vec = vecCol.FindOne(v => v.DocumentMetaId == meta.Id);
if (vec != null)
{
vec.Text = content;
vec.Hash = hash;
vec.Embedding = embedding;
vecCol.Update(vec);
}
else
{
vecCol.Insert(new DocumentVector
{
DocumentMetaId = meta.Id,
ProjectId = projectId,
Text = content,
Hash = hash,
Embedding = embedding
});
}
progress?.Report($" ↳ ✓ {embedding.Length} Dimensionen");
}
progress?.Report("Indexierung abgeschlossen.");
}
public async Task<List<RagSearchResult>> SearchAsync(int projectId, string query, int topN = 5)
{
var queryVec = await _ollama.GetEmbeddingAsync(query);
if (queryVec.Length == 0)
return new List<RagSearchResult>();
var vecCol = _db.GetCollection<DocumentVector>("vectors");
var metaCol = _db.GetCollection<DocumentMetaEntity>("documentMeta");
var vectors = vecCol.Find(v => v.ProjectId == projectId).ToList();
var results = new List<RagSearchResult>();
foreach (var vec in vectors)
{
var sim = CosineSimilarity(queryVec, vec.Embedding);
if (sim > 0)
{
var meta = metaCol.FindById(vec.DocumentMetaId);
results.Add(new RagSearchResult
{
Text = vec.Text.Length > 500 ? vec.Text[..500] + "..." : vec.Text,
FilePath = meta?.FilePath ?? "",
Similarity = sim
});
}
}
return results.OrderByDescending(r => r.Similarity).Take(topN).ToList();
}
private static string ComputeHash(string content)
{
var bytes = SHA256.HashData(Encoding.UTF8.GetBytes(content));
return Convert.ToHexStringLower(bytes);
}
private static double CosineSimilarity(float[] a, float[] b)
{
double dotProduct = 0.0, l2A = 0.0, l2B = 0.0;
for (int i = 0; i < a.Length; i++)
{
dotProduct += a[i] * b[i];
l2A += Math.Pow(a[i], 2);
l2B += Math.Pow(b[i], 2);
}
return dotProduct / (Math.Sqrt(l2A) * Math.Sqrt(l2B));
}
}