init
This commit is contained in:
24
qdrant_init/README.md
Normal file
24
qdrant_init/README.md
Normal 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
118
qdrant_init/entrypoint.py
Normal 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())
|
||||
Reference in New Issue
Block a user