Building a Dating site with Neo4j – Part Seven

Now it is time to create the timeline for our users. Most of the time, the user wants to see posts from people they could High Five in order to elicit a conversation. Sometimes, they want to see what their competition is doing and what kind of posts are getting responses… also who they can low five. I don’t think they don’t want to see messages from people who are not like them and don’t want to date them but I could be wrong.

We need a bunch of parameters for our method. There are the obvious ones, but we’re also adding “city”, “state” and “distance” so a user who is traveling can see potential dates from locations outside their typical place. Long distance relationships are hard, but short out of town dates are not. We are also including a “competition” flag to see those posts instead. We’ll make use of these later.

 
    @GET
    public Response getTimeline(@PathParam("username") final String username,
                             @QueryParam("limit") @DefaultValue("100") final Integer limit,
                             @QueryParam("since") final String since,
                             @QueryParam("city") final String city,
                             @QueryParam("state") final String state,
                             @QueryParam("distance") @DefaultValue("40000") Integer distance,
                             @QueryParam("competition") @DefaultValue("false") Boolean competition,
                             @Context GraphDatabaseService db) throws IOException {
        ArrayList<Map<String, Object>> results = new ArrayList<>();

Let’s find our user and figure out what they are (“man”, “woman”, or “it’s complicated”) and also get the set of what they are looking for.

 
            Node user = Users.findUser(username, db);
            Map userProperties = user.getAllProperties();
            String is = (String)userProperties.get(IS);
            HashSet<String> isLookingFor = new HashSet<>(Arrays.asList((String[]) 
               userProperties.get(IS_LOOKING_FOR)));

Next, we get the posts they have already high fived, low fived, and the users they have blocked.

 
            HashSet<Node> highFived = new HashSet<>();
            HashSet<Node> lowFived = new HashSet<>();

            for (Relationship r1 : user.getRelationships(Direction.OUTGOING, RelationshipTypes.HIGH_FIVED)) {
                highFived.add(r1.getEndNode());
            }
            for (Relationship r1 : user.getRelationships(Direction.OUTGOING, RelationshipTypes.LOW_FIVED)) {
                lowFived.add(r1.getEndNode());
            }

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

Now, if the location parameters were not passed in, we will find the user’s location and get the nearby locations as well depending on their search distance. If they did pass in city and state, then we’ll find it and then add nearby locations. To find nearby locations we use a cypher query following the same technique we saw on the Neo4j Geospatial Queries blog post.

 
            // Get the User Location(s) and Nearby Locations
            HashSet<Node> locations = new HashSet<>();
            if (city == null) {
                for (Relationship inLocation : user.getRelationships(Direction.OUTGOING, RelationshipTypes.IN_LOCATION)) {
                    Node location = inLocation.getEndNode();
                    locations.add(location);
                    locations.addAll(Cities.findCitiesNearby(location, distance, db));
                }
            } else {
                Node location = Cities.findCity(city, state, db);
                locations.add(location);
                locations.addAll(Cities.findCitiesNearby(location, distance, db));
            }

We want to get posts that are either new or going backwards since a time that was passed in, but only up to when the user registered.

 
        // Up to the day the user registered
        ZonedDateTime earliest = (ZonedDateTime)userProperties.get(TIME);

        ZonedDateTime dateTime;
        ZonedDateTime latest;
        if (since == null) {
            latest = ZonedDateTime.now(utc);
            dateTime = ZonedDateTime.now(utc);
        } else {
            latest = ZonedDateTime.parse(since);
            dateTime = ZonedDateTime.parse(since);
        }

Wait, that’s not a very good idea for brand new users. We probably want to let them see past posts otherwise their feed will be empty and that’s not good. But we don’t want to run into a bug where a user in a sparse area doesn’t have enough posts to reach the limit and we keep asking for relationship types going into the past forever. So we’ll set a limit of say 90 days. Anything past that is probably too old to matter from a timeline perspective anyway. Ok let’s fix that:

 
ZonedDateTime earliest = ((ZonedDateTime)userProperties.get(TIME)).minusDays(90);

Now we can start getting recent posts. We will dynamically create a relationship type for each day (most likely today) and then for each location find the people inside that location, and then for each one of these people get any posts from our “posted” relationship type.

 
            // Get recent posts
            while (seen.size() < limit && (dateTime.isAfter(earliest))) {
                RelationshipType posted = RelationshipType.withName("POSTED_ON_" +
                        dateTime.format(dateFormatter));

                for (Node location : locations) {
                    for (Relationship inLocation : location.getRelationships(Direction.INCOMING, RelationshipTypes.IN_LOCATION)) {
                        Node person = inLocation.getStartNode();

                        for (Relationship r1 : person.getRelationships(Direction.OUTGOING, posted)) {
                            Node post = r1.getEndNode();

Now, you may be thinking… why don’t we check to see if this person’s posts should even be included before getting them by doing something like this:

 
                            // Before adding post to timeline, check for compatibility or competition
                            Map<String, Object> properties = person.getAllProperties();
                            String theyAre = (String) properties.get(IS);
                            HashSet<String> theyAreLookingFor = new HashSet<>(Arrays.asList((String[]) properties.get(IS_LOOKING_FOR)));

                            boolean include;
                            if (competition) {
                                include = (theyAreLookingFor.stream().anyMatch(isLookingFor::contains)) &&
                                        theyAre.equals(is);
                            } else {
                                include = theyAreLookingFor.contains(is) && isLookingFor.contains(theyAre);
                            }

Well, the reason is that most people will probably lurk. The 80/20 rule tells us only 20% of our users are likely to post something on any given day, while the other 80% will be just reading posts. So it’s probably more efficient to just check if they wrote something before seeing if we should include it. Last thing we want to do is check if this person is blocked or not, and assuming we’re good, we can add it to our result set.

 
                            if (include && !blocked.contains(person)) {
                                if (seen.add(post.getId())) {
                                    ZonedDateTime time = (ZonedDateTime)r1.getProperty("time");
                                    Map<String, Object> posting = r1.getEndNode().getAllProperties();
                                    if(time.isBefore(latest)) {
                                        posting.put(TIME, time);
                                        posting.put(USERNAME, properties.get(USERNAME));
                                        posting.put(NAME, properties.get(NAME));
                                        posting.put(HASH, properties.get(HASH));
                                        posting.put(HIGH_FIVED, highFived.contains(post));
                                        posting.put(LOW_FIVED, lowFived.contains(post));
                                        posting.put(HIGH_FIVES, post.getDegree(RelationshipTypes.HIGH_FIVED, Direction.INCOMING));
                                        posting.put(LOW_FIVES, post.getDegree(RelationshipTypes.LOW_FIVED, Direction.INCOMING));
                                        results.add(posting);
                                    }
                                }
                            }

Once we go through all of the posts for one day, we subtract a day and do it all over again until we reach our limit.

 
dateTime = dateTime.minusDays(1);

Once we have our results, we also want to sort them by time, since we picked up posts from different cities and people, we want them to be resorted chronologically so the world makes sense.

 
results.sort(timedComparator);

Let’s integrate this into our front end. From the API, I’ll just keep it simple for now, we will add past and different location options later.

 
    @GET("users/{username}/timeline")
    Call<List<Post>> getTimeline(@Path("username") String username,
                                 @Query("competition") Boolean competition);

For our Application this is just like Posts, but we get them from the timeline instead of the user.

 
          Response<List<Post>> timelineResponse = api.getTimeline(username, false).execute();
          List<Post> posts = new ArrayList<>();
          if (timelineResponse.isSuccessful()) {
              posts = timelineResponse.body();
          }

Now when I navigate to my home screen, I get a blank page… because I’m the only user. That’s not good, so we’ll add Helene to our dating site, register her in the same city I am in, and have her post something true. Now if I log out and log back in as myself, I can see her post on my timeline:

For those of you too young to understand, here is the reference.

Ok, now we can finally start seeing things come together, but we haven’t written to code to interact with the timeline (the high fives) and we don’t have messaging in place yet either. This could take a while… stay tuned for more.

Tagged , , , , , , , , , ,

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s

%d bloggers like this: