{"id":13424,"date":"2017-08-30T14:35:53","date_gmt":"2017-08-30T14:35:53","guid":{"rendered":"http:\/\/www.doanduyhai.com\/blog\/?p=13424"},"modified":"2017-08-30T15:11:46","modified_gmt":"2017-08-30T15:11:46","slug":"gremlin-recipes-9-pattern-matching","status":"publish","type":"post","link":"https:\/\/www.doanduyhai.com\/blog\/?p=13424","title":{"rendered":"Gremlin recipes: 9 &#8211; Pattern Matching"},"content":{"rendered":"<p>This blog post is the 9<sup>th<\/sup> from the series <strong>Gremlin Recipes<\/strong>. It is recommended to read the previous blog posts first: <\/p>\n<ol>\n<li><a href=\"https:\/\/www.doanduyhai.com\/blog\/?p=13224\" target=\"_blank\"><strong>Gremlin as a Stream<\/strong><\/a><\/li>\n<li><a href=\"https:\/\/www.doanduyhai.com\/blog\/?p=13260\" target=\"_blank\"><strong>SQL to Gremlin<\/strong><\/a><\/li>\n<li><a href=\"https:\/\/www.doanduyhai.com\/blog\/?p=13285\" target=\"_blank\"><strong>Recommendation Engine traversal<\/strong><\/a><\/li>\n<li><a href=\"https:\/\/www.doanduyhai.com\/blog\/?p=13301\" target=\"_blank\"><strong>Recursive traversals<\/strong><\/a><\/li>\n<li><a href=\"https:\/\/www.doanduyhai.com\/blog\/?p=13320\" target=\"_blank\"><strong>Path object<\/strong><\/a><\/li>\n<li><a href=\"https:\/\/www.doanduyhai.com\/blog\/?p=13352\" target=\"_blank\"><strong>Projection and selection<\/strong><\/a><\/li>\n<li><a href=\"https:\/\/www.doanduyhai.com\/blog\/?p=13374\" target=\"_blank\"><strong>Variable handling<\/strong><\/a><\/li>\n<li><a href=\"https:\/\/www.doanduyhai.com\/blog\/?p=13404\" target=\"_blank\"><strong>sack() operator<\/strong><\/a><\/li>\n<\/ol>\n<p><!--more--><\/p>\n<h1>I KillrVideo dataset<\/h1>\n<p>To illustrate this series of recipes, you need first to create the schema for <strong>KillrVideo<\/strong> and import the data. See <a href=\"https:\/\/www.doanduyhai.com\/blog\/?p=13224#killrvideo_dataset\" target=\"_blank\"><strong>here<\/strong><\/a> for more details.<\/p>\n<p>The graph schema of this dataset is :<\/p>\n<p><iframe loading=\"lazy\" src=\"https:\/\/s3.amazonaws.com\/datastax-graph-schema-viewer\/index.html#\/?schema=killr_video_small.json\" height=\"600px\" width=\"100%\"><\/iframe><\/p>\n<h1>II Pattern Matching in Gremlin<\/h1>\n<h4>A. Description<\/h4>\n<p>Until now we always use descriptive traversals with <strong>Gremlin<\/strong>. By <em>descriptive traversals<\/em> I mean we describe all the navigation in the graph using standard steps like <code><strong>in(), out(), ...<\/strong><\/code><\/p>\n<p>However, <strong>Gremlin<\/strong> also offers a <strong>declarative<\/strong> way to do traversals, using pattern matching. Basically you declare all the properties of your vertices\/edges and alias them with label then <strong>Gremlin<\/strong> will compute the most appropriate traversals to match your pattern.<\/p>\n<p>A pattern-matching traversal uses the <code><strong>match()<\/strong><\/code> step and look like:<\/p>\n<pre class=\"brush: java; title: ; wrap-lines: false; notranslate\" title=\"\">\r\ngremlin&gt;g.V().match(\r\n  __.as(&quot;label&quot;).....,      \/\/ predicate 1\r\n  __.as(&quot;label&quot;).....,      \/\/ predicate 2\r\n  ...\r\n  __.as(&quot;label&quot;).where(...)  \/\/ matching predicate\r\n)\r\n...\r\n<\/pre>\n<p>Please note that each predicate <strong>must<\/strong> start with a labelling step <code><strong>as()<\/strong><\/code>. Each predicate can end with a labelling step <code><strong>as()<\/strong><\/code> but it is optional<\/p>\n<p>All the labels you declare in your predicates are global and can be re-called outside of the <code><strong>match()<\/strong><\/code> step. Those labels also serve the purpose of matching different predicates together<\/p>\n<p>There are a few things you should know to exploit fully pattern matching with <strong>Gremlin<\/strong>:<\/p>\n<blockquote>\n<ol>\n<li>all the predicates inside the <code><strong>match()<\/strong><\/code> are executed <strong>concurrently<\/strong> and <strong>independently<\/strong>. Consequently the <strong>lexical order<\/strong> of the declared predicates does not matter during execution time<\/li>\n<li>variables(alias) binding need to hold <strong>true<\/strong> across multiple predicate. The traversers that succeeded all the variable findings in all predicate are the ones that <strong>survive<\/strong> the declared pattern and thus are the result of the <code><strong>match()<\/strong><\/code> step<\/li>\n<li>in other languages like Cypher or SPARQL, reducing barriers (<code><strong>sum()<\/strong><\/code>,<code><strong>count()<\/strong><\/code> &#8230;) or collecting barriers (<code><strong>order()<\/strong><\/code>, <code><strong>group()<\/strong><\/code>, &#8230;) are not <strong>allowed<\/strong>. In <strong>Gremlin<\/strong> pattern matching they are allowed but their scope is <strong>local only<\/strong>, meaning that if you have an <code><strong>order()<\/strong><\/code> clause inside a predicate, it is only useful for this predicate but this ordering cannot be used for the other predicates or outside of the <code><strong>match()<\/strong><\/code> step<\/li>\n<\/ol>\n<\/blockquote>\n<p>The explanation for point 3. is that since each predicate is executed in any order and concurrently, inside a single predicate, at the time the traverser reaches the <code><strong>order().by()<\/strong><\/code> step, all the data are being processed concurrently in other predicates so we don&#8217;t have all the data to perform a <strong>global<\/strong> ordering. Consequently the ordering can only be <strong>local to the current predicate<\/strong><\/p>\n<p>Examples are better than words<\/p>\n<h4>B. Director who played in their own movies<\/h4>\n<pre class=\"brush: java; title: ; wrap-lines: false; notranslate\" title=\"\">\r\ngremlin&gt;g.V().match(\r\n  __.as(&quot;directors&quot;).hasLabel(&quot;person&quot;),                   \/\/ 1st predicate\r\n  __.as(&quot;directors&quot;).in(&quot;director&quot;).as(&quot;directed_movies&quot;), \/\/ 2nd predicate\r\n  __.as(&quot;directed_movies&quot;).out(&quot;actor&quot;).as(&quot;directors&quot;)    \/\/ 3rd predicate  \r\n  ).\r\n  select(&quot;directors&quot;,&quot;directed_movies&quot;).       \/\/ Iterator&lt;Map&lt;String, Vertex=Movie|Person&gt;&gt;\r\n  dedup().\r\n  group().\r\n    by(select(&quot;directors&quot;).by(&quot;name&quot;)).\r\n    by(select(&quot;directed_movies&quot;).by(&quot;title&quot;).fold()).\r\n                                              \/\/ Iterator&lt;Map&lt;String==director, List&lt;String&gt;=movies&gt;&gt;\r\n  unfold()                                    \/\/ Iterator&lt;MapEntry&lt;String==director, List&lt;String&gt;=movies&gt;&gt; \r\n\r\n==&gt;Charles Chaplin=[The Gold Rush, Modern Times, City Lights, The Great Dictator, The Kid]\r\n==&gt;Keenen Ivory Wayans=[Scary Movie]\r\n==&gt;Terry Jones=[Monty Python's Life of Brian, Monty Python's The Meaning of Life, Monty Python and the Holy Grail]\r\n==&gt;Kenneth Branagh=[Much Ado About Nothing]\r\n==&gt;Kevin Costner=[Open Range, Dances with Wolves]\r\n==&gt;Kevin Smith=[Chasing Amy, Clerks]\r\n==&gt;Buster Keaton=[The General]\r\n==&gt;Dany Boon=[Welcome to the Sticks]\r\n==&gt;Ben Stiller=[The Cable Guy, Zoolander]\r\n...\r\n<\/pre>\n<ul>\n<li>the 1<sup>st<\/sup> predicate is labeled as <em>directors<\/em> and picks all person vertices<\/li>\n<li>in the 2<sup>nd<\/sup> predicate, we pick the previous <em>directors<\/em> movies and label them as <em>directed_movies<\/em><\/li>\n<li>in the 3<sup>rd<\/sup> predicate, we pick the previous <em>directed_movies<\/em> and find their actors and label them as <em>directors<\/em>. This implicitly means that the actors playing in movies <em>directed_movies<\/em> should be the previously labeled <em>directors<\/em> themselves!<\/li>\n<\/ul>\n<p>At the step <code><strong>select(\"directors\",\"directed_movies\")<\/strong><\/code> we have an <code><strong>Iterator&lt;Map&lt;String, Vertex=Movie|Person&gt;&gt;<\/strong><\/code> as follow:<\/p>\n<p><code><br \/>\n==>{directors=v[{~label=person, community_id=1325149184, member_id=418}], directed_movies=v[{~label=movie, community_id=1757220352, member_id=30}]}<br \/>\n==>{directors=v[{~label=person, community_id=652999808, member_id=184}], directed_movies=v[{~label=movie, community_id=704287872, member_id=7}]}<br \/>\n==>{directors=v[{~label=person, community_id=652999808, member_id=184}], directed_movies=v[{~label=movie, community_id=1026409472, member_id=23}]}<br \/>\n<\/code><\/p>\n<p>Each map contains a single entry director=director vertex and directed_movies=ONE of the movie he directed\/acted in.<\/p>\n<p>The step <code><strong>group().by(select(\"directors\").by(\"name\")).by(select(\"directed_movies\").by(\"title\").fold())<br \/>\n<\/strong><\/code> will transform the previous stream into an <code><strong>Iterator&lt;Map&lt;String==director,List&lt;String&gt;==movies&gt;&gt;<\/strong><\/code>. Please notice the interesting usage of the <code><strong>select()<\/strong><\/code> operator to pick the right map entry by its key. The <code><strong>fold()<\/strong><\/code> step in the 2<sup>nd<\/sup> <code><strong>by()<\/strong><\/code> modulator is necessary to pick <strong>ALL<\/strong> the movies for each director\/actor<\/p>\n<h4>C. Director who played in their own Sci-Fi movies<\/h4>\n<p>Now we add an extra condition to the previous pattern matching, we only want to look at Sci-Fi movies:<\/p>\n<pre class=\"brush: java; title: ; wrap-lines: false; notranslate\" title=\"\">\r\ngremlin&gt;g.V().match(\r\n  __.as(&quot;directors&quot;).hasLabel(&quot;person&quot;),                   \/\/ 1st predicate\r\n  __.as(&quot;directors&quot;).in(&quot;director&quot;).as(&quot;directed_movies&quot;), \/\/ 2nd predicate\r\n  __.as(&quot;directed_movies&quot;).out(&quot;actor&quot;).as(&quot;directors&quot;),   \/\/ 3rd predicate  \r\n  __.as(&quot;directed_movies&quot;).out(&quot;belongsTo&quot;).as(&quot;genres&quot;),  \/\/ 4th predicate \r\n  __.as(&quot;genres&quot;).filter(values(&quot;name&quot;).is(eq(&quot;Sci-Fi&quot;)))  \/\/ 5th predicate\r\n  ).\r\n  select(&quot;directors&quot;,&quot;directed_movies&quot;).       \r\n  dedup().\r\n  group().\r\n    by(select(&quot;directors&quot;).by(&quot;name&quot;)).\r\n    by(select(&quot;directed_movies&quot;).by(&quot;title&quot;).fold()).\r\n  unfold()                                    \r\n==&gt;David Cronenberg=[The Fly]\r\n==&gt;M. Night Shyamalan=[Signs]\r\n==&gt;Kurt Wimmer=[Equilibrium]\r\n==&gt;George Lucas=[Star Wars: Episode III Revenge of the Sith]\r\n<\/pre>\n<p>We added 2 extra predicates (4<sup>th<\/sup> and 5<sup>th<\/sup>) at the end to force the genres of the movies to contain &#8220;Sci-Fi&#8221;. The results are now limited to only 4 movies.<\/p>\n<h4>D. Movies that are both Sci-Fi and Action<\/h4>\n<pre class=\"brush: java; title: ; wrap-lines: false; notranslate\" title=\"\">\r\ngremlin&gt;g.V().match(\r\n  __.as(&quot;movies&quot;).hasLabel(&quot;movie&quot;),\r\n  __.as(&quot;movies&quot;).filter(out(&quot;belongsTo&quot;).values(&quot;name&quot;).is(eq(&quot;Sci-Fi&quot;))).as(&quot;scifi_movies&quot;),\r\n  __.as(&quot;scifi_movies&quot;).filter(out(&quot;belongsTo&quot;).values(&quot;name&quot;).is(eq(&quot;Action&quot;))).as(&quot;scifi_movies&quot;)\r\n  ).\r\n  select(&quot;scifi_movies&quot;).\r\n  project(&quot;title&quot;,&quot;genres&quot;).\r\n    by(&quot;title&quot;).\r\n    by(out(&quot;belongsTo&quot;).values(&quot;name&quot;).fold())                      \r\n==&gt;{title=Total Recall, genres=[Sci-Fi, Action]}\r\n==&gt;{title=Battlefield Earth, genres=[Sci-Fi, Action]}\r\n==&gt;{title=The Avengers, genres=[Sci-Fi, Action, Fantasy]}\r\n==&gt;{title=Source Code, genres=[Sci-Fi, Thriller, Action]}\r\n==&gt;{title=R.O.T.O.R., genres=[Sci-Fi, Action]}\r\n==&gt;{title=Inception, genres=[Mystery, Sci-Fi, Thriller, Action]}\r\n==&gt;{title=Ghost in the Shell, genres=[Sci-Fi, Action, Animation]}\r\n...\r\n<\/pre>\n<p>It&#8217;s quite explanatory, nothing complicated here<\/p>\n<h4>E. Top 5 well rated Sci-Fi movies<\/h4>\n<pre class=\"brush: java; title: ; wrap-lines: false; notranslate\" title=\"\">\r\ngremlin&gt;g.V().match(\r\n  __.as(&quot;movies&quot;).hasLabel(&quot;movie&quot;),\r\n  __.as(&quot;movies&quot;).filter(out(&quot;belongsTo&quot;).values(&quot;name&quot;).is(eq(&quot;Sci-Fi&quot;))).as(&quot;scifi_movies&quot;),\r\n  __.as(&quot;scifi_movies&quot;).dedup().order().by(inE(&quot;rated&quot;).values(&quot;rating&quot;).mean(), decr).as(&quot;well_rated_scifi&quot;)\r\n  ).\r\n  select(&quot;well_rated_scifi&quot;).limit(5).\r\n  project(&quot;movie&quot;,&quot;rating&quot;).\r\n    by(&quot;title&quot;).\r\n    by(inE(&quot;rated&quot;).values(&quot;rating&quot;).mean())                      \r\n==&gt;{movie=Alien Resurrection, rating=5.543478260869565}\r\n==&gt;{movie=Total Recall, rating=6.714285714285714}\r\n==&gt;{movie=Battlefield Earth, rating=2.5}\r\n==&gt;{movie=Eternal Sunshine of the Spotless Mind, rating=7.597701149425287}\r\n==&gt;{movie=Back to the Future. Part III, rating=6.604651162790698}\r\n<\/pre>\n<p>Strangely, the results are incorrectly ordered. In fact we are hitting here the limitation I mentioned above. The ordering can only be local to the current step so in <code><strong>__.as(\"scifi_movies\").dedup().order().by(inE(\"rated\").values(\"rating\").mean(), decr).as(\"well_rated_scifi\")<\/strong><\/code> the traverser enters the predicate from a <strong>single sci-fi movie<\/strong> so there is only 1 mean rating for this single movie and ordering on it is useless. Consequently <em>scifi_movies<\/em> == <em>well_rated_scifi<\/em><\/p>\n<p>If we want the ordering, it should be done <strong>outside<\/strong> of the <code><strong>match()<\/strong><\/code> step<\/p>\n<pre class=\"brush: java; title: ; wrap-lines: false; notranslate\" title=\"\">\r\ngremlin&gt;g.V().match(\r\n  __.as(&quot;movies&quot;).hasLabel(&quot;movie&quot;),\r\n  __.as(&quot;movies&quot;).filter(out(&quot;belongsTo&quot;).values(&quot;name&quot;).is(eq(&quot;Sci-Fi&quot;))).as(&quot;scifi_movies&quot;)\r\n  ).\r\n  select(&quot;scifi_movies&quot;).\r\n  order().by(inE(&quot;rated&quot;).values(&quot;rating&quot;).mean(), decr).\r\n  limit(5).\r\n  project(&quot;movie&quot;,&quot;rating&quot;).\r\n    by(&quot;title&quot;).\r\n    by(inE(&quot;rated&quot;).values(&quot;rating&quot;).mean())                     \r\n==&gt;{movie=Metropolis, rating=8.25}\r\n==&gt;{movie=A Clockwork Orange, rating=8.215686274509803}\r\n==&gt;{movie=Blade Runner, rating=8.20353982300885}\r\n==&gt;{movie=Star Wars. Episode V: The Empire Strikes Back, rating=8.121739130434783}\r\n==&gt;{movie=Stalker, rating=8.03191489361702}\r\n<\/pre>\n<h4>F. Recommendation engine with pattern-matching<\/h4>\n<p>We can re-write our classical movies recommendation engine seen in previous blog posts using pattern matching<\/p>\n<pre class=\"brush: java; title: ; wrap-lines: false; notranslate\" title=\"\">\r\ngremlin&gt;g.V().match(\r\n  __.as(&quot;blade_runner&quot;).has(&quot;movie&quot;, &quot;title&quot;, &quot;Blade Runner&quot;),\r\n  __.as(&quot;blade_runner&quot;).map(inE(&quot;rated&quot;).values(&quot;rating&quot;).mean()).as(&quot;avg_rating&quot;),\r\n  __.as(&quot;blade_runner&quot;).map(out(&quot;belongsTo&quot;).values(&quot;name&quot;).fold()).as(&quot;genres&quot;),\r\n  __.as(&quot;blade_runner&quot;).inE(&quot;rated&quot;).filter(values(&quot;rating&quot;).where(gte(&quot;avg_rating&quot;))).outV().as(&quot;raters_of_blade_runner&quot;),\r\n  \r\n __.as(&quot;raters_of_blade_runner&quot;).outE(&quot;rated&quot;).filter(values(&quot;rating&quot;).where(gte(&quot;avg_rating&quot;))).inV().where(neq(&quot;blade_runner&quot;)).as(&quot;potential_movies&quot;),\r\n  __.as(&quot;potential_movies&quot;).where(&quot;potential_movies&quot;, gte(&quot;avg_rating&quot;)).by(inE(&quot;rated&quot;).values(&quot;rating&quot;).mean()).by().as(&quot;filtered_movies1&quot;),\r\n  __.as(&quot;filtered_movies1&quot;).filter(out(&quot;belongsTo&quot;).has(&quot;name&quot;, where(within(&quot;genres&quot;)))).as(&quot;matching_movies&quot;)\r\n  ).\r\n  select(&quot;matching_movies&quot;).\r\n  project(&quot;movie&quot;, &quot;average_rating&quot;, &quot;genres&quot;).\r\n    by(&quot;title&quot;).                                \r\n    by(inE(&quot;rated&quot;).values(&quot;rating&quot;).mean()).   \r\n    by(out(&quot;belongsTo&quot;).values(&quot;name&quot;).fold())\r\nNeither the sideEffects, map, nor path has a genres-key: WherePredicateStep(within([genres]))\r\nType ':help' or ':h' for help.\r\n<\/pre>\n<p>Against, the error is due to a bug in Gremlin, reported in <a href=\"https:\/\/issues.apache.org\/jira\/browse\/TINKERPOP-1762\" target=\"_blank\"><strong>TINKERPOP-1762<\/strong><\/a>.<\/p>\n<p>A simple work-around right now is to add an extra <code><strong>dedup()<\/strong><\/code> after the <code><strong>select()<\/strong><\/code> step<\/p>\n<pre class=\"brush: java; title: ; wrap-lines: false; notranslate\" title=\"\">\r\ngremlin&gt;g.V().match(\r\n  __.as(&quot;blade_runner&quot;).has(&quot;movie&quot;, &quot;title&quot;, &quot;Blade Runner&quot;),\r\n  __.as(&quot;blade_runner&quot;).map(inE(&quot;rated&quot;).values(&quot;rating&quot;).mean()).as(&quot;avg_rating&quot;),\r\n  __.as(&quot;blade_runner&quot;).map(out(&quot;belongsTo&quot;).values(&quot;name&quot;).fold()).as(&quot;genres&quot;),\r\n  __.as(&quot;blade_runner&quot;).inE(&quot;rated&quot;).filter(values(&quot;rating&quot;).where(gte(&quot;avg_rating&quot;))).outV().as(&quot;raters_of_blade_runner&quot;),\r\n  __.as(&quot;raters_of_blade_runner&quot;).outE(&quot;rated&quot;).filter(values(&quot;rating&quot;).where(gte(&quot;avg_rating&quot;))).inV().where(neq(&quot;blade_runner&quot;)).as(&quot;potential_movies&quot;),\r\n  __.as(&quot;potential_movies&quot;).where(&quot;potential_movies&quot;, gte(&quot;avg_rating&quot;)).by(inE(&quot;rated&quot;).values(&quot;rating&quot;).mean()).by().as(&quot;filtered_movies1&quot;),\r\n  __.as(&quot;filtered_movies1&quot;).filter(out(&quot;belongsTo&quot;).has(&quot;name&quot;, where(within(&quot;genres&quot;)))).as(&quot;matching_movies&quot;)\r\n  ).\r\n  select(&quot;matching_movies&quot;).dedup().\r\n  project(&quot;movie&quot;, &quot;average_rating&quot;, &quot;genres&quot;).\r\n    by(&quot;title&quot;).                                \r\n    by(inE(&quot;rated&quot;).values(&quot;rating&quot;).mean()).   \r\n    by(out(&quot;belongsTo&quot;).values(&quot;name&quot;).fold())\r\n==&gt;{movie=Pulp Fiction, average_rating=8.581005586592179, genres=[Thriller, Action]}\r\n==&gt;{movie=Seven Samurai, average_rating=8.470588235294118, genres=[Action, Adventure, Drama]}\r\n==&gt;{movie=A Clockwork Orange, average_rating=8.215686274509803, genres=[Sci-Fi, Drama]}\r\n<\/pre>\n<p>And that&#8217;s all folks! <strong>Do not miss the other Gremlin recipes in this series<\/strong>.<\/p>\n<p>If you have any question about <strong>Gremlin<\/strong>, find me on the <strong><a href=\"http:\/\/datastaxacademy.slack.com\" target=\"_blank\">datastaxacademy.slack.com<\/a><\/strong>, channel <strong>dse-graph<\/strong>. My id is <em>@doanduyhai<\/em>   <\/p>\n","protected":false},"excerpt":{"rendered":"<p>This blog post is the 9th from the series Gremlin Recipes. It is recommended to read the previous blog posts first: Gremlin as a Stream SQL to Gremlin Recommendation Engine traversal Recursive traversals Path object Projection and selection Variable handling&#8230;<br \/><a class=\"read-more-button\" href=\"https:\/\/www.doanduyhai.com\/blog\/?p=13424\">Read more<\/a><\/p>\n","protected":false},"author":1,"featured_media":0,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":[],"categories":[58,10],"tags":[],"_links":{"self":[{"href":"https:\/\/www.doanduyhai.com\/blog\/index.php?rest_route=\/wp\/v2\/posts\/13424"}],"collection":[{"href":"https:\/\/www.doanduyhai.com\/blog\/index.php?rest_route=\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/www.doanduyhai.com\/blog\/index.php?rest_route=\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/www.doanduyhai.com\/blog\/index.php?rest_route=\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/www.doanduyhai.com\/blog\/index.php?rest_route=%2Fwp%2Fv2%2Fcomments&post=13424"}],"version-history":[{"count":14,"href":"https:\/\/www.doanduyhai.com\/blog\/index.php?rest_route=\/wp\/v2\/posts\/13424\/revisions"}],"predecessor-version":[{"id":13438,"href":"https:\/\/www.doanduyhai.com\/blog\/index.php?rest_route=\/wp\/v2\/posts\/13424\/revisions\/13438"}],"wp:attachment":[{"href":"https:\/\/www.doanduyhai.com\/blog\/index.php?rest_route=%2Fwp%2Fv2%2Fmedia&parent=13424"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/www.doanduyhai.com\/blog\/index.php?rest_route=%2Fwp%2Fv2%2Fcategories&post=13424"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/www.doanduyhai.com\/blog\/index.php?rest_route=%2Fwp%2Fv2%2Ftags&post=13424"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}