Building a Questionnaire with Neo4j part 3/3 — A dynamic list

Stefan Dreverman
Sep 13 · 7 min read

Last but not least, you might have questions for someone that are only asked when needed. How is it modeled in Neo4j, how is progress tracked and how can information be stored/fetched? I’ll show you in this last article.

Be sure to read Part 1 and Part 2 so you know how to deal with a single question and with a static list… It’ll be a small step from there to add some dynamics to the list.

So you have a question that must only be asked if a specific answer is given in a previous question.

Flow

With this new requirement, there’s something different going on: No longer is there a static flow where the Respondent always answers Questions in a specific order: a -> b -> c. Some Respondents might answer more questions, because an Answer given at Question a might require Question K to be answered as well: a-> K-> b -> c. This path can be different for every Respondent, so the path has to be made specific for every Respondent. And when Question K brings about Question L, then the string of questions will look like: a-> K -> L -> b -> c, etc…

So, where previously the order of questions was fixed, we’ll have to let that go. The order of questions is different per user and it will be determined after questions have been answers. As you can guess, this requires a new approach to how answers are stored.

Let’s take a look at the model…

Meta model (3)

The changes in the model are:

  • a new relation: initialQ — indicates that this Question is part of the initial Questions. These are linked in the initalization of the list for the Respondent. (see below)
  • a new relation: requires — indicates that a question is required (and was not in the list of initial questions. Note that a node that has the requires-relation cannot have the initialQ-relation. If so, it might be added twice during the process of answering.
  • Requirement! Every Question, required or not, needs to be linked to the ListOfQuestions with a hasQ-relation. It is part of the collection of questions that belongs to this list, even though they might not be answered by every Respondent. The purpose of this is so they can also be queried easily to show statistics and results.
  • An invisible change is that the next-relation needs to be created per Respondent, so this relation needs a property respondent (use ID! :-). This way, every Respondent can walk his/her own path.

Another difference here is that we do not create the order of questions in design time, but we create them at runtime. This requires one query to create the first set of next-relations for a ListOfQuestions for a specific Respondent. You’ll need to decide how you can determine the order for a ListOfQuestions.

Design choices:

  • Choose how to set the order of questions: With next-relations, use an order-property on Question or an order-property in the hasQ-relation? (I’d go with the order-property on Question, because that gives the next-relation between questions a single purpose.)
  • The required-relation can be either linked to one Question or to a ListOfQuestions (containing one or more questions). So, linking to a ListOfQuestions is a better option. Please do this. For the simplicity of the example, we’ll stick with the required-relation linked to one Question. This way you’ll see the concept and can extend it as you wish.
  • If questions can have multiple answers, this has to be taken into account when implementing the above model: Every answer might have a different required Question. And: Two answers on one Question can yield the same required Question.

Creating the the requires and initialQ relations

I’m going to refer to Part 1 and Part 2 here. I’ve explained how to create relations for hasQ. InitialQ can be done similarly. And creating the requires-relation is basic Neo4j… so, let’s skip to the fun part right away. :-)

Answering questions

Initializing the initial questions— Since the ListOfQuestions is specific for all users, the first thing that we need to do is to create the path for a list of questions. This is done via the initialQ-relation. If it’s a simple questionnaire, you can suffice with a simple query, otherwise I recommend you to adapt the example query from the linked list tutorial from the Neo4j website, which would look like this:

MATCH (r:Respondent)
WHERE r.name='Bill'
WITH r
MATCH (loq:ListOfQuestions)-[:initialQ]->(q:Question)
WHERE loq.name='Cloud computing questionnaire'
WITH r,q
ORDER BY q.order ASC
WITH r, collect(q) as questions
FOREACH (i in range(0, size(questions) - 2) |
FOREACH (node1 in [questions[i]] |
FOREACH (node2 in [questions[i+1]] |
CREATE (node1)-[:next {respondent:r.name }]->(node2))))

Note that the above Query uses the variant with an order-property in Question to specify which Question is first and which is next in line. This works for any number of questions and is easy to implement.

Initializing the currentQuestion — This also needs to be done in order to retrieve the first question (see part 2). Note that the first question is always the first question. It doesn’t matter to whom the next-relation belongs, as long as it’s the first in the list:

MATCH (r:Respondent)
WHERE r.name='Bill'
WITH r
MATCH (loq:ListOfQuestions)-[:hasQ]->(q:Question)
WHERE loq.name='Cloud computing questionnaire'
AND NOT (:Question)-[:next]->(q)
MERGE (r)-[:currentQuestion]->(q)

