Newer
Older
vmk-360-data_collector / docs / SPECIFICATION.md
@Eugene Sukhodolskiy Eugene Sukhodolskiy 1 day ago 19 KB feat: core pipeline + FastAPI API (Phases 0-6)

VMK 360 Data Collector — Техническое задание

1. Общее описание

Сервис приёма, нормализации и ИИ-обогащения данных об объектах недвижимости.

Парсеры отправляют полусырые данные через REST API. Сервис валидирует их с помощью локальной LLM (Ollama), приводит к единому формату, анализирует изображения, обогащает текст ИИ и сохраняет в PostgreSQL. При повторном приёме объявления (по source_id + external_id) создаётся снапшот старой версии и обновляется текущая.

2. Домен: объекты недвижимости

2.1 Типы объектов (property_types)

  • Квартира
  • Дом
  • Таунхаус
  • Коммерческая
  • Земельный участок
  • Гараж
  • Офис
  • Склад
  • Торговая площадь
  • Коттедж
  • Комната
  • Новостройка

2.2 Типы сделок (deal_types)

  • sale — продажа
  • rent_long — долгосрочная аренда
  • rent_short — посуточная аренда

3. Модель данных

3.1 Справочники

Таблица Поля Описание
data_sources id, slug (UNIQUE), name, url_pattern, description, created_at Источники парсеров
property_types id, slug (UNIQUE), name, description Типы объектов недвижимости
deal_types id, slug (UNIQUE), name Типы сделок

3.2 Основные сущности

raw_parsing_data

Приёмник всех данных от парсеров.

Поле Тип Описание
id SERIAL PK
source_id FK → data_sources
external_id VARCHAR ID от парсера
payload JSONB Абсолютно любая структура от парсера
status ENUM pending, processing, completed, failed, invalid
validation_result VARCHAR valid, invalid, uncertain
error_message TEXT nullable
received_at TIMESTAMPTZ
processed_at TIMESTAMPTZ nullable

Уникальность: UNIQUE(source_id, external_id)

property_listings

Нормализованное объявление. Максимум типичных полей для недвижимости.

Поле Тип Описание
id SERIAL PK
raw_data_id FK → raw_parsing_data (UNIQUE)
source_id FK → data_sources
external_id VARCHAR ID от парсера
title VARCHAR Оригинальное название
description TEXT Оригинальное описание
generated_description TEXT AI-описание
deal_type ENUM sale, rent_long, rent_short
property_type_id FK → property_types
price NUMERIC(18,2)
currency VARCHAR(3) UAH / USD / EUR
original_price NUMERIC(18,2) Цена в валюте источника
original_currency VARCHAR(3)
price_per_sqm NUMERIC(18,2)
total_area NUMERIC(8,2) Общая площадь, м²
living_area NUMERIC(8,2) Жилая площадь
kitchen_area NUMERIC(8,2) Площадь кухни
land_area NUMERIC(10,2) Площадь участка, соток/га
rooms_count SMALLINT
bedrooms_count SMALLINT
bathrooms_count SMALLINT
layout VARCHAR studio, separate, adjacent
floor SMALLINT
floors_total SMALLINT
building_year SMALLINT
building_type VARCHAR brick, panel, monolith, gas_block, wood
renovation_status VARCHAR cosmetic, euro, designer, none, construction
ceiling_height NUMERIC(4,2) В метрах
material VARCHAR Материал стен
has_balcony BOOLEAN
has_loggia BOOLEAN
balcony_count SMALLINT
loggia_count SMALLINT
bathroom_type VARCHAR combined, separate, multiple
elevator_count SMALLINT
has_freight_elevator BOOLEAN
parking_type VARCHAR ground, underground, none, garage
heating_type VARCHAR central, autonomous, floor, none
internet BOOLEAN
security BOOLEAN
windows_direction VARCHAR Направление окон
window_view VARCHAR yard, street, park, water, forest
address_raw TEXT Адрес как пришёл
city VARCHAR
district VARCHAR Район
micro_district VARCHAR Микрорайон
street VARCHAR Улица
house_number VARCHAR Номер дома
metro_station VARCHAR Станция метро
metro_distance_min SMALLINT Минут до метро
metro_distance_type VARCHAR walk, transport
latitude NUMERIC(10,7)
longitude NUMERIC(10,7)
contact_phone VARCHAR
contact_name VARCHAR
contact_email VARCHAR
is_agent BOOLEAN
agency_name VARCHAR
publish_date TIMESTAMPTZ
url_source TEXT URL объявления
listing_status ENUM active, sold, rented, removed, archived
images_count SMALLINT
listing_quality_score SMALLINT AI-оценка качества объявления, 1–10
reliability_rating SMALLINT AI-оценка надёжности, 1–5
sentiment_score NUMERIC(3,2) -1..1
created_at TIMESTAMPTZ
updated_at TIMESTAMPTZ

