/// <summary>
        /// Gets the first 4 videos related to the specified video. Does not support paging.
        /// </summary>
        public override async Task <GetRelatedVideosResponse> GetRelatedVideos(GetRelatedVideosRequest request, ServerCallContext context)
        {
            // Lookup the tags for the video
            PreparedStatement tagsForVideoPrepared = await _statementCache.GetOrAddAsync("SELECT tags FROM videos WHERE videoid = ?");

            BoundStatement tagsForVideoBound = tagsForVideoPrepared.Bind(request.VideoId.ToGuid());
            RowSet         tagRows           = await _session.ExecuteAsync(tagsForVideoBound).ConfigureAwait(false);

            // Start with an empty response
            var response = new GetRelatedVideosResponse {
                VideoId = request.VideoId
            };

            Row tagRow = tagRows.SingleOrDefault();

            if (tagRow == null)
            {
                return(response);
            }

            var tagsValue = tagRow.GetValue <IEnumerable <string> >("tags");
            var tags      = tagsValue?.ToList() ?? new List <string>();

            // If there are no tags, we can't find related videos
            if (tags.Count == 0)
            {
                return(response);
            }

            var relatedVideos = new Dictionary <Uuid, SuggestedVideoPreview>();
            PreparedStatement videosForTagPrepared = await _statementCache.GetOrAddAsync("SELECT * FROM videos_by_tag WHERE tag = ? LIMIT ?");

            var inFlightQueries = new List <Task <RowSet> >();

            for (var i = 0; i < tags.Count; i++)
            {
                // Use the number of results we ultimately want * 2 when querying so that we can account for potentially having to filter
                // out the video Id we're using as the basis for the query as well as duplicates
                const int pageSize = RelatedVideosToReturn * 2;

                // Kick off a query for each tag and track them in the inflight requests list
                string     tag   = tags[i];
                IStatement query = videosForTagPrepared.Bind(tag, pageSize);
                inFlightQueries.Add(_session.ExecuteAsync(query));

                // Every third query, or if this is the last tag, wait on all the query results
                if (inFlightQueries.Count == 3 || i == tags.Count - 1)
                {
                    RowSet[] results = await Task.WhenAll(inFlightQueries).ConfigureAwait(false);

                    foreach (RowSet rowSet in results)
                    {
                        foreach (Row row in rowSet)
                        {
                            SuggestedVideoPreview preview = MapRowToVideoPreview(row);

                            // Skip self
                            if (preview.VideoId.Equals(request.VideoId))
                            {
                                continue;
                            }

                            // Skip videos we already have in the results
                            if (relatedVideos.ContainsKey(preview.VideoId))
                            {
                                continue;
                            }

                            // Add to results
                            relatedVideos.Add(preview.VideoId, preview);

                            // If we've got enough, no reason to continue
                            if (relatedVideos.Count >= RelatedVideosToReturn)
                            {
                                break;
                            }
                        }

                        // If we've got enough, no reason to continue
                        if (relatedVideos.Count >= RelatedVideosToReturn)
                        {
                            break;
                        }
                    }

                    // See if we've got enough results now to fulfill our requirement
                    if (relatedVideos.Count >= RelatedVideosToReturn)
                    {
                        break;
                    }

                    // We don't have enough yet, so reset the inflight requests to allow another batch of tags to be queried
                    inFlightQueries.Clear();
                }
            }

            // Add any videos found to the response and return
            response.Videos.Add(relatedVideos.Values);
            return(response);
        }
        /// <summary>
        /// Gets the first 5 videos related to the specified video.
        /// </summary>
        public override async Task <GetRelatedVideosResponse> GetRelatedVideos(GetRelatedVideosRequest request, ServerCallContext context)
        {
            // Get REST client for dse-search service
            Uri searchUri = await GetDseSearchUri().ConfigureAwait(false);

            IRestClient restClient = _createRestClient(searchUri);

            // Example request: http://127.0.2.15:8983/solr/killrvideo.videos/mlt?q=videoid%3Asome-uuid&wt=json&indent=true&qt=mlt&mlt.fl=name&mlt.mindf=1&mlt.mintf=1
            var solrRequest = new RestRequest("killrvideo.videos/mlt");

            solrRequest.AddParameter("q", $"videoid:\"{request.VideoId.Value}\"");
            solrRequest.AddParameter("wt", "json");

            // Paging information
            int start;

            if (request.PagingState == null || int.TryParse(request.PagingState, out start) == false)
            {
                start = 0;
            }

            solrRequest.AddParameter("start", start);
            solrRequest.AddParameter("rows", request.PageSize);

            //MLT Fields to Consider
            solrRequest.AddParameter("mlt.fl", "name,description,tags");

            //MLT Minimum Document Frequency - the frequency at which words will be ignored which do not occur in at least this many docs.
            solrRequest.AddParameter("mlt.mindf", 2);

            //MLT Minimum Term Frequency - the frequency below which terms will be ignored in the source doc.
            solrRequest.AddParameter("mlt.mintf", 2);

            IRestResponse <MLTQueryResult> solrResponse = await restClient.ExecuteTaskAsync <MLTQueryResult>(solrRequest).ConfigureAwait(false);

            // Start with an empty response
            var response = new GetRelatedVideosResponse {
                VideoId = request.VideoId
            };

            // Check for network/timeout errors
            if (solrResponse.ResponseStatus != ResponseStatus.Completed)
            {
                // TODO: Logger.Error(response.ErrorException, "Error while querying Solr video suggestions from {host} for {query}", nodeIp, query);
                return(response);
            }

            // Check for HTTP error codes
            if (solrResponse.StatusCode != HttpStatusCode.OK)
            {
                // TODO: Logger.Error("HTTP status code {code} while querying Solr video suggestions from {host} for {query}", (int) response.StatusCode, nodeIp, query);
                return(response);
            }

            // Success
            int    nextPageStartIndex = solrResponse.Data.Response.Start + solrResponse.Data.Response.Docs.Count;
            string pagingState        = nextPageStartIndex == solrResponse.Data.Response.NumFound ? "" : nextPageStartIndex.ToString();

            response.PagingState = pagingState;
            response.Videos.Add(solrResponse.Data.Response.Docs.Select(doc => new SuggestedVideoPreview
            {
                VideoId              = doc.VideoId.ToUuid(),
                AddedDate            = doc.AddedDate.ToTimestamp(),
                Name                 = doc.Name,
                PreviewImageLocation = doc.PreviewImageLocation,
                UserId               = doc.UserId.ToUuid()
            }));
            return(response);
        }