The data that comes back from GraphQL is not ideal for input to Elastic Search which is designed for full-text searching, which means it loses context when searching arrays. Consider this response from some fictional GraphQL API.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 |
[{ "franchise": { "name": "rambo", "episodes": [{ "name": "first blood", "year": 1982, "actors": [{ "dob": "1946-07-06", "name": "Sylvester Stallone", "nicknames": ["sly"] }, { "name": "Jack Starrett" }, { "name": "Richard Crenna", "nationality": "American" }] }, { "name": "first blood part 2", "year": 1985, "actors": [{ "dob": "1946-07-06", "name": "Sylvester Stallone", "nicknames": ["sly"] }, { "name": "Richard Crenna", "nationality": "American" }] }] } }, { "franchise": { "name": "terminator", "episodes": [{ "name": "the terminator", "year": 1984, "actors": [{ "dob": "1947-07-30", "name": "Arnold Schwarzenegger", "nicknames": ["arnie"] }, { "name": "Linda Hamilton" }] }, { "name": "terminator 2: judgement day", "year": 1985, "actors": [{ "dob": "1947-07-30", "name": "Arnold Schwarzenegger", "nicknames": ["arnie", "schwarzie"] }, { "name": "Michael Biehn" }] }] } }]; |
To make a success of this in Elastic search we need to turn it back into a tabular format, a little like you might get back from a complex SQL join. Something like this.
That’s a little more complex than simple object flattening, as we need to create a duplicate row each time there is an array element, and of course, these arrays might occur at any level.
The code
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 |
const blowup = ({ ob, sep = '_', cloner = item => JSON.parse(JSON.stringify(item)) }) => { // initial value must be an array of objects //if(!Array.isArray(ob)) ob = [ob]; // recursive piece const makeRows = (sob, rows = [], currentKey = '', cob = {}) => { // ignore undefined or null items if (typeof sob === typeof undefined || sob === null) { return rows; } else if (Array.isArray(sob)) { // going to work through an array creating 1 row for each element // but without adding to the current key // make deep clone of current object sob.forEach((f, i) => { // make clone of what we have so far to replicate across const clob = cloner(cob); // the first element updates an existing row // subsequent elements add to the number of rows if (i) { rows.push(clob); } else { rows[rows.length ? rows.length - 1 : 0] = clob; } // recurse for each element makeRows(f, rows, currentKey, clob); }); } else if (typeof (sob) === 'object' && !(sob instanceof Date)) { Object.keys(sob).forEach((k, i) => { // add to the key, but nothing to the accumulating object makeRows(sob[k], rows, currentKey ? currentKey + sep + k : k, cob); }); } else { // its a natural value if (cob.hasOwnProperty('currentKey')) { // something has gone wrong here - show should probably be a throw console.log('attempt to to overwrite property', cob, currentKey, 'row', rows.length); } else { cob[currentKey] = sob; } } return rows; }; // do the work - the input data should be a single item in an array of objects return Array.prototype.concat.apply([], ob.map(f => makeRows(f))); }; |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 |
[{ "franchise_name": "rambo", "franchise_episodes_name": "first blood", "franchise_episodes_year": 1982, "franchise_episodes_actors_dob": "1946-07-06", "franchise_episodes_actors_name": "Sylvester Stallone", "franchise_episodes_actors_nicknames": "sly" }, { "franchise_name": "rambo", "franchise_episodes_name": "first blood", "franchise_episodes_year": 1982, "franchise_episodes_actors_name": "Jack Starrett" }, { "franchise_name": "rambo", "franchise_episodes_name": "first blood", "franchise_episodes_year": 1982, "franchise_episodes_actors_name": "Richard Crenna", "franchise_episodes_actors_nationality": "American" }, { "franchise_name": "rambo", "franchise_episodes_name": "first blood part 2", "franchise_episodes_year": 1985, "franchise_episodes_actors_dob": "1946-07-06", "franchise_episodes_actors_name": "Sylvester Stallone", "franchise_episodes_actors_nicknames": "sly" }, { "franchise_name": "rambo", "franchise_episodes_name": "first blood part 2", "franchise_episodes_year": 1985, "franchise_episodes_actors_name": "Richard Crenna", "franchise_episodes_actors_nationality": "American" }, { "franchise_name": "terminator", "franchise_episodes_name": "the terminator", "franchise_episodes_year": 1984, "franchise_episodes_actors_dob": "1947-07-30", "franchise_episodes_actors_name": "Arnold Schwarzenegger", "franchise_episodes_actors_nicknames": "arnie" }, { "franchise_name": "terminator", "franchise_episodes_name": "the terminator", "franchise_episodes_year": 1984, "franchise_episodes_actors_name": "Linda Hamilton" }, { "franchise_name": "terminator", "franchise_episodes_name": "terminator 2: judgement day", "franchise_episodes_year": 1985, "franchise_episodes_actors_dob": "1947-07-30", "franchise_episodes_actors_name": "Arnold Schwarzenegger", "franchise_episodes_actors_nicknames": "arnie" }, { "franchise_name": "terminator", "franchise_episodes_name": "terminator 2: judgement day", "franchise_episodes_year": 1985, "franchise_episodes_actors_dob": "1947-07-30", "franchise_episodes_actors_name": "Arnold Schwarzenegger", "franchise_episodes_actors_nicknames": "schwarzie" }, { "franchise_name": "terminator", "franchise_episodes_name": "terminator 2: judgement day", "franchise_episodes_year": 1985, "franchise_episodes_actors_name": "Michael Biehn" }] |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
const blownupToTable = ({ blownup, sorter = mentions => Object.keys(mentions).sort((a, b) => a - b) }) => { // collect all the property names const mentions = blownup.reduce((p, c) => { Object.keys(c).forEach((k, i) => { p[k] = i; }); return p; }, {}); // make that into a header row const headerRow = sorter(mentions); // now add the rows after the header // & we dont really like undefined in sheets, so replace with null. return [headerRow] .concat(blownup.map(row => headerRow.map(h => typeof row[h] === typeof undefined ? null : row[h]))); } |
1 2 3 4 5 6 7 8 9 10 11 12 13 |
[ ["franchise_name", "franchise_episodes_name", "franchise_episodes_year", "franchise_episodes_actors_dob", "franchise_episodes_actors_name", "franchise_episodes_actors_nicknames", "franchise_episodes_actors_nationality"], ["rambo", "first blood", 1982, "1946-07-06", "Sylvester Stallone", "sly", null], ["rambo", "first blood", 1982, null, "Jack Starrett", null, null], ["rambo", "first blood", 1982, null, "Richard Crenna", null, "American"], ["rambo", "first blood part 2", 1985, "1946-07-06", "Sylvester Stallone", "sly", null], ["rambo", "first blood part 2", 1985, null, "Richard Crenna", null, "American"], ["terminator", "the terminator", 1984, "1947-07-30", "Arnold Schwarzenegger", "arnie", null], ["terminator", "the terminator", 1984, null, "Linda Hamilton", null, null], ["terminator", "terminator 2: judgement day", 1985, "1947-07-30", "Arnold Schwarzenegger", "arnie", null], ["terminator", "terminator 2: judgement day", 1985, "1947-07-30", "Arnold Schwarzenegger", "schwarzie", null], ["terminator", "terminator 2: judgement day", 1985, null, "Michael Biehn", null, null] ] |
Putting it together
Assuming the JSON data earlier in the article is in a variable called films, here’s the whole thing
1 2 3 4 5 6 |
const objectUnnest = ({ ob, sep, cloner, sorter }) => { const blownup = blowup({ ob, sep, cloner}); const result = blownupToTable ({ blownup, sorter }); return result; } console.log(objectUnnest({ ob: films })); |
You can find an Apps Script version of this here.
Since G+ is closed, you can now star and follow post announcements and discussions on Github, here