SharePoint 2010 introduces the concept of Ranking Models. These models allow you to control the ranking of search results.

But how do you tell the search web parts to use your custom ranking model, once you specified it? As far as I can tell there’s 2 options:

  • Pass in the id of the model through the querystring by appending &rm=<guid of ranking model>
  • Use the Shared Query Manager

The Shared Query Manager is the option for modifying the query the search web parts are going to use.

To use the Query Manager to set an other Ranking Model I’ve created a custom web part. To retrieve the Query Manager all you have to do is call the static GetInstance method of the SharedQueryManager class.

After that, you can loop through the query manager and each location within the LocationList to set the ID of the Ranking Model:

public class SetRankingModel : WebPart
    {
        protected override void OnInit(EventArgs e)
        {
            try
            {
                QueryManager qm = SharedQueryManager.GetInstance(this.Page).QueryManager;
                foreach (LocationList ll in qm)
                {
                    foreach (Location l in ll)
                    {
                        try
                        {
                            l.RankingModelID = "5ea2750c-8165-4f65-bd12-6e6daad45fe1";
                        }
                        catch { }
                    }
                }
                base.OnInit(e);
            }
            catch { }
        }
    }

After you’ve placed this web part on a search results page, each web part will use your custom Ranking Model.

Today I was trying to fix an error in a custom webpart that performs a query on the SharePoint farm. The goal was to retreive the records within a site though the search system.

First I added a metadata property ItemDeclaredRecord that is mapped to ows__vti_ItemDeclaredRecord (DateTime). Then I tried the following query (using Steve Peschka’s Developer Search Tool):

SELECT Size, Rank, Path, Title, Description, Write FROM scope() WHERE  (ItemDeclaredRecord > '1900/01/01 00:00:00')  AND CONTAINS (Path, '"/projecten"')

This query works fine and retreives all records declared after January 1st, 1900 under the “projecten” site. To retreive all records within a specific subsite I then tried the following query:

SELECT Size, Rank, Path, Title, Description, Write FROM scope() WHERE  (ItemDeclaredRecord > '1900/01/01 00:00:00')  AND CONTAINS (Path, '"/projecten/subsite1"')

This threw a FaultException?! After fiddling around with the query for a few hours, I finally found a query that works:

SELECT Size, Rank, Path, Title, Description, Write FROM scope() WHERE  (ItemDeclaredRecord > '1900/01/01 00:00:00')  AND CONTAINS (Path, '"/projecten/subsite1"') ORDER BY Rank DESC

I’m not sure what the ORDER BY part does to the search system internally, but for now I’m guessing it’s SQL for ‘Pretty pretty please!’.

EDIT: The property you add in the ORDER BY part has to have the “Reduce storage requirements for text properties by using a hash for comparison” option checked.

One of the standard Ribbon buttons in SharePoint 2010 allows you to mail a link to a document. Unfortunately the dev team at Microsoft didn’t update the functionality of this button to use one of the enhanced UI features: multiple selection. When you select 2 or more items, the button is automatically disabled.

To bypass this inconvenience, we decided to implement our own version of the button to replace the standard button. Replacing a button is actually quite easy with the UI extensibility options in SP2010.

First of, I started with an empty SharePoint project in Visual Studio 2010. I chose to create a sandboxed solution. Because we need to deploy and inject a JavaScript file on every page, I decided to go for the solution Jan Tielens introduced in his blog post ‘Deploying and using jQuery with a SharePoint 2010 Sandboxed Solution’.

First of I added an empty element item to the project called Buttons. In this element I included the XML to hide the existing button and inject my own button:

<CustomAction Id="TTSendLinksEmailRemoveRibbonButton" Location="CommandUI.Ribbon">
  <CommandUIExtension>
    <CommandUIDefinitions> 
      <CommandUIDefinition Location="Ribbon.Documents.Share.EmailItemLink" />
    </CommandUIDefinitions>
  </CommandUIExtension>
</CustomAction>
<CustomAction Id="TTSendLinksEmailButton" Location="CommandUI.Ribbon" Sequence="15" Title="E-mail a link">
  <CommandUIExtension>
    <CommandUIDefinitions>
      <CommandUIDefinition Location="Ribbon.Documents.Share.Controls._children">
        <Button 
		Id="Ribbon.Documents.Share.TTSendLinksEmailButton"
		Alt="$Resources:core,cui_ButEmailLink;"
		LabelText="$Resources:core,cui_ButEmailLink;"
		ToolTipTitle="$Resources:core,cui_ButEmailLink;"
		ToolTipDescription="$Resources:core,cui_STT_ButEmailLinkDocument;"
		Sequence="15"
		Command="TT_SendLinksEmail_Button"
		Image16by16="/_layouts/$Resources:core,Language;/images/formatmap16x16.png"
		Image16by16Top="-16"
		Image16by16Left="-88"
		Image32by32="/_layouts/$Resources:core,Language;/images/formatmap32x32.png"
		Image32by32Top="-128"
		Image32by32Left="-448"
		TemplateAlias="o1" />
      </CommandUIDefinition>
    </CommandUIDefinitions>
	<CommandUIHandlers>
		<CommandUIHandler 
		Command="TT_SendLinksEmail_Button" 
		CommandAction="javascript:function sendLinksMail() {
			TamTam_SP2010_SendLinksMail_SendLinksMail();
		}
		sendLinksMail();"
		EnabledScript="javascript:function oneOrMoreEnable() {
			var items = SP.ListOperation.Selection.getSelectedItems();
			var ci = CountDictionary(items);
			return (ci > 0);
		}
		oneOrMoreEnable();" />
	</CommandUIHandlers>
  </CommandUIExtension>
