Optional Lesson
This lesson is optional and will not count towards your achievement.
To view the completed code, check out the 12-movie-details
branch.
There are two methods remaining in the MovieDAO
that are currently returning hardcoded data.
-
find_by_id() - should return information about a movie, including a list of actors, director, and genres.
-
get_similar_movies() - should return a list of similar movies, ordered by a score generated by Neoflix’s recommendation algorithm.
In this challenge, you will update these methods to query Neo4j.
First, let’s take a look at how these methods are used.
Movie Page
If you click on any movie, you’ll see that the API currently only returns information about the film Goodfellas.
The page itself is populated by three API calls:
-
The details about the movie are loaded via the
api/movies/{id}
endpoint. This endpoint gets its data from the find_by_id() method. -
The similar movies list is loaded by a call to the
api/movies/{id}/similar
endpoint, which gets its data from the get_similar_movies() method. -
The ratings on the right hand side of the page are loaded by a call to the
api/movies/{id}/ratings
endpoint. You will update this method in the next lesson.
find_by_id()
Movie information is retrieved by the find_by_id()
method.
def find_by_id(self, id, user_id=None):
# TODO: Find a movie by its ID
# MATCH (m:Movie {tmdbId: $id})
return goodfellas
Most of the important information is held as properties on the Movie
node, but the payload should also return some additional information.
We can use a Cypher subquery and the count()
aggregate function to get the number of ratings for the movie and use a Pattern Comprehension to get a list of actors and directors.
As with the other movie methods in the MovieDAO
, we should also provide a favorite
value to represent whether the user has added this movie to their My Favorites list or not.
The following Cypher statement should be run within a read transaction:
MATCH (m:Movie {tmdbId: $id})
RETURN m {
.*,
actors: [ (a)-[r:ACTED_IN]->(m) | a { .*, role: r.role } ],
directors: [ (d)-[:DIRECTED]->(m) | d { .* } ],
genres: [ (m)-[:IN_GENRE]->(g) | g { .name }],
ratingCount: count{ (m)<-[:RATED]-() },
favorite: m.tmdbId IN $favorites
} AS movie
LIMIT 1
Your Task
-
Modify the
find_by_id()
method to query Neo4j and return details for the requested movie. -
The returned object should include a list of actors, directors, genres, and a boolean flag to represent whether the movie exists on the current user’s My Favorites List.
-
If no records are returned, the method should raise a
NotFoundException
.
Click to reveal the completed find_by_id()
method
def find_by_id(self, id, user_id=None):
# Find a movie by its ID
def find_movie_by_id(tx, id, user_id = None):
favorites = self.get_user_favorites(tx, user_id)
cypher = """
MATCH (m:Movie {tmdbId: $id})
RETURN m {
.*,
actors: [ (a)-[r:ACTED_IN]->(m) | a { .*, role: r.role } ],
directors: [ (d)-[:DIRECTED]->(m) | d { .* } ],
genres: [ (m)-[:IN_GENRE]->(g) | g { .name }],
favorite: m.tmdbId IN $favorites
} AS movie
LIMIT 1
"""
first = tx.run(cypher, id=id, favorites=favorites).single()
if first == None:
raise NotFoundException()
return first.get("movie")
with self.driver.session() as session:
return session.execute_read(find_movie_by_id, id, user_id)
get_similar_movies()
Similar movies are found using the get_similar_movies()
method.
To provide a simple set of similar movies, the Cypher statement below uses the number of neighbors in common and their IMDB rating to generate a similarity score.
The following Cypher statement should be run within a read transaction:
MATCH (:Movie {tmdbId: $id})-[:IN_GENRE|ACTED_IN|DIRECTED]->()<-[:IN_GENRE|ACTED_IN|DIRECTED]-(m)
WHERE m.imdbRating IS NOT NULL
WITH m, count(*) AS inCommon
WITH m, inCommon, m.imdbRating * inCommon AS score
ORDER BY score DESC
SKIP $skip
LIMIT $limit
RETURN m {
.*,
score: score,
favorite: m.tmdbId IN $favorites
} AS movie
Your Task
-
Modify the
get_similar_movies()
method to query Neo4j and return a list of similar movies. -
The returned objects should include a list of actors, directors, genres, and a boolean flag to represent whether the movie exists on the current user’s My Favorites List.
Click here to reveal the completed get_similar_movies()
method
def get_similar_movies(self, id, limit=6, skip=0, user_id=None):
# Get similar movies
def find_similar_movies(tx, id, limit, skip, user_id):
favorites = self.get_user_favorites(tx, user_id)
cypher = """
MATCH (:Movie {tmdbId: $id})-[:IN_GENRE|ACTED_IN|DIRECTED]->()<-[:IN_GENRE|ACTED_IN|DIRECTED]-(m)
WHERE m.imdbRating IS NOT NULL
WITH m, count(*) AS inCommon
WITH m, inCommon, m.imdbRating * inCommon AS score
ORDER BY score DESC
SKIP $skip
LIMIT $limit
RETURN m {
.*,
score: score,
favorite: m.tmdbId IN $favorites
} AS movie
"""
result = tx.run(cypher, id=id, limit=limit, skip=skip, favorites=favorites)
return [ row.get("movie") for row in result ]
with self.driver.session() as session:
return session.execute_read(find_similar_movies, id, limit, skip, user_id)
Testing
To test that this functionality has been correctly implemented, run the following code in a new terminal session:
pytest -s tests/12_movie_details__test.py
The test file is located at tests/12_movie_details__test.py
.
Are you stuck? Click here for help
If you get stuck, you can see a working solution by checking out the 12-movie-details
branch by running:
git checkout 12-movie-details
You may have to commit or stash your changes before checking out this branch. You can also click here to expand the Support pane.
Verifying the Test
What is the title of the most similar movie to Lock, Stock and Two Smoking Barrels?
As part of the test suite, the final test will log the title of the most similar movie to Lock, Stock & Two Smoking Barrels (tmdbId: 100).
Paste the title of the movie into the box below without quotes or whitespace and click Check Answer.
-
✓ Pulp Fiction
Hint
You can also find the answer by running the following Cypher statement:
MATCH (:Movie {tmdbId: '100'})-[:IN_GENRE|ACTED_IN|DIRECTED]->()<-[:IN_GENRE|ACTED_IN|DIRECTED]-(m)
WHERE m.imdbRating IS NOT NULL
WITH m, count(*) AS inCommon
WITH m, inCommon, m.imdbRating * inCommon AS score
RETURN m.title
ORDER BY score DESC
LIMIT 1
Copy the answer without any quotes or whitespace.
Lesson Summary
In this Challenge, you modified the find_by_id()
and get_similar_movies()
methods to return movie details from the Neo4j database.
In the next Challenge, you will complete the Movie page implementation by retrieving movie ratings from Neo4j.