Setting the Answer to a Question— Since there is a dynamic list of questions, Answering a question can be split into two part. Of which setting the answer to a Question is the first:

MATCH (r:Respondent)-[:currentQuestion]->(q:Question)<-[:isAnswerTo]-(a:Answer)
WHERE r.name='Bill'
AND a.name='<fill in the answer for q here. Better yet, use IDs>'
MERGE (r)-[:respondedWith]->(a)
WITH a
MATCH (a)-[:requires]->(otherQ:Question)
RETURN otherQ

Now we’ve set the answer and, if present, we’ve returned:

  • otherQ: a question needs to be inserted into the list of questions.
State of the graph where it was concluded that the required question (top right) needs to be added. Observe that the next-relations still run from left to right and only touch Questions that have an initialQ-relation.

In this example, you can see the dynamics. Bill his first Answer is set and we find Bill sending in the answer to the current Question being “Yes, we are”. This query will return otherQ being “When are you planning on…”, meaning that we have to add this in the list.

Inserting a Question into the linked list — If the result of the above query is not null, the list of questions needs to be updated with the new question:

MATCH (r:Respondent)-[:currentQuestion]->(q:Question)
WHERE r.name='Bill'
WITH r,q
OPTIONAL MATCH (q)-[oldrel:next {respondent:r.name}]->(nextQ:Question)
DELETE oldrel
WITH r,q,nextQ
MATCH (otherQ:Question)
WHERE otherQ.name={parameter with otherQ.name from previous query}
MERGE (q)-[:next {respondent:r.name}]->(otherQ)
WITH r,otherQ,nextQ
MERGE (otherQ)-[:next {respondent:r.name}]->(nextQ)

After running this query for the example above, the next state in the graph indicates that there is a Question that was previously not in the list of ‘Questions to be asked’.

State of the graph after adding the required question to the linked list. Observe that the next-relation now takes a turn upwards, to the required question, then towards the last question in the list. The added question was not part of the initial list.

It’s probably possible to store the Answer and add the required Question in one transaction. Personally, I haven’t tried this because of the extra complexity that it brings in the transactionality and the order of query execution. Therefore, I’ve stuck with a sequence of which I’m sure works. Plus that the query above is only executed when there is a required question.

All that’s left now is to set the pointer to the next question (if present).

MATCH (r:Respondent)-[cq:currentQuestion]->(q:Question)
WHERE r.name='Bill'
WITH r,q,cq
DELETE cq
WITH r,q
MATCH (q)-[:next {respondent:r.name}]->(nextQ:Question)
WITH r,nextQ
MERGE (r)-[:currentQuestion]->(nextQ)

Showing results

If you’ve expected fireworks here, I’ll have to disappoint you: the fireworks have just ended with the explanation of how to include extra questions in your list. :-)

Since we’ve not altered the model for the hasQ-relation, results can be generated with queries shown in part 2. However, there is one important difference to take into account: There are now questions in the list that are optional. So, the Answer-count does not have to be the same for every Question if you query via the hasQ-relation. Make sure to take this into account when generating statistics from your data. (If you count the initialQ-relation answers, they still should add up correctly.)

Conclusion

In this small series, you’ve experienced that adding a new feature to the list adds it’s own set of choices and complexities. With the right base model (questions, answers and the static list) you can extend and implement the new functionality step by step and reason your way to a model that fits your needs.

The possibilities and implementations for this model are plenty. You can do a simple Kahoot!-style application, but also create Surveymonkey surveys. And if you want to go all the way, make decision trees like Zingtree. Whatever you do, remember to start small.

For myself, this was a weekend project to write down my own decision making process while designing a solution. A large part of this process is normally done only in the head, so it was a challenge for me to actually write it down. Now that it’s here in black and white, I can see how valuable is to write down your choices and have the ability to experiment and reflect on them.

I hope this series was as helpful for you as it was for me. If you’ve missed something here, please comment so I can answer your questions or write an article about it.

Stefan Dreverman

Written by

www.stefandreverman.nl — Freelance IT Architect. Father of Twins. Explorer. Trail runner.

Welcome to a place where words matter. On Medium, smart voices and original ideas take center stage - with no ads in sight. Watch
Follow all the topics you care about, and we’ll deliver the best stories for you to your homepage and inbox. Explore
Get unlimited access to the best stories on Medium — and support writers while you’re at it. Just $5/month. Upgrade