Уникальность: UNIQUE(source_id, external_id)

property_images

Картинки объявления.

Поле Тип Описание
id SERIAL PK
property_id FK → property_listings
url TEXT Исходный URL
local_path VARCHAR Путь к файлу на диске
hash VARCHAR(64) SHA-256 для дедупликации
file_size INTEGER Байты
width SMALLINT
height SMALLINT
download_status ENUM pending, downloaded, failed
ai_description TEXT AI-описание фото
analysis_status ENUM pending, completed, failed
order_index SMALLINT Порядок в галерее

property_custom_fields

Универсальные кастомные атрибуты. Всё, что не вошло в типичные поля property_listings.

Поле Тип Описание
id SERIAL PK
property_id FK → property_listings
field_name VARCHAR Ключ
field_value TEXT Значение
field_type VARCHAR str, int, float, bool, date, json

Уникальность: UNIQUE(property_id, field_name)

property_snapshots

История обновлений объявлений.

Поле Тип Описание
id SERIAL PK
property_id FK → property_listings
snapshot_data JSONB Полная копия listing + custom_fields
changed_fields JSONB Diff (только изменённые поля)
created_at TIMESTAMPTZ

ai_enrichments

Результат ИИ-анализа.

Поле Тип Описание
id SERIAL PK
property_id FK → property_listings (UNIQUE)
extracted_features JSONB Доп. параметры из текста
price_assessment JSONB { market_estimate, deviation_percent, confidence, comment }
listing_quality_score SMALLINT 1–10
reliability_rating SMALLINT 1–5
sentiment_score NUMERIC(3,2) -1..1
classification VARCHAR AI-классификация объявления
image_analysis_results JSONB Агрегированный анализ фото
generated_description TEXT AI-сгенерированное описание
summary TEXT Краткое резюме
model_version VARCHAR Версия/модель LLM
processing_time_ms INTEGER
created_at TIMESTAMPTZ

3.3 Связи

raw_parsing_data 1:1 property_listings
property_listings 1:N property_images
property_listings 1:N property_custom_fields
property_listings 1:N property_snapshots
property_listings 1:1 ai_enrichments
property_listings N:1 property_types
property_listings N:1 data_sources

4. AI-слой (Ollama)

4.1 Конфигурация

OLLAMA_BASE_URL=http://localhost:11434
OLLAMA_TEXT_MODEL=llama3.2
OLLAMA_VISION_MODEL=llava
OLLAMA_TIMEOUT=120
OLLAMA_MOCK=false
IMAGE_STORAGE_PATH=/var/lib/vmk/images

4.2 AiNormalizer (валидация + структуризация)

Задача: определить, является ли payload объявлением о недвижимости. Если нет — вернуть is_real_estate: false. Если да — вернуть нормализованную структуру.

Границы валидации:

  • Reject: автомобили, телефоны, вакансии, мебель, бытовая техника, животные.
  • Accept: квартиры, дома, гаражи, земля, офисы, склады, торговые площади.
  • Uncertain: стройматериалы, услуги ремонта — требуют внимания, но пока accept.

Поведение при reject:

  • raw_parsing_data.status = invalid
  • validation_result = invalid
  • Ответ API: 202 { job_id, status: "invalid", reason: "..." }
  • НИЧЕГО не создаётся в property_listings.

Структурированный выход (JSON schema в prompt):

{
  "is_real_estate": true,
  "property_type": "apartment",
  "deal_type": "sale",
  "title": "...",
  "description": "...",
  "price": 12200000,
  "currency": "RUB",
  "total_area": 78.5,
  "rooms_count": 3,
  "floor": 12,
  "floors_total": 16,
  "building_year": 2019,
  "address": "г. Москва, ул. Ленина, 15",
  "city": "Москва",
  "metro_station": "Таганская",
  "metro_distance_min": 8,
  "contact_phone": "+7...",
  "contact_name": "...",
  "is_agent": false,
  "images": ["https://..."],
  "custom_fields": {
    "balcony_type": "лоджия",
    "ceiling_height": 2.85
  }
}

