diff --git a/_site/atom.xml b/_site/atom.xml index dfc7ac1..41b3090 100644 --- a/_site/atom.xml +++ b/_site/atom.xml @@ -4,7 +4,7 @@
++Хотим использовать графы, но не можем или хотим позволить себе neo4j, какие есть варианты?
+
Так как вопрос без конкретики и не знаю ситуации целиком, то буду отвечать абстрактно. Расскажу три с половиной варианта хранения и работы с графами. Если до этого работали с графовыми базами и (или) проходили курс дискретной математики – вряд-ли узнаете что-то новое.
+ +Давайте договоримся, что вы уже знаете что такое графы, графовые бд и зачем нужны подобные вещи. Благодаря этому и так большой ответ не станет еще больше. Если не знаете, то советую посомтреть на три ссылки, которые помогут разобраться: первая, вторая и документация neo4j на тему графовых баз.
+ +Второй момент: я выделяю две причины, почему в проекте захотят использовать графовую базу данных:
+ +Поэтому каждый из вариантов, которые опишу ниже, буду также рассматривать в контексте описанных причин.
+ +Вариант о котором стоит упомянуть, но, в рамках ответа, серьезно рассматривать не планирую.
+ +Идея в том, что перед запуском проекта берем библиотеку для работы с графами и загружаем в память графовую структуру, с которой будет работать бизнес логика. Если думаете, что идея сумасшедшая, то вспоминаем о state machines, которые описываются графом. Еще один пример из личного опыта: пять лет назад загорелся идеей сделать автоматическую генерацию «карты» для IoC, чтобы анализировать зависимости и визуально показать как связаны элементы приложения. Хранение элементов реализовал в виде графа, который планировал в будущем кешировать в отдельном файле.
+ +Выглядеть решение может так: пишем init скрипт, который запускаем перед стартом проекта, в котором хардкодим нужный граф.
+ +# before_initial_script.exs
+
+graph =
+ Graph.new()
+ |> Graph.add_vertices([product_1.id, product_2.id, product_3.id])
+ |> Graph.add_edge(product_1.id, product_2.id)
+ |> Graph.add_edge(product_2.id, product_3.id)
+ |> Graph.add_edge(product_3.id, product_1.id)
+
Бонус для сумасшедших храбрых духом, которые хотят динамически расширять граф: можно упороться и перевести объект графа в строку байтов, т.е. воспользоваться [marshalling-ом](https://en.wikipedia.org/wiki/Marshalling_(computer_science). После чего сохранить полученный «граф» в персистенс (например redis SET). Описанная идея тянет больше на шутку, хотя предполагаю, что может возникнуть ситуация, когда подход окажется рабочим (но в голову конкретика не приходит).
Так как описанное «для галочки», то останавливаться на решении не стоит.
+ +Если специфика изначального вопроса в том, что именно neo4j нет возможности добавить в проект (нет компетенций, либо дорого, либо не удовлетворяет требованиям), стоит посмотреть на другие реализации графовых баз данных. Вот список относительно популярных решений (хотя графовую базу, которая не называется neo4j, нельзя назвать популярной):
+ +Кроме этого, на рынке присутствую готовые решения в облаках aws, google и microsoft:
+ +Если списка мало, можно найти еще больше решений по ссылке.
+ +Выбор другой графовой базы данных поможет как в хранении слабосвязанных данных, так и в сложном поиске. При этом, плюсы также сохраняются. Из минусов – низкая популярность и высокая стоимость (инфраструктурная, обучение разработчиков, отсутствие экосистемы), что приводит к рискам.
+ +Если решите пойти по этому пути – кажется, что лучше взять готовое клауд решение, если такая опция в наличии. В противном случае посмотреть в сторону ArangoDB, как второй по популярности.
+ +Дисклаймер: я не эксперт в теории графов. В институте дискретной математики не было, поэтому детали опускаю по незнанию. Если нашли ошибку – пишите в комментарии, а если хотите подробнее изучить теорию графов – стоит обратиться к книге Reinhard Diestel. Плюс, я опущу направленные графы в виду размера ответа, поэтому оставляю эту тему для самостоятельного изучения.
+ +Знаю два варианта, как представить графовую структуру в «простых» структурах: либо через представление графа как матрицы, либо через списки.
+ +Если быть честным, стоит упомянуть еще один вариант, который работает только с бинарными деревьями и binary heap. Такой вид графов представляется в виде массива (именно array, а не list, это важно из-за быстрого доступа к элементу по id). Но так как бинарные графы редко используются в графовых базах данных, подобный вид графов опущу.
+ +Вершина графа нулевой элемент массива, последующие ноды укладываются последовательно. Единственное, важно помнить о пустых элементах, которые также заполняются в массиве
+Давайте разберемся, как хранение графа в виде матрицы и списка работает.
+ +В качестве примера будем рассматривать граф, состоящий из 5 нод и 7 связей. Для примера не важно что это за граф, поэтому можете представить, что это список связанных товаров для рекомендательной системы.
+ +Граф, который будем описывать в виде матрицы. Важный момент, из 1 ноды присутствует связь к этой же ноде, связь указал стрелкой, чтобы понятно было. Представление направленных графов оставлю для самостоятельного изучения
+Чтобы описать граф как матрицу, надо описать каждую ноду и ее связь с другой нодой. Первое, что в голову приходит, сделать таблицу 5x5, где строки – номер ноды (или ее id), а колонки – тоже каждая нода с которой будет связана рассматриваемая нода.
+ +Каждая нода соответствует одному значению строки таблицы (зеленая стрелка) и одной колонке (фиолетовая стрелка)
+Дальше находим первую связь между нодами, например между нодой 1 и нодой 2. Связь отмечаем в таблице как пересечение строки и колонки.
+ +Так как нода 1 и 2 связаны между собой, то перекрестие строки 1 и ноды 2 отмечаем как связь
+Так как, в рассматриваемом графе, связь не только между нодой 1 и нодой 2, а еще и между нодой 2 и нодой 1 (обратная связь), то придется отметить эту связь между и во второй строке.
+ +Строка для ноды 2 будет также отображать связь между первой и второй нодой
+Проделав эту работу для каждой ноды и каждой связи, получим таблицу, в которой отображены пересечения между каждой нодой графа.
+ +Итоговая таблица связей в рассматриваемом графе
+А так как таблицу легко представить в виде матрицы, то считаем, что связь будет 1, а отсутствие связей 0. В результате чего получаем матрицу, которую называют матрицой смежности.
+ +\[M_{graph} = \begin{pmatrix} +1 & 1 & 1 & 0 & 0\\ +1 & 0 & 1 & 1 & 0\\ +1 & 1 & 0 & 1 & 0\\ +0 & 1 & 1 & 0 & 1\\ +0 & 0 & 0 & 1 & 0 +\end{pmatrix}\] + +Второй вариант – вспомнить о связях и описать граф используя node + edge таблицу. Для примера рассмотрим тот же граф, для которого делали матрицу смежности.
+ +Граф, который переводили в матрицу смежности. На этот раз понадобятся ребра, поэтому пометим каждое ребро уникальным id
+Дальше делаем таблицу, только вместо 5х5 таблицы с нодами и в колонках и в строках, получаем другую размерность: строки – ноды (поэтому пять), а колонки – ребра между нодами (поэтому семь).
+ +Полученная таблица 5х7. Зеленым показал строки как ноды, а фиолетовым – колонки как ребра. Получилась мешанина из стрелок, но надеюсь идею передал
+Теперь, чтобы заполнить таблицу необходимо взять ноду и на пересечении с каждым ребром сделать отметку о наличии связи. Например, для первой ноды есть связь с ребром e1
, которую необходимо отметить в таблице.
У первой ноды присутствует связь в саму себя через ребро (e1
), отображаем эту связь в созданной таблице
Аналогично делаем для последующих связей.
+ +Кроме e1
у первой ноды еще присутствует связь с ребром e2
, которую также отображаем в таблице
По итогу получаем заполненную таблицу, в которой указано какие ноды с какими ребрами связаны.
+ +Снова получаем заполненную таблицу, но на этот раз связанную с нодами и ребрами
+Итоговую матрицу получаем также как в случае матрицы смежности (переведя таблицу в набор 0 и 1). Только на этот раз, полученную матрицу называют матрицей инцидентности.
+ +\[M_{graph} = \begin{pmatrix} +1 & 1 & 1 & 0 & 0 & 0 & 0\\ +0 & 1 & 0 & 1 & 1 & 0 & 0\\ +0 & 0 & 1 & 1 & 0 & 1 & 0\\ +0 & 0 & 0 & 0 & 1 & 1 & 1\\ +0 & 0 & 0 & 0 & 0 & 0 & 1 +\end{pmatrix}\] + +Не важно какой из вариантов выбран для получения матрицы, по итогу получим описание графа в виде массива, состоящего из массивов (двумерная матрица). Дальше возникает вопрос того, как хранить такую структуру, для этого накину идей для 4 «часто» встречающихся баз данных:
+ +PostgreSQL
+ +Могу предложить два варианта:
+ +integer[][]
) в которую записываем матрицу смежности или инцидентности;graph_name
в которой будет count(nodes)
колонок, плюс строки под каждую строку матрицы. По итогу получаем таблицу, в которой SELECT * FROM graph_name WHERE id = node_id
покажет с какими нодами связана нода node_id
. А SELECT * FROM graph_name WHERE node_N = 1
покажет ноды, связанные с нодой node_N
;Redis
+ +Ничего лучше не нашел и не придумал, чем представить матрицу в виде json и положить строку в любой из видов ключей, например SET: SET graph_name "[ [...], [...], ... ]"
MongoDB
+ +Так как в монге работаем с документом, можем сразу положить матрицу как двумерный массив json и с ним работать.
+ +ElasticSearch
+ +Действуем аналогично mongoDB.
+ +[1, 2, 3] & [1, 3]
, в python – {1, 2, 3} & {1, 3}
, в java поможет retainAll (возможен вариант лучше), в ванильном js придется фильтровать массив – [1, 2, 3].filter(value => [1, 3].includes(value))
, а в go писать руками;Реализация графов как матрицы поможет с уменьшением размеров хранимых данных и для простых операций может оказаться быстрым решением. Подход поможет связать любые виды данных, но сложный поиск по графу будет проблематичен. При этом, если в проекте нужны динамические графы, придется либо постоянно пересчитывать и обновлять матрицы, либо отказаться от идеи. Плюс, желательно понимать как создаются матрицы смежности и матрицы инцидентности.
+ +Если вариант с матрицами не подходит, обращаемся к старым добрым спискам. Для чего стоит рассказать о двух подходах.
+ +Логическое продолжение матрицы смежности.
+ +Матрица смежности, которую получили ранее
+Идея в том, что не нужно делать матрицу, если можно сделать список, где каждый элемент будет говорить с какими другими нодами связана конкретная нода. Т.е. список будет содержать каждую строку матрицы как отдельный элемент.
+ +В результате, вместо матрицы, получаем список из элементов, в которых показано какие ноды связаны с искомой нодой
+Такой список называют списком смежности, и так как это частный случай матрицы смежности, то дальше вариант не будем рассматривать. Вместо этого поговорим о втором подходе.
+ +Если работали с графовыми библиотеками, то этот вариант уже встречали. Идея в том, чтобы сделать список ребер в виде кортежа (tuple). При этом, никто не мешает добавить второй список уже для нод, если нужен полноценный объект ноды, а не только id.
+ +В качестве примера можно открыть первую попавшуюся библиотеку для работы с графами, напримре jgrapht для джавы:
+ +Graph<URI, DefaultEdge> g = new DefaultDirectedGraph<>(DefaultEdge.class);
+
+URI google = new URI("http://www.google.com");
+URI wikipedia = new URI("http://www.wikipedia.org");
+URI jgrapht = new URI("http://www.jgrapht.org");
+
+// add the vertices
+g.addVertex(google);
+g.addVertex(wikipedia);
+g.addVertex(jgrapht);
+
+// Каждое ребро - tuple из двух нод: {node_from, node_to}
+g.addEdge(jgrapht, wikipedia);
+g.addEdge(google, jgrapht);
+g.addEdge(google, wikipedia);
+g.addEdge(wikipedia, google);
+
При этом, если нужно указать что за связь, tuple можно расширить до трех элементов: {node_from, how, node_to}
(чаще встречается как субъект, предикат, объект
). Например, в случае возраста можно использовать tuple { Alice, age, 25 }
, где Alice
будет нодой, 25
будет нодой, а ребро будет обозначать возраст. Подробнее можно почитать в DDIA, Chapter 2, part 2.3 (первая редакция).
PostgreSQL
+ +Логика такая: делаем таблицу nodes
и edges
. После чего, используя рекурсивные запросы ищем нужные ноды. Подробно описано в этой статье. А подробнее о рекурсивных функциях лучше почитать в документации.
Второй вариант – использовать pgRouting, который изначально создали для поиска гео маршрута, но так как данная задача – частный случай прохода графа, можно искать не только маршрут на картах, но и актеров фильмов, что описывается в статье.
+ +Если в наличии доступ к кабанчику, то еще раз сошлюсь на DDIA, Chapter 2, part 2.3 (первая редакция).
+ +Если решите работать со списком смежности, могу посоветовать статью, где рассказывается как с помощью представления графа как списка смежности и CTE работать с графами в постгресе.
+ +Redis
+ +Сразу скажу, вариант костыльный, но озвучить стоит. Для решения проблемы поможет пересечение множеств и SET типу данных в редисе.
+ +Выглядит это так: для каждого SET создаем список связанных нод, после чего ищем необходимую информацию. Если взять в пример граф выше и представить, что так связаны продукты в магазине, то так будет выглядеть заполнение графа:
+ +> SADD node:product:1 "node:product:1" "node:product:2" "node:product:3"
+> SADD node:product:2 "node:product:1" "node:product:3" "node:product:4"
+> SADD node:product:3 "node:product:1" "node:product:2" "node:product:4"
+> SADD node:product:4 "node:product:2" "node:product:3" "node:product:5"
+> SADD node:product:5 "node:product:4"
+
Для получения связанных нод для конкретной можно воспользоваться SMEMBERS node:product:1
. В случае, если нужно решить проблему связанного товара с товарами из корзины (пересечение множества), можно воспользоваться SINTER node:product:2 node:product:5
:
В случае, когда надо найти товар который можно предложить для пользователя, который хочет купить product:2 и product:5 можно воспользоваться пересечением множеств и получить product:4
+MongoDB
+ +Можно воспользоваться статьей, где показывается, как повторить запросы из neo4j в монге.
+ +ElasticSearch
+ +Статья уже о работе со списком tuples в эластике.
+ +Имхо – лучший вариант для работы с динамическими графами. Сработает для хранения слабосвязанных данных. Для сложного поиска сработает только с напильником. Не требует глубоких знаний в теории графов и дополнительных расчетов матриц. С другой стороны, возникают проблемы со сложными запросами + реализация хранения графов сложная, особенно для разработчиков, которые подобным не занимались.
+ +Логическое продолжение второго варианта: зачем заморачиваться и реализовывать графы в базе, если можно сделать пакет, который будет делать тоже самое, только создавай нужные графы. Главная проблема – установить библиотеку и молиться, что поддержка решения закончится позже поддержки базы данных.
+ +PostgreSQL
+ +Redis
+ +MongoDB
+ +- Полноценная графовая модель с поддержкой графовых языков запросов. Т.е. можно делать запросы любой сложности. При этом, не придется ставить отдельную нонейм базу данных;
+Ленивый вариант для разработчиков, так как не придется заморачиваться с хранением и сложными запросами. При этом, в каждом из решений автоматом идет нормальный язык графовых запросов, которые помогут для сложных запросов. С другой стороны, цена, запары и риски перекладываются на инфраструктуру: придется собирать базу с библиотеками, не каждый клауд провайдер поддерживает подобное решений и так далее. Решение «когда другие варианты не помогли», т.е. если выбирать между графовой бд и подобным решением, стоит сначала посмотреть, существуют ли варианты на рынке под нужные требования.
+ +Нашли опечатку или ошибку? Буду рад PR-у.
+