feat(unitFilter): Оптимизация фильрации юнитов. Проверка значений аттрибутов теперь выполняется в 2 запроса:
- проверяем доступные в базе значения - проверяем наличие значений в юнитах по id значений
This commit is contained in:
@@ -1,26 +1,29 @@
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using PARR.Core.Repositories.Interfaces.Unit;
|
||||
using PARR.Core.Services.UnitFilterService.Matchers.Interfaces;
|
||||
using PARR.Domain.Entities.JobEntities;
|
||||
using System.Diagnostics;
|
||||
|
||||
namespace PARR.Core.Services.UnitFilterService.Matchers;
|
||||
|
||||
/// <summary>
|
||||
/// Сопоставляет юниты с фильтрами по атрибутам.
|
||||
/// Использует двухэтапный поиск: сначала ValueId, затем UnitId.
|
||||
/// </summary>
|
||||
internal class UnitFieldMatcher : IUnitFieldMatcher
|
||||
{
|
||||
private const int chunkSize = 200;
|
||||
private readonly IUnitRepository unitRepository;
|
||||
private readonly ILogger<UnitFieldMatcher> logger;
|
||||
private readonly IUnitRepository _unitRepository;
|
||||
private readonly IUnitFieldValueRepository _unitFieldValueRepository;
|
||||
private readonly ILogger<UnitFieldMatcher> _logger;
|
||||
|
||||
public UnitFieldMatcher(
|
||||
IUnitRepository unitRepository,
|
||||
IUnitFieldValueRepository unitFieldValueRepository,
|
||||
ILogger<UnitFieldMatcher> logger)
|
||||
{
|
||||
this.unitRepository = unitRepository;
|
||||
this.logger = logger;
|
||||
_unitRepository = unitRepository;
|
||||
_unitFieldValueRepository = unitFieldValueRepository;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
public async Task<List<Guid>> MatchAsync(
|
||||
@@ -32,32 +35,63 @@ internal class UnitFieldMatcher : IUnitFieldMatcher
|
||||
if (filters == null || filters.Count == 0)
|
||||
return new List<Guid>(unitIds);
|
||||
|
||||
logger.LogDebug("UnitFieldMatcher: вход {UnitCount} юнитов, фильтров: {FilterCount}",
|
||||
_logger.LogDebug("UnitFieldMatcher: вход {UnitCount} юнитов, фильтров: {FilterCount}",
|
||||
unitIds.Count, filters.Count);
|
||||
|
||||
var result = new List<Guid>(unitIds.Count);
|
||||
var currentIds = new HashSet<Guid>(unitIds);
|
||||
|
||||
foreach (var chunk in unitIds.Chunk(chunkSize))
|
||||
for (int i = 0; i < filters.Count; i++)
|
||||
{
|
||||
var query = unitRepository.Get().AsNoTracking()
|
||||
.Where(u => chunk.Contains(u.Id));
|
||||
var filter = filters[i];
|
||||
var mask = filter.ValueMask?.Trim();
|
||||
|
||||
foreach (var fieldFilter in filters)
|
||||
if (string.IsNullOrEmpty(mask))
|
||||
continue;
|
||||
|
||||
var sw = Stopwatch.StartNew();
|
||||
var inputCount = currentIds.Count;
|
||||
|
||||
var matchingValueIds = await _unitFieldValueRepository
|
||||
.FindValueIdsByMaskAsync(mask, ct);
|
||||
|
||||
if (matchingValueIds.Count == 0 && !filter.IsInverse)
|
||||
{
|
||||
var valueMask = fieldFilter.ValueMask?.Trim();
|
||||
if (string.IsNullOrEmpty(valueMask))
|
||||
continue;
|
||||
|
||||
query = unitRepository.GetUnitByFieldAndValue(
|
||||
query, fieldFilter.FieldId, fieldFilter.ValueMask!, fieldFilter.IsInverse);
|
||||
sw.Stop();
|
||||
_logger.LogDebug(
|
||||
"UnitFieldMatcher: фильтр #{Index} (FieldId={FieldId}, Mask='{Mask}') | Вход: {InCount}, Выход: 0 (нет значений), Время: {Ms}ms",
|
||||
i + 1, filter.FieldId, mask, inputCount, sw.ElapsedMilliseconds);
|
||||
currentIds.Clear();
|
||||
break;
|
||||
}
|
||||
|
||||
var chunkResult = await query.Select(u => u.Id).ToListAsync(ct);
|
||||
result.AddRange(chunkResult);
|
||||
// Шаг 2: Найти/исключить юниты по ValueId
|
||||
if (filter.IsInverse)
|
||||
{
|
||||
var unitsToExclude = await _unitRepository
|
||||
.FindUnitIdsByValueIdsAsync(currentIds.ToList(), filter.FieldId, matchingValueIds, ct);
|
||||
currentIds.ExceptWith(unitsToExclude);
|
||||
}
|
||||
else
|
||||
{
|
||||
var unitsToKeep = await _unitRepository
|
||||
.FindUnitIdsByValueIdsAsync(currentIds.ToList(), filter.FieldId, matchingValueIds, ct);
|
||||
currentIds.IntersectWith(unitsToKeep);
|
||||
}
|
||||
|
||||
sw.Stop();
|
||||
_logger.LogDebug(
|
||||
"UnitFieldMatcher: фильтр #{Index} (FieldId={FieldId}, Mask='{Mask}', Inverse={IsInverse}, Values={ValCount}) | Вход: {InCount}, Выход: {OutCount}, Время: {Ms}ms",
|
||||
i + 1, filter.FieldId, mask, filter.IsInverse, matchingValueIds.Count,
|
||||
inputCount, currentIds.Count, sw.ElapsedMilliseconds);
|
||||
|
||||
if (currentIds.Count == 0)
|
||||
{
|
||||
_logger.LogDebug("UnitFieldMatcher: прерывание на фильтре #{Index} (0 юнитов)", i + 1);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
logger.LogDebug("UnitFieldMatcher: выход {UnitCount} юнитов", result.Count);
|
||||
|
||||
return result;
|
||||
_logger.LogDebug("UnitFieldMatcher: итоговый выход {UnitCount} юнитов", currentIds.Count);
|
||||
return currentIds.ToList();
|
||||
}
|
||||
}
|
||||
@@ -84,8 +84,7 @@ internal class UnitFilterService : IUnitFilterService
|
||||
public async Task<IEnumerable<UnitFilterResultDto>?> GetUnitsByJobFilterAsync(
|
||||
Job job,
|
||||
int? takeCount = null,
|
||||
CancellationToken cancellationToken = default
|
||||
)
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
if (job.Group == null)
|
||||
throw new ArgumentNullException(nameof(job.Group), $"Job {job.Id} не содержит Group");
|
||||
@@ -97,13 +96,9 @@ internal class UnitFilterService : IUnitFilterService
|
||||
var totalStopwatch = Stopwatch.StartNew();
|
||||
logger.LogInformation("Начало фильтрации юнитов для Job {JobId} с {FilterCount} фильтрами", job.Id, job.UnitFilters.Count);
|
||||
|
||||
// Собираем все контексты юнитов, прошедших фильтрацию
|
||||
var allFilteredContexts = new List<UnitFilterMatchResult>();
|
||||
|
||||
// Преобразуем в список для индексации
|
||||
var unitFiltersList = job.UnitFilters.ToList();
|
||||
|
||||
// Этап 1: Применение основных фильтров (Field, Relationship) на уровне SQL
|
||||
for (int i = 0; i < unitFiltersList.Count; i++)
|
||||
{
|
||||
var filter = unitFiltersList[i];
|
||||
@@ -113,14 +108,21 @@ internal class UnitFilterService : IUnitFilterService
|
||||
{
|
||||
logger.LogDebug("Применение фильтра #{Index} (Id={FilterId})", i + 1, filter.Id);
|
||||
|
||||
// 1. Найти ID юнитов по UnitFilter
|
||||
// 1. Resolve
|
||||
var resolveSw = Stopwatch.StartNew();
|
||||
var initialUnitIds = await nameResolver.ResolveAsync(filter.UnitFilter, cancellationToken);
|
||||
resolveSw.Stop();
|
||||
|
||||
if (!initialUnitIds.Any())
|
||||
{
|
||||
logger.LogDebug("Фильтр #{Index}: пропущен (0 юнитов)", i + 1);
|
||||
logger.LogDebug("Фильтр #{Index}: пропущен (0 юнитов после Resolve, {ResolveMs}ms)",
|
||||
i + 1, resolveSw.ElapsedMilliseconds);
|
||||
continue;
|
||||
}
|
||||
|
||||
logger.LogDebug("Фильтр #{Index}: Resolve вернул {Count} юнитов за {Ms}ms",
|
||||
i + 1, initialUnitIds.Count, resolveSw.ElapsedMilliseconds);
|
||||
|
||||
#if DEBUG
|
||||
if (initialUnitIds.Contains(debugTargetUnitId))
|
||||
{
|
||||
@@ -128,14 +130,19 @@ internal class UnitFilterService : IUnitFilterService
|
||||
}
|
||||
#endif
|
||||
|
||||
// 2. Применить FieldFilters
|
||||
var fieldStopwatch = Stopwatch.StartNew();
|
||||
//var fieldFilteredIds = await ApplyFieldFiltersOnDbAsync(initialUnitIds, filter.FieldFilters, cancellationToken);
|
||||
// 2. FieldFilters
|
||||
var fieldSw = Stopwatch.StartNew();
|
||||
var fieldFilteredIds = await unitFieldMatcher.MatchAsync(initialUnitIds, filter.FieldFilters, cancellationToken);
|
||||
fieldSw.Stop();
|
||||
|
||||
logger.LogDebug("Фильтр #{Index}: FieldMatcher вернул {Count} юнитов за {Ms}ms",
|
||||
i + 1, fieldFilteredIds.Count, fieldSw.ElapsedMilliseconds);
|
||||
|
||||
if (!fieldFilteredIds.Any())
|
||||
{
|
||||
logger.LogDebug("Фильтр #{Index}: 0 юнитов после FieldFilters", i + 1);
|
||||
filterStopwatch.Stop();
|
||||
logger.LogDebug("Фильтр #{Index}: завершён (0 юнитов). [Resolve: {R}ms, Field: {F}ms, Total: {T}ms]",
|
||||
i + 1, resolveSw.ElapsedMilliseconds, fieldSw.ElapsedMilliseconds, filterStopwatch.ElapsedMilliseconds);
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -146,16 +153,24 @@ internal class UnitFilterService : IUnitFilterService
|
||||
}
|
||||
#endif
|
||||
|
||||
// 3. Применить RelationshipFilters
|
||||
var relStopwatch = Stopwatch.StartNew();
|
||||
// 3. RelationshipFilters
|
||||
var relSw = Stopwatch.StartNew();
|
||||
var relationshipFilteredContexts = await unitRelationshipMatcher.MatchAsync(
|
||||
fieldFilteredIds, filter.RelationshipFilters, cancellationToken);
|
||||
relSw.Stop();
|
||||
|
||||
if (!relationshipFilteredContexts.Any())
|
||||
{
|
||||
logger.LogDebug("Фильтр #{Index}: 0 юнитов после RelationshipFilters", i + 1);
|
||||
continue;
|
||||
}
|
||||
filterStopwatch.Stop();
|
||||
allFilteredContexts.AddRange(relationshipFilteredContexts);
|
||||
|
||||
logger.LogDebug(
|
||||
"Фильтр #{Index}: добавлено {Count} юнитов. Всего: {Total}. [Resolve: {R}ms, Field: {F}ms, Rel: {Rel}ms, Total: {T}ms]",
|
||||
i + 1,
|
||||
relationshipFilteredContexts.Count,
|
||||
allFilteredContexts.Count,
|
||||
resolveSw.ElapsedMilliseconds,
|
||||
fieldSw.ElapsedMilliseconds,
|
||||
relSw.ElapsedMilliseconds,
|
||||
filterStopwatch.ElapsedMilliseconds);
|
||||
|
||||
#if DEBUG
|
||||
var targetContext = relationshipFilteredContexts.FirstOrDefault(c => c.UnitId == debugTargetUnitId);
|
||||
@@ -165,25 +180,13 @@ internal class UnitFilterService : IUnitFilterService
|
||||
debugTargetUnitId, i + 1, targetContext.ValidParentIds.Count, targetContext.ValidChildIds.Count);
|
||||
}
|
||||
#endif
|
||||
|
||||
// Добавляем отфильтрованные контексты в общий набор
|
||||
allFilteredContexts.AddRange(relationshipFilteredContexts);
|
||||
|
||||
filterStopwatch.Stop();
|
||||
logger.LogDebug(
|
||||
"Фильтр #{Index}: добавлено {Count} юнитов. Всего: {Total}. [Field: {F}ms, Rel: {R}ms, Total: {T}ms]",
|
||||
i + 1,
|
||||
relationshipFilteredContexts.Count,
|
||||
allFilteredContexts.Count,
|
||||
fieldStopwatch.ElapsedMilliseconds,
|
||||
relStopwatch.ElapsedMilliseconds,
|
||||
filterStopwatch.ElapsedMilliseconds
|
||||
);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
logger.LogError(ex, "Ошибка при обработке фильтра {FilterId} для Job {JobId}",
|
||||
filter.Id, job.Id);
|
||||
filterStopwatch.Stop();
|
||||
logger.LogError(ex, "Ошибка при обработке фильтра #{Index} (Id={FilterId}) для Job {JobId}. Время до ошибки: {Ms}ms",
|
||||
i + 1, filter.Id, job.Id, filterStopwatch.ElapsedMilliseconds);
|
||||
throw; // КРИТИЧНО: прерываем выполнение, чтобы не маскировать проблему
|
||||
}
|
||||
}
|
||||
|
||||
@@ -197,6 +200,7 @@ internal class UnitFilterService : IUnitFilterService
|
||||
ValidChildIds = new HashSet<Guid>(g.SelectMany(c => c.ValidChildIds))
|
||||
})
|
||||
.ToList();
|
||||
|
||||
logger.LogInformation("Этап базовой фильтрации завершён: собрано {UnitCount} уникальных юнитов", mergedContexts.Count);
|
||||
|
||||
// Этап 2: Применение Umbrella-фильтра
|
||||
@@ -219,6 +223,346 @@ internal class UnitFilterService : IUnitFilterService
|
||||
return result;
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
private async Task<Job?> LoadJobWithFiltersAsync(Guid jobId, CancellationToken cancellationToken = default)
|
||||
{
|
||||
return await jobRepository
|
||||
|
||||
Reference in New Issue
Block a user