/// <summary>
        /// Execute bulk index, create, update, or delete actions. This version uses explicit action parameters
        /// for each action to be executed.
        /// </summary>
        /// <param name="index">The index and type.</param>
        /// <param name="requests">
        /// The documents, if <see cref="BulkActionType.Index"/> or <see cref="BulkActionType.Create"/>
        /// is specified, this is the source document to index or create, if <see cref="BulkActionType.Update"/>
        /// is specified this should be an update statement, and if <see cref="BulkActionType.Delete"/> is specified
        /// this is either the ID or a document which implements <see cref="IKeyDocument"/>.
        /// </param>
        /// <param name="options">Any options for the request such as 'refresh'.</param>
        /// <param name="throwOnFailure">
        /// If true, an exception will be thrown if any of the requested actions fail. (Normally the _bulk
        /// API endpoint returns a 200 if the request was processed successfully, even if document actions fail.)
        /// </param>
        /// <param name="cancel">A cancellation token for the request.</param>
        /// <returns></returns>
        /// <exception cref="ArgumentNullException">An index is required - index</exception>
        public Task <BulkActionResult> BulkActionAsync(string index, IEnumerable <BulkActionRequest> requests, object options = null, bool throwOnFailure = true, CancellationToken cancel = default(CancellationToken))
        {
            // No punishment for providing null/empty requests
            if (requests?.Any() != true)
            {
                return(Task.FromResult(new BulkActionResult
                {
                    Took = TimeSpan.Zero,
                    HasErrors = false,
                    Items = Array.Empty <BulkActionResultItem>()
                }));
            }

            var request = new StringBuilder();

            foreach (var requestItem in requests)
            {
                request.Append(JsonConvert.SerializeObject(requestItem, _jsonSettings));
            }

            var path       = String.IsNullOrEmpty(index) ? "_bulk" : $"{index}/_bulk";
            var requestUri = new Uri(_hostProvider.Next() + $"{path}{QueryStringParser.GetQueryString(options)}");

            return(MakeRequestAsync <BulkActionResult>(
                       HttpMethod.Post,
                       requestUri,
                       request.ToString(),
                       MediaTypes.ApplicationNewlineDelimittedJson,
                       cancel,
                       "bulk_action"));
        }
        /// <summary>
        /// Execute bulk index, create, update, or delete actions. This is the simpler version which
        /// assumes you are only performing a single action type on a single index and document type.
        /// </summary>
        /// <param name="index">The index and type.</param>
        /// <param name="type">The document type.</param>
        /// <param name="actionType">Type of the action.</param>
        /// <param name="documents">The documents, if <see cref="BulkActionType.Index" /> or <see cref="BulkActionType.Create" />
        /// is specified, this is the source document to index or create, if <see cref="BulkActionType.Update" />
        /// is specified this should be an update statement, and if <see cref="BulkActionType.Delete" /> is specified
        /// this is either the ID or a document which implements <see cref="IKeyDocument" />.</param>
        /// <param name="options">Any options for the request such as 'refresh'.</param>
        /// <param name="throwOnFailure">If true, an exception will be thrown if any of the requested actions fail. (Normally the _bulk
        /// API endpoint returns a 200 if the request was processed successfully, even if document actions fail.)</param>
        /// <param name="cancel">A cancellation token for the request.</param>
        /// <returns></returns>
        public Task <BulkActionResult> BulkActionAsync(string index, string type, BulkActionType actionType, IEnumerable <object> documents, object options = null, bool throwOnFailure = true, CancellationToken cancel = default(CancellationToken))
        {
            if (index == null)
            {
                throw new ArgumentNullException("An index is required", nameof(index));
            }

            // No punishment for providing null/empty requests
            if (documents?.Any() != true)
            {
                return(Task.FromResult(new BulkActionResult
                {
                    Took = TimeSpan.Zero,
                    HasErrors = false,
                    Items = Array.Empty <BulkActionResultItem>()
                }));
            }

            var actionName = actionType.ToString().ToLower();
            var request    = new StringBuilder();

            foreach (var document in documents)
            {
                if (actionType == BulkActionType.Delete)
                {
                    var key         = (document as IKeyDocument)?.Key ?? document ?? throw new ArgumentException("No key was provided for delete operation", nameof(documents));
                    var requestItem = new BulkActionRequest(actionType)
                    {
                        ID = key
                    };
                    request.Append(JsonConvert.SerializeObject(requestItem, _jsonSettings));
                }
                else
                {
                    var requestItem = new BulkActionRequest(actionType)
                    {
                        ID       = (document as IKeyDocument)?.Key,
                        Document = document
                    };
                    request.Append(JsonConvert.SerializeObject(requestItem, _jsonSettings));
                }
            }

            var requestUri = new Uri(_hostProvider.Next() + $"{index}/{type}/_bulk{QueryStringParser.GetQueryString(options)}");

            return(MakeRequestAsync <BulkActionResult>(
                       HttpMethod.Post,
                       requestUri,
                       request.ToString(),
                       MediaTypes.ApplicationNewlineDelimittedJson,
                       cancel,
                       "bulk_action"));
        }
        /// <summary>
        /// Executes a _search with the specified parameters.
        /// </summary>
        /// <typeparam name="TSource">
        /// The index type model to use, this should support the mapping from the '_source'
        /// document object.
        /// </typeparam>
        /// <param name="index">The index (or indexes or index pattern) to search.</param>
        /// <param name="query">The query object.</param>
        /// <param name="options">The options for the search query.</param>
        /// <param name="cancel">A cancellation token for the request.</param>
        /// <returns>The result of the requested search.</returns>
        public async Task <SearchResult <TSource> > SearchAsync <TSource>(string index, object query = null, object options = null, CancellationToken cancel = default(CancellationToken))
        {
            var requestUri = new Uri(_hostProvider.Next() + $"{index}/_search{QueryStringParser.GetQueryString(options)}");

            SearchResponse <TSource> result;

            if (query == null)
            {
                result = await MakeRequestAsync <SearchResponse <TSource> >(
                    HttpMethod.Get,
                    requestUri,
                    cancel,
                    "search");
            }
            else
            {
                result = await MakeRequestAsync <SearchResponse <TSource> >(
                    HttpMethod.Post,
                    requestUri,
                    JsonConvert.SerializeObject(query, _jsonSettings),
                    MediaTypes.ApplicationJson,
                    cancel,
                    "search");
            }


            return(new SearchResult <TSource>(
                       hits: result.Hits.Hits.Select(h =>
            {
                if (h.Source is IScoreDocument scoreDoc)
                {
                    scoreDoc.Score = h.Score;
                }

                if (h.Source is IKeyDocument keyDoc)
                {
                    keyDoc.Key = h.ID;
                }

                return h.Source;
            }),
                       total: result.Hits.Total,
                       aggregations: result.Aggregations,
                       suggestions: null
                       ));
        }
        /// <summary>
        /// Executes a get document by ID request.
        /// </summary>
        /// <typeparam name="TSource">
        /// The index type model to use, this should support the mapping from the '_source'
        /// document object.
        /// </typeparam>
        /// <param name="index">The index to search.</param>
        /// <param name="document">The document type to return.</param>
        /// <param name="id">The document  ID.</param>
        /// <param name="options">The options for the search query.</param>
        /// <param name="cancel">A cancellation token for the request.</param>
        /// <returns>Result of the get request.</returns>
        public Task <TSource> GetSourceAsync <TSource>(
            string index,
            string document,
            string id,
            object options           = null,
            CancellationToken cancel = default(CancellationToken))
        {
            var requestUri = new Uri(_hostProvider.Next() + $"{index}/{document}/{id}/_source{QueryStringParser.GetQueryString(options)}");

            return(MakeRequestAsync <TSource>(HttpMethod.Get, requestUri, cancel, "get_source"));
        }