Protiviti / SharePoint Blog

SharePoint Blog

January 20
Get Accurate Search Results from SharePoint 2013
If you have ever tried to customize SharePoint search results, specifically pagination, you surely will have noticed the phrase “About X results”.  This is good enough if you want to see approximately how many results you get back or how your query affected those results, but say you want to know exactly how many results you have.  This is how I figured out how to get the exact number from SharePoint. 
I was pointed in the direction of this blog post by Moudhafer Salhi http://moudhafersalhi.com/2014/04/24/search-results-web-part-exact-pagination/.  In this post are a few lines of Javascript which are supposed to give you the exact counts:
    var resultsPerPage = ctx.DataProvider.get_resultsPerPage();
    var totalRows = ctx.DataProvider.get_totalRows();
    var nbPages = Math.round(totalRows / resultsPerPage);
    var exactCount = Srch.Res.rs_ResultCount;
The problem is “exactCount” doesn’t actually give you an exact count most of the time.  It is still an estimation.  Worst yet is trying to apply a “Last Page” link in pagination based on the “nbPages” variable which is an estimation of an estimation.  What you will find is your “Last Page” link often takes one or more pages from your actual last page.  Through experimentation I discovered that the real control for “which result does the page start on and therefore which page” is #s=X where X is the starting item for that page.  This goes on the end of the query string in your browser when a pagination link is clicked.  What’s interesting is that if you put in a really high number, something over your total number of results, the “exactCount” variable ends up being your actual total number of results.  So with a little Javascript you can fake the “Last Page” functionality and re-click the link to put your browser on the actual last page.  See below for details.  Some of this I borrowed from Salhi’s post above.
First I initialize a few variables for use later:
    var firstOnPage = 0;
    var lastOnPage = 0;
    var currentPage = 1;
    var resultsPerPage = ctx.DataProvider.get_resultsPerPage();
    var start = ctx.DataProvider.get_currentQueryState().s;
    var totalRows = ctx.DataProvider.get_totalRows();               
    var nbPages = Math.ceil(totalRows / resultsPerPage);
Then I find the current page:
    for(var i=0;i < pagingInfo.length ; i++){
        if(pagingInfo[i].startItem == -1){
             currentPage = pagingInfo[i].pageNumber;
        }
    }                
Then I set firstOnPage and lastOnPage.  firstOnPage is the first result returned, assuming there is a result on the page we are trying to retrieve.  lastOnPage is assumed to be the last result on the page.  This is always going to be correct because we are asking SharePoint for a page that is beyond the size of the result set.  This forces SharePoint to look at every result:
    var pagingMin = currentPage < 5 ? 1 : currentPage - 4;
    var pagingMax = pagingMin + 8 < nbPages ? pagingMin + 8 : nbPages;                       
    firstOnPage = ((currentPage - 1) * resultsPerPage) + 1;
    lastOnPage = ((currentPage) * resultsPerPage) < totalRows ? ((currentPage) * resultsPerPage) : totalRows;
Finally I check if the “Last Page” link exists and if it does check if the assumed first result is greater than the actual last result on the page.  If so re-click “Last Page” to put the user on the actual last page:
    if (jQuery('#PageLinkLast').length && firstOnPage > lastOnPage) {
        jQuery('#PageLinkLast').click();
    }
