Одним из наиболее распространенных применений 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 score ORDER 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 Марка Нидхэма в блоге Марка Нидхэма . |