Match Making with Neo4j

groucho_marx

It is better to have loft and lost than to never have loft at all.” — Groucho Marx

In the “Matches are the new Hotness” blog post, I showed how to connect a person to a job via a location and skills. We’re going to look at a variation on the theme today by matching people to other people by what they want in a potential mate. We’re gonna use Neo4j to bring the love.

There are a ton of opinions on what’s wrong with current dating sites. I don’t claim to know how to fix them, I’m just giving what may be a piece of the puzzle. We could try to match people on the things they have in common, but the saying “opposites attract” exists for a reason. We often don’t want mirrors of ourselves, but rather to supplement some perceived deficiency. However complete opposites may result in exciting relationships, but may not be long-lasting. Some kind of happy middle ground is probably best.

Do they like dogs?
Are they energetic?
Have a good sense of humor?
Are they neat and tidy, but not crazy about it?
…and a million other things

So if we have a list of attributes, and let people select which ones they have and which ones they want their potential mate to have, how would we connect people together? Let’s try writing a Cypher query:

              START me=node:users_index(name={user})
              MATCH me-[:lives_in]->city<-[:lives_in]-person
              WHERE me.orientation = person.orientation AND
                  ((me.gender <> person.gender AND me.orientation = "straight") OR
                   (me.gender = person.gender AND me.orientation = "gay")) AND
                    me-[:wants]->()<-[:has]-person AND 
                    me-[:has]->()<-[:wants]-person 
              WITH DISTINCT city.name AS city_name, person, me
              MATCH  me-[:wants]->attributes<-[:has]-person-[:wants]->requirements<-[:has]-me
              RETURN city_name, person.name AS person_name,
                     COLLECT(attributes.name) AS my_interests,
                     COLLECT(requirements.name) AS their_interests,
                     COUNT(attributes) AS matching_wants, 
                    COUNT(requirements) AS matching_has
              ORDER BY matching_wants / (1.0 / matching_has) DESC
              LIMIT 10

That looks a little complicated, let’s break it down piece by piece.

START me=node:users_index(name={user})

We start with the things we know, and right now all we know is we have a user with a name that we can look up in our users_index and use as our starting point in the traversal. This query will be used by other users, so we parametrize it with {user}.

MATCH me-[:lives_in]->city<-[:lives_in]-person

We’ll find potential mates in the same city. While modern technology has made long distance relationships easier, we won’t. Can you imagine waiting 3 months for a letter from your loved one to arrive? Oh how romantic… in our fast paced society your love interest will have married and divorced twice before the letter arrives.

WHERE me.orientation = person.orientation AND

We’ll want to limit ourselves to people with the same sexual orientation. We are finding dates, not friends.

((me.gender <> person.gender AND me.orientation = "straight") OR
 (me.gender = person.gender AND me.orientation = "gay")) 

So either the gender has to be different if the orientation is “straight” or the same if the orientation is “gay”. If Cypher had an XOR predicate this could be a lot easier… we’ll look at adding it to Cypher in an up coming blog post.

AND me-[:wants]->()<-[:has]-person

We want to match people who have what we want…

AND me-[:has]->()<-[:wants]-person

…and want what we have. Unrequited love sucks.
335529-pepe_cover_super

WITH DISTINCT city.name AS city_name, person, me

Then we pick up the name of the city, ourselves, and our matching people.

MATCH  me-[:wants]->attributes<-[:has]-person-[:wants]->requirements<-[:has]-me

Then we’ll match all the attributes we want that a person has, and all the requirements this person wants that we have.

RETURN city_name, person.name AS person_name,
       COLLECT(attributes.name) AS my_interests,
       COLLECT(requirements.name) AS their_interests,
       COUNT(attributes) AS matching_wants, 
       COUNT(requirements) AS matching_has

We’ll actually want to return the name of the city (in case our user is looking for love in multiple cities), the name of the person that was matched as well as the collection of items we matched on and their counts so we can order by these.

ORDER BY matching_wants / (1.0 / matching_has) DESC

We’ll order by the ratio of the matching wants and 1 over the matching has and…

LIMIT 10

…we’ll just grab the top ten matching users.

Below is what our application looks like:

Screen Shot 2013-04-19 at 10.21.57 AM

As usual, the code is available on github, and you can see it running live on Heroku. If you are up for a challenge, try including bisexual and transgender daters, or giving users the ability to “HATE” some attributes and subtract or eliminate any potential matches. So go out and use Neo4j to build the best Dating Site ever!

9 thoughts on “Match Making with Neo4j

  1. The syntax looks quite powerful.

    I have 1 question though. Why do you have this clause twice?

    “where…… me-[:wants]->()()attributesrequirements<-[:has]-me".

    Or are they slightly different because of the ()?

    • maxdemarzi says:

      In once case we just check that they have at least one. In the other we want them all.
      There is a way to rewrite the query so we only do this once… but it’s kinda ugly:


      START me=node:users_index(name='Andreas')
      MATCH me-[:lives_in]->city<-[:lives_in]-person
      WHERE me.orientation = person.orientation AND
      ((me.gender person.gender AND me.orientation = "straight") OR
      (me.gender = person.gender AND me.orientation = "gay"))
      with me, person, city.name as city_name, extract(p in me-[:wants]->()() 0  and length(has) > 0            
      return city_name, person, me, length(wants) as matching_wants, length(has) as matching_has, extract(w in wants : w.name) as interests
      ORDER BY matching_wants / (1.0 / matching_has) DESC
      LIMIT 10

  2. Truly excellent, thank you Max! We are working in a recommender system based in Neo4j, it has been great to find your post on match-making.

  3. […] taught us not to be wasteful. Let’s take a look at an example from our past. Look back at the Neo Love application, the one with the picture of Marilyn Monroe and Groucho Marx. Let’s see what a […]

  4. Dylan Mura says:

    Hi Max, just started neo4j and I wanted to try this. First I just copy pasted your code in neo4j but he asked me parameters so I added some to the query but it still doesn’t work. Can you give me more informations? Just trying to educate me! Thanks :)

  5. Pavan Kumar says:

    Hi Max, just started neo4j which version this matchmaking is working i wanted to try this for recommendation systems.Please help me.

    Thanks in advance..

  6. […] to a recommendation engine in Neo4j and pretty trivial to write. Take a look back at a few old blog posts for […]

  7. […] downloaded here. It has been prepared by Max de Marzi of Neo Technology who used to show how Neo4j can be used for match making. Here is a quick overview of the underlying data model […]

  8. […] can be downloaded here. It has been prepared by Max de Marzi of Neo Technology who used to show how Neo4j can be used for match making. Here is a quick overview of the underlying data model […]

Leave a comment