Статьи

Neo4j и Cypher: перевод Cypher на Java

зашифровывать-перевод-600x293

Выразительная сила Cypher уже удивительна и улучшается с выпуском Neo4j 2.0 . Давайте сделаем шаг назад от передового края и увидим Cypher в 1.9.4 и как это можно перевести на Java. Сначала простой пример, где мы ищем узел User по индексу и возвращаем список имен пользователей, принадлежащих людям, которые являются друзьями этого пользователя:

START me = node:Users(username='maxdemarzi')
MATCH me -[:FRIENDS]-> people
RETURN people.username

Заявление Cypher выражает то, что я хочу, даже лучше, чем мое испорченное объяснение на английском языке. Итак, как бы мы сделали это в Neo4j Java API ?

ObjectMapper objectMapper = new ObjectMapper();
private static final RelationshipType FRIENDS = DynamicRelationshipType.withName("FRIENDS");
 
@GET
@Path("/friends/{username}")
public Response getFriends(@PathParam("username") String username, @Context GraphDatabaseService db) throws IOException {
    List<String> results = new ArrayList<String>();
    IndexHits<Node> users = db.index().forNodes("Users").get("username", username);
    Node user = users.getSingle();
 
    for ( Relationship relationship : user.getRelationships(FRIENDS, Direction.OUTGOING) ){
        Node friend = relationship.getEndNode();
        results.add((String)friend.getProperty("username"));
    }
 
    return Response.ok().entity(objectMapper.writeValueAsString(results)).build();
}

Это немного многословно, но не ужасно. Мы ищем имя пользователя в индексе, а затем пересекаем отношения Outgoing FRIENDS с дружественными узлами и получаем их свойства имени пользователя. Как насчет того, чтобы попробовать что-то немного сложнее:

START me = node:Users(username='maxdemarzi')
MATCH me -[:FRIENDS]- people -[:FRIENDS]- fof
WHERE NOT(me -[:FRIENDS]- fof)
RETURN fof, COUNT(people) AS friend_count
ORDER BY friend_count DESC
LIMIT 10

В приведенном выше запросе мы получаем друзей друзей, которые не являются прямыми друзьями, и возвращаем топ-10, упорядоченные по количеству общих друзей, в порядке убывания. Мы собираемся извлечь части этого запроса в их собственные методы, чтобы сделать нашу жизнь проще, поэтому сейчас давайте просто обманем и притворимся, что это будет работать:

@GET
@Path("/fofs/{username}")
public Response getFofs(@PathParam("username") String username, @Context GraphDatabaseService db) throws IOException {
    List<Map<String, Object>> results = new ArrayList<Map<String, Object>>();
 
    HashMap<Node, AtomicInteger> fofs = new HashMap<Node, AtomicInteger>();
 
    IndexHits<Node> users = db.index().forNodes("Users").get("username", username);
    Node user = users.getSingle();
 
    findFofs(fofs, user);
    List<Entry<Node, AtomicInteger>> fofList = orderFofs(fofs);
    returnFofs(results, fofList.subList(0, Math.min(fofList.size(), 10)));
 
    return Response.ok().entity(objectMapper.writeValueAsString(results)).build();
}

Мы вызываем метод «findFofs», чтобы получить друзей друзей и их количество. Сначала мы найдем друзей пользователя и поместим их в список. Во-вторых, мы найдем друзей этих друзей, и пока они не являются теми пользователями, с которыми мы начинали, или они уже являются друзьями этого пользователя, мы добавляем их в HashMap, поскольку они вводят ключи и увеличивают их каждый раз, когда мы видим их снова.

private void findFofs(HashMap<Node, AtomicInteger> fofs, Node user) {
    List<Node> friends = new ArrayList<Node>();
 
    if (user != null){
        for ( Relationship relationship : user.getRelationships(FRIENDS, Direction.BOTH) ){
            Node friend = relationship.getOtherNode(user);
            friends.add(friend);
        }
 
        for ( Node friend : friends ){
            for (Relationship otherRelationship : friend.getRelationships(FRIENDS, Direction.BOTH) ){
                Node fof = otherRelationship.getOtherNode(friend);
                if (!user.equals(fof) && !friends.contains(fof)) {
                    AtomicInteger atomicInteger = fofs.get(fof);
                    if (atomicInteger == null) {
                        fofs.put(fof, new AtomicInteger(1));
                    } else {
                        atomicInteger.incrementAndGet();
                    }
                }
            }
        }
    }
}

Далее нам нужно заказать наш список друзей друзей в порядке убывания следующим способом:

private List<Entry<Node, AtomicInteger>> orderFofs(HashMap<Node, AtomicInteger> fofs) {
    List<Entry<Node, AtomicInteger>> fofList = new ArrayList<Entry<Node, AtomicInteger>>(fofs.entrySet());
    Collections.sort(fofList, new Comparator<Entry<Node, AtomicInteger>>() {
        @Override
        public int compare(Entry<Node, AtomicInteger> a,
                           Entry<Node, AtomicInteger> b) {
            return ( b.getValue().get() - a.getValue().get() );
 
        }
    });
    return fofList;
}

Наконец, мы получим все свойства узлов друзей и друзей и добавим их в наши результаты.

private void returnFofs(List<Map<String, Object>> results, List<Entry<Node, AtomicInteger>> fofList) {
    Map<String, Object> resultsEntry;
    Map<String, Object> fofEntry;
    Node fof;
    for (Entry<Node, AtomicInteger> entry : fofList) {
        resultsEntry = new HashMap<String, Object>();
        fofEntry = new HashMap<String, Object>();
        fof = entry.getKey();
 
        for (String prop : fof.getPropertyKeys()) {
            fofEntry.put(prop, fof.getProperty(prop));
        }
 
        resultsEntry.put("fof", fofEntry);
        resultsEntry.put("friend_count", entry.getValue());
        results.add(resultsEntry);
    }
}

Обратите внимание, что когда мы вызывали этот метод, мы передавали только 10 лучших друзей друзей, уже заказанных этим другом.

returnFofs(results, fofList.subList(0, Math.min(fofList.size(), 10)));

Наши 6 строк Cypher быстро взлетели до 70 строк Java. Смотрите полное неуправляемое расширение с тестами на github.

Я думаю, что такие посты забавны, и они действительно показывают силу Cypher с точки зрения выразительности, читабельности и ловкости. Я сделаю еще несколько в ближайшее время. Напишите мне, если у вас есть какие-то конкретные запросы шифров, которые вы хотели бы видеть переведенными.