[nevr]
· 10 мин чтения

PostgreSQL как граф-база: CTE + pgvector + pg_trgm = Neo4j не нужен

«Для графов нужна графовая база данных» — звучит убедительно, но рассыпается при контакте с реальностью. Я построил production knowledge graph на PostgreSQL. Без Neo4j. Вот как и почему.

Миф: для графов нужна графовая БД

Графовые БД решают конкретную проблему: обход связей произвольной глубины. “Друзья друзей друзей на 6 уровней” — да, Neo4j оправдан. Но knowledge graph для продуктовой аналитики, где типичный запрос depth 2-3 — PostgreSQL справится не хуже.

PostgreSQL покрывает ~95% use cases для графов до 100K нод с traversal depth ≤ 4.

Мой стек: четыре расширения

ЗадачаИнструментЧто даёт
Semantic searchpgvectorCosine similarity, HNSW-индексы
Fuzzy matchingpg_trgmTrigram similarity, Levenshtein
Graph traversalRecursive CTEBFS/DFS произвольной глубины
Full-text searchNative tsvectorKeyword search с морфологией

Один сервер, один pg_dump, одна точка мониторинга.

Hybrid Search: три сигнала → RRF fusion

-- Сигнал 1: Vector search (pgvector)
SELECT id FROM kg2_nodes
WHERE embedding IS NOT NULL
ORDER BY embedding <=> $query_embedding
LIMIT 40;

-- Сигнал 2: Keyword search (tsvector)
SELECT id FROM kg2_nodes
WHERE to_tsvector('simple', label || ' ' || COALESCE(summary, ''))
      @@ plainto_tsquery('simple', $query);

-- Сигнал 3: Graph traversal (recursive CTE)
WITH RECURSIVE graph AS (
  SELECT target_node_id AS node_id, 1 AS depth
  FROM kg2_edges WHERE source_node_id IN ($seed_ids) AND invalid_at IS NULL
  UNION
  SELECT e.target_node_id, g.depth + 1
  FROM kg2_edges e JOIN graph g ON e.source_node_id = g.node_id
  WHERE g.depth < $max_depth AND e.invalid_at IS NULL
)
SELECT DISTINCT node_id FROM graph;

RRF fusion в Ruby:

RRF_K = 60
def fuse(vector, keyword, graph, limit)
  scores = Hash.new(0.0)
  [vector, keyword, graph].each do |results|
    results.each_with_index do |node, rank|
      scores[node.id] += 1.0 / (RRF_K + rank + 1)
    end
  end
  scores.sort_by { |_, s| -s }.first(limit)
end

Performance: реальные цифры

ОперацияОбъёмВремя
Depth-2 CTE1000 нод, 650 рёбер< 5ms
pgvector nearest neighbor1000 нод< 2ms
Keyword search1000 нод< 1ms
Full hybrid search1000 нод< 15ms
Community detection (BFS)1000 нод< 50ms

Scaling Roadmap

ЭтапТриггерЧто делаем
S0CTE + pgvector
S1>5K нодReal embeddings, HNSW tune
S2>20K нодMaterialized views, partitioning
S3>50K нодRead replica
S4>100K + variable-length pathsApache AGE (расширение PostgreSQL!)

Даже S4 — не “переехать на Neo4j”. Apache AGE добавляет Cypher как расширение. Данные в той же базе.

Когда РЕАЛЬНО нужен Neo4j

  • >100K нод + variable-length pathsMATCH (a)-[*1..10]->(b) в Cypher быстрее CTE
  • Graph algorithms из коробки — PageRank, centrality через GDS
  • Команда уже знает Cypher

Сколько из этих пунктов актуальны сегодня?

Заключение

PostgreSQL — не компромисс. CTE + pgvector + pg_trgm + tsvector = полноценный графовый стек. 1000 нод, 650 рёбер, hybrid search, bi-temporal model — и ни одного повода смотреть в сторону Neo4j.

Не добавляйте инфраструктуру, пока нет проблемы.


Мой Knowledge Graph на PostgreSQL — AICPO или напишите nevr@aicpo.com