Свойства в фигурных скобках паттерна, которые мы писали в прошлом уроке, умеют только одно: проверить равенство. «Родился после 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.
домашка