Extending Search Web Parts

Here is code sample showing how to extend the search core results web part in SharePoint 2013. This can also be used for the content by search web part because it inherits from the search core results web part. In this sample I modify the query programmatically to retrieve items with metadata that matches a claim for the current user. This would be really helpful if you want to target content for a customer based on their demographics or browsing history. You know…like the Disneyland ads that appear in GMail a day after you research a Disneyland vacation :) .

Modifying the query on the fly

Modifying the query of a search web part programmatically is pretty straightforward. I can’t take credit for this one. I followed the steps in this blog post to the T and it worked great.

Exposing custom web part properties

I wanted to make this web part configurable by exposing custom web part properties. By default your custom web part properties will not display in the browser even if you mark them as WebBrowsable. This is because the GetToolParts method only returns a WebPartToolPart and a ResultScriptToolPart object. To have your custom properties display you have 2 options:

  1. Override the GetToolParts method and return a CustomPropertyToolPart object along with the base toolparts.
  2. Create your own WebPartToolPart class, override the GetToolParts method, and return your custom class along with the base toolparts.

I went with option #1. And as a bonus (or danger, depending on how you view it) this exposes several hidden properties that we can leverage.

The code

public class ResultScriptWebPartExt : ResultScriptWebPart {
private string _manageProperty = "CustomerAge";
private string _claimType = "http://schemas.xyz.org/customers/age";
private string _allValue = "Everyone";

[Personalizable(PersonalizationScope.Shared),
WebBrowsable(true),
Category("Custom"),
WebDisplayName("Claim Type")]
public string ClaimType {
get { return this._claimType; }
set { this._claimType = value; }
}

[Personalizable(PersonalizationScope.Shared),
WebBrowsable(true),
Category("Custom"),
WebDisplayName("Managed Property")]
public string ManagedProperty {
get { return this._manageProperty; }
set { this._manageProperty = value; }
}

[Personalizable(PersonalizationScope.Shared),
WebBrowsable(true),
Category("Custom"),
WebDisplayName("All Value")]
public string AllValue {
get { return this._allValue; }
set { this._allValue = value; }
}

public override ToolPart[] GetToolParts() {
var toolParts = new List<ToolPart>();
toolParts.AddRange(base.GetToolParts());
toolParts.Add(new CustomPropertyToolPart());

return toolParts.ToArray();
}

public ResultScriptWebPartExt() { }

protected override void OnLoad(EventArgs e) {
if (this.AppManager != null) {
if (this.AppManager.QueryGroups.ContainsKey(this.QueryGroupName) &&
this.AppManager.QueryGroups[this.QueryGroupName].DataProvider != null) {
this.AppManager.QueryGroups[this.QueryGroupName].DataProvider.BeforeSerializeToClient +=
new BeforeSerializeToClientEventHandler(EnhanceQuery);
}
}

base.OnLoad(e);
}

private void EnhanceQuery(object sender, BeforeSerializeToClientEventArgs e) {
DataProviderScriptWebPart dataProvider = sender as DataProviderScriptWebPart;
SetFilteredResults(dataProvider, this.ClaimType, this.ManagedProperty, this.AllValue);
}

private void SetFilteredResults(DataProviderScriptWebPart dataProvider, string claimType, string managedProperty, string allValue) {
//if there's no managed property or claim type given, do nothing
if (string.IsNullOrEmpty(managedProperty) || string.IsNullOrEmpty(claimType))
return;

//if the current query already contains filters for the segment property, do nothing
if (dataProvider.QueryTemplate.Contains(managedProperty))
return;

//if this is a windows user, don't continue
if (IsWindowsUser(Thread.CurrentPrincipal.Identity.Name))
return;

//grab the current user's claimsidentity
var currentIdentity = Thread.CurrentPrincipal.Identity as IClaimsIdentity;

var claimValues = new List<string>();

currentIdentity.Claims.Where(c => c.ClaimType == claimType).ToList().ForEach(
c => {
//only get unique claim values
if (!claimValues.Contains(c.Value)) { claimValues.Add(c.Value); }
}
);

var qry = new StringBuilder();
qry.Append(dataProvider.QueryTemplate);

//if there is a value that everyone can get, include it in list of claim values
if (!string.IsNullOrEmpty(allValue))
claimValues.Add(allValue);

//if there are any items, start the query
if (claimValues.Count > 0) {
qry.Append("(");
for (int i = 0; i < claimValues.Count; i++) {
var value = claimValues[i];
var filter = string.Format("{0}:\"{1}\"", managedProperty, value);

//add the segment filter
qry.Append(filter);

//if this isn't the last value, we need to append an or
if (i < claimValues.Count - 1)
qry.Append(" OR ");

}

qry.Append(")");
}

dataProvider.QueryTemplate = qry.ToString();
}

private bool IsWindowsUser(string userLoginName) {
if (string.IsNullOrEmpty(userLoginName))
return false;

var enocoded = SPClaimProviderManager.Local.GetUserIdentifierEncodedClaim(userLoginName);

SPClaim claim = SPClaimProviderManager.Local.DecodeClaim(enocoded);
return SPOriginalIssuers.GetIssuerType(claim.OriginalIssuer) == SPOriginalIssuerType.Windows;
}
}

The download

SearchWebPartsExt.zip

12 thoughts on “Extending Search Web Parts

  1. Ivan

    You do not need to add OR, based on MSDN KQL reference Author:”Author1″ Author:”Author2″ is the same as Author:”Author1″ OR Author:”Author2″

    Reply
        1. LesterLester Post author

          Not sure about that but I think it could be done client side. Out of curiosity, what are you trying to accomplish?

          Reply
  2. Shane

    What about overriding the query from the client side. I have a page that is an aggregation of an item and all the details. The page also has a Search Results web part that will pull all the documents that are related to this item. I would like to extend the search results to include documents that are related to the item based on values of the item. Example: House “123 Main St” has properties County and State. Its easy enough to have the Search Results pull all documents related to “123 Main St”, but I also would like to return documents that have a managed property County and State that have the same values. I hope this makes sense.

    The idea if I could override the query on the client you could append the values of the County and State. Similar to the way you can get those values on a display template.

    Reply
    1. LesterLester Post author

      You want those returned in the same query on a search results page? It sounds like you’re trying to alter the relevancy of results returned which would be difficult. You could use the search api but I think it’ll take multiple queries to accomplish.

      Reply
  3. Pingback: SharePoint Development Material(Links) | @Coding BlackHole

  4. Krish

    I want to do the similar case over ride the search result with SPFarm account, please suggest how to pass claims account as form account

    Reply

Leave a Reply

Your email address will not be published. Required fields are marked *

You may use these HTML tags and attributes: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <strike> <strong>