Около 6 месяцев назад мы рассмотрели, как перевести несколько строк Cypher в слишком много Java-кода в версии 1.9.x. С тех пор Cypher изменился, и я немного отстой в Java, поэтому я хотел поделиться несколькими различными способами перевести один в другой на тот случай, если вы застряли в середине восьмидесятых и изменили количество строк кода вы пишете в час.
Но сначала,
давайте сделаем некоторые данные. У Майкла Хангера есть серия постов в блоге о получении и создании данных в Neo4j., хорошо
дай мне взять #Selfie
позаимствовать его идеи. Давайте создадим 100 тыс. Узлов:
украсть
WITH ["Jennifer","Michelle","Tanya","Julie","Christie","Sophie","Amanda","Khloe","Sarah","Kaylee"] AS names FOREACH (r IN range(0,100000) | CREATE (:User {username:names[r % size(names)]+r}))
… И давайте создадим около 500 тыс. Отношений между ними:
MATCH (u1:User),(u2:User) WITH u1,u2 LIMIT 5000000 WHERE rand() < 0.1 CREATE (u1)-[:FRIENDS]->(u2);
… И давайте не забудем добавить индекс:
CREATE INDEX ON :User(username);
Теперь, когда мы смотрим на наши данные, мы видим:
Теперь, если мы хотим составить рекомендацию для 10 лучших пользователей, «Мишель 1» должна дружить, но не сейчас, мы бы написали что-то вроде этого:
MATCH (me:User {username:'Michelle1'}) -[:FRIENDS]- people -[:FRIENDS]- fof WHERE NOT(me -[:FRIENDS]- fof) RETURN fof, COUNT(people) AS friend_count ORDER BY friend_count DESC LIMIT 10
… И мы получили бы такую ошибку после 60 секундного тайм-аута в окне браузера:
Cypher, начиная с 2.0.2, не оптимизирован для этого типа запросов (он будет готов), поэтому давайте обратимся к Java API. Первое, что мы хотим сделать, — это найти пользователя, а затем заставить его друзей просто привыкнуть к новым методам Java API.
@GET @Path("/friends/{username}") public Response getFriends(@PathParam("username") String username, @Context GraphDatabaseService db) throws IOException { List<String> results = new ArrayList<String>(); try ( Transaction tx = db.beginTx() ) { final Node user = IteratorUtil.singleOrNull(db.findNodesByLabelAndProperty(DynamicLabel.label("User"), "username", username)); if(user != null){ for ( Relationship relationship : user.getRelationships(FRIENDS, Direction.OUTGOING) ){ Node friend = relationship.getOtherNode(user); results.add((String)friend.getProperty("username")); } } } return Response.ok().entity(objectMapper.writeValueAsString(results)).build(); }
Вместо непосредственного перехода к индексу мы используем метод findNodesByLabelAndProperty, чтобы найти нашего пользователя. Также обратите внимание, что все заключено в блок Try с транзакцией. В 2.0 все взаимодействия с базой данных должны быть внутри транзакции. В связи с этим давайте рассмотрим список 10 лучших друзей друзей, которые не являются моими нынешними друзьями, по количеству общих друзей в Java:
@GET @Path("/fofs/{username}") public Response getFofs(@PathParam("username") String username, @Context GraphDatabaseService db) throws IOException { List<Map<String, Object>> results = new ArrayList<>(); HashMap<Node, MutableInt> fofs = new HashMap<>(); try ( Transaction tx = db.beginTx() ) { final Node user = IteratorUtil.singleOrNull(db.findNodesByLabelAndProperty(DynamicLabel.label("User"), "username", username)); findFofs(fofs, user); List<Map.Entry<Node, MutableInt>> fofList = orderFofs(db, fofs); returnFofs(results, fofList.subList(0, Math.min(fofList.size(), 10))); } return Response.ok().entity(objectMapper.writeValueAsString(results)).build(); }
Я поместил findFofs, orderFofs и returnFofs в их собственные методы. Сначала мы рассмотрим findFofs, и я хочу, чтобы вы обратили внимание, потому что есть явная ошибка, которую я пропустил в первый раз, когда делал это, и повторяю здесь. Посмотри, сможешь ли ты заметить это.
private void findFofs(HashMap<Node, MutableInt> fofs, Node user) { List<Node> friends = new ArrayList<>(); if (user != null){ getFirstLevelFriends2(user, friends); getSecondLevelFriends2(fofs, user, friends); } }
private void getFirstLevelFriends(Node user, List<Node> friends) { for ( Relationship relationship : user.getRelationships(FRIENDS, Direction.BOTH) ){ Node friend = relationship.getOtherNode(user); friends.add(friend); } }
Теперь, здесь вы действительно хотите обратить внимание …
private void getSecondLevelFriends(HashMap<Node, MutableInt> fofs, Node user, List<Node> friends) { for ( Node friend : friends ){ for (Relationship otherRelationship : friend.getRelationships(FRIENDS, Direction.BOTH) ){ Node fof = otherRelationship.getOtherNode(friend); if ((!user.equals(fof) && !friends.contains(fof))) { MutableInt mutableInt = fofs.get(fof); if (mutableInt == null) { fofs.put(fof, new MutableInt(1)); } else { mutableInt.increment(); } } } } }
Видел это? И я нет. Давайте проверим производительность этой конечной точки с помощью ApacheBench :
ab -k -c 1 -n 1 'http://127.0.0.1:7474/example/service/fofs/Michelle1'
Наши результаты намного лучше, чем раньше. 2,670 секунды против времени ожидания, которое мы видели раньше.
Concurrency Level: 1 Time taken for tests: 2.670 seconds Complete requests: 1 Failed requests: 0 Write errors: 0 Keep-Alive requests: 1 Total transferred: 655 bytes HTML transferred: 522 bytes Requests per second: 0.37 [#/sec] (mean) Time per request: 2670.414 [ms] (mean) Time per request: 2670.414 [ms] (mean, across all concurrent requests) Transfer rate: 0.24 [Kbytes/sec] received
Это огромное улучшение, но Neo4j выполняет миллионы обходов в секунду и может предоставить рекомендации в режиме реального времени … 2,670 секунды просто не звучат правильно. Итак, давайте немного покопаемся с помощью YourKit .
YourKit — это профилировщик Java, который мы можем подключить к работающему серверу Neo4j, и он позволит нам увидеть, что происходит, когда мы добавим немного больше нагрузки, чем 1 запрос. Это не очевидно, но когда вы запускаете Neo4j, имя, которое он отображает, называется «Bootstrapper». Посмотрите руководство YourKit для более подробной информации .
ab -k -c 8 -n 800 'http://127.0.0.1:7474/example/service/fofs/Michelle1'
A little while after we start collecting profile information and begin running our test, this pops up:
Oh oh… something is obviously wrong… let’s dig in.
So something in getSecondLevelFriends is wasting time doing what looks like nothing…
private void getSecondLevelFriends2(HashMap<Node, MutableInt> fofs, Node user, List<Node> friends) { for ( Node friend : friends ){ for (Relationship otherRelationship : friend.getRelationships(FRIENDS, Direction.BOTH) ){ Node fof = otherRelationship.getOtherNode(friend); if ((!user.equals(fof) && !friends.contains(fof))) {
… and there it is. We’re calling contains on a List of Nodes instead of a Set of Nodes, so it’s going to scan it instead of go right to it. Log(n) vs Log(1) type of problem because I used the wrong data structure. So let’s change this to a Set and try it again.
ab -k -c 1 -n 1 'http://127.0.0.1:7474/example/service/fofs/Michelle1'
Our results are WAY better than before. 91 milliseconds vs the 2.670 seconds we were taking before, vs the timeout from where we started.
Concurrency Level: 1 Time taken for tests: 0.091 seconds Complete requests: 1 Failed requests: 0 Write errors: 0 Keep-Alive requests: 1 Total transferred: 655 bytes HTML transferred: 522 bytes Requests per second: 10.99 [#/sec] (mean) Time per request: 91.019 [ms] (mean) Time per request: 91.019 [ms] (mean, across all concurrent requests) Transfer rate: 7.03 [Kbytes/sec] received
Let’s try giving it some load:
ab -k -c 8 -n 800 'http://127.0.0.1:7474/example/service/fofs/Michelle1'
… and now we’re getting 55 requests per second real time recommendations on my laptop.
Concurrency Level: 8 Time taken for tests: 14.536 seconds Complete requests: 800 Failed requests: 0 Write errors: 0 Keep-Alive requests: 800 Total transferred: 524000 bytes HTML transferred: 417600 bytes Requests per second: 55.04 [#/sec] (mean) Time per request: 145.361 [ms] (mean) Time per request: 18.170 [ms] (mean, across all concurrent requests) Transfer rate: 35.20 [Kbytes/sec] received
As always, the full source code is available on Github.
One last thing… in Neo4j 2.1… it goes almost twice as fast.
Concurrency Level: 8 Time taken for tests: 8.523 seconds Complete requests: 800 Failed requests: 0 Write errors: 0 Keep-Alive requests: 800 Total transferred: 524000 bytes HTML transferred: 417600 bytes Requests per second: 93.86 [#/sec] (mean) Time per request: 85.234 [ms] (mean) Time per request: 10.654 [ms] (mean, across all concurrent requests) Transfer rate: 60.04 [Kbytes/sec] received
Now that’s Amazing.
[Editor’s Note: Download your free copy of Neo4j now]