Friday, December 30, 2011

SharePoint 2010 Search Scope to Aggregate Content

For scalability in SharePoint, I’m in favor of provisioning site collections instead of sub sites.  However, the challenge with Site Collections is that they are isolated from each other so you need to create custom solutions to aggregate content from across site collections.  Using SharePoint Search Scopes and the Search Core Results web part provides a way where an administrator could create an aggregated solution.  I also like to use search query web parts because it takes SQL out of the query loop and by modifying the web part’s XSL you have options for formatting the way results display (SharePoint Designer helps).

I had a requirement where a client wanted to display a feed of recent updates across site collections in a web part on a top level site.  I previously implemented a site provisioning solution for them which uses a site template – the template contains a discussion board, an images library, an announcement list, a document library, and a calendar.  The feed had to display similar to the My Sites Newsfeed where the feed item gives brief description of what type of content has been added or updated (e.g., A new image has been added…, A new discussion has been posted…, etc.).

The first thing I did was to create a Search Scope in the Search Service Application.  Within the search scope, I added the following rules:

Scope Rule Type Web Address Behavior Comment
Web Address

https://sites.xyz.com/sites

Include Rule to only include sites under the web address (we’re using a managed path under which all the sites are provisioned – in sample “sites” is the managed path)

The below rules use the property “contentclass”.  Contentclass is a search administration property commonly used in SharePoint search scopes.  More information on the ContentClass property can readily be found by doing a web search.

Scope Rule Type Property Query Behavior Comment
Property Query

contentclass=STS_ListItem_DiscussionBoard

Include This brings back all of the discussion board items
Property Query

contentclass=STS_ListItem_PictureLibrary

Include This brings back all of the Picture Library items
Property Query

contentclass=STS_ListItem_Events

Include This brings back all of the Calendar list items
Property Query

contentclass=STS_ListItem_Announcements

Include This brings back all of the Announcements list items
Property Query

contentclass=STS_ListItem_DocumentLibrary

Include This brings back all of the Document Library items
Property Query

contentclass=STS_List_850

Exclude The site template uses the Publishing Features which includes a “Pages” library – we don’t want results from the Pages library, just Documents, Announcements, Images, Events, and Discussion Board items

With this scope created, the next thing to do is to add a Search Core Results web part to a page on your top level site and set the Fixed Keyword Query value to use this scope.  With the fixed query, the results will display in the web part whenever the page loads.  Also set the Cross-Web Part query ID to something other than “User Query” (in the sample below, it is set to “Query 3”).

image

My client also wanted the results to display in Newsfeed format.  To do this, I needed to switch the Search Core Results Display properties to set sorting to Modified Date, deselect Use Location Visualization where I add contentclass as a column to bring back in search results (I need this to create conditions in my XSL to determine what to display in the feed statement) and also added the column for the managed property “SiteTitle” which I use to display the name of the site where the update occurred.

image

Finally, using SharePoint designer, I was able to take the raw XML search results and generate the XSLT to display the results in feed format which I added via the XSL editor in the Search Core Results Web Part (see MSDN guidance here for customizing search results and Tobias Zimmergren has the step-by-step for SP 2007 here which isn’t much different in SP 2010).

SharePoint 2010 and the Usage BLOB

I had a client who wanted to show a list of all site collections and sort them by most popular using site hits.  I was able to use Martin Kearn’s ShrarePoint 2010 solution on Codeplex (here) to return the aggregated site collection list however Martin’s solution wasn’t grabbing site hits even though there is a method within his code that appears to do that – I confirmed that it doesn’t work.

I then came across the solution in the SharePoint Diaries Blog, in the post where Diego Bruno Galeota documents Parsing the SharePoint Usage BLOB and provides the code.  Site hits are collected if you have SharePoint’s Web Analytics enabled where the analytics is stored as a BLOB in the database – so to get site hits out of that BLOB, you have to parse it – which is what Diego’s code does.  I deployed Diego’s code and it works for SharePoint 2010.  However, one update I made to Diego’s code was to change the GetUsageBlob function in the UsageBlob.cs class to remove the NetworkCredential property (snippet below).  Martin’s SharePoint directory solution is running at the farm level using elevated credentials so it works without having to hard code credentials.

