text: const request = require('requestretry')
  const jsonParser = require('../jsonparser.js')
  const parseBaseURL = require('../parse-base-url')
  const aws4signer = require('../aws4signer')
  const async = require('async')
  const _ = require('lodash')
  const { parseMetaFields } = require('../parse-meta-data')
  const fs = require('fs')
  const agent = require('socks5-http-client/lib/Agent')
  const sslAgent = require('socks5-https-client/lib/Agent')
  
  class elasticsearch {
    constructor (parent, url, options) {
      this.base = parseBaseURL(url, options)
      this.parent = parent
      this.lastScrollId = null
      this.settingsExclude = ['settings.index.version', 'settings.index.creation_date', 'settings.index.uuid', 'settings.index.provided_name']
      /**
       * Note that _parent, has been deprecated since ES 6.0
       * Note that _timestamp & _ttl have been deprecated since ES 5.0
       */
      this.defaultMetaFields = ['routing', 'parent', 'timestamp', 'ttl']
      this.totalSearchResults = 0
      this.elementsToSkip = 0
      this.searchBody = this.parent.options.searchBody
      this.ESversion = null
  
      const defaultOptions = {
        timeout: this.parent.options.timeout,
        headers: Object.assign({
          'User-Agent': 'elasticdump',
          'Content-Type': 'application/json'
        }, options.headers),
        maxAttempts: this.parent.options.retryAttempts || 5, // (default) try 5 times
        retryDelay: this.parent.options.retryDelay || 5000, // (default) wait for 5s before trying again
        retryStrategy: request.RetryStrategies.HTTPOrNetworkError // (default) retry on 5xx or network errors
      }
  
      if (parent.options[`${options.type}SocksProxy`]) {
        Object.assign(defaultOptions,
          elasticsearch.applyProxy(url, parent.options[`${options.type}SocksProxy`], parent.options[`${options.type}SocksPort`]))
      }
  
      if (this.parent.options.tlsAuth) {
        Object.assign(defaultOptions,
          this.applySSL([`${options.type}-cert`, `${options.type}-key`, `${options.type}-pass`, `${options.type}-ca`]))
  
        Object.assign(defaultOptions, this.applySSL(['cert', 'key', 'pass', 'ca']))
      }
  
      this.baseRequest = request.defaults(defaultOptions)
    }
  
    static applyProxy (url, proxyHost, proxyPort) {
      let reqAgent = agent
  
      if (url.indexOf('https://') === 0) {
        reqAgent = sslAgent
      }
  
      return {
        agentClass: reqAgent,
        agentOptions: {
          socksHost: proxyHost,
          socksPort: proxyPort
        }
      }
    }
  
    applySSL (props) {
      const options = {}
      const parent = this.parent
      props.forEach(prop => {
        const val = parent.options[prop]
        if (val) {
          const newProp = prop.replace(/^input-/, '').replace(/^output-/, '')
          if (newProp === 'pass') {
            options.passphrase = val
          } else {
            options[newProp] = fs.readFileSync(val)
          }
        }
      })
      return options
    }
  
    // accept callback
    // return (error, arr) where arr is an array of objects
    get (limit, offset, callback) {
      const type = this.parent.options.type
      this.version('input', err => {
        if (err) { return callback(err) }
  
        if (type === 'data') {
          this.getData(limit, offset, callback)
        } else if (type === 'mapping') {
          this.getMapping(limit, offset, callback)
        } else if (type === 'analyzer' || type === 'settings') {
          this.getSettings(limit, offset, callback)
        } else if (type === 'alias') {
          this.getAliases(limit, offset, callback)
        } else if (type === 'template') {
          this.getTemplates(limit, offset, callback)
        } else {
          callback(new Error('unknown type option'), null)
        }
      })
    }
  
    version (prefix, callback) {
      if (this.ESversion) { return callback() }
      const esRequest = {
        url: `${this.base.host}/`,
        method: 'GET'
      }
      aws4signer(esRequest, this.parent)
      this.baseRequest(esRequest, (err, response) => {
        if (err) { return callback(err) }
        response = jsonParser.parse(response.body)
  
        if (response.version) {
          this.ESversion = response.version.number.split('.')[0]
          this.parent.emit('debug', `discovered elasticsearch ${prefix} major version: ${this.ESversion}`)
        } else {
          this.ESversion = 5
          this.parent.emit('debug', `cannot discover elasticsearch ${prefix} major version, assuming: ${this.ESversion}`)
        }
  
        if (!this.searchBody) {
          if (this.ESversion >= 5) {
            this.searchBody = { query: { match_all: {} }, stored_fields: ['*'], _source: true }
          } else {
            this.searchBody = { query: { match_all: {} }, fields: ['*'], _source: true }
          }
        }
  
        callback()
      })
    }
  
    getMapping (limit, offset, callback) {
      if (this.gotMapping === true) {
        callback(null, [])
      } else {
        const esRequest = {
          url: `${this.base.url}/_mapping`,
          method: 'GET'
        }
        aws4signer(esRequest, this.parent)
  
        this.baseRequest(esRequest, (err, response) => {
          this.gotMapping = true
          const payload = []
          if (!err) {
            response = payload.push(jsonParser.parse(response.body))
          }
          callback(err, payload)
        })
      }
    }
  
    getSettings (limit, offset, callback) {
      if (this.gotSettings === true) {
        callback(null, [])
      } else {
        const esRequest = {
          url: `${this.base.url}/_settings`,
          method: 'GET'
        }
        aws4signer(esRequest, this.parent)
  
        this.baseRequest(esRequest, (err, response) => {
          this.gotSettings = true
          const payload = []
          if (!err) {
            const output = jsonParser.parse(response.body)
            output[this.base.index] = _.omit(output[this.base.index], this.settingsExclude)
            payload.push(jsonParser.stringify(output))
          }
          callback(err, payload)
        })
      }
    }
  
    getAliases (limit, offset, callback) {
      if (this.gotAliases === true) {
        callback(null, [])
      } else {
        const esRequest = {
          url: `${this.base.host}/${this.base.index}/_alias/${this.base.type || '*'}`,
          method: 'GET'
        }
        aws4signer(esRequest, this.parent)
  
        this.baseRequest(esRequest, (err, response) => {
          this.gotAliases = true
          const payload = []
          if (!err) {
            payload.push(response.body)
          }
          callback(err, payload)
        })
      }
    }
  
    getTemplates (limit, offset, callback) {
      if (this.gotTemplates === true) {
        callback(null, [])
      } else {
        const esRequest = {
          url: `${this.base.host}/_template/${this.base.index}`,
          method: 'GET'
        }
        aws4signer(esRequest, this.parent)
  
        this.baseRequest(esRequest, (err, response) => {
          this.gotTemplates = true
          const payload = []
          if (!err) {
            payload.push(response.body)
          }
          callback(err, payload)
        })
      }
    }
  
    getData (limit, offset, callback) {
      let searchRequest, uri
      const searchBody = this.searchBody
  
      if (offset >= this.totalSearchResults && this.totalSearchResults !== 0) {
        callback(null, [])
        return
      }
  
      if (this.lastScrollId !== null) {
        scrollResultSet(this, callback)
      } else {
        // previously we used the scan/scroll method, but now we need to change the sort
        // https://www.elastic.co/guide/en/elasticsearch/reference/master/breaking_50_search_changes.html#_literal_search_type_scan_literal_removed
  
        // if this is the first time we run, we need to log how many elements we should be skipping
        if (!this.elementsToSkip) { this.elementsToSkip = offset }
  
        const paramsObj = this.parent.options.params
        const additionalParams = paramsObj
          ? `&${Object.keys(paramsObj).map(key => `${key}=${paramsObj[key]}`).join('&')}`
          : ''
  
        // https://www.elastic.co/guide/en/elasticsearch/reference/6.0/breaking_60_search_changes.html#_scroll
        // The from parameter can no longer be used in the search request body when initiating a scroll.
        // The parameter was already ignored in these situations, now in addition an error is thrown.
        uri = `${this.base.url}/_search?scroll=${this.parent.options.scrollTime}&from=${offset}${additionalParams}`
  
        searchBody.size = this.parent.options.size >= 0 && this.parent.options.size < limit ? this.parent.options.size : limit
  
        searchRequest = {
          uri: uri,
          method: 'GET',
          sort: ['_doc'],
          body: jsonParser.stringify(searchBody)
        }
        aws4signer(searchRequest, this.parent)
  
        this.baseRequest(searchRequest, (err, response) => {
          if (err) {
            callback(err, [])
            return
          } else if (response.statusCode !== 200) {
            err = new Error(response.body)
            callback(err, [])
            return
          }
  
          const body = jsonParser.parse(response.body, this.parent)
          this.lastScrollId = body._scroll_id
  
          if (this.lastScrollId === undefined) {
            err = new Error('Unable to obtain scrollId; This tends to indicate an error with your index(es)')
            callback(err, [])
            return
          }
  
          // hits.total is now an object in the search response
          // https://www.elastic.co/guide/en/elasticsearch/reference/7.0/breaking-changes-7.0.html#_literal_hits_total_literal_is_now_an_object_in_the_search_response
          const hitsTotal = _.get(body, 'hits.total.value', body.hits.total)
          this.totalSearchResults = this.parent.options.size >= 0 ? this.parent.options.size : hitsTotal
  
          scrollResultSet(this, callback, body.hits.hits, response)
        })
      }
    }
  
    // accept arr, callback where arr is an array of objects
    // return (error, writes)
    set (data, limit, offset, callback) {
      const type = this.parent.options.type
      this.version('output', err => {
        if (err) { return callback(err) }
  
        if (type === 'data') {
          this.setData(data, limit, offset, callback)
        } else if (type === 'mapping') {
          this.setMapping(data, limit, offset, callback)
        } else if (type === 'analyzer') {
          this.setAnalyzer(data, limit, offset, callback)
        } else if (type === 'settings') {
          this.setSettings(data, limit, offset, callback)
        } else if (type === 'alias') {
          this.setAliases(data, limit, offset, callback)
        } else if (type === 'template') {
          this.setTemplates(data, limit, offset, callback)
        } else {
          callback(new Error('unknown type option'), null)
        }
      })
    }
  
    setMapping (data, limit, offset, callback) {
      if (this.haveSetMapping === true || data.length === 0) {
        callback(null, 0)
      } else {
        const esRequest = {
          url: this.base.url,
          method: 'PUT'
        }
        aws4signer(esRequest, this.parent)
  
        this.baseRequest(esRequest, (err, response) => { // ensure the index exists
          if (err) { return callback(err) }
  
          try {
            data = data[0]
          } catch (e) {
            return callback(e)
          }
          let started = 0
          let count = 0
          for (const index in data) {
            const mappings = data[index].mappings
            let sortedMappings = []
  
            // make sure new mappings inserted before parent and after child
            for (const key in mappings) {
              if (mappings[key]._parent) {
                const parentIndex = sortedMappings.findIndex(set => set.key === mappings[key]._parent.type) // find parent
                if (parentIndex > -1) {
                  sortedMappings.splice(parentIndex, 0, { key, index, data: mappings[key] })
                } else {
                  const childIndex = sortedMappings.findIndex(set => (set.data._parent) && (set.data._parent.type === key)) // find child
                  if (childIndex > -1) {
                    sortedMappings.splice(childIndex + 1, 0, { key, index, data: mappings[key] })
                  } else {
                    sortedMappings = [{ key, index, data: mappings[key] }].concat(sortedMappings)
                  }
                }
              } else {
                sortedMappings.push({ key, index, data: mappings[key] })
              }
            }
  
            async.eachSeries(sortedMappings, (set, done) => {
              let __type = ''
              if (this.ESversion < 7) {
                __type = `/${encodeURIComponent(set.key)}`
              } else if (!set.data.properties) {
                set.data = { properties: set.data }
              }
  
              if (!this.base.index) {
                __type = `/${set.index}${__type}`
              }
  
              const url = `${this.base.url}${__type}/_mapping`
              const payload = {
                url,
                method: 'PUT',
                body: jsonParser.stringify(set.data)
              }
              aws4signer(payload, this.parent)
  
              started++
              count++
  
              this.baseRequest(payload, (err, response) => {
                started--
                done(null) // we always call this with no error because this is a dirty hack and we are already handling errors...
                if (!err) {
                  const bodyError = jsonParser.parse(response.body).error
                  if (bodyError) { err = bodyError }
                }
                if (started === 0) {
                  this.haveSetMapping = true
                  callback(err, count)
                }
              })
            })
          }
        })
      }
    }
  
    setAnalyzer (data, limit, offset, callback) {
      const updateAnalyzer = (err, response) => {
        if (err) { return callback(err) }
  
        try {
          data = jsonParser.parse(data[0])
        } catch (e) {
          return callback(e)
        }
        let started = 0
        let count = 0
        for (const index in data) {
          const settings = data[index].settings
          for (const key in settings) { // iterate through settings
            const setting = {}
            setting[key] = settings[key]
            const url = `${this.base.url}/_settings`
            started++
            count++
  
            // ignore all other settings other than 'analysis'
            for (const p in setting[key]) { // iterate through index
              if (p !== 'analysis') { // remove everything not 'analysis'
                delete setting[key][p]
              }
            }
  
            const esRequest = {
              url: `${this.base.url}/_close`, // close the index
              method: 'POST'
            }
            aws4signer(esRequest, this.parent)
  
            this.baseRequest(esRequest, (err, response, body) => {
              if (!err) {
                const bodyError = jsonParser.parse(response.body).error
                if (bodyError) {
                  err = bodyError
                }
                const payload = {
                  url: url,
                  method: 'PUT',
                  body: jsonParser.stringify(setting)
                }
                aws4signer(payload, this.parent)
  
                this.baseRequest(payload, (err, response) => { // upload the analysis settings
                  started--
                  if (!err) {
                    var bodyError = jsonParser.parse(response.body).error
                    if (bodyError) {
                      err = bodyError
                    }
                  } else {
                    callback(err, count)
                  }
                  if (started === 0) {
                    this.haveSetAnalyzer = true
                    const esRequest = {
                      url: `${this.base.url}/_open`, // open the index
                      method: 'POST'
                    }
                    aws4signer(esRequest, this.parent)
  
                    this.baseRequest(esRequest, (err, response) => {
                      if (!err) {
                        const bodyError = jsonParser.parse(response.body).error
                        if (bodyError) {
                          err = bodyError
                        }
                      }
                      callback(err, count)
                    })
                  }
                })
              } else {
                callback(err, count)
              }
            })
          }
        }
      }
      if (this.haveSetAnalyzer === true || data.length === 0) {
        callback(null, 0)
      } else {
        var esRequest = {
          url: this.base.url,
          method: 'PUT'
        }
        aws4signer(esRequest, this.parent)
  
        this.baseRequest(esRequest, (err, response) => { // ensure the index exists
          if (err) {
            return callback(err, [])
          } else if (response.statusCode !== 200) {
            err = new Error(response.body)
            callback(err, [])
            return
          }
  
          // use cluster health api to check if the index is ready
          esRequest = {
            url: `${this.base.host}/_cluster/health/${this.base.index}?wait_for_status=green`,
            method: 'GET'
          }
          aws4signer(esRequest, this.parent)
          this.baseRequest(esRequest, updateAnalyzer)
        })
      }
    }
  
    setSettings (data, limit, offset, callback) {
      if (this.haveSetSettings === true || data.length === 0) {
        return callback(null, 0)
      }
  
      try {
        data = jsonParser.parse(data[0])
      } catch (e) {
        return callback(e)
      }
  
      async.forEachOf(data, (index, name, cb) => {
        let settings = _.omit(index, this.settingsExclude)
        if (this.ESversion < 7) {
          settings = settings.settings
        }
  
        const esRequest = {
          url: this.base.url,
          method: 'PUT',
          body: jsonParser.stringify(settings)
        }
        aws4signer(esRequest, this.parent)
  
        this.baseRequest(esRequest, (err, response) => { // ensure the index exists
          if (err) {
            return cb(err, [])
          } else if (response.statusCode !== 200) {
            err = new Error(response.body)
            callback(err, [])
            return
          }
          return cb()
        })
      }, err => {
        if (err) { return callback(err) }
        this.haveSetSettings = true
        return callback(null, 0)
      })
    }
  
    setAliases (data, limit, offset, callback) {
      if (this.haveSetAliases === true || data.length === 0) {
        return callback(null, 0)
      }
  
      try {
        data = jsonParser.parse(data[0])
      } catch (e) {
        return callback(e)
      }
  
      const payload = {
        actions: []
      }
  
      async.forEachOf(data, async.ensureAsync((_data, index, cb) => {
        if (!_.has(_data, 'aliases') || _.isEmpty(_data.aliases)) {
          return cb(new Error('no aliases detected'))
        }
  
        async.forEachOf(_data.aliases, async.ensureAsync((aliasOptions, alias, acb) => {
          payload.actions.push({ add: Object.assign({ index, alias }, aliasOptions) })
          return acb()
        }), () => {
          return cb()
        })
      }), err => {
        if (err) { return callback(err) }
        this.haveSetAliases = true
  
        const esRequest = {
          url: `${this.base.host}/_aliases`,
          method: 'POST',
          body: jsonParser.stringify(payload)
        }
        aws4signer(esRequest, this.parent)
  
        this.baseRequest(esRequest, (err, response) => {
          if (err) { return callback(err) }
          return callback(null, 0)
        })
      })
    }
  
    setTemplates (data, limit, offset, callback) {
      if (this.haveSetTemplates === true || data.length === 0) {
        return callback(null, 0)
      }
  
      try {
        data = jsonParser.parse(data[0])
      } catch (e) {
        return callback(e)
      }
  
      async.forEachOf(data, async.ensureAsync((_template, templateName, cb) => {
        const esRequest = {
          url: `${this.base.host}/_template/${templateName}`,
          method: 'PUT',
          body: jsonParser.stringify(_template)
        }
        aws4signer(esRequest, this.parent)
  
        this.baseRequest(esRequest, (err, response) => {
          if (err) { return cb(err) }
          return cb()
        })
      }), err => {
        if (err) { return callback(err) }
        this.haveSetTemplates = true
        return callback(null, 0)
      })
    }
  
    setData (data, limit, offset, callback) {
      if (data.length === 0) { return callback(null, 0) }
  
      let writes = 0
  
      const extraFields = _.chain(this.parent.options.parseExtraFields)
        .split(',')
        .concat(this.defaultMetaFields)
        .flatten()
        .compact()
        .uniq()
        .value()
  
      const thisUrl = `${this.base.url}/_bulk`
  
      const payload = {
        url: thisUrl,
        body: '',
        method: 'PUT',
        headers: Object.assign({
          'User-Agent': 'elasticdump',
          'Content-Type': 'application/x-ndjson'
        }, this.parent.options.headers)
      }
  
      data.forEach(elem => {
        const actionMeta = { index: {} }
  
        // use index from base otherwise fallback to elem
        actionMeta.index._index = this.base.index || elem._index
  
        // https://www.elastic.co/guide/en/elasticsearch/reference/master/removal-of-types.html
        if (this.ESversion < 7) {
          // use type from base otherwise fallback to elem
          actionMeta.index._type = this.base.type || elem._type
        }
        actionMeta.index._id = elem._id
  
        parseMetaFields(extraFields, [elem, elem.fields], actionMeta)
  
        payload.body += `${jsonParser.stringify(actionMeta, this.parent)}
  `
        payload.body += `${jsonParser.stringify(elem._source, this.parent)}
  `
      })
  
      this.parent.emit('debug', `thisUrl: ${thisUrl}, payload.body: ${jsonParser.stringify(payload.body, this.parent)}`)
  
      aws4signer(payload, this.parent)
      this.baseRequest(payload, (err, response) => {
        if (err) {
          callback(err, [])
          return
        } else if (response.statusCode !== 200) {
          err = new Error(response.body)
          callback(err, [])
          return
        }
  
        try {
          const r = jsonParser.parse(response.body, this.parent)
          if (r.items !== null && r.items !== undefined) {
            if (r.ok === true) {
              writes = data.length
            } else {
              r.items.forEach(item => {
                if (item.index.status < 400) {
                  writes++
                } else {
                  console.error(item.index)
                }
              })
            }
          }
        } catch (e) { return callback(e) }
  
        this.reindex(err => callback(err, writes))
      })
    }
  
    del (elem, callback) {
      const thisUrl = `${this.base.host}/${encodeURIComponent(elem._index)}/${encodeURIComponent(elem._type)}/${encodeURIComponent(elem._id)}`
  
      this.parent.emit('debug', `deleteUrl: ${thisUrl}`)
      const esRequest = {
        url: thisUrl,
        method: 'DELETE'
      }
      aws4signer(esRequest, this.parent)
  
      this.baseRequest(esRequest, (err, response, body) => {
        if (typeof callback === 'function') {
          callback(err, response, body)
        }
      })
    }
  
    reindex (callback) {
      if (this.parent.options.noRefresh) {
        callback()
      } else {
        const esRequest = {
          url: `${this.base.url}/_refresh`,
          method: 'POST'
        }
        aws4signer(esRequest, this.parent)
  
        this.baseRequest(esRequest, (err, response) => {
          callback(err, response)
        })
      }
    }
  }
  
  module.exports = {
    elasticsearch
  }
  
  // ///////////
  // HELPERS //
  // ///////////
  
  /**
   * Posts requests to the _search api to fetch the latest
   * scan result with scroll id
   * @param self
   * @param callback
   * @param loadedHits
   * @param response
   */
  const scrollResultSet = (self, callback, loadedHits, response) => {
    let body
  
    if (loadedHits && loadedHits.length > 0) {
      // are we skipping and we have hits?
      if (self.elementsToSkip > 0) {
        while (loadedHits.length > 0 && self.elementsToSkip > 0) {
          loadedHits.splice(0, 1)
          self.elementsToSkip--
        }
      }
  
      if (loadedHits.length > 0) {
        if (self.parent.options.delete === true) {
          var started = 0
          loadedHits.forEach(elem => {
            started++
            self.del(elem, () => {
              started--
              if (started === 0) {
                self.reindex(err => callback(err, loadedHits, response))
              }
            })
          })
        } else {
          return callback(null, loadedHits, response)
        }
      } else {
        return scrollResultSet(self, callback)
      }
    } else {
      const scrollRequest = {
        uri: `${self.base.host}/_search/scroll`,
        method: 'POST'
      }
  
      var { awsChain, awsAccessKeyId, awsIniFileProfile } = self.parent.options
  
      if (awsChain || awsAccessKeyId || awsIniFileProfile) {
        Object.assign(scrollRequest, {
          uri: `${scrollRequest.uri}?scroll=${self.parent.options.scrollTime}`,
          body: jsonParser.stringify({
            scroll_id: self.lastScrollId
          }),
          method: 'GET'
        })
      } else if (self.ESversion === '1') {
        // body based parameters were added in 2.0.0
        // scroll_id needs to be sent raw (base64 encoded)
        Object.assign(scrollRequest, {
          uri: `${scrollRequest.uri}?scroll=${self.parent.options.scrollTime}`,
          body: self.lastScrollId
        })
      } else {
        Object.assign(scrollRequest, {
          body: jsonParser.stringify({
            scroll: self.parent.options.scrollTime,
            scroll_id: self.lastScrollId
          })
        })
      }
  
      aws4signer(scrollRequest, self.parent)
  
      self.baseRequest(scrollRequest, (err, response) => {
        if (err) {
          callback(err, [])
          return
        }
  
        self.parent.emit('debug', `scrollRequest: ${jsonParser.stringify(scrollRequest)}`)
        self.parent.emit('debug', `body: ${jsonParser.stringify(response.body)}`)
  
        if (err === null && response.statusCode !== 200) {
          err = new Error(response.body)
          callback(err, [])
          return
        }
  
        try {
          body = jsonParser.parse(response.body, self.parent)
        } catch (e) {
          e.message = `${e.message} | Cannot Parse: ${response.body}`
          callback(e, [])
          return
        }
  
        self.lastScrollId = body._scroll_id
        const hits = body.hits.hits
  
        if (self.parent.options.delete === true && hits.length > 0) {
          let started = 0
          hits.forEach(elem => {
            started++
            self.del(elem, () => {
              started--
              if (started === 0) {
                self.reindex(err => {
                  if (hits.length === 0) {
                    self.lastScrollId = null
                  }
                  callback(err, hits)
                })
              }
            })
          })
        } else {
          if (hits.length === 0) {
            self.lastScrollId = null
          }
  
          // are we skipping and we have hits?
          if (self.elementsToSkip > 0 && hits.length > 0) {
            while (hits.length > 0 && self.elementsToSkip > 0) {
              hits.splice(0, 1)
              self.elementsToSkip--
            }
  
            if (hits.length > 0) {
              // we have some hits after skipping, lets callback
              return callback(err, hits)
            } else {
              // we skipped, but now we don't have any hits,
              // scroll again for more data if possible
              return scrollResultSet(self, callback)
            }
          } else {
            // not skipping or done skipping
            return callback(err, hits)
          }
        }
      })
    }
  }
  