fix(admin): покрытие L3 мерить по индексу, а не по отметкам в базе

Проверка на живых данных вскрыла, что панель отладки врала в мою же пользу:
показывала 154 046 «документов с эмбеддингом» (87%), тогда как в FAISS реально
93 053 вектора (52.5%). Колонка documents.faiss_id для этого непригодна —
отметка остаётся после пересоздания индекса (смена модели: 768 → 1024) и после
сбоев worker-gpu. Выборочная проверка: у 8 из 20 «отмеченных» вектора нет.

- gpu.index_stats — новая задача, отдаёт реальное содержимое активного
  векторного бэкенда; API спрашивает её для панели отладки;
- панель показывает «векторов в индексе» и отдельно предупреждает о ложных
  отметках (их 60 993), потому что такие документы молча выпадают из L3:
  в индексе их нет, а на пересчёт они не попадут — reembed ищет faiss_id IS NULL;
- scripts/ops/faiss_reconcile.py — сверяет отметки с индексом и обнуляет ложные,
  после чего reembed_missing.py отправляет их на пересчёт. Проверен вживую
  (dry-run на проде: 93 053 в индексе, 60 993 ложных отметок).

Документация: зафиксирована реальная глубина корпуса — полный текст только у
27.5% документов, у 79% меньше 500 отпечатков (уровень аннотации). Система
ловит списывание из того, что есть целиком; это граница, а не поломка.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
jze9
2026-08-28 17:17:41 +05:00
parent ea62f10829
commit d8630ca0f9
7 changed files with 226 additions and 15 deletions

View File

@@ -23,7 +23,8 @@ interface DebugData {
celery: { workers: Worker[]; error?: string };
queues: Record<string, { messages: number; unacked: number; consumers: number }> | { error: string };
corpus: {
documents: number; documents_embedded: number; documents_without_embedding: number;
documents: number; documents_marked_embedded: number;
vector_index: { backend?: string; vectors?: number; dim?: number; embed_model?: string; error?: string };
fingerprints_estimate: number; elasticsearch_documents: number | null;
};
sources: {
@@ -79,8 +80,14 @@ export function Debug() {
const queuesErr = 'error' in data.queues ? (data.queues as { error: string }).error : null;
const queues = queuesErr ? {} : (data.queues as Record<string, { messages: number; unacked: number; consumers: number }>);
const embedPercent = data.corpus.documents
? (data.corpus.documents_embedded / data.corpus.documents) * 100
// Покрытие L3 считаем по РЕАЛЬНОМУ содержимому индекса: пометка faiss_id в БД
// остаётся и когда вектор туда не попал, и завышает картину в разы
const vectors = data.corpus.vector_index?.vectors ?? null;
const embedPercent = data.corpus.documents && vectors != null
? (vectors / data.corpus.documents) * 100
: 0;
const staleMarks = vectors != null
? Math.max(0, data.corpus.documents_marked_embedded - vectors)
: 0;
return (
@@ -191,15 +198,32 @@ export function Debug() {
<Card icon={Database} title="Корпус сравнения">
<div className="grid grid-cols-2 lg:grid-cols-4 gap-4 mb-4">
<Metric label="Документов" value={fmtNum(data.corpus.documents)} />
<Metric label="С эмбеддингом" value={fmtNum(data.corpus.documents_embedded)} />
<Metric label="Векторов в индексе" value={fmtNum(vectors)} />
<Metric label="Отпечатков (оценка)" value={fmtNum(data.corpus.fingerprints_estimate)} />
<Metric label="В Elasticsearch" value={fmtNum(data.corpus.elasticsearch_documents)} />
</div>
{data.corpus.vector_index?.error && (
<p className="text-xs text-red-600 mb-2">
индекс недоступен: {data.corpus.vector_index.error}
</p>
)}
<ProgressBar
percent={embedPercent}
status={data.corpus.documents_without_embedding > 0 ? 'partial' : 'done'}
label={`покрытие эмбеддингами (L3): без вектора ${fmtNum(data.corpus.documents_without_embedding)} документов`}
status={embedPercent >= 99 ? 'done' : 'partial'}
label={`покрытие L3 (реально в индексе ${data.corpus.vector_index?.backend || '—'}): ` +
`без вектора ${fmtNum(Math.max(0, data.corpus.documents - (vectors ?? 0)))} документов`}
/>
{staleMarks > 0 && (
<div className="mt-3 flex items-start gap-2 text-sm text-amber-700 bg-amber-50 border border-amber-100 rounded-lg p-3">
<AlertTriangle className="w-4 h-4 mt-0.5 shrink-0" />
<span>
У {fmtNum(staleMarks)} документов в базе стоит отметка faiss_id, но вектора в индексе нет —
обычно это след пересоздания индекса после смены модели эмбеддингов.
Такие документы не участвуют в семантическом поиске и не будут пересчитаны,
пока отметку не сбросить: <code>scripts/ops/faiss_reconcile.py</code>.
</span>
</div>
)}
</Card>
{/* Проблемные прогоны */}