This is all fine and dandy for getting to the actual last page but it requires going to that last page to accurately get a result count.  What if we wanted to know how many results there are ahead of time, say when we get to the first result page?  After some thought I decided to do essentially what I just did above except with Ajax.  I tried requesting the results page with #s=100000 with the below Javascript.
The first part of this code adds a section to the “ResultHeader” element on the page.  This section is formatted “X-Y of Z result” so “1-10 of 14 results” for example.
if (totalRows != 0){
    if (lastOnPage != totalRows)
       jQuery('#ResultHeader').before('<div id="ResultCount">' + firstOnPage + '-' + lastOnPage + ' of <span class="spanTotalResults"></span> results </div>');                               
    else
        jQuery('#ResultHeader').before('<div id="ResultCount">' + firstOnPage + '-<span class="spanTotalResults"></span> of <span class="spanTotalResults"></span> results </div>');                               
}
The second part takes the current page url from the browser location, strips off the hash (leaving the query), then makes an Ajax request with that url and the new #s=1000000.  This is a fairly heavy request because it is essentially a request for the same page twice, even though we only want a small piece of information found at the very bottom in “TotalRows:”.  Regardless we have that bit of information and we now apply it to our “spanTotalResults” spans.
var strippedLoc = window.location.href.replace(window.location.hash, '');
jQuery.get(strippedLoc + '#s=1000000', function( data ) {
    var res = data.substring(data.indexOf(',"TotalRows":') + ',"TotalRows":'.length);
    var res = res.substring(0, res.indexOf(','))
    jQuery('span.spanTotalResults').text(res);                               
});
Great!  Now we finally have an exact result count without having to go to the last page of results!  The only problem is SharePoint sometimes, for whatever reason, still returns the incorrect count.  On the site where I developed this I noticed with some queries I had a higher count, rather than the lower estimation SharePoint provides, with this method.  I was unable to figure out why this was the case.  I had 14 results on the page, but the second request always returned a count of 15.  I verified this with a search query tool.  I was able to run the same query through it and retrieved a count of 14 so something strange was going on when requesting the results page.  I ended up getting the exact number of search results every time by going directly to the search service API (/_api/search/query).  But how exactly do we get the query to execute within the service dynamically?  I thought to myself “There must be some place on the page where the query is stored.  There just has to be.”  After poking around in the page source for a little I noticed at the bottom of the page there is a script block with a single variable in it named “ctl00_ctl107_g_c072bf9b_25ad_4b35_9b29_5db9e77968f1_ctl00results”, yours will be something similar.  This variable contains the query used by the results page in the “QueryModification” property found in the location below:
ctl00_ctl107_g_c072bf9b_25ad_4b35_9b29_5db9e77968f1_ctl00results.ResultTables[0].Properties.QueryModification
Knowing that Javascript variables are often available within the context of the “window” I gave it a shot.  Sure enough I was able to access the variable as follows:
window[‘ctl00_ctl107_g_c072bf9b_25ad_4b35_9b29_5db9e77968f1_ctl00results’]
But how do we access this dynamically, at run time?  Further scanning of the page source revealed a <div> surrounded by a <noscript> tag called “ctl00_ctl107_g_c072bf9b_25ad_4b35_9b29_5db9e77968f1_ctl00_csr”, which is essentially the same as the property I’m looking for, just with a different suffix.  This <div> tag is the parent of “ms-srch-dataProvider”.  So now that we have a way to retrieve the value of the query from the window’s Javascript variable let’s put it all together:
I utilized the “setTimeout()” function called from “countTimeout()” because the “ms-srch-dataProvider” <div> is not guaranteed to exist right as the page loads. 
if (totalRows != 0){
    if (lastOnPage != totalRows)
       jQuery('#ResultHeader').before('<div id="ResultCount">' + firstOnPage + '-' + lastOnPage + ' of <span class="spanTotalResults"></span> results</div>');                               
    else
        jQuery('#ResultHeader').before('<div id="ResultCount">' + firstOnPage + '-<span class="spanTotalResults"></span> of <span class="spanTotalResults"></span> results</div>');                               
}                                                   
countTimeout();
   
Much of the code remains the same as above.  The only different parts are retrieving the query from the “QueryModification” property, writing the url of the search service with “window.location.protocol + ‘//’ + window.location.host”, parsing the xml result from the service call, and lastly rewriting parts of the “Last Page” link when the actual result count is greater than the estimated result count.

function search(webUrl, queryText, rowLimit, startRow)
{
                var url = webUrl + "/_api/search/query?clienttype='ContentSearchRegular'&rowlimit=" + rowLimit + "&startrow=" + startRow + "&querytext='" + queryText + "'";

                jQuery.get(url, function(data) {
                                var xml = jQuery.parseXML(data);
                                var val = parseInt(jQuery(xml).find('d\\:RowCount, RowCount').text())
                                var total = parseInt(jQuery(xml).find('d\\:TotalRows, TotalRows').text())

                                if (total > startRow + val)
                                {
                                                search(webUrl, queryText, rowLimit, startRow + val);
                                }
                                else
                                {
                                                jQuery('span.spanTotalResults').text(total);
                                                var title = jQuery('#PageLinkLast').attr('title');
                                                var intVal = parseInt(total);
                                                var intTitle = parseInt(title);
                                                if (intVal && intTitle && (intVal > intTitle))
                                                {
                                                                jQuery('#PageLinkLast').attr('title', (intTitle + 10));
                                                                jQuery('#PageLinkLast').attr('onclick', '$getClientControl(this).page(' + (intTitle + 10) + ');return Srch.U.cancelEvent(event);');
                                                }             
                                }
                }, 'text');
}


function countTimeout() {
                setTimeout(function () {
                                if (jQuery('.ms-srch-dataProvider')) {
                                                var totalRowsInit = window[jQuery('.ms-srch-dataProvider').parent().attr('id').replace('_csr', 'results')].ResultTables[0].TotalRows
                                                var query = window[jQuery('.ms-srch-dataProvider').parent().attr('id').replace('_csr', 'results')].ResultTables[0].Properties.QueryModification;
                                                search(window.location.protocol + "//" + window.location.host, query, 500, totalRowsInit);
                                                return stop();                                                           
                                }
                                countTimeout();
                }, 500);
}
This is used to cancel the “setTimeout()” after it has run through once successfully.
function stop() {
    try {
        if (timer) {
            clearTimeout(timer);
            timer = 0;
        }
    }
    catch (err) {}
}
There it is. The result was less network traffic and a more responsive browser for the user as well as the correct result count.  If anyone has questions, comments, or suggestions please do not hesitate to reach out.


Quick Launch


© Protiviti 2019. All rights reserved.   |   Privacy Policy