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.
[{ "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.
The code
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))); };
That will give this result
[{ "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" }]
We can take that one stage further and convert the result into a tabular format (ideal for writing to a sheet)
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]))); }
Which gives this result, with the header row in the first element
[ ["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
const objectUnnest = ({ ob, sep, cloner, sorter }) => { const blownup = blowup({ ob, sep, cloner}); const result = blownupToTable ({ blownup, sorter }); return result; } console.log(objectUnnest({ ob: films }));