diff --git a/README.md b/README.md index 5f5e387..39974a4 100644 --- a/README.md +++ b/README.md @@ -11,8 +11,8 @@ - [Возможности](#возможности) - [Архитектура](#архитектура) +- [Требования к базе данных](#требования-к-базе-данных) - [Инструменты MCP](#инструменты-mcp) -- [Модель данных](#модель-данных) - [Быстрый старт](#быстрый-старт) - [Конфигурация](#конфигурация) - [Развёртывание](#развёртывание) @@ -63,6 +63,125 @@ --- +## Требования к базе данных + +MCP-сервер рассчитывает, что в PostgreSQL уже существует подготовленная база +`vmk_data` (или любая другая, указанная в `DATABASE_URL`). + +### Обязательные расширения PostgreSQL + +Подключитесь к базе от имени суперпользователя и выполните: + +```sql +-- pgvector — для векторного поиска +CREATE EXTENSION IF NOT EXISTS vector; + +-- Для FTS (обычно встроено, но проверьте) +CREATE EXTENSION IF NOT EXISTS pg_trgm; -- опционально, для ILIKE-оптимизации +``` + +### Таблица `property_listings` + +Сервер читает только из этой таблицы. Она должна содержать следующие колонки: + +| Колонка | Тип | Обязательна | Описание | +|---------|-----|-------------|----------| +| `id` | `bigint` | ✅ | Первичный ключ | +| `title` | `text` | ✅ | Заголовок объявления | +| `description` | `text` | ✅ | Описание | +| `generated_description` | `text` | ❌ | AI-сгенерированное описание | +| `price` | `numeric` | ❌ | Цена | +| `currency` | `varchar(3)` | ❌ | `USD`, `EUR`, `UAH` | +| `deal_type` | `varchar` | ❌ | `sale`, `rent_long`, `rent_short` | +| `city` | `varchar` | ❌ | Город (украинский) | +| `district` | `varchar` | ❌ | Район (украинский) | +| `rooms_count` | `int` | ❌ | Количество комнат | +| `total_area` | `float` | ❌ | Общая площадь, м² | +| `living_area` | `float` | ❌ | Жилая площадь, м² | +| `kitchen_area` | `float` | ❌ | Площадь кухни, м² | +| `floor` | `int` | ❌ | Этаж | +| `floors_count` | `int` | ❌ | Этажность дома | +| `building_type` | `varchar` | ❌ | `brick`, `panel`, `monolith`, `gas_block`, `wood` | +| `building_year` | `int` | ❌ | Год постройки | +| `renovation_status` | `varchar` | ❌ | Статус ремонта | +| `balcony_count` | `int` | ❌ | Количество балконов | +| `bathroom_type` | `varchar` | ❌ | Тип санузла | +| `parking_type` | `varchar` | ❌ | Тип парковки | +| `heating_type` | `varchar` | ❌ | Тип отопления | +| `layout_type` | `varchar` | ❌ | Тип планировки | +| `window_view` | `varchar` | ❌ | Вид из окон | +| `metro_station` | `varchar` | ❌ | Станция метро | +| `metro_distance_type` | `varchar` | ❌ | `walking`, `transport` | +| `metro_distance_meters` | `int` | ❌ | Расстояние до метро, м | +| `url_source` | `text` | ❌ | Ссылка на источник | +| `publish_date` | `date` | ❌ | Дата публикации | +| `images_count` | `int` | ❌ | Количество фото | +| `contact_phone` | `varchar` | ❌ | Телефон контакта | +| `listing_status` | `varchar` | ❌ | `active`, `sold`, `rented`, `removed`, `archived` | +| `archived_at` | `date` | ❌ | Дата архивации | +| `created_at` / `updated_at` | `date` или `timestamp` | ❌ | Служебные таймстампы | +| `embedding` | `vector(768)` | ✅ | **Вектор эмбеддинга** (pgvector) | +| `search_vector` | `tsvector` | ✅ | **Полнотекстовый индекс** (украинский конфиг) | + +> ℹ️ Если в вашей базе нет каких-то колонок (например, `generated_description` или `kitchen_area`), +> сервер всё равно запустится, но эти поля будут возвращаться как `null`. +> **Обязательны** только `id`, `title`, `description`, `embedding`, `search_vector`. + +### Индексы (обязательны для производительности) + +```sql +-- Векторный HNSW-индекс (cosine distance) +CREATE INDEX IF NOT EXISTS property_listings_embedding_idx +ON property_listings +USING hnsw (embedding vector_cosine_ops); + +-- Полнотекстовый GIN-индекс +CREATE INDEX IF NOT EXISTS property_listings_search_vector_idx +ON property_listings +USING gin (search_vector); +``` + +### Генерация `search_vector` (если у вас его ещё нет) + +Если колонка `search_vector` отсутствует, создайте её как **generated column**: + +```sql +ALTER TABLE property_listings +ADD COLUMN search_vector tsvector +GENERATED ALWAYS AS ( + setweight(to_tsvector('ukrainian', coalesce(title, '')), 'A') || + setweight(to_tsvector('ukrainian', coalesce(description, '')), 'B') || + setweight(to_tsvector('ukrainian', coalesce(city, '')), 'C') || + setweight(to_tsvector('ukrainian', coalesce(district, '')), 'C') || + setweight(to_tsvector('ukrainian', coalesce(metro_station, '')), 'B') +) STORED; + +-- Пересоздать GIN-индекс после добавления колонки +CREATE INDEX property_listings_search_vector_idx +ON property_listings USING gin (search_vector); +``` + +> ⚠️ Если украинский конфиг `ukrainian` отсутствует в `SELECT cfgname FROM pg_ts_config;`, +> замените `'ukrainian'` на `'simple'`. + +### Генерация `embedding` (если колонка пустая) + +Для заполнения `embedding` в существующих записях используйте внешний скрипт +(не входит в MCP-сервер). Примерная логика: + +```python +# Псевдокод — выполняется отдельно от MCP-сервера +for batch in chunks(listings_without_embedding, 100): + texts = [f"{l.title}. {l.description}" for l in batch] + vectors = ollama.embed(model="nomic-embed-text", input=texts) + for l, v in zip(batch, vectors): + UPDATE property_listings SET embedding = v::vector WHERE id = l.id +``` + +Сервер ожидает, что эмбеддинги уже сгенерированы и сохранены в БД. + +--- + ## Инструменты MCP Сервер регистрирует 4 инструмента, доступных через MCP-протокол: @@ -116,136 +235,172 @@ --- -## Модель данных - -### Таблица `property_listings` - -Ключевые колонки, доступные для чтения (белый список `USER_COLUMNS`): - -| Колонка | Тип | Описание | -|---------|-----|----------| -| `id` | `bigint` | Первичный ключ | -| `title` | `text` | Заголовок объявления | -| `description` | `text` | Описание | -| `generated_description` | `text` | AI-сгенерированное описание | -| `price` | `numeric` | Цена | -| `currency` | `varchar(3)` | Валюта: `USD`, `EUR`, `UAH` | -| `deal_type` | `varchar` | Тип сделки: `sale`, `rent_long`, `rent_short` | -| `city` | `varchar` | Город (украинский) | -| `district` | `varchar` | Район (украинский) | -| `rooms_count` | `int` | Количество комнат | -| `total_area` | `float` | Общая площадь, м² | -| `living_area` | `float` | Жилая площадь, м² | -| `kitchen_area` | `float` | Площадь кухни, м² | -| `floor` | `int` | Этаж | -| `floors_count` | `int` | Этажность дома | -| `building_type` | `varchar` | `brick`, `panel`, `monolith`, `gas_block`, `wood` | -| `building_year` | `int` | Год постройки | -| `renovation_status` | `varchar` | Статус ремонта | -| `balcony_count` | `int` | Количество балконов | -| `bathroom_type` | `varchar` | Тип санузла | -| `parking_type` | `varchar` | Тип парковки | -| `heating_type` | `varchar` | Тип отопления | -| `layout_type` | `varchar` | Тип планировки | -| `window_view` | `varchar` | Вид из окон | -| `metro_station` | `varchar` | Станция метро | -| `metro_distance_type` | `varchar` | `walking`, `transport` | -| `metro_distance_meters` | `int` | Расстояние до метро, м | -| `url_source` | `text` | Ссылка на источник | -| `publish_date` | `date` | Дата публикации | -| `images_count` | `int` | Количество фото | -| `contact_phone` | `varchar` | Телефон контакта | -| `listing_status` | `varchar` | `active`, `sold`, `rented`, `removed`, `archived` | -| `archived_at` | `date` | Дата архивации | -| `created_at` / `updated_at` | `date` | Служебные таймстампы | -| `embedding` | `vector(768)` | Вектор эмбеддинга (pgvector) | -| `search_vector` | `tsvector` | Полнотекстовый индекс (украинский конфиг) | - -### Индексы базы данных - -- `property_listings_embedding_idx` — **HNSW** на `embedding` (`vector_cosine_ops`) -- `property_listings_search_vector_idx` — **GIN** на `search_vector` - ---- - ## Быстрый старт -### 1. Клонирование и установка +### Вариант А: Локальный запуск (для разработки) + +**Шаг 1 — Клонирование и окружение** ```bash git clone cd data_mcp python -m venv .venv -source .venv/bin/activate # Linux/macOS -# .venv\Scripts\activate # Windows +source .venv/bin/activate # Linux / macOS +# .venv\Scripts\activate # Windows PowerShell pip install -e "." ``` -### 2. Настройка окружения +**Шаг 2 — Настройка `.env`** + +Скопируйте пример и отредактируйте: ```bash cp .env.example .env -# Отредактируйте .env — укажите DATABASE_URL и OLLAMA_BASE_URL +nano .env # или vim / VS Code ``` -### 3. Запуск +Минимально необходимые переменные: + +```env +# Куда подключаться к PostgreSQL +DATABASE_URL=postgresql://postgres:postgres@localhost:5432/vmk_data + +# Куда обращаться за эмбеддингами +OLLAMA_BASE_URL=http://192.168.1.75:11434 +OLLAMA_EMBED_MODEL=nomic-embed-text +OLLAMA_EMBED_DIMENSIONS=768 + +# На каком порту слушать MCP +MCP_PORT=8080 +``` + +> 🔍 Где взять значения: +> - `DATABASE_URL` — DSN вашей PostgreSQL с базой `vmk_data`. +> - `OLLAMA_BASE_URL` — URL хоста, где запущена Ollama. Если на той же машине — `http://localhost:11434`. +> - `OLLAMA_EMBED_MODEL` — должна совпадать с моделью, которой вы генерировали `embedding` в БД. + +**Шаг 3 — Проверка зависимостей** + +```bash +# Проверьте, что Ollama отвечает +curl http://localhost:11434/api/tags + +# Проверьте, что PostgreSQL доступна +psql "${DATABASE_URL}" -c "SELECT COUNT(*) FROM property_listings;" +``` + +**Шаг 4 — Запуск** ```bash python -m vmk_data_mcp.main -# или -uvicorn vmk_data_mcp.main:app --host 0.0.0.0 --port 8080 ``` -Сервер стартует на `http://localhost:8080/mcp`. +В терминале появится: +``` +INFO: Started server process [12345] +INFO: Uvicorn running on http://0.0.0.0:8080 (Press CTRL+C to quit) +``` ---- +MCP endpoint доступен на `http://localhost:8080/mcp`. -## Конфигурация +### Вариант Б: Docker Compose -Все параметры задаются через `.env` или переменные окружения: +**Шаг 1 — Настройка `.env`** -| Переменная | По умолчанию | Описание | -|------------|-------------|----------| -| `DATABASE_URL` | `postgresql://postgres:postgres@localhost:5432/vmk_data` | DSN для asyncpg | -| `DB_POOL_MIN_SIZE` | `2` | Минимум соединений в пуле | -| `DB_POOL_MAX_SIZE` | `10` | Максимум соединений в пуле | -| `DB_QUERY_TIMEOUT` | `30` | Таймаут SQL-запросов, сек | -| `OLLAMA_BASE_URL` | `http://192.168.1.75:11434` | URL Ollama API | -| `OLLAMA_EMBED_MODEL` | `nomic-embed-text` | Модель эмбеддинга | -| `OLLAMA_EMBED_DIMENSIONS` | `768` | Размерность вектора | -| `OLLAMA_REQUEST_TIMEOUT` | `60.0` | Таймаут запроса к Ollama, сек | -| `MCP_SERVER_NAME` | `vmk-data-mcp` | Имя сервера в MCP | -| `MCP_PORT` | `8080` | Порт HTTP-транспорта | +```bash +cp .env.example .env +# Отредактируйте — для Docker обычно нужен host.docker.internal +``` ---- +Пример `.env` для Docker на Linux: -## Развёртывание +```env +DATABASE_URL=postgresql://postgres:postgres@host.docker.internal:5433/vmk_data +OLLAMA_BASE_URL=http://host.docker.internal:11434 +MCP_PORT=8080 +``` -### Docker Compose (рекомендуется) +> `host.docker.internal` — это хост-машина. Убедитесь, что PostgreSQL и Ollama +> слушают не только `127.0.0.1`, а также внешний интерфейс (или настроен проброс портов). + +**Шаг 2 — Сборка и запуск** ```bash docker-compose up --build -d ``` -`docker-compose.yml` использует `host.docker.internal` для доступа к -PostgreSQL и Ollama, запущенным на хост-машине. +Проверка: +```bash +curl http://localhost:8080/mcp +docker logs -f vmk-data-mcp +``` -> **Linux:** `host.docker.internal` требует `extra_hosts: ["host.docker.internal:host-gateway"]` -> (уже прописано в `docker-compose.yml`). +Остановка: +```bash +docker-compose down +``` + +--- + +## Конфигурация + +Все параметры задаются через `.env` или переменные окружения. + +### Обязательные переменные + +| Переменная | Пример значения | Откуда взять | +|------------|-----------------|--------------| +| `DATABASE_URL` | `postgresql://user:pass@host:5432/vmk_data` | DSN вашей PostgreSQL | +| `OLLAMA_BASE_URL` | `http://192.168.1.75:11434` | URL сервера Ollama | +| `OLLAMA_EMBED_MODEL` | `nomic-embed-text` | Модель, использованная для генерации `embedding` в БД | +| `OLLAMA_EMBED_DIMENSIONS` | `768` | Размерность вектора модели (768 для `nomic-embed-text`) | + +### Опциональные переменные + +| Переменная | По умолчанию | Описание | +|------------|-------------|----------| +| `DB_POOL_MIN_SIZE` | `2` | Минимум соединений в пуле asyncpg | +| `DB_POOL_MAX_SIZE` | `10` | Максимум соединений в пуле | +| `DB_QUERY_TIMEOUT` | `30` | Таймаут SQL-запросов, секунды | +| `OLLAMA_REQUEST_TIMEOUT` | `60.0` | Таймаут HTTP-запроса к Ollama, секунды | +| `MCP_SERVER_NAME` | `vmk-data-mcp` | Имя сервера в MCP-протоколе | +| `MCP_PORT` | `8080` | Порт HTTP-транспорта | + +> 💡 Совет: если Ollama и PostgreSQL работают локально, минимальный `.env` — это +> всего 4 строчки (см. раздел **Быстрый старт → Вариант А**). + +--- + +## Развёртывание ### Требования к инфраструктуре -- **PostgreSQL 15+** с расширениями: - ```sql - CREATE EXTENSION IF NOT EXISTS vector; - CREATE EXTENSION IF NOT EXISTS pg_trgm; -- опционально - ``` -- **Ollama** с моделью `nomic-embed-text` (768 dimensions): +- **PostgreSQL 15+** с расширением `pgvector` +- **Ollama** с моделью `nomic-embed-text` (768d): ```bash ollama pull nomic-embed-text ``` -- Для `search_vector` требуется украинская конфигурация FTS (либо `simple`, - если украинский конфиг отсутствует — проверьте `pg_ts_config`). +- Python 3.11+ (если запускаете без Docker) + +### Docker (один контейнер) + +```bash +docker build -t vmk-data-mcp . +docker run -d \ + -p 8080:8080 \ + -e DATABASE_URL=postgresql://... \ + -e OLLAMA_BASE_URL=http://... \ + --name vmk-data-mcp \ + vmk-data-mcp +``` + +### Docker Compose (рекомендуется) + +Файл `docker-compose.yml` уже в репозитории. Он использует `host.docker.internal` +для доступа к PostgreSQL и Ollama на хост-машине. + +> **Linux:** `host.docker.internal` работает благодаря `extra_hosts` в `docker-compose.yml`. +> На Windows/macOS это работает из коробки. --- @@ -259,7 +414,7 @@ - только префиксы `select`, `with`, `values`, `explain` - запрещён символ `;` внутри строки (блокировка multi-statement атак) 3. **На уровне колонок:** только `USER_COLUMNS` участвуют в `SELECT`; -попытка запросить другую колонку вызывает ошибку. + попытка запросить другую колонку вызывает ошибку. ### SQL-инъекции @@ -302,6 +457,9 @@ │ └── config.py # Настройки из .env (pydantic-settings) ├── tests/ │ └── test_models.py # Pytest +├── docs/ +│ ├── API.md # JSON-схемы и примеры запросов +│ └── ARCHITECTURE.md # Архитектура и data flow ├── Dockerfile ├── docker-compose.yml ├── pyproject.toml @@ -319,7 +477,11 @@ Сервис эмбеддингов недоступен: ConnectError(...) ``` - Проверьте `OLLAMA_BASE_URL` -- Убедитесь, что Ollama слушает на `0.0.0.0` (не только `127.0.0.1`) +- Убедитесь, что Ollama слушает на `0.0.0.0` (не только `127.0.0.1`): + ```bash + # На Linux: проверьте переменную окружения + echo $OLLAMA_HOST # должно быть 0.0.0.0 или пусто + ``` - Проверьте firewall / Docker network ### Нет результатов при семантическом поиске @@ -327,7 +489,10 @@ - Убедитесь, что запрос переведён на **украинский** перед вызовом инструмента. - Проверьте порог `min_similarity` — слишком высокий (0.95+) может исключить все записи. -- Убедитесь, что в БД заполнена колонка `embedding` (не NULL). +- Убедитесь, что в БД заполнена колонка `embedding` (не NULL): + ```sql + SELECT COUNT(*) FROM property_listings WHERE embedding IS NOT NULL; + ``` ### PostgreSQL: отсутствует `vector` @@ -340,6 +505,11 @@ Сервер ожидает `OLLAMA_EMBED_DIMENSIONS=768` (модель `nomic-embed-text`). Если используется другая модель — обновите переменную окружения. +### `search_vector` не найдена + +Если в БД нет колонки `search_vector` — создайте её как generated column +(см. раздел [Требования к базе данных → Генерация search_vector](#требования-к-базе-данных)). + --- ## Лицензия