UsageBlob.cs Snippet
  1. //public static UsageBlob GetUsageBlob(string url, bool currentBlob, NetworkCredential credential)
  2. public static UsageBlob GetUsageBlob(string url, bool currentBlob)
  3. {
  4.     var uri = new Uri(url);            
  5.     
  6.     ....

Then, using Martin’s Site Directory Solution I was able to add the Site Hits to the Site Directory list with the following function:

ScanJobHelperMethods.cs Snip
  1. public int GetHitsCount(SPWeb webInSite)
  2. {
  3.     int intHits = 0;
  4.     GlobalMethods globalMethods = new GlobalMethods();
  5.     System.Net.ServicePointManager.ServerCertificateValidationCallback = ((sender, certificate, chain, sslPolicyErrors) => true);
  6.  
  7.     try
  8.     {
  9.         string thisUrl = webInSite.Url.ToString();
  10.         var blob = UsageBlob.GetUsageBlob(thisUrl, true);
  11.         var now = DateTime.Now;
  12.         var minDate = new DateTime(now.Year, now.Month, now.Day);
  13.         var browsers = from b in blob.Browsers.UsageRecords
  14.                         select new
  15.                         {
  16.                             Name = b.Key,
  17.                             Hits = (
  18.                                     from v in b.Values
  19.                                     where v.Date <= minDate
  20.                                     select v.Count
  21.                                     ).Sum(),
  22.                             Percentage = 0
  23.                         };
  24.  
  25.         var totalHits = browsers.Sum(b => b.Hits);
  26.         if (totalHits > 0)
  27.         {
  28.             browsers = from b in browsers
  29.                         orderby b.Hits descending
  30.                         select new
  31.                         {
  32.                             Name = b.Name,
  33.                             Hits = b.Hits,
  34.                             Percentage = (int)Math.Round(((double)b.Hits / totalHits) * 100)
  35.                         };
  36.         }
  37.         intHits = totalHits;
  38.     }
  39.     catch (Exception ex)
  40.     {
  41.         globalMethods.WriteULSEntry(ex, TraceSeverity.Unexpected, null);
  42.     }
  43.  
  44.     return intHits;  
  45. }

Thursday, December 29, 2011

Adding XSL to a SharePoint Visual Studio Solution

When working with custom SharePoint search results web parts, inheriting from CoreResultsWebpart you’ll often use XSL to transform the search results display (this is not limited to this type of web part, you can use for list views too).  You could deploy the web part and configure the XSL after deployment or you could include the link to your XSL in your web part and package the XSL with your Visual Studio 2010 project.  To do the latter, from your Visual Studio Project, right click the name of the project in the Solution Explorer pane, select Add, and select SharePoint Mapped Folder.

image

In resulting window, select the folder at path Template\LAYOUTS\XSL to map.  Once mapped, I suggest adding a custom folder so it’s clear that this is custom XSL and then either upload your existing *.xsl file or create a new one and add your XSL (Side Note, SharePoint Designer does a great job of generating XSL based on any sample XML).

image

In the inherited class’ ConfigureDataSourceProperties set the XslLink property to the path of the XSL in the hive (“/_layouts/xsl/xyzcustom” in this case).

protected override void ConfigureDataSourceProperties()
{
...
this.XslLink = "/_layouts/xsl/xyzcustom/mmsearch.xsl";

....

Now, wherever you deploy the web part and add to a page, it will use your custom XSL.

Using this method, the XSL can not be overridden, If you want to overide after adding the web part, instead add to your web part's properties like this.



<?xml version="1.0" encoding="utf-8"?>
<webParts>
<webPart xmlns="http://schemas.microsoft.com/WebPart/v3">
<metaData>
<type name="XYZSearchBuilder, $SharePoint.Project.AssemblyFullName$" />
<importErrorMessage>$Resources:core,ImportErrorMessage;</importErrorMessage>
</metaData>
<data>
<properties>
<property name="Title" type="string">My Core Search Results</property>
<property name="Description" type="string">Inherits from Core Results Web Part. This web part provides a fixed query builder configuration to set keyword values to provide results containg the currently logged in user. The web part also provides configuration to sort results by a managed property (reduce storage may need to be set).</property>
<property name="XslLink" type="string">/_layouts/xsl/xyzcustom/xyzsearch.xsl</property>
</properties>
</data>
</webPart>
</webParts>

SharePoint 2010 Custom “My” Search Core Results

I’ve had a need to return search results specific to the currently logged in user which display on page load so I developed a My Search Core Results web part which inherits from the OOTB CoreResultsWebpart. Scott Hillier's solution for returning appended "My" results was a basis for this web part. But where Scott's example code appends to the user query, this generates a dynamic fixed query so results appear on page load.

The core of this web part is the following snippet:

Code Snippet
  1. namespace WP.CustomSearch.MySearchBuilder
  2. {
  3.     [ToolboxItemAttribute(false)]
  4.  
  5.     public class MySearchBuilder : CoreResultsWebPart
  6.     {
  7.         [Personalizable(PersonalizationScope.Shared)]
  8.         [WebBrowsable(true)]
  9.         [WebDescription("Main Keyword Query to search")]
  10.         [WebDisplayName("Fixed Query to Search")]
  11.         [Category("My Query Builder")]
  12.         public string MainQueryPrefix { get; set; }
  13.  
  14.         [Personalizable(PersonalizationScope.Shared)]
  15.         [WebBrowsable(true)]
  16.         [WebDescription("Mananged Property that should be searched for current user")]
  17.         [WebDisplayName("My Query Managed Property")]
  18.         [Category("My Query Builder")]
  19.         public string MyQueryPrefix { get; set; }
  20.  
  21.         [Personalizable(PersonalizationScope.Shared)]
  22.         [WebBrowsable(true)]
  23.         [WebDescription("Sort by this managed property")]
  24.         [WebDisplayName("Managed Property")]
  25.         [Category("Sort Override")]
  26.         public string OrderByProperty { get; set; }
  27.  
  28.         [Personalizable(PersonalizationScope.Shared)]
  29.         [WebBrowsable(true)]
  30.         [WebDescription("Sort direction")]
  31.         [Category("Sort Override")]
  32.         public Microsoft.Office.Server.Search.Query.SortDirection SortDirection { get; set; }
  33.  
  34.         protected override void ConfigureDataSourceProperties()
  35.         {
  36.             string QueryFormat = MainQueryPrefix + " " + MyQueryPrefix + ":\"{0}\"";
  37.  
  38.             string currentUserName = SPContext.Current.Web.CurrentUser.LoginName;
  39.             string currentName = SPContext.Current.Web.CurrentUser.Name;
  40.  
  41.             StringBuilder sb = new StringBuilder(String.Format(QueryFormat, currentUserName));
  42.             sb.Append(String.Format(" " + MyQueryPrefix + ":\"{0}\"", currentName));
  43.  
  44.             this.FixedQuery = sb.ToString();
  45.  
  46.             // only do stuff when search results are visible
  47.             if (this.ShowSearchResults)
  48.             {
  49.                 // run the base code
  50.                 base.ConfigureDataSourceProperties();
  51.                 try
  52.                 {
  53.                     // if OrderByProperty is not set, use default behavior
  54.                     if (!string.IsNullOrEmpty(OrderByProperty))
  55.                     {
  56.                         // get the datasource and change the sortorder
  57.                         CoreResultsDatasource dataSource = this.DataSource as CoreResultsDatasource;
  58.                         dataSource.SortOrder.Clear();
  59.                         dataSource.SortOrder.Add(OrderByProperty, SortDirection);
  60.                     }
  61.                 }
  62.                 catch (Exception ex)
  63.                 {
  64.                     SPDiagnosticsService.Local.WriteTrace(0, new SPDiagnosticsCategory("MYWEBPART", TraceSeverity.Unexpected, EventSeverity.Error), TraceSeverity.Unexpected, ex.Message, ex.StackTrace);
  65.                 }
  66.             }
  67.         }
  68.     }
  69. }

The Web Part has configuration settings for "My Query Builder" which provides the ability to add an initial fixed query value (e.g.., to return just documents you could enter "IsDocument:1" in this field) and a field where the Managed Property contains the current user (e.g. ModifiedBy, Author, or your own Managed Property) so when the web part renders it adds the fixed query (e.g IsDocument:1 ModifiedBy:"<user login>"). Note: If you want to throw more than one of this webpart on a single page, make sure to set a unique Cross-Web Part query ID.  As you can see in screenshot below, the values from “My Query Builder” dynamically populate the fixed keyword query adding the currently logged in users ID to the fixed query.

image

The full Visual Studio solution can be downloaded here.

SharePoint 2010 Custom Advanced Search for Power Users

A few months ago, I had the need to aggregate list items from a list which exists on multiple site collections and display them as a list, filterable by column, on a top level site (as displayed below). 

image

The lists all use the same content type so it was simple enough to go into the Search Service Application and create Managed Properties and a Scope, go to a page on my top level site and add a Search Core Results web part where I cleared the Use Location Visualization selection, added the Columns to include my new Managed Properties (see TechNet), changed the XSLT to display the search results formatted as a list (as a start for previously mentioned steps, see MSDN guidance here for customizing search results and Tobias Zimmergren has the step-by-step for SP 2007 here which isn’t much different in SP 2010), and set the Append Text to Query to the custom Scope keyword so it automatically brings back search results for this Scope (You could use one of your custom managed properties here too – I used Append Text To Query instead of Fixed Keyword Query because Fixed Query won’t allow you to filter by other keywords – there are ways to dynamically change this fixed query which I developed later and is in a future post). 

image

The filter was a challenge.  Using the out-of-the-box Advanced Search web part was my initial thought but the designer’s wireframes had dropdown boxes where the Advanced Search Web Part only allows plain text inputs and doesn’t display the way the designer had imagined.  I did a little research and came across a post from Tom Clarkson where he had a low-dev solution using a content editor web part  to create a Custom Advanced Search Box in SharePoint 2007 (please review Tom’s post to understand what the “ASB_” fields are).  So I thought I could try this for SharePoint 2010.  I did…and it did nothing.  I added some of the properties as a test and placed in a content editor web part as Tom had documented, switched my core results web part results query options to use “Query 2” so it would use the query from the custom search instead of the search query control on the page, ran the query…and nothing.

image

I came across several posts on the web where people implemented Tom’s solution in SharePoint 2007, upgraded to SharePoint 2010, and found the solution no longer worked.  I was under a time crunch so I didn’t have time to start building something in Visual Studio and there was nothing that fit the bill on codeplex so I investigated further by reflecting the code for the Advanced Search web part to see that a lot of what the web part does is generate JavaScript functions so it became clearer that I wasn’t getting results because I didn’t have the JavaScript functions beginning with the key one “SearchButtonOnClick.”  So I needed the JavaScript…simple…I placed an OOTB Advanced Search Web Part on my page and set it’s layout property to “Hidden.”  Not so simple, I had the JavaScript, but it didn’t want to use the HTML form from my content editor web part.  So I created my own function based on SearchButtonOnClick called SearchButtonOnClick2 (as displayed below -- In total, I had to customize 4 of the OOTB functions to get the search results to fit the requirement).

function SearchButtonOnClick2(switchElement)
{
ResetPageHashCode();
DoAdvancedSearch2('k', '\u002fPages\u002fAll-Projects.aspx', 'xyzProject', 'ASB_TQS_AndQ_tb', 'ASB_TQS_PhraseQ_tb', 'ASB_TQS_OrQ_tb', 'ASB_TQS_NotQ_tb', 'ASB_SS_scb_', 'ASB_SS_lcb_', 'ASB_SS_rtlb', 'ASB_PS_plb_', 'ASB_PS_olb_', 'ASB_PS_pvtb_', 'ASB_PS_lolb_');
}



Notice the called function DoAdvancedSearch2 -- it is based on the OOTB function DoAdvancedSearch – and is where I instruct the search to grab values from my form (in sample, 4th element value “xyzProject”) and I also set the results page to the current page in the 2nd element.



The DoAdvancedSearch2 function is as follows:



function DoAdvancedSearch2(queryParameterName, resultsPage, 
idOuterTable, idAndQueryTextBox, idPhraseQueryTextBox,
idOrQueryTextBox, idNotQueryTextBox, idPrefixScopeCheckBox,
idPrefixLangsCheckBox, idResultTypeList,
idPrefixPropNameSelect, idPrefixPropOperatorSelect,
idPrefixPropValueTextBox, idPrefixPropAndOrSelect) {

if (ValidateForm()) {
var elements = findElements(idOuterTable, idAndQueryTextBox,
idPhraseQueryTextBox, idOrQueryTextBox,
idNotQueryTextBox, idPrefixScopeCheckBox,
idPrefixLangsCheckBox, idResultTypeList,
idPrefixPropNameSelect, idPrefixPropOperatorSelect,
idPrefixPropValueTextBox, idPrefixPropAndOrSelect);

var query = ComposeQuery2(elements);
//alert(query);
if (query != '')
{
var url = resultsPage + '?' + queryParameterName + '=' + query;
navigateTo(url);
}
else
{
alert(emptyQueryMessage);
}
}
}


Notice the ComposeQuery2 function being called – again this are based on the OOTB generated function where the only reason I need to change it is to instead call the new ComposePropertySectionQuery2 function.



function ComposeQuery2(elements) {
return encodeURIComponent(ConcatenateQueryParts(
ComposeTextSectionQuery(elements),
ComposeScopingSectionQuery(elements),
ComposePropertySectionQuery2(elements)
));
}

function ComposePropertySectionQuery2(elements) {
var queryParts = [];
var querySeparators = [];
for (var i = 0; i < elements.PropNameSelectArray.length; i++) {
propNameSelect = elements.PropNameSelectArray[i];
propOperatorSelect = elements.PropOperatorSelectArray[i];
propAndOrSelect = elements.PropAndOrSelectArray[i];

if (propNameSelect.selectedIndex >= 0) {
var propIndex = findInArray(arrPropNames, propNameSelect.value);
var propName = arrPropNames[propIndex];
var propDataType = arrPropDTs[propIndex];
var opIndex = findInArray(arrDTOps[propDataType], propOperatorSelect.value);
var propPrefix = arrDTOpsStrPrefix[propDataType][opIndex];
var propOperator = arrDTOpsStr[propDataType][opIndex];
var propValue = elements.PropValueTextboxArray[i].value;
//handle when no input entered or value selected
if (propValue == '')
{

continue;
}
if ((propValue.indexOf(' ') != -1) ||
(propName.toLowerCase() === 'path'))
{
propValue = enquote(propValue);
}
queryParts.push(propPrefix + propName + propOperator + propValue);
querySeparators.push(arrAndOrKeywords[propAndOrSelect.selectedIndex]);

}
}

var query = ConcatenateQueryPartsWithSeparator(querySeparators, queryParts);

if (queryParts.length > 1)
return '(' + query + ')';
else
return query;

}


The difference between ComposePropertySectionQuery2 and the OOTB ComposePropertySectionQuery function is the change to condition “if (propNameSelect.selectedIndex >= 0)” which allows the user to submit a query with no filter values – this is used when the user basically wants to reset to show all results after having previously queried using a filter value, and the logic I added under the comment “handle when no input enter in value selected.” This logic is required because the user needs to filter by all fields, one field, or no fields (a reset).  If there is only one field they filter by, I don’t want the Query to be built to include searching by the other properties.



So there is also a limitation where these search functions will only work on plain text inputs -- however, I have requirement for dropdown boxes.  To get this to work, I need to add hidden text inputs to my form which correlate to the dropdown (sample below).



<select name="projPhase" id="projPhase" >
<option selected="selected" value="">Any Phase</option>
<option value="Project Control">Project Control</option>
<option value="Pre&#45;Initiate">Pre&#45;Initiate</option>
<option value="Intitate">Initiate</option>
<option value="Requirements">Requirements</option>
<option value="Architecture">Architecture&#47;Design</option>
<option value="Development">Development</option>
<option value="Testing">Testing</option>
<option value="Deployment">Deployment</option>
</select>
<input id="nameprefix_ASB_PS_pvtb_4" name="nameprefix$ASB_PS_pvtb_4" type="text" style="display:none" />



With the hidden text inputs, I can do actions on my form dropdown values in the SearchButtonOnClick2 function where I’m grabbing the selected value for my dropdown box using the JavaScript function getElementById, and setting the hidden text input’s value to that value so the search functions can use it.



SearchButtonOnClick2(switchElement)
{
ResetPageHashCode();
var elementStatus = document.getElementById('projStatus');
var elementStatusText = document.getElementById('nameprefix_ASB_PS_pvtb_2');
elementStatusText.value = elementStatus.value;
var elementSecurity = document.getElementById('projSecurity');
var elementSecurityText = document.getElementById('nameprefix_ASB_PS_pvtb_3');
elementSecurityText.value = elementSecurity.value;
var elementPhase = document.getElementById('projPhase');
var elementPhaseText = document.getElementById('nameprefix_ASB_PS_pvtb_4');
elementPhaseText.value = elementPhase.value;
DoAdvancedSearch2('k', '\u002fPages\u002fAll-Projects.aspx', 'mmProject', 'ASB_TQS_AndQ_tb', 'ASB_TQS_PhraseQ_tb', 'ASB_TQS_OrQ_tb', 'ASB_TQS_NotQ_tb', 'ASB_SS_scb_', 'ASB_SS_lcb_', 'ASB_SS_rtlb', 'ASB_PS_plb_', 'ASB_PS_olb_', 'ASB_PS_pvtb_', 'ASB_PS_lolb_');
}



For complete reference, here’s a sample of the full script that I placed in the HTML source of the content editor web part (notice the scopes are hidden from display but they are necessary for the query to run and they need to match whatever scopes you are showing in the hidden advanced search web part.  I’m also hiding the property values and conditions to meet the wireframe requirements but the query functions need them so they are included).



<table id="xyzProject">
<tr>
<td>
<input id="nameprefix_ASB_SS_scb_0_12" type="checkbox" name="nameprefix$ASB_SS_scb_0_12" style="display:none"/>
<input id="nameprefix_ASB_SS_scb_1_10" type="checkbox" name="nameprefix$ASB_SS_scb_1_10" checked="checked" style="display:none"/>
<input id="nameprefix_ASB_SS_scb_2_11" type="checkbox" name="nameprefix$ASB_SS_scb_2_11" style="display:none"/>

<select name="nameprefix$ASB_SS_rtlb" id="nameprefix_ASB_SS_rtlb" title="Result Type" style="display:none">
<option selected="selected" value="default">All Results</option>
</select>
</td>
</tr>

<tr class="ms-vh" nowrap="nowrap" style="font-family: Verdana, Helvetica, sans-serif; font-weight: bold; color: #666; text-decoration: none">

<td>
Project Name:
</td>
<td>
Program Name:
</td>
<td>
Status:
</td>
<td>
Security:
</td>
<td>
Current Phase:
</td>
<td>
Project Manager:
</td>
<td>
Description:
</td>
</tr>
<tr>
<td>
<select name="nameprefix$ASB_PS_plb_0" id="nameprefix_ASB_PS_plb_0" title="Pick a Property" style="display:none">
<option selected="selected" value="xyzProjectName">Project Name</option>
</select>
<select name="nameprefix$ASB_PS_olb_0" id="nameprefix_ASB_PS_olb_0" title="Inclusion Operator" style="display:none">
<option selected="selected" value="Contains">Contains</option>
</select>
<input id="nameprefix_ASB_PS_pvtb_0" name="nameprefix$ASB_PS_pvtb_0" type="text"/>
<select name="nameprefix$ASB_PS_lolb_0" id="nameprefix_ASB_PS_lolb_0" title="And Or Operator" style="display:none">
<option selected="selected" value="And">And</option>
</select>
</td>

<td>
<select name="nameprefix$ASB_PS_plb_1" id="nameprefix_ASB_PS_plb_1" title="Pick a Property" style="display:none">
<option selected="selected" value="xyzProgramName">Program Name</option>
</select>
<select name="nameprefix$ASB_PS_olb_1" id="nameprefix_ASB_PS_olb_1" title="Inclusion Operator" style="display:none">
<option selected="selected" value="Contains">Contains</option>
</select>
<input id="nameprefix_ASB_PS_pvtb_1" name="nameprefix$ASB_PS_pvtb_1" type="text"/>
<select name="nameprefix$ASB_PS_lolb_1" id="nameprefix_ASB_PS_lolb_1" title="And Or Operator" style="display:none">
<option selected="selected" value="And">And</option>
</select>
</td>

<td>
<select name="nameprefix$ASB_PS_plb_2" id="nameprefix_ASB_PS_plb_2" title="Pick a Property" style="display:none">
<option selected="selected" value="xyzProjectStatus">Project Status</option>
</select>
<select name="nameprefix$ASB_PS_olb_2" id="nameprefix_ASB_PS_olb_2" title="Inclusion Operator" style="display:none">
<option selected="selected" value="Contains">Contains</option>
</select>
<select name="projStatus" id="projStatus" >
<option selected="selected" value="">Any Status</option>
<option value="Active">Active</option>
<option value="Locked">Locked</option>
<option value="Cancelled">Cancelled</option>
</select>
<input id="nameprefix_ASB_PS_pvtb_2" name="nameprefix$ASB_PS_pvtb_2" type="text" style="display:none" />
<select name="nameprefix$ASB_PS_lolb_2" id="nameprefix_ASB_PS_lolb_2" title="And Or Operator" style="display:none">
<option selected="selected" value="And">And</option>
</select>
</td>

<td>
<select name="nameprefix$ASB_PS_plb_3" id="nameprefix_ASB_PS_plb_3" title="Pick a Property" style="display:none">
<option selected="selected" value="xyzProjectSecurity">Project Status</option>
</select>
<select name="nameprefix$ASB_PS_olb_3" id="nameprefix_ASB_PS_olb_3" title="Inclusion Operator" style="display:none">
<option selected="selected" value="Contains">Contains</option>
</select>
<select name="projSecurity" id="projSecurity" >
<option selected="selected" value="">Any Level</option>
<option value="FALSE">Normal Level</option>
<option value="TRUE">Sensitive Level</option>
</select>
<input id="nameprefix_ASB_PS_pvtb_3" name="nameprefix$ASB_PS_pvtb_3" type="text" style="display:none" />
<select name="nameprefix$ASB_PS_lolb_3" id="nameprefix_ASB_PS_lolb_3" title="And Or Operator" style="display:none">
<option selected="selected" value="And">And</option>
</select>
</td>

<td>
<select name="nameprefix$ASB_PS_plb_4" id="nameprefix_ASB_PS_plb_4" title="Pick a Property" style="display:none">
<option selected="selected" value="xyzProjectPhase">Project Phase</option>
</select>
<select name="nameprefix$ASB_PS_olb_4" id="nameprefix_ASB_PS_olb_4" title="Inclusion Operator" style="display:none">
<option selected="selected" value="Contains">Contains</option>
</select>
<select name="projPhase" id="projPhase" >
<option selected="selected" value="">Any Phase</option>
<option value="Project Control">Project Control</option>
<option value="Pre&#45;Initiate">Pre&#45;Initiate</option>
<option value="Intitate">Initiate</option>
<option value="Requirements">Requirements</option>
<option value="Architecture">Architecture&#47;Design</option>
<option value="Development">Development</option>
<option value="Testing">Testing</option>
<option value="Deployment">Deployment</option>
</select>
<input id="nameprefix_ASB_PS_pvtb_4" name="nameprefix$ASB_PS_pvtb_4" type="text" style="display:none" />
<select name="nameprefix$ASB_PS_lolb_4" id="nameprefix_ASB_PS_lolb_4" title="And Or Operator" style="display:none">
<option selected="selected" value="And">And</option>
</select>
</td>

<td>
<select name="nameprefix$ASB_PS_plb_5" id="nameprefix_ASB_PS_plb_5" title="Pick a Property" style="display:none">
<option selected="selected" value="xyzProjectManager">Project Manager</option>
</select>
<select name="nameprefix$ASB_PS_olb_5" id="nameprefix_ASB_PS_olb_5" title="Inclusion Operator" style="display:none">
<option selected="selected" value="Contains">Contains</option>
</select>
<input id="nameprefix_ASB_PS_pvtb_5" name="nameprefix$ASB_PS_pvtb_5" type="text"/>
<select name="nameprefix$ASB_PS_lolb_5" id="nameprefix_ASB_PS_lolb_5" title="And Or Operator" style="display:none">
<option selected="selected" value="And">And</option>
</select>
</td>

<td>
<select name="nameprefix$ASB_PS_plb_6" id="nameprefix_ASB_PS_plb_6" title="Pick a Property" style="display:none">
<option selected="selected" value="xyzProjectDescription">Project Description</option>
</select>
<select name="nameprefix$ASB_PS_olb_6" id="nameprefix_ASB_PS_olb_6" title="Inclusion Operator" style="display:none">
<option selected="selected" value="Contains">Contains</option>
</select>
<input id="nameprefix_ASB_PS_pvtb_6" name="nameprefix$ASB_PS_pvtb_6" type="text"/>
<select name="nameprefix$ASB_PS_lolb_6" id="nameprefix_ASB_PS_lolb_6" title="And Or Operator" style="display:none">
<option selected="selected" value="And">And</option>
</select>
</td>

</tr>

<tr><td></td><td></td><td></td><td></td><td></td><td></td>
<td align="right">
<input type="submit" name="nameprefix$ASB_BS_SRCH_1" value="Submit Query" onclick="SearchButtonOnClick2(); return false; WebForm_DoPostBackWithOptions(new WebForm_PostBackOptions(&quot;nameprefix$ASB_BS_SRCH_1&quot;, &quot;&quot;, true, &quot;&quot;, &quot;&quot;, false, false))" id="nameprefix_ASB_BS_SRCH_1" title="Search" />
</td></tr>
</table>

<script type="text/javascript">

function SearchButtonOnClick2(switchElement)
{
ResetPageHashCode();
var elementStatus = document.getElementById('projStatus');
var elementStatusText = document.getElementById('nameprefix_ASB_PS_pvtb_2');
elementStatusText.value = elementStatus.value;
var elementSecurity = document.getElementById('projSecurity');
var elementSecurityText = document.getElementById('nameprefix_ASB_PS_pvtb_3');
elementSecurityText.value = elementSecurity.value;
var elementPhase = document.getElementById('projPhase');
var elementPhaseText = document.getElementById('nameprefix_ASB_PS_pvtb_4');
elementPhaseText.value = elementPhase.value;
DoAdvancedSearch2('k', '\u002fPages\u002fAll-Projects.aspx', 'xyzProject', 'ASB_TQS_AndQ_tb', 'ASB_TQS_PhraseQ_tb', 'ASB_TQS_OrQ_tb', 'ASB_TQS_NotQ_tb', 'ASB_SS_scb_', 'ASB_SS_lcb_', 'ASB_SS_rtlb', 'ASB_PS_plb_', 'ASB_PS_olb_', 'ASB_PS_pvtb_', 'ASB_PS_lolb_');
}

function DoAdvancedSearch2(queryParameterName, resultsPage,
idOuterTable, idAndQueryTextBox, idPhraseQueryTextBox,
idOrQueryTextBox, idNotQueryTextBox, idPrefixScopeCheckBox,
idPrefixLangsCheckBox, idResultTypeList,
idPrefixPropNameSelect, idPrefixPropOperatorSelect,
idPrefixPropValueTextBox, idPrefixPropAndOrSelect) {

if (ValidateForm()) {
var elements = findElements2(idOuterTable, idAndQueryTextBox,
idPhraseQueryTextBox, idOrQueryTextBox,
idNotQueryTextBox, idPrefixScopeCheckBox,
idPrefixLangsCheckBox, idResultTypeList,
idPrefixPropNameSelect, idPrefixPropOperatorSelect,
idPrefixPropValueTextBox, idPrefixPropAndOrSelect);

var query = ComposeQuery2(elements);
//alert(query);
if (query != '')
{
var url = resultsPage + '?' + queryParameterName + '=' + query;
navigateTo(url);
}
else
{
alert(emptyQueryMessage);
}
}
}

function ComposeQuery2(elements) {
return encodeURIComponent(ConcatenateQueryParts(
ComposeTextSectionQuery(elements),
ComposeScopingSectionQuery(elements),
ComposePropertySectionQuery2(elements)
));
}

function ComposePropertySectionQuery2(elements) {
var queryParts = [];
var querySeparators = [];
for (var i = 0; i < elements.PropNameSelectArray.length; i++) {
propNameSelect = elements.PropNameSelectArray[i];
propOperatorSelect = elements.PropOperatorSelectArray[i];
propAndOrSelect = elements.PropAndOrSelectArray[i];

if (propNameSelect.selectedIndex >= 0) {
var propIndex = findInArray(arrPropNames, propNameSelect.value);
var propName = arrPropNames[propIndex];
var propDataType = arrPropDTs[propIndex];
var opIndex = findInArray(arrDTOps[propDataType], propOperatorSelect.value);
var propPrefix = arrDTOpsStrPrefix[propDataType][opIndex];
var propOperator = arrDTOpsStr[propDataType][opIndex];
var propValue = elements.PropValueTextboxArray[i].value;
//handle when no input entered or value selected
if (propValue == '')
{
continue;
}
if ((propValue.indexOf(' ') != -1) ||
(propName.toLowerCase() === 'path'))
{
propValue = enquote(propValue);
}
queryParts.push(propPrefix + propName + propOperator + propValue);
querySeparators.push(arrAndOrKeywords[propAndOrSelect.selectedIndex]);

}
}

var query = ConcatenateQueryPartsWithSeparator(querySeparators, queryParts);

if (queryParts.length > 1)
return '(' + query + ')';
else
return query;

}

function findElements2(idOuterTable, idAndQueryTextBox, idPhraseQueryTextBox, idOrQueryTextBox, idNotQueryTextBox,
idPrefixScopeCheckBox, idPrefixLangsCheckBox, idResultTypeList,
idPrefixPropNameSelect, idPrefixPropOperatorSelect,
idPrefixPropValueTextBox, idPrefixPropAndOrSelect) {
var inputFields = document.getElementById(idOuterTable).getElementsByTagName('input');
var selectFields = document.getElementById(idOuterTable).getElementsByTagName('select');

var elements = createEmptyElements();

for (var i = 0; i < inputFields.length; i++) {
if (matchRegex(idAndQueryTextBox, inputFields[i].id)) {
elements.AndQueryTextBox = inputFields[i];
}
else if (matchRegex(idPhraseQueryTextBox, inputFields[i].id)) {
elements.PhraseQueryTextBox = inputFields[i];
}
else if (matchRegex(idOrQueryTextBox, inputFields[i].id)) {
elements.OrQueryTextBox = inputFields[i];
}
else if (matchRegex(idNotQueryTextBox, inputFields[i].id)) {
elements.NotQueryTextBox = inputFields[i];
}
else if (matchRegex(idPrefixScopeCheckBox, inputFields[i].id)) {
elements.ScopeCheckBoxArray[elements.ScopeCheckBoxArray.length] = inputFields[i];
}
else if (matchRegex(idPrefixLangsCheckBox, inputFields[i].id)) {
elements.LangCheckBoxArray[elements.LangCheckBoxArray.length] = inputFields[i];
}
else if (matchRegex(idPrefixPropValueTextBox, inputFields[i].id)) {
elements.PropValueTextboxArray[elements.PropValueTextboxArray.length] = inputFields[i];
}
}

for (i = 0; i < selectFields.length; i++) {
if (matchRegex(idResultTypeList, selectFields[i].id)) {
elements.ResultTypeList = selectFields[i];
}
else if (matchRegex(idPrefixPropNameSelect, selectFields[i].id)) {
elements.PropNameSelectArray[elements.PropNameSelectArray.length] = selectFields[i];
}
else if (matchRegex(idPrefixPropOperatorSelect, selectFields[i].id)) {
elements.PropOperatorSelectArray[elements.PropOperatorSelectArray.length] = selectFields[i];
}
else if (matchRegex(idPrefixPropAndOrSelect, selectFields[i].id)) {
elements.PropAndOrSelectArray[elements.PropAndOrSelectArray.length] = selectFields[i];
}
else if (matchRegex(idPrefixPropValueTextBox, selectFields[i].id)) {
elements.PropValueTextboxArray[elements.PropValueTextboxArray.length] = selectFields[i];
}
}
return elements;
}

</script>

Tuesday, December 20, 2011

Add SharePoint 2010 Search Crawl and Managed Properties using PowerShell

The following PowerShell script can be used to add Search Crawl properties and Managed properties.  The properties are added based on vales read from an XML file.  This script comes in handy if you have multiple properties you need to add to multiple environments.

PowerShell Script
  1. #script reads from xml file to add crawled properties and managed properties
  2. cls
  3. #add sharepoint cmdlets
  4. if ( (Get-PSSnapin -Name Microsoft.SharePoint.PowerShell -ErrorAction SilentlyContinue) -eq $null )
  5. {    
  6.       Add-PsSnapin Microsoft.SharePoint.PowerShell
  7. }
  8.  
  9. #get the XML file
  10. [System.Xml.XmlDocument] $XmlDoc = new-object System.Xml.XmlDocument
  11. $file = resolve-path(".\searchprop.xml")
  12. if (!$file)
  13. {
  14.         Write-Host "Could not find the configuration file specified. Aborting." -ForegroundColor red    
  15.         Break
  16. }
  17.  
  18. write-host "Parsing file: " $file
  19. $XmlDoc = [xml](Get-Content $file)
  20.  
  21. #get the node containing the name of the search service application where you want to add properties
  22. $sa = $XmlDoc.SearchProperties.ServiceName
  23. $searchapp = Get-SPEnterpriseSearchServiceApplication $sa
  24.  
  25. #loop through crawled properties to check or add -- don't add if it already exists
  26. $CrawledPropNodeList = $XmlDoc.SearchProperties.CrawledProperties
  27.  
  28. foreach ($CrawledPropNode in $CrawledPropNodeList.CrawledProperty)
  29. {
  30.     $SPCrawlProp = $CrawledPropNode.Name
  31.     $SPCrawlPropType = $CrawledPropNode.Type
  32.     
  33.     #Create Crawled Property if it doesn't exist
  34.     if (!(Get-SPEnterpriseSearchMetadataCrawledProperty -SearchApplication $searchapp -Name $SPCrawlProp -ea "silentlycontinue"))
  35.     {
  36.         switch ($SPCrawlPropType)
  37.         {
  38.         "Text" {$crawlprop = New-SPEnterpriseSearchMetadataCrawledProperty -SearchApplication $searchapp -Category SharePoint -VariantType 31 -Name $SPCrawlProp -IsNameEnum $false -PropSet "00130329-0000-0130-c000-000000131346"}
  39.         "Integer" {$crawlprop = New-SPEnterpriseSearchMetadataCrawledProperty -SearchApplication $searchapp -Category SharePoint -VariantType 20 -Name $SPCrawlProp -IsNameEnum $false -PropSet "00130329-0000-0130-c000-000000131346"}  
  40.         "Decimal" {$crawlprop = New-SPEnterpriseSearchMetadataCrawledProperty -SearchApplication $searchapp -Category SharePoint -VariantType 5 -Name $SPCrawlProp -IsNameEnum $false -PropSet "00130329-0000-0130-c000-000000131346"}  
  41.         "DateTime" {$crawlprop = New-SPEnterpriseSearchMetadataCrawledProperty -SearchApplication $searchapp -Category SharePoint -VariantType 64 -Name $SPCrawlProp -IsNameEnum $false -PropSet "00130329-0000-0130-c000-000000131346"}
  42.         "YesNo" {$crawlprop = New-SPEnterpriseSearchMetadataCrawledProperty -SearchApplication $searchapp -Category SharePoint -VariantType 11 -Name $SPCrawlProp -IsNameEnum $false -PropSet "00130329-0000-0130-c000-000000131346"}
  43.         default {$crawlprop = New-SPEnterpriseSearchMetadataCrawledProperty -SearchApplication $searchapp -Category SharePoint -VariantType 31 -Name $SPCrawlProp -IsNameEnum $false -PropSet "00130329-0000-0130-c000-000000131346"}
  44.         }
  45.     }
  46. }
  47.  
  48. #now that the crawled properties exist, loop through managed properties and add
  49. $PropertyNodeList = $XmlDoc.SearchProperties.ManagedProperties
  50.  
  51. foreach ($PropertyNode in $PropertyNodeList.ManagedProperty)
  52. {
  53.     $SharePointProp = $PropertyNode.Name
  54.     $SharePointPropType = $PropertyNode.Type
  55.     $SharePointPropMapList = $PropertyNode.Map
  56.     #add managed property
  57.     #remove it if it already exists
  58.     if ($mp = Get-SPEnterpriseSearchMetadataManagedProperty -SearchApplication $searchapp -Identity $SharePointProp -ea "silentlycontinue")
  59.     {
  60.          #$mp | Remove-SPEnterpriseSearchMetadataManagedProperty -Confirm
  61.         $mp.DeleteAllMappings()
  62.         $mp.Delete()
  63.         $searchapp.Update()
  64.     }
  65.     New-SPEnterpriseSearchMetadataManagedProperty -SearchApplication $searchapp -Name $SharePointProp -Type $SharePointPropType
  66.     $mp = Get-SPEnterpriseSearchMetadataManagedProperty -SearchApplication $searchapp -Identity $SharePointProp
  67.     #add multiple crawled property mappings
  68.     foreach ($SharePointPropMap in $SharePointPropMapList)
  69.     {
  70.         $SPMapCat = $SharePointPropMap.Category
  71.         $SPMapName = $SharePointPropMap.InnerText
  72.         $cat = Get-SPEnterpriseSearchMetadataCategory –SearchApplication $searchapp –Identity $SPMapCat
  73.         $prop = Get-SPEnterpriseSearchMetadataCrawledProperty -SearchApplication $searchapp -Category $cat -Name $SPMapName
  74.         New-SPEnterpriseSearchMetadataMapping -SearchApplication $searchapp -CrawledProperty $prop -ManagedProperty $mp
  75.     }
  76. }

Here’s a sample of the XML file where the properties to be added can be read from:

searchprop.xml Sample File
  1. <?xml version="1.0" encoding="utf-8"?>
  2. <SearchProperties>
  3.     <ServiceName>Search Service Application</ServiceName>
  4.   <CrawledProperties>
  5.     <CrawledProperty Type="Text" Name="ows_SiteDescription" />
  6.     <CrawledProperty Type="Decimal" Name="ows_MemberCount" />
  7.     <CrawledProperty Type="Text" Name="ows_SiteURL" />
  8.     <CrawledProperty Type="Decimal" Name="ows_SiteHits" />
  9.     <CrawledProperty Type="YesNo" Name="ows_ProjSensitive" />
  10.     <CrawledProperty Type="DateTime" Name="ows_ProjStartDate" />
  11.     <CrawledProperty Type="Text" Name="ows_ProjectName" />
  12.   </CrawledProperties>
  13.     <ManagedProperties>
  14.         <ManagedProperty Type="1" Name="XYZSiteDesc">
  15.             <Map Category="SharePoint">ows_SiteDescription</Map>
  16.         </ManagedProperty>
  17.         <ManagedProperty Type="3" Name="XYZMembers">
  18.             <Map Category="SharePoint">ows_MemberCount</Map>
  19.         </ManagedProperty>
  20.         <ManagedProperty Type="1" Name="XYZSiteUrl">
  21.             <Map Category="SharePoint">ows_SiteURL</Map>
  22.         </ManagedProperty>
  23.         <ManagedProperty Type="3" Name="XYZSiteHits">
  24.             <Map Category="SharePoint">ows_SiteHits</Map>
  25.         </ManagedProperty>
  26.     <ManagedProperty Type="5" Name="XYZProjectSecurity">
  27.       <Map Category="SharePoint">ows_ProjSensitive</Map>
  28.     </ManagedProperty>
  29.     <ManagedProperty Type="4" Name="XYZProjectStartDate">
  30.       <Map Category="SharePoint">ows_ProjStartDate</Map>
  31.     </ManagedProperty>
  32.     <ManagedProperty Type="1" Name="XYZProjectName">
  33.       <Map Category="Basic">urn:schemas.microsoft.com:fulltextqueryinfo:displaytitle</Map>
  34.       <Map Category="SharePoint">ows_Title</Map>
  35.     </ManagedProperty>
  36.   </ManagedProperties>
  37. </SearchProperties>
Note:  The PowerShell script and XML file should be placed in same folder prior to running.

References:

Technet link for cmdlet New-SPEnterpriseSearchMetadataCrawledProperty to add a new crawl property.

Technet link for cmdlet New-SPEnterpriseSearchMetadataManagedProperty to add new managed property.

Technet link for cmdlet New-SPEnterpriseSearchMetadataMapping to add new mapping to a managed property.

Managed Property type key values are as follows:
  • Text = 1
  • Integer = 2
  • Decimal = 3
  • DateTime = 4
  • YesNo = 5
  • Binary = 6