4.3 AiImageAnalyzer

Задача: анализ каждого изображения объекта.

Вход: base64 картинка. Выход:

{
  "overall_condition": "хороший ремонт, современный стиль",
  "rooms_observed": 3,
  "issues_found": ["пятно на потолке"],
  "positive_highlights": ["панорамные окна", "встроенная кухня"],
  "view_from_window": "двор, детская площадка",
  "furniture_included": true,
  "appliances_included": ["холодильник", "стиральная машина"]
}

Процесс:

  1. Скачивание картинок по URL (asyncio.gather).
  2. Сохранение в IMAGE_STORAGE_PATH / {property_id} / {hash}.ext.
  3. Дедупликация по SHA-256.
  4. Извлечение width/height (Pillow).
  5. Vision-анализ через Ollama (llava / llama3.2-vision).

4.4 AiEnricher (финальное обогащение)

Задача: анализ текста + результатов image_analysis.

Вход:

  • Нормализованный текст (title, description, custom_fields)
  • Агрегированные image_analysis_results

Выход:

{
  "extracted_features": {
    "property_complex": "ЖК Солнечный",
    "developer": "Группа Самолет",
    "material": "монолит",
    "additional_params": { "кондиционер": true }
  },
  "price_assessment": {
    "market_estimate": 12500000,
    "deviation_percent": -3.5,
    "confidence": 0.82,
    "comment": "Цена соответствует рынку"
  },
  "listing_quality_score": 8,
  "reliability_rating": 4,
  "sentiment_score": 0.6,
  "classification": "secondary_sale_apartment",
  "generated_description": "Продаётся уютная 3-комнатная квартира...",
  "summary": "3-комнатная квартира, 78 м², ЖК Солнечный, цена 12.2 млн",
  "language": "ru"
}

4.5 Ollama клиент

  • Текст: POST /api/chat с JSON mode (system prompt: «Ответь ТОЛЬКО JSON»).
  • Vision: /api/chat с images: [base64...].
  • Retries: exponential backoff (2^n сек), max 5 попыток.
  • Timeout: 60–120 секунд.
  • Circuit breaker: при 5 ошибок подряд — fallback на mock или queue.
  • Mock режим: при OLLAMA_MOCK=true возвращается фиксированный JSON без реального вызова.

5. Пайплайн обработки (PropertyPipeline)

Parser JSON
    │
    ▼
POST /api/v1/properties/ingest
    │
    ▼
Save raw_parsing_data (status = pending)
    │
    ▼
AiNormalizer (Ollama text model)
    │
    ├──► "Это не недвижимость"
    │       └──► raw_data.status = invalid
    │           Response: 202 { job_id, status: invalid }
    │
    └──► "Это недвижимость"
            │
            ▼
        Structured JSON от Ollama
            │
            ▼
        Check: source_id + external_id exists?
            │
            ├──► Нет → CREATE property_listings
            │
            └──► Да  → UPDATE
                    │
                    ├──► Копируем текущее → property_snapshots
                    ├──► Обновляем property_listings
                    └──► Удаляем старые custom_fields, пересоздаём
            │
            ▼
        Download images (asyncio.gather)
        URL → local dir (IMAGE_STORAGE_PATH) → SHA-256 → property_images
            │
            ▼
        AiImageAnalyzer (Ollama vision model)
        Каждое фото → описание, ремонт, комнаты, проблемы
            │
            ▼
        AiEnricher (Ollama text model)
        Текст + image_analysis → summary, price_assessment,
        quality_score, reliability_rating, generated_description
            │
            ▼
        Commit всё в одной транзакции
        raw_data.status = completed
            │
            ▼
        Response 202 { job_id, property_id, status: completed }

5.1 Повторные объявления (Upsert)

При совпадении source_id + external_id:

  1. Создаём снапшот текущей версии в property_snapshots.
  2. Обновляем property_listings.
  3. Удаляем старые property_custom_fields, создаём новые.
  4. Обрабатываем изображения: новые добавляем, существующие по hash пропускаем.
  5. Пересоздаём ai_enrichments.
  6. Обновляем raw_parsing_data.status = completed.

