Sunday 6 January 2013

Calling SharePoint search using REST (e.g. from JavaScript or an app)

Sidenote - although this article focuses on search, I’ve tried to generalize slightly so that it may also be useful for:

  • Understanding the pattern for working with *any* SharePoint 2013 REST API (e.g. social, querying lists/libraries etc.) – the URL used for the REST call is the main piece that would change
  • Seeing (broadly) how to process REST API results formatted in the two available formats - JSON or XML
  • Understand how to call a REST API from both a SharePoint-hosted app (JavaScript) and a cloud app (provider-hosted or auto-hosted – C#)
  • Briefly discuss using CSOM to call search instead of REST

Often it’s valuable to leverage SharePoint services such as search, user profiles, Managed Metadata and so on from an app. With search specifically, an example I’ve been giving recently is my ‘learner’ time-tracking app, which displays a ‘Related content’ bar with links to documents and other things which relate to completing timesheets:

MainPage3

RelatedContent2

My search query is somewhat simplistic (I’m currently just searching for any documents and pages which contain the term ‘timesheets’), but it’s easy to imagine that with some refinement we could return only highly-relevant items. A nice approach in real-life could be to have a configurable value to allow each client (who installs the app) to edit/store the search query that works best for them. Regardless, I think the main point is that search can be a valuable ‘bridge’ between an app and the rest of the SharePoint environment. In my example, consider that:

  • As the app vendor, I have no idea what your organization’s timesheet policy/guidelines are
  • If I can bring in some of your content around this, users will have more success with my app, and that’s good for everyone

In terms of implementation, I’ll now talk about the basics of accessing search via REST, and also how to do this from an app.

Broadly, the process is:

  1. Identify (and test) the REST URL to use
  2. Deal with app authentication if necessary (i.e. you’re calling from outside SharePoint, e.g. a cloud app)
  3. Add a Permission Request to your app, so that a request to use search is successful
  4. Develop the success handler code to process the results

Accessing search via REST (_api)

The beauty of REST is that it’s just a URL, and you can test/refine it in the browser before writing a single line of code. From SharePoint 2013, such a URL takes the format:

[site]/_api/[name of SP API]/[name of operation]?[parameters]

So in the case of search, a basic query can be executed with the following:

[site]/_api/search/query?querytext='timesheets'

If such a URL is entered into the browser address bar, then you should see an XML response like this – you’ll find some interesting properties, like those I’ve highlighted in the images:

SearchRESTCallResults

SearchRESTCall_HitHighlightedSummary

Common operations

So far we’ve just looked at executing a basic query where the query text is specified. Here are a couple of other common operations you might want to do:

Operation

Sample REST URL

Use a search Result Source (i.e. a scope) /_api/search/query?querytext='search term'&sourceid='B09A7990-05EA-4AF9-81EF-EDFAB16C4E31' (this example is to search the ‘People’ result source)
Specify a start row (i.e. in paging) /_api/search/query?querytext='search term'&startrow=11
Specify a number of results to return /_api/search/query?querytext='search term'&startrow=11&rowlimit=10 (but note 10 is the default)
Specify particular (managed) properties to return /_api/search/query?querytext='search term'&selectproperties='Author,Path,Title,Url'

For a more comprehensive reference, the best source I can find is the SharePoint 2013 Search REST API post in the ‘Search Space’ blog by ‘nadeemis’ on MSDN – kudos to the author.

Working with the data

As with other SharePoint REST operations, you can choose to have the data in XML or JSON – and JSON might be preferable if you are in JavaScript. As I’ve mentioned before, this is accomplished with the “Accept” header – specify one of the following:

  • headers: { "Accept": "application/json; odata=verbose" }
  • headers: { "Accept": "application/xml" }

Your code which processes the results will need to deal with JSON/XML as appropriate. In either case, you’ll usually have some drilling down to do to get the valuable data – for example, the main result rows are in a property called jsonObject.d.query.PrimaryQueryResult.RelevantResults.Table.Rows.results (where jsonObject is the object which was parsed from the JSON string). And when you’re iterating through the results, if you want the URL for the item, then you can get that from the cell at index 6 with result.Cells.results[6].Value. The title is at index 3, so result.Cells.results[3].Value. These are basically just the things you work out with a JavaScript debugger, or from a code sample like mine below.

Calling the search REST service from the JavaScript Object Model (JSOM)

In this sample, I have a web page which has a DIV waiting to output the search results:

<div id="related-content-results"></div>

If I’m in a page running on SharePoint (including a SharePoint-hosted app), I can use SP.RequestExecutor (as opposed to a jQuery.get() or jQuery.getJson() for instance) and this simplifies things by meaning I don’t have to worry about adding an OAuth header to my request. So, this code would look like:

** N.B. My newer code samples do not show in RSS Readers - click here for full article **

Notice a couple of things:

  • The URL we’re using is the app web, rather than needing to reference the host web somehow – this is fine for search (but isn’t for accessing data in the host web, for example)
  • We need something to iterate over the result items within the JSON – jQuery’s each() operator is a good choice here
  • There are some ‘magic numbers’ in processing the JSON results to get to the values e.g. result.Cells.results[6] to get to the URL of the item. That’s just a reflection of the data structure SharePoint returns, and you’ll see the same if you’re working with XML instead

Calling the search REST service from a cloud app (provider-hosted/auto-hosted) in .NET CSOM

Accessing the search REST API (just like any SP2013 REST API) in other contexts can be a bit more involved. The main consideration is that if your page isn’t on the SharePoint server, you’ll need to deal with OAuth and adding the ‘Authorization’ token to the REST request. If you’re in C# (i.e. the .NET CSOM), then the TokenHelper class automatically added to your Visual Studio project simplifies this – you’ll need to use a HttpWebRequest object (or similar) to call the REST URL, and then process the response. Since I’m on the server this time and have access to .NET, XML could be a better choice here:

And just for clarity, here’s my ASPX page which renders this – as you can see, I’m using an ASP.NET ListView control to put the results on the page:

Sidenote: Other contexts – calling SP2013 REST APIs using jQuery in a cloud app

As I worked through these samples, I realized that another useful approach would be calling the REST API client-side from a cloud app (in an AJAX way). In this case, we’d probably be coding in jQuery, and would need to deal with obtaining an OAuth token and adding it to the web request – but in this case, we don’t have TokenHelper to support us. Sure, you could perhaps wrap TokenHelper in a WCF service and call that from jQuery, but that would mean an inefficient process of multiple web requests (firstly to the service, then the exchange to get the OAuth token, and finally the call to the SharePoint REST API).

Far better would be to eliminate the extra call and just directly obtain the OAuth token and call the REST URL directly from jQuery – but some work is needed to facilitate this. I’m sure someone will look at this soon as I expect it to be a common pattern for SharePoint 2013 app development.

Using CSOM instead of REST

I actually can’t think of too many reasons to do this any more, but if CSOM is your client-side preference you’ll want Microsoft.SharePoint.Client.Search.Query.SearchExecutor.ExecuteQuery(). 

Permission Request for an app to use search

In order to be able to call the search REST API from an app (of any hosting type), the app needs to be granted permission to use the search service – this doesn’t happen by default. Like any permission which relates to “core SharePoint” rather than the app itself, the developer must specify the requirement in the form of a Permission Request, and the administrator installing the app must agree to granting this permission at install time.

So, the developer needs to add the following to the appmanifest.xml file:

Note that the value specified for the Right attribute gives a very descriptive string reminding you what’s being asked for – although most actions within an app run “as the app’s identity” (i.e. as the ‘app principal’), the search will instead be performed as the user (with the associated security-trimming).

In the Visual Studio designer for the appmanifest.xml file, this looks like this:

SearchPermRequest

And, of course, when someone is installing (or upgrading) the app, they’ll see search listed as one of the permissions that the app requires – and can make an informed decision on whether they are OK with this:

Grant_SearchPermRequest

An issue for on-premises deployments?

So everything above works great for Office 365 environments. However, I see something weird when I deploy an app which uses search to my local developer environment. I haven’t yet tested the app against another on-premises environment, but I’d be interested to hear if anyone else sees this – the app in fact cannot be added, and I see a message saying “Sorry, only tenant administrators can add or give access to this app”:

OnPremisesError_AppWithSearchPermRequest

This is curious to me for a couple of reasons:

So, does anyone else see this? Am I missing something? Please let me know in the comments.

Finally, it’s worth pausing to consider – that fetching content via search across an entire SharePoint environment is a fairly elevated level of permission. The app is basically being given access to content created within your business, and anything in the search index is available. Clearly an app could potentially do some ‘interesting’ things with this, so it might be prudent to do some evaluation of such apps before blindly installing.

Conclusion

Calling into search from an app can be valuable, and here we’ve looked at how to do it from both a SharePoint-hosted app (in JavaScript) and a cloud app (in C#). The template used here for making such REST calls can be applied to many other operations – ultimately the pattern is to identify the URL, deal with authentication if outside SharePoint, ensure a permission request exists, then drill down in the JSON/XML response to get to the valuable data.

15 comments:

Tobias Zimmergren said...

Great article Chris!

Jan Steenbeek said...

Great article, thanks!

I must say I'm pretty horrified by the way search results are returned. I was hoping to use JSON to parse indivual results to my DTO and now find myself using things like;


$(data.d.query.PrimaryQueryResult.Table.Rows.results).each(function () {
var myData = new MyDTO({
title: (this).Cells.results.cells[2].Value
});
myArray.push(myData);
});

Any thoughts on how to clear this mess up?

Chris O'Brien said...

@Jan,

Yep, I understand why you have that need. It's fair to say that somewhere in your/my code, there'll be this gunk to drill down and get the values out.

I can't say I have a good pattern for it yet. Maybe some kind of JavaScript factory which abstracts this away somewhat, by taking the JSON data and returning the DTO?

Chris.

Kourosh said...

Hi and thank you for this wonderful post. I have a problem, I have used your code JSOM in a usual javascript file which I added it to my MasterPage however when I run and debugg it. it complain on SP.RequestExecutor and says undefind. thanks for your help.

Chris O'Brien said...

@Kourosh,

It's difficult to determine the specific, but one way or another the core JavaScript used by SharePoint is not executing before your code does. Maybe:

- the right references have not been added to the master page
- the references (e.g. to SP.js) are *below* your JavaScript code

A good way to check exactly which JavaScript files are required would be to create a SharePoint-hosted app and look at the default ASPX page which is created.

HTH,

Chris.

Anonymous said...

This code is fine but calling JSON.parse(data) withing onSuccess handler method should not happen.

Chris O'Brien said...

@Anonymous,

Yes, that's probably a somewhat redundant safety-check - it's probably not necessary.

Cheers,

Chris.

Marco van Wieren said...

Hi Chris

I believe that if no tenancy is enabled by default all site collections created are added to a default tenant (the app domain is also a tenant and eventually the whole thing is managed through subscriptions and corresponding settings).

Cheers, Marco

Anonymous said...

any news on the Sorry, only tenant administrators can add or give access to this app”

i have the same problem (on-premise) while on 365 its no problem

Chris O'Brien said...

@Anonymous,

This issue is fixed in the March 2013 Public Update for SP2013.

HTH,

Chris.

butchersoft said...

Hello Chris,

I took your code and altered it to run against a On-Site SharePoint server. I just needed to change the way the runsearch authenticates and everything worked great

Heres the change
private void runSearch( string queryText)
{
if (IsPostBack)
{
try
{

sharepointHostWeb = new Uri(Request.QueryString["SPHostUrl"]);
}
catch
{
sharepointHostWeb = new Uri(sharepointServer);
}
}

using (clientContext = TokenHelper.GetS2SClientContextWithWindowsIdentity(sharepointHostWeb, Request.LogonUserIdentity))
{

accessToken = TokenHelper.GetS2SAccessTokenWithWindowsIdentity(sharepointHostWeb, Request.LogonUserIdentity);

string searchRestUrl = string.Format("/_api/search/query?querytext='{0}'", queryText);

HttpWebRequest request = (HttpWebRequest)HttpWebRequest.Create(sharepointHostWeb.ToString() + searchRestUrl);
request.Method = "GET";
request.Accept = "application/atom+xml";
request.ContentType = "application/atom+xml;type=entry";
request.Headers.Add("Authorization", "Bearer " + accessToken);

HttpWebResponse response = (HttpWebResponse)request.GetResponse();

// process response..
XDocument oDataXML = XDocument.Load(response.GetResponseStream(), LoadOptions.None);
XNamespace atom = "http://www.w3.org/2005/Atom";
XNamespace d = "http://schemas.microsoft.com/ado/2007/08/dataservices";
XNamespace m = "http://schemas.microsoft.com/ado/2007/08/dataservices/metadata";

List items = oDataXML.Descendants(d + "query")
.Elements(d + "PrimaryQueryResult")
.Elements(d + "RelevantResults")
.Elements(d + "Table")
.Elements(d + "Rows")
.Elements(d + "element")
.ToList();

// N.B. there might be a more elegant/efficient way of extracting the values from the (slightly awkward) XML than this..
var searchResults = from item in items
select new
{
Title = item.Element(d + "Cells").Descendants(d + "Key").First(a => a.Value == "Title").Parent.Element(d + "Value").Value,
Author = item.Element(d + "Cells").Descendants(d + "Key").First(a => a.Value == "Author").Parent.Element(d + "Value").Value,
HitHighlightedSummary = item.Element(d + "Cells").Descendants(d + "Key").First(a => a.Value == "HitHighlightedSummary").Parent.Element(d + "Value").Value,
Path = item.Element(d + "Cells").Descendants(d + "Key").First(a => a.Value == "Path").Parent.Element(d + "Value").Value
};

// data-bind to ListView..
lvSearchResults.DataSource = searchResults;
lvSearchResults.DataBind();
}
}

where - String sharepointServer = "http://sharepoint13/_vti_bin/Longitude5";

Chris O'Brien said...

Great stuff, thanks for sharing!

Chris.

Dan@Desenv said...
This comment has been removed by the author.
Shawn Domer said...

Great article Chris. Have you experienced any issues while trying to call _api/search/suggest API from a different web app (e.g. calling into portal web app from mysite host web app)? I receive an error with all the SP_ResponseInfo properties undefined.

Thoughts?

Unknown said...

Thanks