145 lines
4.6 KiB
C#
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));
|
|
}
|
|
}
|