Building a Search-Driven SharePoint App (I)–Setting a Foundation

At SharePoint Conference 2014 a few months ago, a good friend and colleague of mine did a presentation about how to build search-driven SharePoint apps. This topic remains interesting and relevant but I am starting to think we didn’t quite present it correctly. Our sad score reflects that.

In the next few posts, I plan to do with a blog what I apparently couldn’t do while presenting to 500 people in Las Vegas: give the reader a foundation for consuming SharePoint search from their SharePoint 2013 (or Online) apps using JavaScript and the SharePoint REST API. The goal of this is not to create a conversation about how to use search in interesting ways. Rather, I want to provide a technical foundation so those that find those interesting ways aren’t starting from scratch. I probably won’t get every last nuance covered so please provide your feedback and questions in comments. And, to reiterate expectations properly, this set of posts will focus on using JavaScript and REST to build a SharePoint-hosted app.

The subject of this first post will be creating some core object model components and getting search results back.

The first thing you need to know  is that there really is no JavaScript Object Model (JSOM) API for search. You, we, have to build that OM around the REST API.

The Search JSOM that we want to build will contain the following:

  • The query parameters accessible to us when submitting a search via the REST API
  • The actions, or methods, we can execute when actually submitting that search
  • The components of the response that we need to render our UI, including refiners, result blocks, etc.

First, I want to start by showing the targeted simplicity of our application logic code that will call this Search JSOM. If we’re building a simple results page, it can be as simple as the following:

 var theQuery = new Query();
theQuery.SPSite = "https://my.sharepoint-site.url";
theQuery.QueryText = $('#searchBox').Value;
 
theQuery.RunSearch(
  function (results) {     alert(results);
 },
   function (error) {     alert(error);
    }
);

Of course, there is some nuance in there which I hope we will make clear in this or subsequent blog posts but as you can see, with an API, we hide a lot of the complexity of the search call and response processing.

Now that we know what our basic search call should look like, let’s start building this Query object in JavaScript by creating a JavaScript file called “SP.Search.js” and adding the following code:

 function Query() {        
 var self = this;        
 self.SPSite = "";             
 self.SearchApiPath = "/_api/search/query"        
 self.QueryText = "";       
 self.SourceId = "";             
 self.SortFields = "";       
 self.SelectProperties = "";            
 self.Refiners = "";       
 self.RefinementFilters = [];            
 self.RankingModelId = "";       
 self.StartRow = 0;       
 self.RowLimit = 10;            
 self.EnableStemming = "true";       
 self.EnablePhonetic = "true";       
 self.EnableFQL = "false";            
 self.EnableQueryRules = "true";            
 self.RequestMethod = "GET";       
 self.RequestContentType = "application/json;odata=verbose";     
  

This code encapsulates the query parameters we can submit to the search engine via the REST API. It’s not 100% exhaustive but it hits the critical query parameters. It also sets some defaults for less-often used parameters.

For a full list of the query parameters available via the REST API, please check out Nadeem Ishqair’s EXCELLENT blog on the topic. I’m not going to go through the effort of explaining each parameter as Nadeem’s blog does that already. However, if you need clarification, please do not hesitate to post to comments.

Once I have the set of parameters to submit to the search engine, I build an HTTP request string to send off to the REST end point. I do that in the BuildQuery method. However, the API client doesn’t really need to know much about this method. This is an abstraction that my API provides but I show it here for your understanding (and feedback):

 self.BuildQuery = function () {            
     var queryExpr = "querytext='" + self.QueryText + "'";            
     queryExpr += "&selectproperties='" + self.SelectProperties + "'";            
     queryExpr += "&refiners='" + self.Refiners + "'";                     
     if (self.SourceId != "") queryExpr += "&sourceid='" + self.SourceId + "'";                     
     if (self.SortFields != null && self.SortFields != "") queryExpr += "&sortlist='" + self.SortFields + "'";                    
     if (self.RefinementFilters.length > 0) {                            
         var refinementStr = "&refinementFilters='";                            
         for (var i = 0; i < self.RefinementFilters.length; i++) {                   
             if (self.RefinementFilters.length > 1 && i == 0) refinementStr += "and(";                   
             if (i > 0) refinementStr += ",";                                    
             refinementStr += self.RefinementFilters[i].PropertyName + ":ends-with(\"" + self.RefinementFilters[i].GetDisplayString(self.RefinementFilters[i].FilterValue) + "\")";               
         }                            
         if (self.RefinementFilters.length > 1) refinementStr += ")";               
         queryExpr += refinementStr + "'";           
     }                    
     queryExpr += "&startrow=" + self.StartRow;
     queryExpr += "&rowlimit=" + self.RowLimit;     
     queryExpr += "&enablefql=" + self.EnableFQL;    
     queryExpr += "&enablequeryrules=" + self.EnableQueryRules;           
     queryExpr += "&enablephonetic=" + self.EnablePhonetic;           
     queryExpr += "&enablestemming=" + self.EnableStemming;                    
     return queryExpr;       
 };

There is quite a bit of logic going on this. I hope the reader can follow most of it. What might be a bit confusing is the 10 lines of code starting with if(self.RefinementFilters.length… . When we submit refinement filters to the search engine as part of a REST call, it must be formatted as a valid FQL expression. This is an odd requirement for a number of reasons, least of which being the fact that the default query text language is KQL. However, it’s required nonetheless and it happens to surface a lot of potential (a topic for another post -- sorry).

Moving on, the next thing we need to do is actually make a call to the REST endpoint. The following code does that using a standard JQuery AJAX request:

 self.RunSearch = function (successFunc, failureFunc) {            
      var queryParamStr = self.BuildQuery();                     
      var searchUrl = self.SPSite + self.SearchApiPath + "?" + queryParamStr;                     
      $.ajax({
                 url: searchUrl,
                 type: self.RequestMethod,
                 contentType: self.RequestContentType,
                headers: {
                    "Accept": self.RequestContentType,
                },
                success: function (data) {
                    var theSearchResults = new SearchResults();
                    theSearchResults.RawJson = data;
                    theSearchResults.BuildResultObject();
                    successFunc(theSearchResults);
                },
                error: function (data) { failureFunc(data);}           
     });       
 };

The data argument submitted to the success callback function is a big JSON object representing the response from the search engine. That success function does a number of things, including calling methods to deserialize the response as well as invoke the callback method that is passed into the RunSearch method itself. All topics for the next post.

If you are an experienced SharePoint App developer, one thing that might surprise you is that this code does not leverage the RequestExecutor to handle the cross-domain call. Well, search happens to be somewhat unique in this respect in that it can make a call to the local domain (the App Web) and still consume the farm/tenant’s search service. This is because search is a service that spans the entire farm/tenant scope. All that said, I plan to refactor this code to use the proper cross-domain calls, mainly to account for the variety of scenarios and code reuse objectives that might surface. I’ll make sure this post reflects those code updates.

The core objective of this post is to start to build the foundation for a JavaScript object model API for SharePoint Search (2013 and Online). In this post, we defined the basic properties of a Query that reflect a search as well as the methods used to actually make the call.

In my next post, I will talk about how to take that JSON response, process it and return it to the calling application so that the data can be rendered for the user.

Did this post help you? What additional detail are you looking for? Please provide feedback, good and bad, so that I can refine and improve the content going forward.

P.S. I am still trying to figure out why my code sections have all the lines. I beg your patience.

Update: I have been able to get properly formatted code but have lost line numbers. Gonna live with that for now. No other updates to this post.