6. API

POST /api/v1/properties/ingest

Гибкий формат входа (парсеры гарантированно шлют):

{
  "source_slug": "olx_parser",
  "external_id": "olx_987654",
  "payload": {
    "title": "Продам 2-комнатную квартиру",
    "url": "https://olx.ua/...",
    "images": ["https://..."],
    "published_at": "2024-06-01T10:00:00Z"
  }
}

Опционально могут шлют:

  • payload.description — текст с кашей параметров
  • payload.price + payload.currency
  • payload.contacts — phone, name
  • payload.address — строка
  • payload.params — разнобой ключей от парсера

Response (invalid / rejected):

{
  "job_id": 1,
  "property_id": null,
  "status": "invalid",
  "reason": "Payload appears to be a car listing, not real estate",
  "message": "Data rejected: not a real estate listing"
}

Response (new listing created):

{
  "job_id": 1,
  "property_id": 42,
  "status": "completed",
  "message": "New property listing created and enriched"
}

Response (existing listing updated):

{
  "job_id": 1,
  "property_id": 42,
  "status": "completed",
  "message": "Existing property listing updated, snapshot saved",
  "snapshot_id": 15
}

7. Тесты

Unit-тесты

  • test_ai_normalizer.py — валидный reject (авто), валидный accept (квартира), распознавание из каши.
  • test_ai_image_analyzer.py — мок Ollama vision.
  • test_ai_enricher.py — мок Ollama text.
  • test_upsert_pipeline.py — snapshot создан при обновлении.
  • test_image_downloader.py — скачивание, hash, дедупликация.

Integration-тесты

  • test_api_ingest.py — FastAPI TestClient.
  • test_end_to_end.py — минимальный payload → проверка создания raw + listing + images + custom_fields + enrichment.
  • test_invalid_reject.py — payload про авто → только raw со статусом invalid.
  • test_upsert_integration.py — duplicate external_id → snapshot + update.

Фикстуры

  • async_engine — test DB engine.
  • db_session — async session с rollback.
  • client — FastAPI TestClient.
  • mock_ollama_client — фиксированные JSON-ответы.
  • sample_payloads — примеры от разных парсеров (olx, avito, domria).

8. Переменные окружения (.env)

# Database
DATABASE_URL=postgresql+asyncpg://postgres:postgres@localhost:5432/vmk_data
DATABASE_POOL_SIZE=20
DATABASE_MAX_OVERFLOW=10
DATABASE_ECHO=false

# Application
APP_HOST=0.0.0.0
APP_PORT=8000
LOG_LEVEL=info
DEBUG=false

# Ollama
OLLAMA_BASE_URL=http://localhost:11434
OLLAMA_TEXT_MODEL=llama3.2
OLLAMA_VISION_MODEL=llava
OLLAMA_TIMEOUT=120
OLLAMA_MOCK=false

# Storage
IMAGE_STORAGE_PATH=/var/lib/vmk/images

# Feature flags
ENABLE_IMAGE_ANALYSIS=true
ENABLE_PRICE_ESTIMATION=true

9. Технологический стек

  • Python 3.12+
  • FastAPI — REST API
  • PostgreSQL + asyncpg — БД
  • SQLAlchemy 2.x (async) + Alembic — ORM и миграции
  • Pydantic — валидация и Settings
  • httpx — HTTP клиент (Ollama API + скачивание картинок)
  • Pillow — обработка изображений (width/height)
  • tenacity — retries
  • structlog — логирование
  • pytest + pytest-asyncio — тестирование
  • Docker Compose — PostgreSQL и Ollama

10. Порядок реализации

Фаза Что делаем
1. Фундамент pyproject.toml, .env.example, docker-compose.yml, директории
2. Модели + миграции SQLAlchemy модели (8 таблиц), Alembic initial migration, seed property_types
3. Инфраструктура БД async engine, session, репозитории
4. AI слой Ollama HTTP client, AiNormalizer, AiImageAnalyzer, AiEnricher
5. Pipeline Image downloader, PropertyPipeline (upsert + snapshots), FastAPI router
6. Тесты unit + integration
7. Документация SPECIFICATION.md, ARCHITECTURE.md, README.md