def convert_to_rows(response, agg_layers, prefix={}):
    """
    Converts the hierarchical, dictionary type of response from an
    an Elasticsearch query into a more SQL-like list of rows (tuples).

    The Elasticsearch response is expected to have a specific format, where
    a series of bucket aggregations are nested in each other. The names of
    these aggregations are listed in order in <agg_layers>, from top to bottom.

    The bottom-level bucket (after all these nested aggregations) must contain
    a doc_count as well as a numerically-valued aggregation named total_obs.

    Each row of the output is a `dict` of values, matching each of the buckets
    described in <agg_layers> followed by doc_count and the total_obs value.
    The keys in the `dict` are descriptive terms, not necessarily the same as
    the property names in Elasticsearch.

    :param response: An Elasticsearch query response dictionary.
    :param agg_layers: List of aggregation names from top to bottom.
    :returns: Contents of the Elasticsearch response in list-of-rows format.
    """

    if 'aggregations' in response:
        response = response['aggregations']

    if len(agg_layers) == 0:
        total_files = response['doc_count']
        total_obs = int(response['total_obs']['value'])

        if total_files == 0:
            return []
        else:
            row = prefix.copy()
            row['total_files'] = total_files
            row['total_obs'] = total_obs

            return [row]
    else:
        layer_input_name, layer_output_name = agg_layers[0]
        remaining_layers = agg_layers[1:]

        rows = []
        for bucket in response[layer_input_name]['buckets']:
            if 'key_as_string' in bucket:
                key = bucket['key_as_string']
            else:
                key = bucket['key']

            next_prefix = prefix.copy()
            next_prefix[layer_output_name] = key

            rows.extend(convert_to_rows(bucket, remaining_layers, next_prefix))

        return rows