0

I am running a query against my Cosmos db instance, and I am occasionally getting 0 results back, when I know that I should be getting some results.

        var options = new QueryRequestOptions()
        {
            MaxItemCount = 25
        };
        var query = @"
                    select c.id,c.callTime,c.direction,c.action,c.result,c.duration,c.hasR,c.hasV,c.callersIndexed,c.callers,c.files
                    from c
                    where
                    c.ownerId=@ownerId
                    and c.callTime>=@dateFrom
                    and c.callTime<=@dateTo
                    and (CONTAINS(c.phoneNums_s, @name)
                    or CONTAINS(c.names_s, @name)
                    or CONTAINS(c.xNums_s, @name))
                    order by c.callTime desc";
        var queryIterator = container.GetItemQueryIterator<CallIndex>(new QueryDefinition(query)
        .WithParameter("@ownerId", "62371255008")
        .WithParameter("@name", "harr")
        .WithParameter("@dateFrom", dateFrom) // 5/30/2020 5:00:00 AM +00:00
        .WithParameter("@dateTo", dateTo) // 8/29/2020 4:59:59 AM +00:00
        .WithParameter("@xnum", null), requestOptions: options, continuationToken: null);
        if (queryIterator.HasMoreResults)
        {
            var feed = queryIterator.ReadNextAsync().Result;
            model.calls = feed.ToList(); //feed.Resource is empty; feed.Count is 0;
            model.CosmosContinuationToken = feed.ContinuationToken; //feed.ContinuationToken is populated with a large token value, indicating that there are more results, even though this fetch returned 0 items.
            model.TotalRecords = feed.Count(); // 0
        }

As you can see, even though I received 0 results, the continuation token indicates that there is more data there after this first request. And, after visually inspecting the data directly in the database (data explorer in the Azure portal), I see records that should match, but they are not found in this query. To further test, I ran the same exact query a few seconds later, and received results:

        var query = @"
                    select c.id,c.callTime,c.direction,c.action,c.result,c.duration,c.hasR,c.hasV,c.callersIndexed,c.callers,c.files
                    from c
                    where
                    c.ownerId=@ownerId
                    and c.callTime>=@dateFrom
                    and c.callTime<=@dateTo
                    and (CONTAINS(c.phoneNums_s, @name)
                    or CONTAINS(c.names_s, @name)
                    or CONTAINS(c.xNums_s, @name))
                    order by c.callTime desc";
        var queryIterator = container.GetItemQueryIterator<CallIndex>(new QueryDefinition(query)
        .WithParameter("@ownerId", "62371255008")
        .WithParameter("@name", "harr")
        .WithParameter("@dateFrom", dateFrom) // 5/30/2020 5:00:00 AM +00:00
        .WithParameter("@dateTo", dateTo) // 8/29/2020 4:59:59 AM +00:00
        .WithParameter("@xnum", null), requestOptions: options, continuationToken: null);
        if (queryIterator.HasMoreResults)
        {
            var feed = queryIterator.ReadNextAsync().Result;
            model.calls = feed.ToList(); //feed.Resource has 25 items; feed.Count is 25;
            model.CosmosContinuationToken = feed.ContinuationToken; //feed.ContinuationToken is populated, but it is considerably smaller than the token I received from the first request.
            model.TotalRecords = feed.Count(); // 25
        }

This is the exact query as before, but this time the feed gave me the results I expected. This has happened more than once, and continues to happen intermittently. What gives with this? Is this a bug in Azure Cosmos? If so, it seems like a serious bug that breaks the very core functionality of Cosmos (and databases in general).

Or, is this expected? Is it possible that in the first query, I need to continue to ReadNextAsync until I get some results back using the continuation token?

Any help is appreciated, as this is breaking very basic functionality in my app.

Also, I would like to add that the data returned from the query has not been newly added between the times of my first query attempt, and my second query attempt. That data has been there for a while.

José Pedro
  • 1,097
  • 3
  • 14
  • 24
Matt Spinks
  • 6,380
  • 3
  • 28
  • 47
  • This is just a guess based on what caused me a problem in the past. Can you set EnableCrossPartitionQuery to true and see if it helps? https://learn.microsoft.com/en-us/dotnet/api/microsoft.azure.documents.client.feedoptions.enablecrosspartitionquery?view=azure-dotnet – Tomasz Bartoszewski Aug 28 '20 at 21:18
  • That does seem odd, but is there a reason you don't want to iterate the iterator? Every implementation I've seen has you in a while loop until no more results, not an if. – Noah Stahl Aug 28 '20 at 22:04
  • Also, shouldn't you be `await`-ing the ReadNextAsync, rather than getting `.Result`? – Noah Stahl Aug 28 '20 at 22:06
  • Hi Noah, I'm not iterating over all results because I am displaying only 25 results to the user at a time. This particular query will return millions of documents. I store the continuation token to allow the user to click and retrieve the next "page" of results. – Matt Spinks Aug 28 '20 at 22:30
  • OK. The description for MaxItemCount does state "Query can return 0 items in the page", so I'm not sure you can count on the behavior you're expecting. Hopefully the SDK team can chime in. – Noah Stahl Aug 28 '20 at 23:51
  • Hmmm, that's interesting. Even still, the results feed is empty too. – Matt Spinks Aug 29 '20 at 00:11
  • If you change from `.Result` to `await`, does this still occur? See https://stackoverflow.com/questions/24623120/await-on-a-completed-task-same-as-task-result – Noah Stahl Aug 29 '20 at 17:37
  • As @Noah Stahl said above in comments, pls convert `.Result` to `await` and also addittionally add `ConfigureAwait(false)` and then check. Also, what is the data type of `dateFrom` and `dateTo`? – Harshita Singh Aug 30 '20 at 09:42

