| | | 1 | | using Microsoft.EntityFrameworkCore; |
| | | 2 | | using Microsoft.Extensions.Configuration; |
| | | 3 | | using OServices.FiltersServices.FilterModels; |
| | | 4 | | using OzonRepositories.Context; |
| | | 5 | | using Services.FiltersServices.DataFilterManagers; |
| | | 6 | | |
| | | 7 | | namespace Services.SearchServices.OrderSearch; |
| | | 8 | | |
| | | 9 | | public interface IOrderSearchDiagnosticsService |
| | | 10 | | { |
| | | 11 | | Task<OrderSearchDiagnosticsSnapshot> GetSnapshotAsync(CancellationToken cancellationToken = default); |
| | | 12 | | |
| | | 13 | | Task<OrderSearchLookupResponse> LookupAsync(int? orderId, string? shipmentNumber, |
| | | 14 | | CancellationToken cancellationToken = default); |
| | | 15 | | } |
| | | 16 | | |
| | | 17 | | public sealed class OrderSearchDiagnosticsService : IOrderSearchDiagnosticsService |
| | | 18 | | { |
| | | 19 | | private readonly IOrderSearchService _searchService; |
| | | 20 | | private readonly IOrderSearchDiagnosticsStore _store; |
| | | 21 | | private readonly ApplicationDbContext _context; |
| | | 22 | | private readonly IConfiguration _configuration; |
| | | 23 | | |
| | 27 | 24 | | public OrderSearchDiagnosticsService( |
| | 27 | 25 | | IOrderSearchService searchService, |
| | 27 | 26 | | IOrderSearchDiagnosticsStore store, |
| | 27 | 27 | | ApplicationDbContext context, |
| | 27 | 28 | | IConfiguration configuration) |
| | | 29 | | { |
| | 27 | 30 | | _searchService = searchService; |
| | 27 | 31 | | _store = store; |
| | 27 | 32 | | _context = context; |
| | 27 | 33 | | _configuration = configuration; |
| | 27 | 34 | | } |
| | | 35 | | |
| | | 36 | | public async Task<OrderSearchDiagnosticsSnapshot> GetSnapshotAsync(CancellationToken cancellationToken = default) |
| | | 37 | | { |
| | 10 | 38 | | var esEnabled = _configuration.GetValue<bool>("OrdersSearch:UseElasticsearch"); |
| | 10 | 39 | | var (avgMs, maxMs, searchCount) = _store.GetSearchStats(); |
| | | 40 | | |
| | 10 | 41 | | long indexCount = 0; |
| | 10 | 42 | | var indexExists = false; |
| | 10 | 43 | | if (esEnabled) |
| | | 44 | | { |
| | 9 | 45 | | indexExists = await _searchService.IndexExistsAsync(); |
| | 9 | 46 | | if (indexExists) |
| | | 47 | | { |
| | 8 | 48 | | indexCount = await _searchService.GetTotalCountAsync(); |
| | | 49 | | } |
| | | 50 | | } |
| | | 51 | | |
| | 10 | 52 | | var dbOrderCount = await _context.Orders.AsNoTracking().CountAsync(cancellationToken); |
| | 10 | 53 | | var driftThreshold = OrderSearchDriftChecker.GetAlertThreshold(_configuration); |
| | 10 | 54 | | var countDrift = Math.Abs(indexCount - dbOrderCount); |
| | 10 | 55 | | var isDriftAlert = esEnabled && indexExists && countDrift > driftThreshold; |
| | 10 | 56 | | var lastDrift = _store.GetLastDriftEvent(); |
| | 10 | 57 | | var syncState = await _context.OrderSearchSyncStates.AsNoTracking() |
| | 10 | 58 | | .FirstOrDefaultAsync(s => s.Id == 1, cancellationToken); |
| | | 59 | | |
| | 10 | 60 | | return new OrderSearchDiagnosticsSnapshot |
| | 10 | 61 | | { |
| | 10 | 62 | | EsEnabled = esEnabled, |
| | 10 | 63 | | EsUrl = _configuration["OrdersSearch:Url"] ?? "http://localhost:9201", |
| | 10 | 64 | | IndexName = _configuration["OrdersSearch:IndexName"] ?? "orders", |
| | 10 | 65 | | IndexExists = indexExists, |
| | 10 | 66 | | IndexCount = indexCount, |
| | 10 | 67 | | DbOrderCount = dbOrderCount, |
| | 10 | 68 | | CountDrift = countDrift, |
| | 10 | 69 | | DriftAlertThreshold = driftThreshold, |
| | 10 | 70 | | IsDriftAlert = isDriftAlert, |
| | 10 | 71 | | LastDriftCheckAt = lastDrift?.TimestampUtc, |
| | 10 | 72 | | LastSyncedAt = syncState?.LastSyncedAt, |
| | 10 | 73 | | LastFullReindexAt = syncState?.LastFullReindexAt, |
| | 10 | 74 | | AvgSearchMs = Math.Round(avgMs, 1), |
| | 10 | 75 | | MaxSearchMs = maxMs, |
| | 10 | 76 | | SearchSampleCount = searchCount, |
| | 10 | 77 | | IndexEvents = _store.GetIndexEvents(), |
| | 10 | 78 | | SearchEvents = _store.GetSearchEvents(), |
| | 10 | 79 | | DriftEvents = _store.GetDriftEvents() |
| | 10 | 80 | | }; |
| | 10 | 81 | | } |
| | | 82 | | |
| | | 83 | | public async Task<OrderSearchLookupResponse> LookupAsync(int? orderId, string? shipmentNumber, |
| | | 84 | | CancellationToken cancellationToken = default) |
| | | 85 | | { |
| | 17 | 86 | | if (orderId is > 0) |
| | | 87 | | { |
| | 5 | 88 | | var item = await BuildLookupItemAsync(orderId.Value, cancellationToken); |
| | 5 | 89 | | return new OrderSearchLookupResponse |
| | 5 | 90 | | { |
| | 5 | 91 | | QueryLabel = $"ID {orderId.Value}", |
| | 5 | 92 | | Items = new[] { item } |
| | 5 | 93 | | }; |
| | | 94 | | } |
| | | 95 | | |
| | 12 | 96 | | var term = shipmentNumber?.Trim(); |
| | 12 | 97 | | if (string.IsNullOrWhiteSpace(term)) |
| | | 98 | | { |
| | 7 | 99 | | return new OrderSearchLookupResponse(); |
| | | 100 | | } |
| | | 101 | | |
| | 5 | 102 | | return await LookupByShipmentNumberAsync(term, cancellationToken); |
| | 17 | 103 | | } |
| | | 104 | | |
| | | 105 | | private async Task<OrderSearchLookupResponse> LookupByShipmentNumberAsync(string shipmentNumber, |
| | | 106 | | CancellationToken cancellationToken) |
| | | 107 | | { |
| | | 108 | | const int maxResults = 50; |
| | 5 | 109 | | var normalized = OrderDataFilterManager.NormalizeShipmentNumberForSearch(shipmentNumber); |
| | | 110 | | |
| | 5 | 111 | | var dbOrders = string.IsNullOrEmpty(normalized) |
| | 5 | 112 | | ? new List<DbOrderRow>() |
| | 5 | 113 | | : await _context.Orders.AsNoTracking() |
| | 5 | 114 | | .Where(o => o.ShipmentNumber != null && o.ShipmentNumber.Contains(normalized)) |
| | 5 | 115 | | .OrderByDescending(o => o.Id) |
| | 5 | 116 | | .Take(maxResults) |
| | 5 | 117 | | .Select(o => new DbOrderRow(o.Id, o.ShipmentNumber, o.Article, o.UpdatedAt)) |
| | 5 | 118 | | .ToListAsync(cancellationToken); |
| | | 119 | | |
| | 5 | 120 | | HashSet<int> indexIds = new(); |
| | 5 | 121 | | if (_configuration.GetValue<bool>("OrdersSearch:UseElasticsearch")) |
| | | 122 | | { |
| | 2 | 123 | | var matched = await _searchService.SearchOrderIdsAsync( |
| | 2 | 124 | | new OrderFilterModel { ShipmentNumber = shipmentNumber }, |
| | 2 | 125 | | maxResults: maxResults); |
| | 2 | 126 | | indexIds = matched.Ids.ToHashSet(); |
| | | 127 | | } |
| | | 128 | | |
| | 5 | 129 | | var items = new List<OrderSearchLookupResult>(); |
| | 122 | 130 | | foreach (var row in dbOrders) |
| | | 131 | | { |
| | 56 | 132 | | OrderSearchDocument? doc = null; |
| | 56 | 133 | | var inIndex = indexIds.Contains(row.Id); |
| | 56 | 134 | | if (inIndex && _configuration.GetValue<bool>("OrdersSearch:UseElasticsearch")) |
| | | 135 | | { |
| | 3 | 136 | | doc = await _searchService.GetDocumentByIdAsync(row.Id); |
| | | 137 | | } |
| | | 138 | | |
| | 56 | 139 | | items.Add(new OrderSearchLookupResult |
| | 56 | 140 | | { |
| | 56 | 141 | | OrderId = row.Id, |
| | 56 | 142 | | InIndex = inIndex, |
| | 56 | 143 | | InDatabase = true, |
| | 56 | 144 | | Document = doc, |
| | 56 | 145 | | DbShipmentNumber = row.ShipmentNumber, |
| | 56 | 146 | | DbArticle = row.Article, |
| | 56 | 147 | | DbUpdatedAt = row.UpdatedAt |
| | 56 | 148 | | }); |
| | 56 | 149 | | } |
| | | 150 | | |
| | 5 | 151 | | return new OrderSearchLookupResponse |
| | 5 | 152 | | { |
| | 5 | 153 | | QueryLabel = $"номер «{shipmentNumber}»", |
| | 5 | 154 | | Items = items |
| | 5 | 155 | | }; |
| | 5 | 156 | | } |
| | | 157 | | |
| | | 158 | | private async Task<OrderSearchLookupResult> BuildLookupItemAsync(int orderId, CancellationToken cancellationToken) |
| | | 159 | | { |
| | 5 | 160 | | var dbOrder = await _context.Orders.AsNoTracking() |
| | 5 | 161 | | .Where(o => o.Id == orderId) |
| | 5 | 162 | | .Select(o => new DbOrderRow(o.Id, o.ShipmentNumber, o.Article, o.UpdatedAt)) |
| | 5 | 163 | | .FirstOrDefaultAsync(cancellationToken); |
| | | 164 | | |
| | 5 | 165 | | OrderSearchDocument? doc = null; |
| | 5 | 166 | | if (_configuration.GetValue<bool>("OrdersSearch:UseElasticsearch")) |
| | | 167 | | { |
| | 4 | 168 | | doc = await _searchService.GetDocumentByIdAsync(orderId); |
| | | 169 | | } |
| | | 170 | | |
| | 5 | 171 | | return new OrderSearchLookupResult |
| | 5 | 172 | | { |
| | 5 | 173 | | OrderId = orderId, |
| | 5 | 174 | | InIndex = doc != null, |
| | 5 | 175 | | InDatabase = dbOrder != null, |
| | 5 | 176 | | Document = doc, |
| | 5 | 177 | | DbShipmentNumber = dbOrder?.ShipmentNumber, |
| | 5 | 178 | | DbArticle = dbOrder?.Article, |
| | 5 | 179 | | DbUpdatedAt = dbOrder?.UpdatedAt |
| | 5 | 180 | | }; |
| | 5 | 181 | | } |
| | | 182 | | |
| | 351 | 183 | | private sealed record DbOrderRow(int Id, string? ShipmentNumber, string? Article, DateTime? UpdatedAt); |
| | | 184 | | } |