feat(sprint2.5): чистка чанков и переиндексация
Чанкер тащил в базу markdown-мусор: навигационные блоки «Вернуться на:»
со списками ссылок, инлайн-ссылки [текст](url) в теле, служебные
пометки _Источник: .../file.md_, лишние пустые строки. Всё это ело
контекст LLM и засоряло правую панель отладки.
- services/text_cleanup: clean_markdown_text — удаляет навигационные
строки, строки-только-ссылки (обычно это меню), служебные _Источник:_,
раскрывает инлайн-ссылки [x](url) → x, сжимает 3+ переносов до 2.
- services/document_processor: process_document теперь возвращает
(id, raw_text, sections, chunks); чистку применяем к заголовкам и
телам секций; чанки короче 20 символов выбрасываем с пересчётом
индексов. Вспомогательная rechunk_raw_text — для переиндексации.
Чтобы переиндексировать без повторной загрузки файла, нужен исходный
текст. Вводим отдельный слой:
- новая таблица SQLite documents (id, name, file_type, raw_text,
created_at, updated_at) + миграция Alembic 7ee7296ccd6d.
- db/models/Document + регистрация в db.models.__init__.
- services/document_service: save/get/list/delete для raw_text.
- routers/documents.upload: сохраняет raw_text в SQLite перед
индексацией в Chroma; delete убирает и из SQLite, и из Chroma.
- Новые эндпоинты POST /documents/{id}/reindex и
POST /documents/reindex-all — берут raw_text из SQLite, пропускают
через rechunk_raw_text, заменяют чанки в Chroma.
Существующие 4 документа были перезалиты вручную (решение: не делать
одноразовый backfill, проще залить заново). Старая Chroma очищена,
новые чанки прошли через чистку — мусор ушёл.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,50 @@
|
||||
"""SQLite-слой для raw-текстов документов — для переиндексации."""
|
||||
import logging
|
||||
|
||||
from sqlalchemy import select
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from db.models import Document
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
async def save_document_raw(
|
||||
session: AsyncSession,
|
||||
document_id: str,
|
||||
name: str,
|
||||
file_type: str,
|
||||
raw_text: str,
|
||||
) -> None:
|
||||
"""Сохранить (или перезаписать) исходный текст документа в SQLite."""
|
||||
existing = await session.get(Document, document_id)
|
||||
if existing:
|
||||
existing.name = name
|
||||
existing.file_type = file_type
|
||||
existing.raw_text = raw_text
|
||||
else:
|
||||
session.add(Document(
|
||||
id=document_id,
|
||||
name=name,
|
||||
file_type=file_type,
|
||||
raw_text=raw_text,
|
||||
))
|
||||
await session.commit()
|
||||
|
||||
|
||||
async def get_document_raw(session: AsyncSession, document_id: str) -> Document | None:
|
||||
return await session.get(Document, document_id)
|
||||
|
||||
|
||||
async def list_documents_raw(session: AsyncSession) -> list[Document]:
|
||||
stmt = select(Document).order_by(Document.created_at)
|
||||
return list((await session.execute(stmt)).scalars().all())
|
||||
|
||||
|
||||
async def delete_document_raw(session: AsyncSession, document_id: str) -> bool:
|
||||
doc = await session.get(Document, document_id)
|
||||
if doc is None:
|
||||
return False
|
||||
await session.delete(doc)
|
||||
await session.commit()
|
||||
return True
|
||||
Reference in New Issue
Block a user