Введение
До сегодняшнего дня мои ИИ-агенты использовали векторную БД Pinecone для долгосрочной памяти, а таблицы Supabase — для краткосрочной (контекстное окно). Краткосрочная память работает просто: подгружаются только последние N сообщений. А вот у Pinecone возникла проблема — он не позволяет полноценно фильтровать воспоминания по метаданным (дата, тема, эмоции, ключевые слова и т.д.). А фильтрация необходима, чтобы повысить релевантность выдачи.
Например, когда ИИ-агент делает запрос к векторной БД, его текст переводится в вектор и сравнивается с векторами воспоминаний. Результаты сортируются по семантической близости. Но близость по смыслу ≠ актуальность: БД может вернуть старые и неактуальные воспоминания, потому что они ближе по смыслу. Pinecone (на бесплатном тарифе) не позволял фильтровать такие результаты по дате и прочим признакам.
Кроме того, бесплатный период Supabase подошёл к концу. Мне дали 2 недели на переход на платный тариф — $25 в месяц. Я не захотел платить «ни за что» и решил развернуть собственный Supabase с pgvector на VPS за 600₽ в месяц.
Вот как я это сделал (всё выполнялось через SSH-клиент "Terminus" кроме пункта "0"):
0. Разворачиваем VPS сервер с установленной на нем Supabase на beget прямо через личный кабинет.
Если у вашего сервиса, где вы хотите разворачивать VPS нет возможности сразу установить Supabase, просто устанавливаете Ubuntu (она уж точно есть), а затем командами:
- Установка зависимостей:
- sudo apt update
- sudo apt install -y curl unzip git
- Установка Supabase CLI:
curl -sL https://github.com/supabase/cli/releases/latest/download/supabase_linux_amd64.tar.gz | tar -xz
sudo mv supabase /usr/local/bin/
- Проверь, установилась ли сама Supabase:
supabase --version
1. Заход в PostgreSQL внутри контейнера:
psql -U postgres
Запускаем PostgreSQL внутри контейнера, используя пользователя postgres. Это нужно, чтобы проверить установленные расширения или подключить новые.
2. Проверка установленных расширений и попытка включить pgvector:
\dx
CREATE EXTENSION vector;
\dx показывает список расширений. Если pgvector уже установлен, CREATE EXTENSION активирует его.
3. Установка pgvector вручную, если оно отсутствует:
docker exec -it supabase-db bash
Заходим внутрь контейнера supabase-db, где установлен PostgreSQL.
apt update
apt install -y git postgresql-server-dev-all
Устанавливаем нужные зависимости для сборки расширений PostgreSQL.
cd /tmp
git clone https://github.com/pgvector/pgvector.git
cd pgvector
make && make install
Клонируем исходники pgvector и компилируем его вручную прямо внутри контейнера.
4. Повторная активация расширения после установки:
CREATE EXTENSION vector;
Теперь команда выполнится успешно. Проверить можно повторно через \dx.
5. Создание таблицы с полем эмбеддинга:
CREATE TABLE memories (
id TEXT PRIMARY KEY,
content TEXT,
embedding VECTOR(1536),
metadata JSONB,
timestamp TIMESTAMPTZ
);
Создаём основную таблицу, в которую n8n будет вставлять эмбеддинги. Тип VECTOR(1536) подходит для OpenAI text-embedding-3-large.
6. Добавление проброса порта PostgreSQL в docker-compose:
ports:
- "5432:5432"
Добавляем внутрь секции db: в docker-compose.yml. Без этого база будет недоступна извне.
7. Отключение Supavisor:
В .env нужно закомментировать параметр POOLER_TENANT_ID, иначе Supabase будет использовать Supavisor, который ограничивает доступ и требует другую схему аутентификации:
# POOLER_TENANT_ID=...
7.1. Настройка доступа в pg_hba.conf:
nano /var/lib/postgresql/data/pg_hba.conf
В конец файла добавляется строка:
host all all 0.0.0.0/0 md5
Это разрешает подключение к PostgreSQL с любого IP-адреса (можно ограничить при необходимости).
8. Перезапуск Supabase:
cd /opt/beget/supabase
docker compose down
docker compose up -d
Обновляем контейнеры после правки конфигурации.
9. Подключение к базе из n8n:
Создаём PostgreSQL credentials в интерфейсе n8n:
- Host: parukesepem.beget.app
- Port: 5432
- Database: postgres
- User: supabase_admin
- Password: b1ZcdBzOX0q9odaXRU6a5g2X1SJWJiEK
- SSL: Off
10. Проверка подключения через psql с другой машины:
psql -h parukesepem.beget.app -U supabase_admin -d postgres
Если соединение успешно — значит, Supabase полностью доступен извне, и pgvector работает.
Выводы
- Pinecone не подходит для продвинутого использования: отсутствует фильтрация по метаданным, неясное ценообразование.
- Supabase дал возможность поднять собственную инфраструктуру с поддержкой pgvector и SQL-запросов.
- pgvector пришлось собирать вручную внутри контейнера PostgreSQL.
- Supavisor был отключён, чтобы не мешал прямому подключению к базе.
- Через pg_hba.conf был разрешён внешний доступ.
- n8n теперь может напрямую взаимодействовать с таблицей memories — вставлять эмбеддинги, фильтровать по JSONB, делать семантический поиск.
⚙️ Система полностью автономна, дёшева в обслуживании и масштабируема.
Затем я написал SQL функцию (конечно же с помощью Клода) для своей таблицы:
CREATE OR REPLACE FUNCTION match_documents(
filter jsonb DEFAULT '{}',
match_count int DEFAULT 10,
query_embedding vector DEFAULT NULL
)
RETURNS TABLE (
id text,
content text,
metadata jsonb,
similarity float
)
LANGUAGE plpgsql
AS $$
BEGIN
IF query_embedding IS NULL THEN
RAISE EXCEPTION 'query_embedding cannot be NULL';
END IF;
RETURN QUERY
SELECT
m.id,
m.content,
m.metadata,
1 - (m.embedding <=> query_embedding) as similarity
FROM memories m
WHERE 1 - (m.embedding <=> query_embedding) > 0.5
ORDER BY m.embedding <=> query_embedding
LIMIT match_count;
END;
$$;
И теперь могу делать ГИБРИДНЫЕ запросы в таблицу, которые совмещают и векторный поиск и SQL запросы для фильтрации:
Вот запрос для функции match_documents в нужном формате:
sql
SELECT * FROMmatch_documents(
'{}'::jsonb,
5,
'{{ "[" + $json.data[0].embedding.join(",") + "]" }}'::vector
);
С фильтрами по метаданным:
Поиск по эмоции:
sql
SELECT * FROMmatch_documents(
'{"emotion": "интерес"}'::jsonb,
5,
'{{ "[" + $json.data[0].embedding.join(",") + "]" }}'::vector
);
Поиск за сегодняшний день:
sql
SELECT * FROMmatch_documents(
'{"today": true}'::jsonb,
5,
'{{ "[" + $json.data[0].embedding.join(",") + "]" }}'::vector
);
Поиск за диапазон дат:
sql
SELECT * FROMmatch_documents(
'{"date_from": "2025-08-01", "date_to": "2025-08-06"}'::jsonb,
5,
'{{ "[" + $json.data[0].embedding.join(",") + "]" }}'::vector
);
Комбинированный поиск (эмоция + дата):
sql
SELECT * FROMmatch_documents(
'{"emotion": "интерес", "today": true}'::jsonb,
5,
'{{ "[" + $json.data[0].embedding.join(",") + "]" }}'::vector
);