Одним из наиболее распространенных применений Neo4j является создание механизмов рекомендаций в реальном времени, и общая тема заключается в том, что они используют много разных битов данных, чтобы предложить интересную рекомендацию.
Например, в этом видео Аманда показывает, как сайты знакомств создают движки рекомендаций в режиме реального времени, начиная с социальных связей, а затем рассказывая о страстях, местоположении и некоторых других вещах.
У Graph Aware есть аккуратная структура, которая поможет вам создать собственный механизм рекомендаций с использованием Java, и мне было любопытно, как будет выглядеть версия Cypher.
Это пример графика:
|
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
|
CREATE (m:Person:Male {name:'Michal', age:30}), (d:Person:Female {name:'Daniela', age:20}), (v:Person:Male {name:'Vince', age:40}), (a:Person:Male {name:'Adam', age:30}), (l:Person:Female {name:'Luanne', age:25}), (c:Person:Male {name:'Christophe', age:60}), (lon:City {name:'London'}), (mum:City {name:'Mumbai'}), (m)-[:FRIEND_OF]->(d), (m)-[:FRIEND_OF]->(l), (m)-[:FRIEND_OF]->(a), (m)-[:FRIEND_OF]->(v), (d)-[:FRIEND_OF]->(v), (c)-[:FRIEND_OF]->(v), (d)-[:LIVES_IN]->(lon), (v)-[:LIVES_IN]->(lon), (m)-[:LIVES_IN]->(lon), (l)-[:LIVES_IN]->(mum); |
Мы хотим порекомендовать некоторых потенциальных друзей «Адаму», поэтому первый слой нашего запроса — найти его друзей друзей, так как среди них обязательно будет несколько потенциальных друзей:
|
01
02
03
04
05
06
07
08
09
10
11
12
|
MATCH (me:Person {name: "Adam"})MATCH (me)-[:FRIEND_OF]-()-[:FRIEND_OF]-(potentialFriend)RETURN me, potentialFriend, COUNT(*) AS friendsInCommon ==> +--------------------------------------------------------------------------------------+==> | me | potentialFriend | friendsInCommon |==> +--------------------------------------------------------------------------------------+==> | Node[1007]{name:"Adam",age:30} | Node[1006]{name:"Vince",age:40} | 1 |==> | Node[1007]{name:"Adam",age:30} | Node[1005]{name:"Daniela",age:20} | 1 |==> | Node[1007]{name:"Adam",age:30} | Node[1008]{name:"Luanne",age:25} | 1 |==> +--------------------------------------------------------------------------------------+==> 3 rows |
Этот запрос возвращает нам список потенциальных друзей и сколько у нас общих друзей.
Теперь, когда у нас есть несколько потенциальных друзей, давайте начнем строить рейтинг для каждого из них. Один из индикаторов, который может повлиять на потенциального друга, — это если они живут в том же месте, что и мы, поэтому давайте добавим это к нашему запросу:
|
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
|
MATCH (me:Person {name: "Adam"})MATCH (me)-[:FRIEND_OF]-()-[:FRIEND_OF]-(potentialFriend) WITH me, potentialFriend, COUNT(*) AS friendsInCommon RETURN me, potentialFriend, SIZE((potentialFriend)-[:LIVES_IN]->()<-[:LIVES_IN]-(me)) AS sameLocation ==> +-----------------------------------------------------------------------------------+==> | me | potentialFriend | sameLocation |==> +-----------------------------------------------------------------------------------+==> | Node[1007]{name:"Adam",age:30} | Node[1006]{name:"Vince",age:40} | 0 |==> | Node[1007]{name:"Adam",age:30} | Node[1005]{name:"Daniela",age:20} | 0 |==> | Node[1007]{name:"Adam",age:30} | Node[1008]{name:"Luanne",age:25} | 0 |==> +-----------------------------------------------------------------------------------+==> 3 rows |
Далее мы проверим, имеют ли потенциальные друзья Адамса тот же пол, что и он, сравнивая ярлыки каждого узла. У нас есть ярлыки «Мужской» и «Женский», которые указывают на пол.
|
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
|
MATCH (me:Person {name: "Adam"})MATCH (me)-[:FRIEND_OF]-()-[:FRIEND_OF]-(potentialFriend) WITH me, potentialFriend, COUNT(*) AS friendsInCommon RETURN me, potentialFriend, SIZE((potentialFriend)-[:LIVES_IN]->()<-[:LIVES_IN]-(me)) AS sameLocation, LABELS(me) = LABELS(potentialFriend) AS gender ==> +--------------------------------------------------------------------------------------------+==> | me | potentialFriend | sameLocation | gender |==> +--------------------------------------------------------------------------------------------+==> | Node[1007]{name:"Adam",age:30} | Node[1006]{name:"Vince",age:40} | 0 | true |==> | Node[1007]{name:"Adam",age:30} | Node[1005]{name:"Daniela",age:20} | 0 | false |==> | Node[1007]{name:"Adam",age:30} | Node[1008]{name:"Luanne",age:25} | 0 | false |==> +--------------------------------------------------------------------------------------------+==> 3 rows |
Далее давайте посчитаем возраст, различный для Адама и его потенциальных друзей
|
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
|
MATCH (me:Person {name: "Adam"})MATCH (me)-[:FRIEND_OF]-()-[:FRIEND_OF]-(potentialFriend) WITH me, potentialFriend, COUNT(*) AS friendsInCommon RETURN me, potentialFriend, SIZE((potentialFriend)-[:LIVES_IN]->()<-[:LIVES_IN]-(me)) AS sameLocation, abs( me.age - potentialFriend.age) AS ageDifference, LABELS(me) = LABELS(potentialFriend) AS gender, friendsInCommon ==> +--------------------------------------------------------------------------------------+==> | me | potentialFriend | sameLocation | ageDifference | gender | friendsInCommon |==> +--------------------------------------------------------------------------------------+==> | Node[1007]{name:"Adam",age:30} | Node[1006]{name:"Vince",age:40} | 0 | 10.0 | true | 1 |==> | Node[1007]{name:"Adam",age:30} | Node[1005]{name:"Daniela",age:20} | 0 | 10.0 | false | 1 |==> | Node[1007]{name:"Adam",age:30} | Node[1008]{name:"Luanne",age:25} | 0 | 5.0 | false | 1 |==> +--------------------------------------------------------------------------------------+==> 3 rows |
Теперь давайте сделаем фильтрацию, чтобы избавиться от людей, с которыми Адам уже дружит — не было бы особого смысла рекомендовать этих людей!
|
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
|
MATCH (me:Person {name: "Adam"})MATCH (me)-[:FRIEND_OF]-()-[:FRIEND_OF]-(potentialFriend) WITH me, potentialFriend, COUNT(*) AS friendsInCommon WITH me, potentialFriend, SIZE((potentialFriend)-[:LIVES_IN]->()<-[:LIVES_IN]-(me)) AS sameLocation, abs( me.age - potentialFriend.age) AS ageDifference, LABELS(me) = LABELS(potentialFriend) AS gender, friendsInCommon WHERE NOT (me)-[:FRIEND_OF]-(potentialFriend) RETURN me, potentialFriend, SIZE((potentialFriend)-[:LIVES_IN]->()<-[:LIVES_IN]-(me)) AS sameLocation, abs( me.age - potentialFriend.age) AS ageDifference, LABELS(me) = LABELS(potentialFriend) AS gender, friendsInCommon ==> +---------------------------------------------------------------------------------------+==> | me | potentialFriend | sameLocation | ageDifference | gender | friendsInCommon |==> +---------------------------------------------------------------------------------------+==> | Node[1007]{name:"Adam",age:30} | Node[1006]{name:"Vince",age:40} | 0 | 10.0 | true | 1 |==> | Node[1007]{name:"Adam",age:30} | Node[1005]{name:"Daniela",age:20} | 0 | 10.0 | false | 1 |==> | Node[1007]{name:"Adam",age:30} | Node[1008]{name:"Luanne",age:25} | 0 | 5.0 | false | 1 |==> +---------------------------------------------------------------------------------------+==> 3 rows |
В этом случае мы фактически никого не отфильтровали, но для некоторых других людей мы увидим сокращение числа потенциальных друзей.
Наш последний шаг — получить оценку для каждой из функций, которые мы определили как важные для предложения друзей.
Мы присвоим 10 баллов, если люди живут в одном месте или имеют тот же пол, что и Адам, и 0, если нет. Для ageDifference и friendsInCommon мы будем применять логарифмическую кривую, чтобы эти значения не оказывали непропорционального влияния на наш окончательный результат. Мы будем использовать формулу, определенную в ParetoScoreTransfomer, чтобы сделать это:
|
1
2
3
4
5
6
7
8
9
|
public <OUT> float transform(OUT item, float score) { if (score < minimumThreshold) { return 0; } double alpha = Math.log((double) 5) / eightyPercentLevel; double exp = Math.exp(-alpha * score); return new Double(maxScore * (1 - exp)).floatValue(); } |
А теперь для нашего завершенного запроса рекомендаций:
|
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
|
MATCH (me:Person {name: "Adam"})MATCH (me)-[:FRIEND_OF]-()-[:FRIEND_OF]-(potentialFriend) WITH me, potentialFriend, COUNT(*) AS friendsInCommon WITH me, potentialFriend, SIZE((potentialFriend)-[:LIVES_IN]->()<-[:LIVES_IN]-(me)) AS sameLocation, abs( me.age - potentialFriend.age) AS ageDifference, LABELS(me) = LABELS(potentialFriend) AS gender, friendsInCommon WHERE NOT (me)-[:FRIEND_OF]-(potentialFriend) WITH potentialFriend, // 100 -> maxScore, 10 -> eightyPercentLevel, friendsInCommon -> score (from the formula above) 100 * (1 - exp((-1.0 * (log(5.0) / 10)) * friendsInCommon)) AS friendsInCommon, sameLocation * 10 AS sameLocation, -1 * (10 * (1 - exp((-1.0 * (log(5.0) / 20)) * ageDifference))) AS ageDifference, CASE WHEN gender THEN 10 ELSE 0 END as sameGender RETURN potentialFriend, {friendsInCommon: friendsInCommon, sameLocation: sameLocation, ageDifference:ageDifference, sameGender: sameGender} AS parts, friendsInCommon + sameLocation + ageDifference + sameGender AS scoreORDER BY score DESC ==> +---------------------------------------------------------------------------------------+==> | potentialFriend | parts | score |==> +---------------------------------------------------------------------------------------+==> | Node[1006]{name:"Vince",age:40} | {friendsInCommon -> 14.86600774792154, sameLocation -> 0, ageDifference -> -5.52786404500042, sameGender -> 10} | 19.33814370292112 |==> | Node[1008]{name:"Luanne",age:25} | {friendsInCommon -> 14.86600774792154, sameLocation -> 0, ageDifference -> -3.312596950235779, sameGender -> 0} | 11.55341079768576 |==> | Node[1005]{name:"Daniela",age:20} | {friendsInCommon -> 14.86600774792154, sameLocation -> 0, ageDifference -> -5.52786404500042, sameGender -> 0} | 9.33814370292112 |==> +----------------------------------------------------------------------------------------+ |
Последний запрос не так уж и плох — единственный действительно сложный бит — это расчет кривой логарифма. Здесь пользовательские функции вступят в свои права в будущем.
Хорошая особенность этого подхода заключается в том, что нам не нужно выходить за пределы шифра, поэтому, если вы не знакомы с Java, вы все равно можете делать рекомендации в реальном времени! С другой стороны, разные части механизма рекомендаций перепутаны, поэтому не так просто увидеть весь конвейер, как если бы вы использовали платформу с поддержкой графов.
Следующий шаг — применить это к графику Twitter и выработать там рекомендации для подписчиков.
| Ссылка: | Neo4j: Генерация рекомендаций в реальном времени с Cypher от нашего партнера по JCG Марка Нидхэма в блоге Марка Нидхэма . |