This commit is contained in:
2026-02-01 17:01:21 +03:00
commit 9575eaf8ee
144 changed files with 24025 additions and 0 deletions

24
qdrant_init/README.md Normal file
View File

@@ -0,0 +1,24 @@
# Инициализация Qdrant при старте
При старте Docker-стека контейнер `qdrant-init` ждёт доступности Qdrant, проверяет наличие коллекции `chapter_analyses` (size=1024, Cosine) и создаёт её при отсутствии.
## Переменные окружения
| Переменная | По умолчанию | Описание |
|------------|--------------|----------|
| `QDRANT_URL` | `http://qdrant:6333` | URL Qdrant (внутри сети — имя сервиса `qdrant`) |
| `QDRANT_COLLECTION_CHAPTER_ANALYSES` | `chapter_analyses` | Имя коллекции для анализов глав |
| `QDRANT_VECTOR_SIZE` | `1024` | Размерность вектора (bge-m3) |
| `QDRANT_INIT_DELAY` | `15` | Пауза (сек) перед первой попыткой подключения — даёт Qdrant время открыть порт |
## Подключение к docker-compose
В `docker-compose.yml` сервис `qdrant-init` использует образ `python:3.11-alpine` и монтирует `entrypoint.py` (без отдельного Dockerfile). Запускается после healthcheck Qdrant, выполняет скрипт и завершается (restart: "no"). Другие сервисы могут зависеть от него через `depends_on: qdrant-init`, если нужно гарантировать наличие коллекции перед работой.
## Локальный запуск (без Docker)
Для проверки при работающем Qdrant на localhost:
```bash
QDRANT_URL=http://localhost:6333 python3 entrypoint.py
```

118
qdrant_init/entrypoint.py Normal file
View File

@@ -0,0 +1,118 @@
#!/usr/bin/env python3
"""
Entrypoint при старте стека: проверяет, что в Qdrant есть коллекция chapter_analyses,
и создаёт её при отсутствии. Запускается отдельным контейнером после старта Qdrant.
"""
import os
import sys
import time
import json
import urllib.error
import urllib.request
from typing import Any
def env(name: str, default: str) -> str:
"""Читает переменную окружения или default."""
return os.environ.get(name, default).strip()
def wait_for_qdrant(
base_url: str,
max_attempts: int = 45,
interval: float = 2.0,
initial_delay: float = 15.0,
) -> bool:
"""Ждёт, пока Qdrant станет доступен (сначала пауза, затем повторные попытки)."""
if initial_delay > 0:
print(f"Ожидание {initial_delay:.0f} с, пока Qdrant запустится...", flush=True)
time.sleep(initial_delay)
health = f"{base_url.rstrip('/')}/readyz"
last_error: Exception | None = None
for i in range(max_attempts):
try:
req = urllib.request.Request(health, method="GET")
with urllib.request.urlopen(req, timeout=5) as resp:
if resp.status == 200:
return True
except Exception as e:
last_error = e
time.sleep(interval)
if last_error is not None:
print(f"Последняя ошибка при подключении: {last_error}", file=sys.stderr)
return False
def collection_exists(base_url: str, name: str) -> bool:
"""Проверяет, существует ли коллекция."""
url = f"{base_url.rstrip('/')}/collections/{name}"
try:
req = urllib.request.Request(url, method="GET")
with urllib.request.urlopen(req, timeout=10) as resp:
return resp.status == 200
except urllib.error.HTTPError as e:
if e.code == 404:
return False
raise
except Exception:
return False
def create_collection(base_url: str, name: str, size: int) -> dict[str, Any]:
"""Создаёт коллекцию с заданной размерностью вектора (Cosine)."""
url = f"{base_url.rstrip('/')}/collections/{name}"
payload = {
"vectors": {
"size": size,
"distance": "Cosine",
},
}
body = json.dumps(payload).encode("utf-8")
req = urllib.request.Request(
url,
data=body,
headers={"Content-Type": "application/json"},
method="PUT",
)
with urllib.request.urlopen(req, timeout=30) as resp:
return json.loads(resp.read().decode("utf-8"))
def main() -> int:
"""Ждёт Qdrant, при отсутствии коллекции создаёт её, выходит 0."""
base_url = env("QDRANT_URL", "http://qdrant:6333")
collection_name = env("QDRANT_COLLECTION_CHAPTER_ANALYSES", "chapter_analyses")
vector_size = int(env("QDRANT_VECTOR_SIZE", "1024"))
initial_delay = float(env("QDRANT_INIT_DELAY", "15"))
print(f"Qdrant init: URL={base_url}, collection={collection_name}, size={vector_size}", flush=True)
if not wait_for_qdrant(base_url, initial_delay=initial_delay):
print("Ошибка: Qdrant недоступен.", file=sys.stderr)
return 1
if collection_exists(base_url, collection_name):
print(f"Коллекция '{collection_name}' уже существует.", flush=True)
return 0
try:
create_collection(base_url, collection_name, vector_size)
print(f"Коллекция '{collection_name}' создана.", flush=True)
except urllib.error.HTTPError as e:
print(f"Ошибка HTTP {e.code}: {e.reason}", file=sys.stderr)
if e.fp:
try:
print(e.fp.read().decode("utf-8")[:500], file=sys.stderr)
except Exception:
pass
return 1
except Exception as e:
print(f"Ошибка: {e}", file=sys.stderr)
return 1
return 0
if __name__ == "__main__":
sys.exit(main())