Files
Deal/src/core/Deal.Infrastructure/Services/FtsMaintenance.cs
T
Rustam Khalimov 9e07568ddd Инициализировать репозиторий «Дейл»
Первый коммит: модульный монолит ядра (.NET 10) и gRPC-сервисы
ai/ml/telegram, фронтенд Vue 3/Vite/Tailwind, документация (ТЗ,
инструкция пользователя, техдокументация, код-стайл), бэклог,
скрипты развёртывания и архив прототипа LeadRadar.
2026-09-11 02:50:17 +03:00

59 lines
3.7 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
using Deal.Infrastructure.Persistence;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;
namespace Deal.Infrastructure.Services;
/// <summary>
/// Обслуживание FTS-индексов схемы тенанта — POST /api/admin/fts/rebuild (Ruling 6, план Task 10;
/// fts.py rebuild L48–67).
/// </summary>
/// <remarks>
/// SearchTsv — генерируемые STORED-колонки Cards/RejectedItems (миграция TenantPipeline, Ruling 6): tsvector
/// авто-актуален при записи строк, «пересборка индекса раз в сутки» как в DuckDB-прототипе не нужна.
/// <see cref="RebuildAsync"/> — реальная идемпотентная обслуживающая операция: CREATE INDEX IF NOT EXISTS
/// обоих GIN-индексов (самовосстановление, если индекс отсутствует) + ANALYZE обеих таблиц (актуальная
/// статистика для планировщика Postgres). Имена индексов/таблиц — внутренние константы (не пользовательский
/// ввод), поэтому SQL без параметров (как fts.py L51–52). Успех — true ({ok:true, ready:true} ответа);
/// сбой (нет DDL-прав и т.п.) логируется и возвращает false — {ok:false, ready:false}, как rebuild() python
/// (frontend rebuildFts store.js L1883–1889 показывает ошибку по ready).
/// </remarks>
public sealed class FtsMaintenance(TenantDbContext dbContext, ILogger<FtsMaintenance> logger)
{
// Самовосстановление GIN-индекса карточек (имя — из миграции TenantPipeline, Ruling 6).
private const string CardsIndexSql =
"CREATE INDEX IF NOT EXISTS \"IX_Cards_SearchTsv\" ON \"Cards\" USING gin (\"SearchTsv\")";
// Самовосстановление GIN-индекса отсева (имя — из миграции TenantPipeline, Ruling 6).
private const string RejectedIndexSql =
"CREATE INDEX IF NOT EXISTS \"IX_RejectedItems_SearchTsv\" ON \"RejectedItems\" USING gin (\"SearchTsv\")";
// Актуализация статистики таблицы карточек для планировщика (fts.py rebuild L58–60).
private const string AnalyzeCardsSql = "ANALYZE \"Cards\"";
// Актуализация статистики таблицы отсева для планировщика (fts.py rebuild L58–60).
private const string AnalyzeRejectedSql = "ANALYZE \"RejectedItems\"";
/// <summary>
/// Пересобирает FTS-индексы текущей схемы тенанта: CREATE INDEX IF NOT EXISTS + ANALYZE.
/// </summary>
/// <param name="ct">Токен отмены.</param>
/// <returns>true — индексы на месте и статистика собрана; false — сбой (эндпоинт отвечает {ok:false, ready:false}).</returns>
public async Task<bool> RebuildAsync(CancellationToken ct)
{
try
{
await dbContext.Database.ExecuteSqlRawAsync(CardsIndexSql, ct);
await dbContext.Database.ExecuteSqlRawAsync(RejectedIndexSql, ct);
await dbContext.Database.ExecuteSqlRawAsync(AnalyzeCardsSql, ct);
await dbContext.Database.ExecuteSqlRawAsync(AnalyzeRejectedSql, ct);
return true;
}
catch (Exception exception)
{
logger.LogWarning(exception, "Пересборка FTS-индексов не удалась");
return false;
}
}
}