523 lines
19 KiB
C#
523 lines
19 KiB
C#
#pragma warning disable SKEXP0001
|
|
|
|
using ChatRAG.Contracts.VectorSearch;
|
|
using ChatRAG.Data;
|
|
using ChatRAG.Models;
|
|
using ChatRAG.Services.Contracts;
|
|
using Microsoft.SemanticKernel.Embeddings;
|
|
using System.Text;
|
|
|
|
namespace ChatRAG.Services.TextServices
|
|
{
|
|
public class QdrantTextDataService : ITextDataService
|
|
{
|
|
private readonly IVectorSearchService _vectorSearchService;
|
|
private readonly ITextEmbeddingGenerationService _embeddingService;
|
|
private readonly ILogger<QdrantTextDataService> _logger;
|
|
|
|
public QdrantTextDataService(
|
|
IVectorSearchService vectorSearchService,
|
|
ITextEmbeddingGenerationService embeddingService,
|
|
ILogger<QdrantTextDataService> logger)
|
|
{
|
|
_vectorSearchService = vectorSearchService;
|
|
_embeddingService = embeddingService;
|
|
_logger = logger;
|
|
}
|
|
|
|
public string ProviderName => "Qdrant";
|
|
|
|
// ========================================
|
|
// MÉTODOS ORIGINAIS (compatibilidade com MongoDB)
|
|
// ========================================
|
|
|
|
public async Task SalvarNoMongoDB(string titulo, string texto, string projectId)
|
|
{
|
|
await SalvarNoMongoDB(null, titulo, texto, projectId);
|
|
}
|
|
|
|
public async Task SalvarNoMongoDB(string? id, string titulo, string texto, string projectId)
|
|
{
|
|
try
|
|
{
|
|
var conteudo = $"**{titulo}** \n\n {texto}";
|
|
|
|
// Gera embedding
|
|
var embedding = await _embeddingService.GenerateEmbeddingAsync(conteudo);
|
|
var embeddingArray = embedding.ToArray().Select(e => (double)e).ToArray();
|
|
|
|
if (string.IsNullOrEmpty(id))
|
|
{
|
|
// Cria novo documento
|
|
await _vectorSearchService.AddDocumentAsync(titulo, texto, projectId, embeddingArray);
|
|
_logger.LogDebug("Documento '{Title}' criado no Qdrant", titulo);
|
|
}
|
|
else
|
|
{
|
|
// Atualiza documento existente
|
|
await _vectorSearchService.UpdateDocumentAsync(id, titulo, texto, projectId, embeddingArray);
|
|
_logger.LogDebug("Documento '{Id}' atualizado no Qdrant", id);
|
|
}
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
_logger.LogError(ex, "Erro ao salvar documento '{Title}' no Qdrant", titulo);
|
|
throw;
|
|
}
|
|
}
|
|
|
|
public async Task SalvarTextoComEmbeddingNoMongoDB(string textoCompleto, string projectId)
|
|
{
|
|
try
|
|
{
|
|
var textoArray = new List<string>();
|
|
string[] textolinhas = textoCompleto.Split(
|
|
new string[] { "\n" },
|
|
StringSplitOptions.None
|
|
);
|
|
|
|
var title = textolinhas[0];
|
|
var builder = new StringBuilder();
|
|
|
|
foreach (string line in textolinhas)
|
|
{
|
|
if (line.StartsWith("**") || line.StartsWith("\r**"))
|
|
{
|
|
if (builder.Length > 0)
|
|
{
|
|
textoArray.Add(title.Replace("**", "").Replace("\r", "") + ": " + Environment.NewLine + builder.ToString());
|
|
builder = new StringBuilder();
|
|
title = line;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
builder.AppendLine(line);
|
|
}
|
|
}
|
|
|
|
// Adiciona último bloco se houver
|
|
if (builder.Length > 0)
|
|
{
|
|
textoArray.Add(title.Replace("**", "").Replace("\r", "") + ": " + Environment.NewLine + builder.ToString());
|
|
}
|
|
|
|
// Processa cada seção
|
|
foreach (var item in textoArray)
|
|
{
|
|
await SalvarNoMongoDB(title.Replace("**", "").Replace("\r", ""), item, projectId);
|
|
}
|
|
|
|
_logger.LogInformation("Texto completo processado: {SectionCount} seções salvas no Qdrant", textoArray.Count);
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
_logger.LogError(ex, "Erro ao processar texto completo no Qdrant");
|
|
throw;
|
|
}
|
|
}
|
|
|
|
public async Task<IEnumerable<TextoComEmbedding>> GetAll()
|
|
{
|
|
try
|
|
{
|
|
// Busca todos os projetos e depois todos os documentos
|
|
var allDocuments = new List<VectorSearchResult>();
|
|
|
|
// Como Qdrant não tem um "GetAll" direto, vamos usar scroll
|
|
// Isso é uma limitação vs MongoDB, mas é mais eficiente
|
|
var projects = await GetAllProjectIds();
|
|
|
|
foreach (var projectId in projects)
|
|
{
|
|
var projectDocs = await _vectorSearchService.GetDocumentsByProjectAsync(projectId);
|
|
allDocuments.AddRange(projectDocs);
|
|
}
|
|
|
|
return allDocuments.Select(ConvertToTextoComEmbedding);
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
_logger.LogError(ex, "Erro ao recuperar todos os documentos do Qdrant");
|
|
throw;
|
|
}
|
|
}
|
|
|
|
public async Task<IEnumerable<TextoComEmbedding>> GetByPorjectId(string projectId)
|
|
{
|
|
try
|
|
{
|
|
var documents = await _vectorSearchService.GetDocumentsByProjectAsync(projectId);
|
|
return documents.Select(ConvertToTextoComEmbedding);
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
_logger.LogError(ex, "Erro ao recuperar documentos do projeto {ProjectId} no Qdrant", projectId);
|
|
throw;
|
|
}
|
|
}
|
|
|
|
public async Task<TextoComEmbedding> GetById(string id)
|
|
{
|
|
try
|
|
{
|
|
var document = await _vectorSearchService.GetDocumentAsync(id);
|
|
if (document == null)
|
|
{
|
|
throw new ArgumentException($"Documento {id} não encontrado no Qdrant");
|
|
}
|
|
|
|
return ConvertToTextoComEmbedding(document);
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
_logger.LogError(ex, "Erro ao recuperar documento {Id} do Qdrant", id);
|
|
throw;
|
|
}
|
|
}
|
|
|
|
// ========================================
|
|
// MÉTODOS NOVOS DA INTERFACE
|
|
// ========================================
|
|
|
|
public async Task<string> SaveDocumentAsync(DocumentInput document)
|
|
{
|
|
try
|
|
{
|
|
var conteudo = $"**{document.Title}** \n\n {document.Content}";
|
|
var embedding = await _embeddingService.GenerateEmbeddingAsync(conteudo);
|
|
var embeddingArray = embedding.ToArray().Select(e => (double)e).ToArray();
|
|
|
|
string id;
|
|
if (!string.IsNullOrEmpty(document.Id))
|
|
{
|
|
// Atualizar documento existente
|
|
await _vectorSearchService.UpdateDocumentAsync(
|
|
document.Id,
|
|
document.Title,
|
|
document.Content,
|
|
document.ProjectId,
|
|
embeddingArray,
|
|
document.Metadata);
|
|
id = document.Id;
|
|
}
|
|
else
|
|
{
|
|
// Criar novo documento
|
|
id = await _vectorSearchService.AddDocumentAsync(
|
|
document.Title,
|
|
document.Content,
|
|
document.ProjectId,
|
|
embeddingArray,
|
|
document.Metadata);
|
|
}
|
|
|
|
_logger.LogDebug("Documento {Id} salvo no Qdrant via SaveDocumentAsync", id);
|
|
return id;
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
_logger.LogError(ex, "Erro ao salvar documento no Qdrant");
|
|
throw;
|
|
}
|
|
}
|
|
|
|
public async Task UpdateDocumentAsync(string id, DocumentInput document)
|
|
{
|
|
try
|
|
{
|
|
var conteudo = $"**{document.Title}** \n\n {document.Content}";
|
|
var embedding = await _embeddingService.GenerateEmbeddingAsync(conteudo);
|
|
var embeddingArray = embedding.ToArray().Select(e => (double)e).ToArray();
|
|
|
|
await _vectorSearchService.UpdateDocumentAsync(
|
|
id,
|
|
document.Title,
|
|
document.Content,
|
|
document.ProjectId,
|
|
embeddingArray,
|
|
document.Metadata);
|
|
|
|
_logger.LogDebug("Documento {Id} atualizado no Qdrant", id);
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
_logger.LogError(ex, "Erro ao atualizar documento {Id} no Qdrant", id);
|
|
throw;
|
|
}
|
|
}
|
|
|
|
public async Task DeleteDocumentAsync(string id)
|
|
{
|
|
try
|
|
{
|
|
await _vectorSearchService.DeleteDocumentAsync(id);
|
|
_logger.LogDebug("Documento {Id} deletado do Qdrant", id);
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
_logger.LogError(ex, "Erro ao deletar documento {Id} do Qdrant", id);
|
|
throw;
|
|
}
|
|
}
|
|
|
|
public async Task<bool> DocumentExistsAsync(string id)
|
|
{
|
|
try
|
|
{
|
|
return await _vectorSearchService.DocumentExistsAsync(id);
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
_logger.LogError(ex, "Erro ao verificar existência do documento {Id} no Qdrant", id);
|
|
return false;
|
|
}
|
|
}
|
|
|
|
public async Task<DocumentOutput?> GetDocumentAsync(string id)
|
|
{
|
|
try
|
|
{
|
|
var result = await _vectorSearchService.GetDocumentAsync(id);
|
|
if (result == null) return null;
|
|
|
|
return new DocumentOutput
|
|
{
|
|
Id = result.Id,
|
|
Title = result.Title,
|
|
Content = result.Content,
|
|
ProjectId = result.ProjectId,
|
|
Embedding = result.Embedding,
|
|
CreatedAt = result.CreatedAt,
|
|
UpdatedAt = result.UpdatedAt,
|
|
Metadata = result.Metadata
|
|
};
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
_logger.LogError(ex, "Erro ao recuperar documento {Id} do Qdrant", id);
|
|
return null;
|
|
}
|
|
}
|
|
|
|
public async Task<List<DocumentOutput>> GetDocumentsByProjectAsync(string projectId)
|
|
{
|
|
try
|
|
{
|
|
var results = await _vectorSearchService.GetDocumentsByProjectAsync(projectId);
|
|
|
|
return results.Select(result => new DocumentOutput
|
|
{
|
|
Id = result.Id,
|
|
Title = result.Title,
|
|
Content = result.Content,
|
|
ProjectId = result.ProjectId,
|
|
Embedding = result.Embedding,
|
|
CreatedAt = result.CreatedAt,
|
|
UpdatedAt = result.UpdatedAt,
|
|
Metadata = result.Metadata
|
|
}).ToList();
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
_logger.LogError(ex, "Erro ao recuperar documentos do projeto {ProjectId} do Qdrant", projectId);
|
|
throw;
|
|
}
|
|
}
|
|
|
|
public async Task<int> GetDocumentCountAsync(string? projectId = null)
|
|
{
|
|
try
|
|
{
|
|
return await _vectorSearchService.GetDocumentCountAsync(projectId);
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
_logger.LogError(ex, "Erro ao contar documentos no Qdrant");
|
|
return 0;
|
|
}
|
|
}
|
|
|
|
// ========================================
|
|
// OPERAÇÕES EM LOTE
|
|
// ========================================
|
|
|
|
public async Task<List<string>> SaveDocumentsBatchAsync(List<DocumentInput> documents)
|
|
{
|
|
var ids = new List<string>();
|
|
var errors = new List<Exception>();
|
|
|
|
// Processa em lotes menores para performance
|
|
var batchSize = 10;
|
|
for (int i = 0; i < documents.Count; i += batchSize)
|
|
{
|
|
var batch = documents.Skip(i).Take(batchSize);
|
|
var tasks = batch.Select(async doc =>
|
|
{
|
|
try
|
|
{
|
|
var id = await SaveDocumentAsync(doc);
|
|
return id;
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
errors.Add(ex);
|
|
_logger.LogError(ex, "Erro ao salvar documento '{Title}' em lote", doc.Title);
|
|
return null;
|
|
}
|
|
});
|
|
|
|
var batchResults = await Task.WhenAll(tasks);
|
|
ids.AddRange(batchResults.Where(id => id != null)!);
|
|
}
|
|
|
|
if (errors.Any())
|
|
{
|
|
_logger.LogWarning("Batch save completado com {ErrorCount} erros de {TotalCount} documentos",
|
|
errors.Count, documents.Count);
|
|
}
|
|
|
|
_logger.LogInformation("Batch save: {SuccessCount}/{TotalCount} documentos salvos no Qdrant",
|
|
ids.Count, documents.Count);
|
|
|
|
return ids;
|
|
}
|
|
|
|
public async Task DeleteDocumentsBatchAsync(List<string> ids)
|
|
{
|
|
var errors = new List<Exception>();
|
|
|
|
// Processa em lotes para não sobrecarregar
|
|
var batchSize = 20;
|
|
for (int i = 0; i < ids.Count; i += batchSize)
|
|
{
|
|
var batch = ids.Skip(i).Take(batchSize);
|
|
var tasks = batch.Select(async id =>
|
|
{
|
|
try
|
|
{
|
|
await DeleteDocumentAsync(id);
|
|
return true;
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
errors.Add(ex);
|
|
_logger.LogError(ex, "Erro ao deletar documento {Id} em lote", id);
|
|
return false;
|
|
}
|
|
});
|
|
|
|
await Task.WhenAll(tasks);
|
|
}
|
|
|
|
if (errors.Any())
|
|
{
|
|
_logger.LogWarning("Batch delete completado com {ErrorCount} erros de {TotalCount} documentos",
|
|
errors.Count, ids.Count);
|
|
}
|
|
else
|
|
{
|
|
_logger.LogInformation("Batch delete: {TotalCount} documentos removidos do Qdrant", ids.Count);
|
|
}
|
|
}
|
|
|
|
// ========================================
|
|
// ESTATÍSTICAS DO PROVIDER
|
|
// ========================================
|
|
|
|
public async Task<Dictionary<string, object>> GetProviderStatsAsync()
|
|
{
|
|
try
|
|
{
|
|
var baseStats = await _vectorSearchService.GetStatsAsync();
|
|
var totalDocs = await GetDocumentCountAsync();
|
|
|
|
// Adiciona estatísticas específicas do TextData
|
|
var projectIds = await GetAllProjectIds();
|
|
var projectStats = new Dictionary<string, int>();
|
|
|
|
foreach (var projectId in projectIds)
|
|
{
|
|
var count = await GetDocumentCountAsync(projectId);
|
|
projectStats[projectId] = count;
|
|
}
|
|
|
|
var enhancedStats = new Dictionary<string, object>(baseStats)
|
|
{
|
|
["text_service_provider"] = "Qdrant",
|
|
["total_documents_via_text_service"] = totalDocs,
|
|
["projects_count"] = projectIds.Count,
|
|
["documents_by_project"] = projectStats,
|
|
["supports_batch_operations"] = true,
|
|
["supports_metadata"] = true,
|
|
["embedding_auto_generation"] = true
|
|
};
|
|
|
|
return enhancedStats;
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
return new Dictionary<string, object>
|
|
{
|
|
["provider"] = "Qdrant",
|
|
["text_service_provider"] = "Qdrant",
|
|
["health"] = "error",
|
|
["error"] = ex.Message,
|
|
["last_check"] = DateTime.UtcNow
|
|
};
|
|
}
|
|
}
|
|
|
|
// ========================================
|
|
// MÉTODOS AUXILIARES PRIVADOS
|
|
// ========================================
|
|
|
|
private static TextoComEmbedding ConvertToTextoComEmbedding(VectorSearchResult result)
|
|
{
|
|
return new TextoComEmbedding
|
|
{
|
|
Id = result.Id,
|
|
Titulo = result.Title,
|
|
Conteudo = result.Content,
|
|
ProjetoId = result.ProjectId,
|
|
Embedding = result.Embedding,
|
|
// Campos que podem não existir no Qdrant
|
|
ProjetoNome = result.Metadata?.GetValueOrDefault("project_name")?.ToString() ?? "",
|
|
TipoDocumento = result.Metadata?.GetValueOrDefault("document_type")?.ToString() ?? "",
|
|
Categoria = result.Metadata?.GetValueOrDefault("category")?.ToString() ?? "",
|
|
Tags = result.Metadata?.GetValueOrDefault("tags") as string[] ?? Array.Empty<string>()
|
|
};
|
|
}
|
|
|
|
private async Task<List<string>> GetAllProjectIds()
|
|
{
|
|
try
|
|
{
|
|
// Esta é uma operação custosa no Qdrant
|
|
// Em produção, seria melhor manter um cache de project IDs
|
|
// ou usar uma estrutura de dados separada
|
|
|
|
// Por agora, vamos usar uma busca com um vetor dummy para pegar todos os documentos
|
|
var dummyVector = new double[1536]; // Assumindo embeddings OpenAI
|
|
var allResults = await _vectorSearchService.SearchSimilarAsync(
|
|
dummyVector,
|
|
projectId: null,
|
|
threshold: 0.0,
|
|
limit: 10000);
|
|
|
|
return allResults
|
|
.Select(r => r.ProjectId)
|
|
.Where(pid => !string.IsNullOrEmpty(pid))
|
|
.Distinct()
|
|
.ToList();
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
_logger.LogError(ex, "Erro ao recuperar IDs de projetos do Qdrant");
|
|
return new List<string>();
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
#pragma warning restore SKEXP0001 |