</CustomAction>

For my own button, I copied the resourcestrings from the original XML that is in CMDUI.xml (14\TEMPLATE\GLOBAL\XML). This way the button gets the same look and feel and localized labels as the original button. When pressing the button the JavaScript function TamTam_SP2010_SendLinksMail_SendLinksMail is called. The EnabledScript enables the button when 1 or more items are selected.

To make sure the JavaScript is included, we’ll use a CustomAction element with ScriptLink as location (for more information see the previously mentioned blogpost by Jan Tielens):

<CustomAction
  ScriptSrc="~SiteCollection/SiteAssets/TamTam.SP2010.EmailALinkMultiple.js"
  Location="ScriptLink"
  Sequence="10">
</CustomAction>

Now all we have to do is create the necessary JavaScript. The out-of-the-box functionality opens the users email client with the link to the selected document in the body of the message.

To create the body of the message we’ll need to retrieve the link for every selected item by use of the Client Object Model. First we’ll get the ClientContext, the SPSite, the list were in and the id’s of the selected items:

TamTam_SP2010_SendLinksMail_SendLinksMail = function () {
  this.selectedItems = SP.ListOperation.Selection.getSelectedItems();
  this.selectedListGuid = SP.ListOperation.Selection.getSelectedList();
  this.context = SP.ClientContext.get_current();
  this.site = this.context.get_site();
  this.context.load(this.site);
  this.web = this.context.get_web()
  this.selectedList = this.web.get_lists().getById(this.selectedListGuid);
}

We then need to get the listitem object for every selected item. Because this is an asynchronous operation we’ll create an array, include every selected listitem in there and tell the context to load each item before calling executeQueryAsync:

this.selectedFiles = new Array();
var k;

for (k in this.selectedItems) {
  this.selectedFiles.push(this.selectedList.getItemById(this.selectedItems[k].id).get_file());
  this.context.load(this.selectedFiles[k]);
}

this.context.executeQueryAsync(
  Function.createDelegate(this, TamTam_SP2010_SendLinksMail_onQuerySucceeded), 
  Function.createDelegate(this, TamTam_SP2010_SendLinksMail_onQueryFailed)
);

Then in the onQuerySucceeded callback function we can get the url’s for the items and construct the email:

TamTam_SP2010_SendLinksMail_onQuerySucceeded = function () {
  var siteUrl = this.site.get_url();
  var k;
  var bodystring = "";
  
  for (k in this.selectedFiles) {
    var fileUrl = this.selectedFiles[k].get_serverRelativeUrl();
    bodystring += siteUrl + fileUrl + "%0d%0a%0d%0a";
  }
  
  window.open('mailto:?body=' + bodystring);
}

At a customer we created a few custom site templates by configuring them and then saving them as template. When creating new sites based on this template, we had the strange issue that the contents of the Alternate CSS (AlternateCSSUrl) were included and showing in the header on layouts pages in the sites. It appears that the AlternateCSSUrl is also set on the AlternateHeader property of the SPWeb object.

This post on the SharePoint 2010 forums also mentions this issue.

You can off course use powershell to get rid of this problem:

$site = Get-SPSite "http://sitecollectionurl"
$web = $site.OpenWeb("/weburl")
$web.AlternateHeader = ""
$web.Update()

Update: The post on the forums got answered and according to that it has to do with the publishing feature that was activated before saving the site as a template. However in my case that feature wasn’t activated, because we’re aware that such a scenario isn’t officially supported by Microsoft.

The new version of SharePoint offers more capabilities in the keyword syntax to enhance the search experience. While in the previous version you had to resort to FullText SQL queries, a lot of things can now be accomplished with keyword syntax.

For instance if you would like to filter on a date, you could use the following query:

LastModifiedTime>=01/06/2010

The actual format of the date depends on your regional settings (I’m using Dutch (nl-NL) in this case).

To search within a range of dates you can use the following query syntax:

LastModifiedTime:28/06/2010..30/06/2010

To exclude items you could use the following syntax:

LastModifiedTime<>28/06/2010

More info from MSDN about keyword syntax kan be found here: http://msdn.microsoft.com/en-us/library/ee558911.aspx