How I started with Tumblr mappings and ended up writing a parser for a custom query language
Tumblr and
Game demand
- Initial catalog: 5546 games
- Scraped from Wiki by the data science team
- Contains game titles and wikibase IDs
Is Tumblr a suitable data source for games?
Tumblr V2 pipeline
Tumblr
S3
Posts + Likes
Dictionary post_id to parrot_id
ElasticSearch Posts
SIT
data
Mappings
First step —
use titles as a search term
- ~400k posts about games per day
- Coverage: 3986 of 5546 (~72%)
Top games by posts per day
+-----------------------+-------------------+
|game_title |posts_cnt_daily_avg|
+-----------------------+-------------------+
|D |10000 |
|Baldur's Gate |10000 |
|Forced |10000 |
|Hearts |10000 |
|Sky |10000 |
|Air |10000 |
|LoveR |10000 |
|Spider |10000 |
|Journey |10000 |
|Blood |10000 |
|Snake |9723 |
|Stray |9499 |
|720° |8781 |
|SiN |8125 |
|Bless |7893 |
|Hatred |7745 |
|The Forest |7119 |
+-----------------------+-------------------+
We need better mappings!
Improving mappings
V2 approach: phrase search
{
"query": {
"multi_match": {
"query": "Resident Evil",
"fields": [
"g", // tags
"t", // title
"h", // caption
"b" // body
],
"type": "phrase"
}
}
}
Improving mappings
V1 approach: tag search
{
"query": {
"term": {
"g.array": {
"value": "Blood game",
"case_insensitive": true
}
}
}
}
Combining queries with "must"
{
"query": {
"bool": {
"must": [
{
"multi_match": {
"query": "Air",
"fields": ["g","t","h","b"],
"type": "phrase"
}
},
{
"multi_match": {
"query": "visual novel",
"fields": ["g","t","h","b"],
"type": "phrase"
}
}
]
}
}
}
Works as AND
Combining queries with "should"
{
"query": {
"bool": {
"should": [
{
"term": {
"g.array": {
"value": "blood game",
"case_insensitive": true
}
}
},
{
"term": {
"g.array": {
"value": "caleb blood",
"case_insensitive": true
}
}
}
],
"minimum_should_match": "1"
}
}
}
Works as OR
Hard cases
Phrase "Obscure game"
and (
Phrase "Josh Carter"
or Phrase "Stan Jones"
or Phrase "Kenny Matthew"
or Phrase "Shannon Matthews"
or Phrase "DreamCatcher Interactive"
or Phrase "Hydravision Entertainment"
or Phrase "MC2 Microids"
)
{
"query": {
"bool": {
"must": [
{
"multi_match": {
"query": "Obscure game",
...
}
},
{
"bool": {
"should": [
{
"multi_match": {
"query": "Josh Carter",
...
}
},
{
"multi_match": {
"query": "Stan Jones",
...
}
},
...
],
"minimum_should_match": "1"
}
}
]
}
}
}
{
"track_total_hits": true,
"size": 100000,
"_source": {
"includes": ["i","b","g","h","r","t"]
},
"query": {
"bool": {
"must": [
{
"multi_match": {
"query": "Obscure game",
"fields": ["g","t","h","b"],
"type": "phrase"
}
},
{
"bool": {
"should": [
{
"multi_match": {
"query": "Josh Carter",
"fields": ["g","t","h","b"],
"type": "phrase"
}
},
{
"multi_match": {
"query": "Stan Jones",
"fields": ["g","t","h","b"],
"type": "phrase"
}
},
{
"multi_match": {
"query": "Kenny Matthew",
"fields": ["g","t","h","b"],
"type": "phrase"
}
},
{
"multi_match": {
"query": "Shannon Matthews",
"fields": ["g","t","h","b"],
"type": "phrase"
}
},
{
"multi_match": {
"query": "DreamCatcher Interactive",
"fields": ["g","t","h","b"],
"type": "phrase"
}
},
{
"multi_match": {
"query": "Hydravision Entertainment",
"fields": ["g","t","h","b"],
"type": "phrase"
}
},
{
"multi_match": {
"query": "MC2 Microids",
"fields": ["g","t","h","b"],
"type": "phrase"
}
}
],
"minimum_should_match": "1"
}
}
]
}
}
}
Phrase "Obscure game"
and (
Phrase "Josh Carter"
or Phrase "Stan Jones"
or Phrase "Kenny Matthew"
or Phrase "Shannon Matthews"
or Phrase "DreamCatcher Interactive"
or Phrase "Hydravision Entertainment"
or Phrase "MC2 Microids"
)
1. Phrase
2. Tag
3. And
4. Or
5. ()
What we really need from ElasticSearch
p"value"
t"tag"
t"foo" and p"bar"
t"foo" or p"bar"
t"foo" and (p"bar" or t"baz")
Custom query language!
Working with a custom query language
- We need to convert queries to an actual ElasticSearch queries
- To do that we need to parse query expressions
- For parsing we need an intermediate representation as a data structure: AST (abstract syntax tree)
- Converting from AST to query is relatively simple
- Parsing string to AST — not simple
AST
interface TumblrMappingExpr {
@Data
class Phrase implements TumblrMappingExpr {
String value;
}
@Data
class Tag implements TumblrMappingExpr {
String value;
}
@Data
class And implements TumblrMappingExpr {
List<TumblrMappingExpr> expressions;
}
@Data
class Or implements TumblrMappingExpr {
List<TumblrMappingExpr> expressions;
}
}
sealed interface TumblrMappingExpr {
record Phrase(String value) implements TumblrMappingExpr {}
record Tag(String value) implements TumblrMappingExpr {}
record And(List<TumblrMappingExpr> expressions) implements TumblrMappingExpr {}
record Or(List<TumblrMappingExpr> expressions) implements TumblrMappingExpr {}
}
Java 21
sealed trait TumblrMappingExpr
object TumblrMappingExpr {
case class Phrase(value: String) extends TumblrMappingExpr
case class Tag(value: String) extends TumblrMappingExpr
case class And(expressions: List[TumblrMappingExpr]) extends TumblrMappingExpr
case class Or(expressions: List[TumblrMappingExpr]) extends TumblrMappingExpr
}
Scala 2
enum TumblrMappingExpr {
case Phrase(value: String)
case Tag(value: String)
case And(expressions: List[TumblrMappingExpr])
case Or(expressions: List[TumblrMappingExpr])
}
Scala 3
class Phrase {
value: string;
}
class Tag {
value: string;
}
class And {
expressions: TumblrMappingExpr[];
}
class Or {
expressions: TumblrMappingExpr[];
}
type TumblrMappingExpr = Phrase | Tag | And | Or;
TypeScript
Parsing
Chomsky hierarchy for grammars
Type 0: Unrestricted Grammar — the most wide and complex
Type 1: Context-Sensitive Grammar — complex grammar, parser should have a state
Type 2: Context-Free Grammar — relatively simple grammars
Type 3: Regular Grammar — the simplest case, can be parsed with regexps
Parsing
Searching for solution
Parsing
1. Manually written parser
2. Regexps
Parsing
TumblrMappingExprGrammar
TumblrMappingExprParserTest
Parsing
Mappings examples
p"bless online"
or p"bless unleashed"
or (p"bless" and (p"mmo" or p"rpg" or p"mmorpg"))
(p"Spider-Man" or p"Spider Man")
and (p"video game" or p"game")
and p"2018"
t"Dante's Inferno game" or (
p"Dante's Inferno" and (
p"video game"
or p"Visceral Games"
or p"EA"
or p"Electronic Arts"
or p"xbox"
or p"PlayStation"
or (p"game" and p"2010")
)
)
Mapping to ElasticSearch query
TumblrMappingExprToESquery
Intermediate results with new mappings
- I generated mappings for 4572 games out of 5546 (~82.8%)
- Posts about games per day: ~115k. Was ~420k.
- Coverage: 2480 of 4565 (~54.3%). Was ~72%.
Intermediate results with new mappings
+--------------+------+
|game_title |posts |
+--------------+------+
|D |10000 |
|Baldur's Gate |10000 |
|Forced |10000 |
|Hearts |10000 |
|Sky |10000 |
|Air |10000 |
|LoveR |10000 |
|Spider |10000 |
|Journey |10000 |
|Blood |10000 |
|Snake |9723 |
|Stray |9499 |
|720° |8781 |
|SiN |8125 |
|Bless |7893 |
|Hatred |7745 |
|The Forest |7119 |
+--------------+------+
+-------------------+------+
|game_title |posts |
+-------------------+------+
|Baldur's Gate III |21429 |
|Minecraft |6729 |
|Pikmin |5168 |
|Genshin Impact |5036 |
|Undertale |4472 |
|Splatoon |3936 |
|Disco Elysium |3569 |
|The Sims |3520 |
|Deltarune |3241 |
|Animal Crossing |2990 |
|Elden Ring |2440 |
|Stardew Valley |1943 |
|Braid |1873 |
|The Last of Us |1798 |
|Fire Emblem |1349 |
|The Legend of Zelda|1347 |
|Honkai: Star Rail |1175 |
+-------------------+------+
Conclusions
- I managed to find a way to create mappings, that:
- Covers V1 cases (tag search)
- Covers V2 cases (phrase search)
- Covers even more complex cases
- Mappings itself are quite simple, intuitive, and easy to write and read.
Conclusions
- Not every day you have an opportunity to work on a custom query language.
- It was a cool, interesting, and technically difficult task.
- Thanks to a modern tooling (ChatGPT, Parser Combinators libraries) it was relatively simple.
- Cool that Parrot provides an opportunity to work on a such task.
Thank you!
Tumbr mappings
By Yury Badalyants
Tumbr mappings
- 75