Over a million developers have joined DZone.
{{announcement.body}}
{{announcement.title}}

Building a Dating Site With Neo4j: Part 10

DZone 's Guide to

Building a Dating Site With Neo4j: Part 10

In part ten of this series, we look at a tutorial that explains how to add a messaging feature to the dating site we've been working on.

· Database Zone ·
Free Resource


To see Part 9, go here! I am now to the point where I want to do model messaging. There are a couple of ways of doing it. The first one is the simplest:

A user node has a MESSAGED relationship to another user node, the message and the time are stored as properties on the relationship and that's it. It's really easy to understand, but there is a problem with this model. As time grows and our user starts to have more conversations with various people, their node will be full of these MESSAGED relationships. How do we know which ones are new? We would have to traverse them all, get their "when" property, sort all the messages by time, and then show the user the most recent ones. This will make our query slower and slower as we add more data, and we want to avoid that. So what do we do? We could try "dated" relationship types:

By "promoting" the date property up to the relationship type, we can traverse just to the most recent messages, and since every node in Neo4j knows which relationship types are connected to it in BOTH directions (and their counts), it will make a "get recent messages" query fast. But we don't want a sorted list of most recent messages. We want a sorted list of the most recent messages grouped by the users involved. So in this case, switching to "dated" relationship types doesn't help us. We need to try a different strategy. What if we promoted what is really happening into a node? In this case, I'm talking about "conversations":

By connecting both users to a Conversation node and then adding the messages to it, we can get the grouping of our messages by the user that we want. But what about getting the most recent messages from this conversation? Should we change the ADDED_TO relationship to a "dated" relationship type? Well, how many messages would two people on a dating site exchange before they switch to another medium like a phone call, text messages, or a date? According to some sources, about a dozen messages should be exchanged before asking for the digits. So in a "worst case" scenario of very chatty users, we're probably looking at less than a thousand messages for sure, probably less than 50 in the great majority of cases, so I do not think it is worth it to bother with that.

This model choice opens up some interesting possibilities. Can more than 2 users be involved in a conversation? Maybe not for this feature, but what if we wanted to add Groups to our dating site? Then we could use the same ideas and connect multiple people to a conversation. This reminded me that we have a Stream, so we could get fancy and add things like Event nodes, where someone proposes a get together (like going to a zombie pub crawl, going to see a show, or for the movie lovers...Netflix and Chill). These events could be public and have "IM_GOING" relationships or they could require approval, so whoever created the event would moderate who is coming and private information about the event could be revealed once approved. We have lots of possibilities with a Dating Stream rather than just profiles. But let's not get ahead of ourselves, we have to get messaging in first.

Let's start with creating the messages and conversations, and then we'll figure out how to display them.

@Path("/users/{username}/conversations")
public class Conversations {