2 Answers2

2

Your code is correct, you are expected to drain the query checking HasMoreResults (although I would change the .Result with await to avoid a possible deadlock). What can happen in cross-partition queries is that you could get some empty page if the initial partitions checked for results have none.

Sometimes queries may have empty pages even when there are results on a future page. Reasons for this could be:

  • The SDK could be doing multiple network calls.
  • The query might be taking a long time to retrieve the documents.

Reference: https://learn.microsoft.com/azure/cosmos-db/troubleshoot-query-performance#common-sdk-issues

Matias Quaranta
  • 13,907
  • 1
  • 22
  • 47
  • Thanks @Matias. I believe this answers my question. After further diving into the docs (including your reference), I found that this is expected behavior. I needed to adjust my code to accommodate the case when 0 results are returned. In my case, I modified the code so that the user receives a visual indicator that "there are more results" coming, and that the query is still running in the background. And that seems to work really well. Thanks for the answer. – Matt Spinks Aug 31 '20 at 11:44
0

Try using below code:

Query Cosmos DB method:

public async Task<DocDbQueryResult> QueryCollectionBaseWithPagingInternalAsync(FeedOptions feedOptions, string queryString, IDictionary<string, object> queryParams, string collectionName)
        {
            string continuationToken = feedOptions.RequestContinuation;
            List<JObject> documents = new List<JObject>();
            IDictionary<string, object> properties = new Dictionary<string, object>();
            int executionCount = 0;
            double requestCharge = default(double);
            double totalRequestCharge = default(double);

            do
            {
                feedOptions.RequestContinuation = continuationToken;
                var query = this.documentDbClient.CreateDocumentQuery<JObject>(
                    UriFactory.CreateDocumentCollectionUri(this.databaseName, collectionName),
                    new SqlQuerySpec
                    {
                        QueryText = queryString,
                        Parameters = ToSqlQueryParamterCollection(queryParams),
                    },
                    feedOptions)
                .AsDocumentQuery();

                var response = await query.ExecuteNextAsync<JObject>().ConfigureAwait(false);
                documents.AddRange(response.AsEnumerable());
                executionCount++;
                requestCharge = executionCount == 1 ? response.RequestCharge : requestCharge;
                totalRequestCharge += response.RequestCharge;
                continuationToken = response.ResponseContinuation;
            }
            while (!string.IsNullOrWhiteSpace(continuationToken) && documents.Count < feedOptions.MaxItemCount);

            var pagedDocuments = documents.Take(feedOptions.MaxItemCount.Value);
            var result = new DocDbQueryResult
            {
                ResultSet = new JArray(pagedDocuments),
                TotalResults = Convert.ToInt32(pagedDocuments.Count()),
                ContinuationToken = continuationToken
            };

            // if query params are not null, use existing query params also to be passed as properties.
            if (queryParams != null)
            {
                properties = queryParams;
            }

            properties.Add("TotalRequestCharge", totalRequestCharge);
            properties.Add("ExecutionCount", executionCount);

            return result;
        }

ToSqlQueryParamterCollection method:

private static SqlParameterCollection ToSqlQueryParamtereCollection(IDictionary<string, object> queryParams)
        {
            var coll = new SqlParameterCollection();
            if (queryParams != null)
            {
                foreach (var paramKey in queryParams.Keys)
                {
                    coll.Add(new SqlParameter(paramKey, queryParams[paramKey]));
                }
            }

            return coll;
        }
Harshita Singh
  • 4,590
  • 1
  • 10
  • 13
  • 2
    It is expected to explain your answer. Not just "use this code" but also why ... – derpirscher Aug 29 '20 at 10:40
  • This is because, I have not used the functionality's implementation like yours, but I have used this, and it worked for me really well and it was reliable. Also, this method is SQL injection attack free because the way parameters are being passed using `ToSqlQueryParamtereCollection`. – Harshita Singh Aug 29 '20 at 10:46
  • 1
    The issue is that your answer doesn't address the OP's question. It's a new implementation. Even if the OP switched to using your code, they wouldn't ever know what the issue is with their existing code, and their question is left unanswered. – David Makogon Aug 29 '20 at 14:07
  • Yup, thanks for the inputs @David Makogon. I will debug the OP's code as well and get back with a fix to the code. – Harshita Singh Aug 29 '20 at 14:41
  • As @Noah Stahl said above in comments, pls convert `.Result` to `await` and also addittionally add `ConfigureAwait(false)` and then check. Also, what is the data type of `dateFrom` and `dateTo`? – Harshita Singh Aug 30 '20 at 09:44