Раздел 39 · Neo4j и графовые базы

WHERE: фильтрация совпадений

junior~25 мин

без входаВойди, чтобы отмечать прогресс, вести заметки и решать задачи в редакторе. войти

Свойства в фигурных скобках паттерна, которые мы писали в прошлом уроке, умеют только одно: проверить равенство. «Родился после 1995 года», «название содержит слово», «у человека нет города» так не запишешь. Для этого после MATCH пишут WHERE. Если ты знаешь WHERE из SQL по уроку про SELECT, почти всё покажется знакомым, но у графа есть и свои условия: на связи и на целые паттерны.

Сито после MATCH

Представь, что MATCH высыпает на стол все найденные совпадения, а WHERE это сито: проходят только строки, для которых условие истинно. Строки, где условие ложно или неизвестно (null), отсеиваются.

Сравнения и логика

Знаки сравнения привычные: =, <> (не равно), <, >, <=, >=. Условия соединяют через AND, OR, NOT и реже XOR, «ровно одно из двух».

MATCH (p:Person)
WHERE p.born >= 1995
RETURN p.name, p.born
ORDER BY p.born;
+-----------------+
| p.name | p.born |
+-----------------+
| "Вера" | 1998   |
| "Зоя"  | 1999   |
| "Дина" | 2001   |
| "Яна"  | 2003   |
+-----------------+

4 rows
MATCH (p:Person)
WHERE p.born < 1990 OR p.born > 2000
RETURN p.name
ORDER BY p.name;
+--------+
| p.name |
+--------+
| "Глеб" |
| "Дина" |
| "Илья" |
| "Яна"  |
+--------+

4 rows

Егора нет ни в одном ответе: у него нет года рождения, сравнение null >= 1995 даёт null, а null сито не пропускает.

Списки и диапазоны

IN проверяет, входит ли значение в список. Имени «Никто» в графе нет, и это не ошибка, оно ничего не найдёт:

MATCH (p:Person)
WHERE p.name IN ['Аня', 'Глеб', 'Никто']
RETURN p.name;
+--------+
| p.name |
+--------+
| "Аня"  |
| "Глеб" |
+--------+

2 rows

Функция range(a, b) строит список чисел от a до b включительно, с ним IN превращается в проверку диапазона. Хотя для диапазона чаще пишут два сравнения через AND.

Поиск по строке

Для строк есть три оператора: STARTS WITH (начинается с), ENDS WITH (заканчивается на) и CONTAINS (содержит). Они различают регистр.

MATCH (b:Book)
WHERE b.title CONTAINS 'на'
RETURN b.title
ORDER BY b.title;
+------------------------------------+
| b.title                            |
+------------------------------------+
| "Двенадцать стульев"               |
| "Пикник на обочине"                |
| "Понедельник начинается в субботу" |
+------------------------------------+

3 rows

Если нужен шаблон сложнее, есть регулярные выражения через оператор =~. Шаблон должен совпасть со всей строкой целиком, поэтому по краям часто ставят .*:

MATCH (b:Book)
WHERE b.title =~ '.*(суббот|обочин).*'
RETURN b.title
ORDER BY b.title;
+------------------------------------+
| b.title                            |
+------------------------------------+
| "Пикник на обочине"                |
| "Понедельник начинается в субботу" |
+------------------------------------+

2 rows

Регэкспы в Neo4j работают по правилам Java, и у них есть подвох с русским текстом. Флаг (?i) в начале шаблона включает поиск без учёта регистра, но только для латиницы:

MATCH (b:Book)
WHERE b.title =~ '(?i)мастер.*'
RETURN b.title;
+---------+
| b.title |
+---------+
+---------+

0 rows

Пусто: строчная «м» не совпала с заглавной «М». Для кириллицы нужен флаг (?iu), где u значит Unicode:

MATCH (b:Book)
WHERE b.title =~ '(?iu)мастер.*'
RETURN b.title;
+----------------------+
| b.title              |
+----------------------+
| "Мастер и Маргарита" |
+----------------------+

1 row

Есть свойство или нет

Проверять отсутствие через = null нельзя: сравнение с null всегда даёт null, и строка не проходит.

MATCH (p:Person)
WHERE p.born = NULL
RETURN p.name;
+--------+
| p.name |
+--------+
+--------+

0 rows

Правильно писать IS NULL и IS NOT NULL:

MATCH (p:Person)
WHERE p.born IS NULL
RETURN p.name;
+--------+
| p.name |
+--------+
| "Егор" |
+--------+

1 row

Условия на связи

Свойства связи фильтруются так же, как свойства узла. Связи READ без оценки:

MATCH (p:Person)-[r:READ]->(b:Book)
WHERE r.rating IS NULL
RETURN p.name, b.title;
+------------------------------+
| p.name | b.title             |
+------------------------------+
| "Илья" | "Пикник на обочине" |
+------------------------------+

1 row

Условие можно написать и прямо внутри паттерна, в скобках узла или связи. Знакомства с 2021 года и позже:

MATCH (a:Person)-[k:KNOWS WHERE k.since >= 2021]->(b)
RETURN a.name, b.name, k.since
ORDER BY k.since;
+---------------------------+
| a.name | b.name | k.since |
+---------------------------+
| "Глеб" | "Дина" | 2021    |
| "Дина" | "Егор" | 2022    |
| "Зоя"  | "Илья" | 2023    |
+---------------------------+

3 rows

Условия на паттерн

Самое графовое в WHERE: условие может быть паттерном. «Участник, который не живёт ни в одном городе»:

MATCH (p:Person)
WHERE NOT (p)-[:LIVES_IN]->()
RETURN p.name
ORDER BY p.name;
+--------+
| p.name |
+--------+
| "Илья" |
| "Яна"  |
+--------+

2 rows

Более общая форма это подзапрос EXISTS. Внутри фигурных скобок паттерн с узлами любой сложности, например «читал хотя бы одну книгу Стругацких»:

MATCH (p:Person)
WHERE EXISTS { (p)-[:READ]->(:Book {author: 'Стругацкие'}) }
RETURN p.name
ORDER BY p.name;
+--------+
| p.name |
+--------+
| "Аня"  |
| "Вера" |
| "Глеб" |
| "Егор" |
| "Зоя"  |
| "Илья" |
+--------+

6 rows

А COUNT { ... } считает совпадения подзапроса. Участники, прочитавшие две книги и больше:

MATCH (p:Person)
WHERE COUNT { (p)-[:READ]->() } >= 2
RETURN p.name, COUNT { (p)-[:READ]->() } AS books
ORDER BY p.name;
+-----------------+
| p.name  | books |
+-----------------+
| "Аня"   | 2     |
| "Борис" | 2     |
| "Вера"  | 2     |
| "Глеб"  | 2     |
| "Жанна" | 2     |
| "Илья"  | 2     |
+-----------------+

6 rows

Попробуй сам

Запрос возвращает знакомства, которые завязались с 2019 года, вместе с людьми. На картинке видно, какая часть клуба познакомилась недавно.

Добавь условие, чтобы оба участника жили в одном городе: AND EXISTS { (a)-[:LIVES_IN]->(:City)<-[:LIVES_IN]-(b) }. Потом найди книги, в названии которых есть буква «о» без учёта регистра.

Проверь себя

Задание

Дальше

Мы увидели, как отсутствующее значение превращается в null и выпадает из ответа. Но иногда участника без города нужно оставить в ответе. В следующем уроке разберём OPTIONAL MATCH и правила работы с null.

домашка

Домашка