| .claude/ plans | 1 day ago | ||
| alembic | 1 day ago | ||
| docs | 1 day ago | ||
| scripts | 1 day ago | ||
| src/ vmk_data_collector | 1 day ago | ||
| tests | 1 day ago | ||
| .dockerignore | 1 day ago | ||
| .env.example | 1 day ago | ||
| .gitignore | 1 day ago | ||
| .plan.md | 1 day ago | ||
| Dockerfile | 1 day ago | ||
| README.md | 1 day ago | ||
| alembic.ini | 1 day ago | ||
| docker-compose.yml | 1 day ago | ||
| pyproject.toml | 1 day ago | ||
| uv.lock | 1 day ago | ||
Сервис приёма, нормализации и ИИ-обогащения данных об объектах недвижимости.
# 1. Клонировать и перейти в директорию cd vmk_data_collector # 2. Скопировать конфиг # Отредактируй .env под себя (OLLAMA_BASE_URL, порты и т.д.) cp .env.example .env # 3. Запуск всего стека (PostgreSQL + FastAPI) docker compose up -d --build # 4. Проверка # Документация API: http://localhost:8020/docs # Health check: http://localhost:8020/api/v1/health
docker compose down # остановить docker compose down -v # остановить + удалить данные БД
# 1. PostgreSQL должен быть доступен (локально или в Docker) # Порт по умолчанию: 5432 (хост) / 5433 (если через docker-compose) # 2. Установка зависимостей pip install -e ".[dev]" # 3. Применение миграций alembic upgrade head # 4. Запуск приложения uvicorn vmk_data_collector.main:app --reload --port 8020
Подробный гайд для разработчиков парсеров с примерами кода, retry-логикой и FAQ:
📄 docs/PARSER_INTEGRATION.md
POST /api/v1/ingestПринимает сырые данные от парсеров, валидирует payload и ставит задачу в очередь на обработку.
| Заголовок | Обязательный | Значение |
|---|---|---|
Content-Type |
Да | application/json |
{
"source_slug": "avito",
"external_id": "avito-12345678",
"payload": {
"title": "2-комнатная квартира, 65 м², 5/25 этаж",
"description": "Продается просторная двухкомнатная квартира в новостройке. Рядом метро.",
"price": 8500000,
"url": "https://avito.ru/item/12345678",
"images": [
"https://avito.ru/img1.jpg",
"https://avito.ru/img2.jpg"
],
"contact_phone": "+7 (999) 123-45-67",
"address": "Москва, Тверская ул., 1",
"area": 65.5,
"rooms": 2,
"floor": 5
}
}
payload| Поле | Тип | Обязательное | Описание |
|---|---|---|---|
title |
string |
Нет | Заголовок объявления |
description |
string |
Нет | Описание объявления |
price |
number / string |
Нет | Цена (руб.) |
url |
string |
Нет | Ссылка на источник |
images |
string[] |
Нет | Массив URL изображений |
contact_phone |
string |
Нет | Телефон продавца |
address |
string |
Нет | Адрес объекта |
area |
number / string |
Нет | Площадь (м²) |
rooms |
integer / string |
Нет | Количество комнат |
floor |
integer / string |
Нет | Этаж |
Важно: хотя бы одно из полей title или description должно присутствовать.
202 Accepted (успех){
"job_id": 42,
"property_id": null,
"status": "pending",
"reason": null,
"message": "Queued for processing",
"snapshot_id": null
}
422 Unprocessable Entity (ошибка валидации){
"detail": "Invalid payload: 1 validation error for PayloadSchema..."
}
POST /api/v1/ingest/with-imagesПринимает бинарные изображения от парсера напрямую. Парсер сам выкачивает фото со своего источника и передаёт их нам — сервер не пытается обходить чужие CDN.
| Эндпоинт | Когда использовать |
|---|---|
/ingest |
Парсер передаёт только URLs изображений |
/ingest/with-images |
Парсер сам скачал фото и передаёт бинарные файлы |
| Заголовок | Обязательный | Значение |
|---|---|---|
Content-Type |
Да | multipart/form-data |
metadata: {"source_slug": "domria", "external_id": "34329468", "payload": {"title": "...", "description": "...", "price": 125000}}
images: [binary-photo-1.jpg]
images: [binary-photo-2.jpg]
images: [binary-photo-3.jpg]
Поле metadata — это JSON-строка с тем же форматом, что и в /ingest. Поле images повторяется для каждого файла (FastAPI принимает list[UploadFile]).
202 Accepted (успех)Тот же формат, что и у /ingest, но pipeline отрабатывает синхронно в рамках запроса — потому что изображения уже локальные и не нужно ждать очереди.
{
"job_id": 42,
"property_id": 7,
"status": "completed",
"reason": null,
"message": "Property ingested successfully",
"snapshot_id": 3
}
# Отправить объявление с 3 фото
curl -X POST http://localhost:8020/api/v1/ingest/with-images \
-F 'metadata={"source_slug": "domria", "external_id": "34329468", "payload": {"title": "2-комнатная квартира", "description": "Продается квартира", "price": 125000}}' \
-F 'images=@/path/to/photo1.jpg' \
-F 'images=@/path/to/photo2.jpg' \
-F 'images=@/path/to/photo3.jpg'
import requests
metadata = {
"source_slug": "domria",
"external_id": "34329468",
"payload": {
"title": "2-комнатная квартира",
"description": "Продается квартира",
"price": 125000,
},
}
files = [
("images", open("photo1.jpg", "rb")),
("images", open("photo2.jpg", "rb")),
("images", open("photo3.jpg", "rb")),
]
response = requests.post(
"http://localhost:8020/api/v1/ingest/with-images",
data={"metadata": json.dumps(metadata)},
files=files,
)
print(response.json())
После приёма задача (job_id) проходит через pipeline:
| Статус | Значение |
|---|---|
pending |
Задача поставлена в очередь |
processing |
AI нормализует данные |
completed |
Объект сохранён в БД |
invalid |
AI определил, что это не недвижимость |
failed |
Ошибка на стадии обработки (Ollama недоступен и т.д.) |
curl -X POST http://localhost:8020/api/v1/ingest \
-H "Content-Type: application/json" \
-d '{
"source_slug": "avito",
"external_id": "avito-99999",
"payload": {
"title": "3-комнатная квартира, 120 м²",
"description": "Элитная квартира в центре",
"price": 25000000,
"url": "https://avito.ru/item/99999",
"address": "Москва, Сити"
}
}'
curl -X POST http://localhost:8020/api/v1/listings/1/archive-check
Все настройки задаются через переменные окружения (.env или docker-compose.yml):
| Переменная | Дефолт | Описание |
|---|---|---|
APP_PORT |
8020 |
Порт приложения (хост) |
DATABASE_URL |
postgresql+asyncpg://postgres:postgres@postgres:5432/vmk_data |
PostgreSQL (async) |
OLLAMA_BASE_URL |
http://192.168.1.75:11434 |
Ollama API |
OLLAMA_TEXT_MODEL |
gemma4:e2b-it-q4_K_M |
Модель для текстовых задач |
OLLAMA_VISION_MODEL |
gemma4:e2b-it-q4_K_M |
Модель для анализа изображений |
OLLAMA_TIMEOUT |
120 |
Таймаут запроса к Ollama (сек) |
LOG_LEVEL |
INFO |
Уровень логирования |
┌──────────────┐ ┌──────────────┐ ┌──────────────────┐
│ Парсеры │────▶│ FastAPI │────▶│ PostgreSQL │
│ (curl/HTTP) │ │ /api/v1/ │ │ (raw_data + │
└──────────────┘ │ ingest │ │ property_...) │
└──────┬───────┘ └──────────────────┘
│
▼
┌──────────────┐
│ QueueWorker │
│ (async bg) │
└──────┬───────┘
│
┌─────────────┼─────────────┐
▼ ▼ ▼
┌─────────┐ ┌─────────┐ ┌─────────┐
│AI Норма-│ │AI Анализ│ │AI Обога-│
│лизатор │ │изображ. │ │щение │
└────┬────┘ └────┬────┘ └────┬────┘
│ │ │
└─────────────┴─────────────┘
│
▼
┌──────────────┐
│ Ollama │
│ 192.168.1.75 │
└──────────────┘
PropertyPipeline, AI-нормализация, обогащение, QueueWorkerhttp://localhost:8020/docshttp://localhost:8020/redocВсе логи выводятся в stdout в формате JSON (structlog), что удобно для сбора через docker compose logs:
# Следить за логами в реальном времени docker compose logs -f app # Посмотреть последние 50 строк docker compose logs --tail=50 app
Ключевые события для мониторинга:
ingest_request — новый запрос от парсераingest_accepted — задача поставлена в очередьingest_validation_failed — ошибка валидации payloadpipeline_start — начало обработки задачиpipeline_completed — задача успешно завершенаpipeline_not_real_estate — AI отбросил объектollama_chat_request/response — запросы к Ollama