    @POST
    @Path("/{username2}")
    public Response addToConversation(String body,
                                      @PathParam("username") final String username,
                                      @PathParam("username2") final String username2,
                                      @Context GraphDatabaseService db) throws IOException {

We will use the PostValidator to clean the input like before and then find our two users in the graph:

        Map<String, Object> results;
        HashMap<String, Object> input = PostValidator.validate(body);
        ZonedDateTime dateTime = ZonedDateTime.now(utc);

        try (Transaction tx = db.beginTx()) {
            Node user = Users.findUser(username, db);
            Node user2 = Users.findUser(username2, db);

Once we have our users, we need to make sure the user receiving the message has not blocked the user sending the message. We should probably double check all avenues of communication on both the creation and retrieval against blocked users.

            HashSet<Node> blocked = new HashSet<>();
            for (Relationship r1 : user2.getRelationships(Direction.OUTGOING, RelationshipTypes.BLOCKS)) {
                blocked.add(r1.getEndNode());
            }
            if (blocked.contains(user)) {
                throw ConversationExceptions.conversationNotAllowed;
            }

Next, we need to find their conversation. We could have used a composite index or NODE KEY as Neo4j calls it on Conversation, but the likeliness of anyone talking to millions of people is zero. I would expect the average to be under 100 with a thousand or so at the top. So what we can do instead is simply traverse the PART_OF relationship twice, looking for the second user.

            Node conversation = null;
            outerloop:
            for (Relationship r1 : user.getRelationships(Direction.OUTGOING, RelationshipTypes.PART_OF)) {
                conversation = r1.getEndNode();
                for (Relationship r2 : conversation.getRelationships(Direction.INCOMING, RelationshipTypes.PART_OF)) {
                    if (user2.equals(r2.getStartNode())) {
                        break outerloop;
                    }
                }
            }

We can optimize this part by finding the user with the least amount of PART_OF relationships and traversing those looking for the other user. We can do this using the getDegree method:

if (user.getDegree(RelationshipTypes.PART_OF, Direction.OUTGOING) 
    < user2.getDegree(RelationshipTypes.PART_OF, Direction.OUTGOING)) {

What if we can't find a conversation? Well then, we have to create one, but we need to check that we are allowed to message this user. According to our rules, they need to have given a "high five" to one of our posts in the last 5 days. So let's get the user's posts:

            if (conversation == null) {
                ZoneId zoneId = ZoneId.of((String) user.getProperty(TIMEZONE));
                ZonedDateTime startOfFiveDays = ZonedDateTime.now(zoneId).with(LocalTime.MIN).minusDays(5);

                // Get their posts
                ArrayList<RelationshipType> types = new ArrayList<>();
                for (RelationshipType t : user.getRelationshipTypes()) {
                    if (t.name().startsWith("POSTED_ON")) {
                        types.add(t);
                    }
                }

                types.sort(relTypeComparator);

...and check for a high five from them. We only need to find one, so we break out of the loop as soon as we do. We are going to assume it is more likely that user2 high fived a recent post, so we will order our POSTED_ON relationship types before traversing them.

                boolean allowed = false;
                outerloop:
                for (Relationship r1 : user.getRelationships(types.toArray(new RelationshipType[0]))) {
                    Node post = r1.getEndNode();
                    for (Relationship r : post.getRelationships(RelationshipTypes.HIGH_FIVED, Direction.INCOMING)) {
                        // Check the user first, then get the time
                        if (user2.equals(r.getStartNode())) {
                            ZonedDateTime when = (ZonedDateTime)r.getProperty(TIME);
                            if (when.isAfter(startOfFiveDays)) {
                                allowed = true;
                                break outerloop;
                            }
                        }
                    }
                }

If we find a recent high five, we go ahead and create the conversation and make both users part of it, otherwise, we deny the request.

                if (allowed) {
                    conversation = db.createNode(Labels.Conversation);
                    user.createRelationshipTo(conversation, RelationshipTypes.PART_OF);
                    user2.createRelationshipTo(conversation, RelationshipTypes.PART_OF);
                } else {
                    throw ConversationExceptions.conversationNotAllowed;
                }

Now, you may be thinking, couldn't we have gone from user2 and look for the high five relationships, filter them by date, and then see if any of them belong to the first user? Yes, we could have. If we decide not to keep the "expired" high fives and delete them instead, it would make more sense go this route. We may end up changing our query to do that or we could check the sum of the number of posted relationships from the first user against the number of high fives from the second user and choose a different path on a case by case basis. As a monetization feature, we could have high fives that last longer than 5 days, maybe 10 days, or they could last forever, but that takes away the urgency to respond, so maybe doubling their time is better than infinite time. We also probably want to keep expired high fives to help us build recommendations.

We are getting ahead of ourselves again. For now, we need to just create the actual message and connect it to our conversation:

            Node message = db.createNode(Labels.Message);
            message.setProperty(STATUS, input.get(STATUS));
            message.setProperty(TIME, dateTime);
            message.setProperty(AUTHOR, username);
            message.createRelationshipTo(conversation, RelationshipTypes.ADDED_TO);

            results = message.getAllProperties();
            tx.success();

Okay, so we can create messages and conversations, but we also need to be able to see them. For this, we need a getConversation method:

    @GET
    @Path("/{username2}")
    public Response getConversation(@PathParam("username") final String username,
                                    @PathParam("username2") final String username2,
                                     @QueryParam("limit") @DefaultValue("25") final Integer limit,
                                     @QueryParam("since") final String since,
                                     @Context GraphDatabaseService db) throws IOException {

It turns out the methods are pretty similar, so I won't go over it in as much detail. The real difference is that once we have our conversation, we need to get the messages, sort them, and return them.

            for (Relationship r1 :  conversation.getRelationships(Direction.INCOMING, RelationshipTypes.ADDED_TO)) {
                Node message = r1.getStartNode();
                if (latest.isAfter((ZonedDateTime) message.getProperty(TIME))) {
                    results.add(message.getAllProperties());
                }
            }
            tx.success();
        }

        results.sort(timedComparator);

Turning to our front end, we can add them both to our API

    @GET("users/{username}/conversations/{username2}")
    Call<List<Message>> getConversation(@Path("username") String username,
                                       @Path("username2") String username2);

    @POST("users/{username}/conversations/{username2}")
    Call<Message> createMessage(@Path("username") String username,
                                @Path("username2") String username2,
                                @Body Message message);

...and then add them to our application. I'll skip that for now since it's pretty straightforward, but take a look at the source code if you are interested. So let's see what our work yielded:

Nice! Our users can now send messages to each other... there is only one problem, we haven't written a way to show the person who high fived a post that their crush has started a conversation with them. We will add that next.

Topics:
database ,tutorial ,how to build a site ,neo4j ,model messaging

Published at DZone with permission of

Opinions expressed by DZone contributors are their own.

{{ parent.title || parent.header.title}}

{{ parent.tldr }}

{{ parent.urlSource.name }}