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

Offers With Neo4j

DZone's Guide to

Offers With Neo4j

Let's take a look at how you might model a more advanced version of sales offers using Neo4j and Cypher. It's easier than you might think!

· Database Zone ·
Free Resource

Compliant Database DevOps and the role of DevSecOps DevOps is becoming the new normal in application development, and DevSecOps is now entering the picture. By balancing the desire to release code faster with the need for the same code to be secure, it addresses increasing demands for data privacy. But what about the database? How can databases be included in both DevOps and DevSecOps? What additional measures should be considered to achieve truly compliant database DevOps? This whitepaper provides a valuable insight. Get the whitepaper

Neo4j has many retailers as clients and one of their use cases is making offers to their customers. I was with a client today who had seen my boolean logic rules engine and decision tree blog posts and they were considering going that route for their offers but threw down the challenge of being able to do offers by just using Cypher. Their requirements were that offers can be of three types: "AllOf" offers require that the customer have all the requirements in order to be triggered, "AnyOf" offers, which required just one of the requirements to be met, and "Majority," which required the majority of requirements to be met. The model could look like this:

Let's go ahead and create some sample data:


CREATE (o:Offer { name: "Offer 1", type:"Majority",
             from_date: date({ year: 2018, month: 5, day: 1 }),
               to_date: date({ year: 2018, month: 5, day: 30 }) }),
(req1:Requirement {id:"Product 1"})<-[:REQUIRES]-(o),
(req2:Requirement {id:"Product 2"})<-[:REQUIRES]-(o),
(req3:Requirement {id:"New Customer"})<-[:REQUIRES]-(o),
(req4:Requirement {id:"In Illinois"})<-[:REQUIRES]-(o),
       (o2:Offer { name: "Offer 2", type:"AnyOf",
              from_date: date({ year: 2018, month: 5, day: 1 }),
                to_date: date({ year: 2018, month: 5, day: 30 })}),
(req5:Requirement {id:"Existing Customer"})<-[:REQUIRES]-(o2),
(req6:Requirement {id:"Last Purchase > 30 Days Ago"})<-[:REQUIRES]-(o2),
(req7:Requirement {id:"In California"})<-[:REQUIRES]-(o2),
       (o3:Offer { name: "Offer 3", type:"AllOf",
              from_date: date({ year: 2018, month: 5, day: 1 }),
                to_date: date({ year: 2018, month: 5, day: 30 })}),
(req1)<-[:REQUIRES]-(o3),
(req2)<-[:REQUIRES]-(o3),
(req3)<-[:REQUIRES]-(o3)

It looks like this in the Neo4j browser:

Now we are ready to write our query. It needs to return offers that are valid today, and they need to be relevant to the customer so they need to have at least one requirement in common with the customer. We must return the offer, the requirements we meet, all of the offers requirements, the missing requirements, and whether or not we meet those requirements. That sounds pretty complicated, but let's see the finished query and then we can walk through it in steps:


MATCH (req:Requirement)<-[:REQUIRES]-(o:Offer)
WHERE o.from_date < date() < o.to_date
  AND req.id IN ["Product 1", "Product 2", "In Illinois", "Existing Customer"]
WITH o, COLLECT(req.id) AS have
MATCH (o)-[:REQUIRES]->(reqs:Requirement)
WITH o, have, COLLECT(reqs.id) AS need
RETURN o, have, need, 
CASE o.type 
WHEN "AnyOf" THEN ANY(x IN need WHERE x IN have)
WHEN "AllOf" THEN ALL(x IN need WHERE x IN have)
WHEN "Majority" THEN SIZE(have) > SIZE(need)/2.0
END AS qualifies, FILTER(x IN need WHERE NOT x IN have) AS missing

Not bad right? If you have never used the Cypher CASE statement or FILTER statement, click on those links to learn more about them. So, what's our query doing? The first thing we want to do is use the "date()" function from Neo4j 3.4. to get today's date and compare it to the from_date and to_date of our offers. The offers need to have at least one requirement that the user has, so we MATCH and use an "IN" clause to find them and collect them into a list by the offer that we call "have."


MATCH (req:Requirement)<-[:REQUIRES]-(o:Offer)
WHERE o.from_date < date() < o.to_date
  AND req.id IN ["Product 1", "Product 2", "In Illinois", "Existing Customer"]
WITH o, COLLECT(req.id) AS have

Next, we find all of the requirements for our offer and collect them in a list we call "need."


MATCH (o)-[:REQUIRES]->(reqs:Requirement)
WITH o, have, COLLECT(reqs.id) AS need

Next, we return the Offer, the have and need lists, and we use a CASE statement to figure out if we meet the requirements of the offer. If the offer is of type "AnyOf," we just need to make sure that any requirement that we have is in the requirements that we need. If the offer is of type "AllOf," we need to make sure ALL the requirements are met. These ANY and ALL keywords are predicates in cypher that return TRUE or FALSE.


RETURN o, have, need, 
CASE o.type 
WHEN "AnyOf" THEN ANY(x IN need WHERE x IN have)
WHEN "AllOf" THEN ALL(x IN need WHERE x IN have)

If the offer is of type "Majority," then we make sure the size of the have list is greater than half the size of the need list. Majority requires 50% + 1, if we wanted "at least 50%" we could make that a greater than or equal to comparison instead. Finally, we want to return the missing requirements as well. We use a FILTER to get the list of missing requirements by checking each requirement in need and seeing if they are missing in the list of have.


WHEN "Majority" THEN SIZE(have) > SIZE(need)/2.0
END AS qualifies, FILTER(x IN need WHERE NOT x IN have) AS missing

and there we have it:

So give it a shot, try changing the requirements passed in the array and see how the results change. Remember, you will need Neo4j 3.4.0 or higher because of the use of the new date datatype. So go get it.

Before we end this, there are other ways to write this query, for example, we could have written the case statement in this way:


RETURN o, have, need, 
CASE o.type 
WHEN "AnyOf" THEN true
WHEN "AllOf" THEN SIZE(have) = SIZE(need)

It works because "AnyOf" is always true since we wouldn't have gotten to the offer if none of the requirements matched. Instead of using the ALL predicate, we could simply compare the sizes of the two lists for AllOf. You may have been tempted to write "have = need" but the order of the items in the lists are not guaranteed and out of order lists are not equal even if they contain the same values.

Compliant Database DevOps and the role of DevSecOps DevOps is becoming the new normal in application development, and DevSecOps is now entering the picture. By balancing the desire to release code faster with the need for the same code to be secure, it addresses increasing demands for data privacy. But what about the database? How can databases be included in both DevOps and DevSecOps? What additional measures should be considered to achieve truly compliant database DevOps? This whitepaper provides a valuable insight. Get the whitepaper

Topics:
neo4j ,cypher ,query ,graph database ,data ,how-to

Published at DZone with permission of

Opinions expressed by DZone contributors are their own.

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

{{ parent.tldr }}

{{ parent.urlSource.name }}