This article is an excerpt from Advanced Microsoft Content Management Server Development published by Packt Publishing; ISBN 1904811531; copyright Packt Publishing 2005; all rights reserved.
Lim Mei Ying is a senior consultant with Avanade and has extensive experience in setting up Microsoft Content Management Server (MCMS) systems at the enterprise level, and she is a Microsoft Valuable Professional for MCMS. Mei Ying's blog
Stefan Goßner works for Microsoft as an escalation engineer in the Developer Support department. He maintains an extensive MCMS 2002 FAQ on the Microsoft Web site and provides MCMS tips and tricks on his personal blog. Stefan's blog
Angus Logan is a product specialist at Data#3 Limited, and he is a MCAD.NET and MCDBA, as well as a Microsoft Valuable Professional for MCMS. Angus' blog
Andrew Connell is a client/server consultant for Fidelity Information Services, and he is a Microsoft Valuable Professional for MCMS. Andrew's blog
No part of these chapters may be reproduced, stored in a retrieval system, or transmitted in any form or by any means—electronic, electrostatic, mechanical, photocopying, recording, or otherwise—without the prior written permission of the publisher, except in the case of brief quotations embodied in critical articles or reviews.
Chapter 5: Searching MCMS with SharePoint (Part 2 of 2)
Contents
5.6 Searching with the MCMS SharePoint Connector
5.7 Building a Custom Search Implementation
5.7.1 About the SharePoint Portal Server Query Service
5.7.2 Building a Search Input Control
5.7.3 The Advanced Search and Results Page
5.8 Building the Microsoft SQL Full-Text Query
5.9 Building the MSQuery XML String
5.10 Summary
We have two options available to implement a search capability for our Tropical Green site:
- Leverage the Microsoft ASP.NET Server Controls included in the MCMS Connector for SharePoint Technologies that allow search queries to be executed.
- Create our own solution.
The MCMS Connector includes the following three controls that assist you in implementing search functionality for an MCMS site by leveraging SharePoint Portal Server search scopes:
- SearchInputControl: Used to create the search form input for a search to be submitted.
- SearchResultControl: Takes search criteria entered in the SearchInputControl, executes the search against the SharePoint Portal Server search Web service, and displays the results in list form.
- SearchMetaTagGenerator: Creates HTML META tags based on the PropertyType setting. META tags generated can include standard page properties as well as custom properties.
You can use these three controls on the same page or separate pages. This is very convenient as you may wish to include a small search keyword input box on all pages in your site that submits the search to a separate results page, but you might want to provide the search input on the search results page as well.
Once we have created a working search page using the MCMS Connector controls, we'll create a custom solution that won't include anything provided in the MCMS Connector. Our solution will include an advanced search, specific to our site, and a customized search result listing.
Both options have distinct advantages and disadvantages. Which one you'll implement on your MCMS site will depend entirely upon your requirements, customization needs, and available development time. The following tables outline a few of the more prominent advantages and disadvantages of using the MCMS Connector controls as well as creating your own solution.
Table 5-2. Implementing search leveraging MCMS Connector controls
| Advantages | Disadvantages |
Fast install and integration into
pages and templates | No customization of search input controls |
Will work out of the box with
minimal configuration | No customization of search result list |
Table 5-3. Implementing search with a custom solution
| Advantages | Disadvantages |
Complete control over layout of
search input form | Requires extra development time and testing |
Complete control over search
result list | |
Create special advanced search
based on specific site requirements | |
5.6 Searching with the MCMS SharePoint Connector
The first thing we'll do for this is to create a new search page in our Tropical Green project. This page will not be a new MCMS template, but a regular ASP.NET page. You could make this a template, but there's no real advantage in doing so because there will only be a single search page on our site with no extra content.
- In Microsoft Visual Studio .NET, right-click on the Tropical Green project and select Add Web Form.
- Name the new ASPX page Search.aspx and click Open.
- If the page doesn't load in Design mode, click Design in the lower-left corner.
- Change the page layout to FlowLayout.
- Drag the /Styles/styles.css, /UserControls/TopMenu.ascx, and /UserControls/RightMenu.ascx files from Solution Explorer onto the designer.
- Switch to HTML mode and modify the body tag as follows:
<body topmargin="0" leftmargin="0" rightmargin="0">
- Add the following code between the <form> and </form> tags, replacing the two user controls that were just added:
<form id="Form1" method="post" runat="server">
<table width="100%" border="0" cellspacing="0" cellpadding="0">
<tr>
<td width="100%" colspan="2" valign="top"
bgcolor="#ffcc00">
<img src="/tropicalgreen/images/Logo.gif">
</td>
<td vAlign="top" rowSpan="10"> </td>
</tr>
<tr bgColor="#66cc33">
<td colSpan="2"><uc1:TopMenu id="TopMenu1"
runat="server"></uc1:TopMenu></td>
</tr>
<tr>
<td vAlign="top" style="PADDING-RIGHT:30px;
PADDING-LEFT:30px;
PADDING-BOTTOM:30px;">
<p> </p>
<table cellspacing="0" cellpadding="10" border="1"
bordercolor="#669900">
<tr vAlign="top">
<td>Tropical Green Search:</td>
</tr>
<tr>
<td vAlign="top">
</td>
</tr>
</table>
</td>
<td class="RightMenuBar" width="20%" valign="top"
height="100%" align="center" rowspan="2"
bgcolor="#669900">
<uc1:RightMenu id="RightMenu1" runat="server">
</uc1:RightMenu>
</td>
</tr>
</table>
</form>
Why did we drag the user controls onto the page and then replace the resulting HTML?
Dragging the user controls onto the page adds the <%@ Register %> lines to the ASPX for us as well as adding the user control ASP.NET tags to the HTML. We then only need to modify the HTML to make it more presentable.
You should now have a page that looks like the following figure when viewed in Design mode.
.gif)
Figure 5-10. Page viewed in Design mode (click picture to see larger image)
Let's save our new search page, build the Tropical Green project, and navigate to it in a browser to make sure everything is in order before we go about adding the search input and results controls.
- Save all changes to the search.aspx page.
- Right-click the TropicalGreen project and select Build.
- If there are no errors in the build, open a browser and navigate to: http://www.tropicalgreen.net/TropicalGreen/Search.aspx.
- If there are any issues, retrace the steps we've taken to this point, address the errors, and retry the URL.
Now that we have a working search page, we need to add some functionality to it. We'll add the two MCMS Connector server controls, make some configuration changes, build the solution, and test our search page.
- Open the search.aspx page in Visual Studio .NET if it's not already open, and switch to Design view.
- Open the Visual Studio .NET Toolbox and drag the SearchInputControl and SearchResultsControl into the table cell below the Tropical Green Search cell. Refer to the following figure for placement.
.gif)
Figure 5-11. SearchInputControl and SearchResultsControl placement (click picture to see larger image)
- Select the SearchInputControl we added to search.aspx and set the following properties in the Visual Studio .NET property window:
- SearchMode: Simple
- SearchResultPage: /TropicalGreen/Search.aspx
- Select the SearchResultControl we added to the search.aspx page and set the following properties using the Visual Studio .NET property window:
- PortalUrl: http://portal.tropicalgreen.net/
- SearchResultPageSize: 10
We're using the URL of the portal created in Appendix A. Replace this URL with whatever portal you configured for the content source and search group in the steps already covered in this chapter.
Let's see if our search is working. Save all changes to search.aspx, build the Tropical Green project, and go to http://www.tropicalgreen.net/TropicalGreen/Search.aspx in a browser. You should see a page similar to the one below:
.gif)
Figure 5-12. Page displayed for Tropical Green search
Enter a word you know will be found on the site, such as ficus. You will see the same list of search results that were returned when searching for the same string in the portal containing the content index.
.gif)
Figure 5-13. Search results for ficus in search page
If you receive an error message stating "There was a problem loading the input control. The error returned by the system is: Could not find part of the path c:\inetpub\wwwroot\tropicalgreen\cms\wssintegration\searchpropertycollection.xml", double-check that you added the CMS virtual directory in your TropicalGreen Web application.
At this point, we have got search capabilities on our site thanks to the MCMS Connector controls and SharePoint Portal Server search features. But this solution is very limited, for instance there is no way to change the look and feel of these controls and there is also no way to configure which properties are displayed in the result page, for instance to show a short description for the returned documents.
To address this, we will now build our own search controls.
5.7 Building a Custom Search Implementation
As outlined previously, there are advantages and disadvantages to the MCMS Connector search controls. The most obvious is the fact that the SearchResultControl does not allow us to configure the results returned by the SharePoint Portal Server search We will now build our own search implementation that will leverage the SharePoint Portal Server search Web service, offer advanced and specialized searching to our users, and present the results in a customizable manner.
5.7.1 About the SharePoint Portal Server Query Service
Everything we are about to build depends upon the Query Service Web service, included in SharePoint Portal Server, that exposes search functionality to remote clients, such as our Web site. This Web service accepts a request in the Microsoft.Search.Query XML format and returns a response in the Microsoft.Search.Response XML format. In order to build a robust solution, the request we submit will use the Microsoft SQL Syntax for full-text Search. One method offered by the Query Service is QueryEx, which we will use as it returns results in the form of a DataSet.
For more information and documentation on the Microsoft SharePoint Portal Server Query Service Web service, see >Query Service.
5.7.2 Building a Search Input Control
The first thing we'll do is build a search input control that will submit a search query to a page for processing. This implementation will allow us to add a small search component to all of our templates quickly. Upon submitting a search query, our user control will add the query parameters to the query string and redirect the request to the results page.
Let's first start by creating a new user control.
- In Visual Studio .NET, right-click the User Controls folder in the Tropical Green project, and select Add | Add Web User Control.
- Name the new control SearchInput.ascx.
- While in Design view, drop controls (shown in the table) from the Toolbox onto the Web Form, and then arrange them as shown in Figure 5-14:
- TextBox: ID = txtSearchInput
- Button: ID = btnExecuteSearch, Text = Go
.gif)
Figure 5-14. Arrangement of controls
- The LinkButton we created will take the user to the search results page, which we'll add some advanced searching features to later. Double-click our LinkButton. Visual Studio .NET will create an empty event handler for the Click() event. Add a single line of code to this empty event handler to redirect the user to the search results page:
private void lnkAdvancedSearch_Click(object sender, System.EventArgs e)
{
Response.Redirect(Request.ApplicationPath
+ "/SearchResults.aspx");
}
- Next, we need to create an event handler for when a user clicks our Go button. We'll take the keywords entered in the TextBox and send the search request to the search results page. Double-click the Go button and add the following code to the event handler:
private void btnExecuteSearch_Click(object sender, System.EventArgs e)
{
string keywords = this.txtSearchInput.Text;
keywords = HttpUtility.UrlEncode(keywords);
Response.Redirect(Request.ApplicationPath
+ "/SearchResults.aspx?keywords=" + keywords);
}
Let's see if everything is okay with our new search input control. Save your changes and build the project. If you receive any error messages, retrace your steps and ensure that there are no typographical errors.
Before this control can be used, we need to add it to an existing template. While we'd ideally want to provide the search on all pages on our site (typically by adding it to a global heading control), we'll just add it to the home page for now.
- Open the
\Templates\HomePage.aspx template and drag our new SearchInput.ascx into the top cell, to the right of the logo. - Switch to HTML view and find the control we just added. It will likely have an opening tag of uc1:SearchInput. Wrap this control in an HTML DIV and set its alignment to right as shown in the following code:
<td width="100%" colspan="2" valign="top" bgcolor="#ffcc00">
<img src="/tropicalgreen/images/Logo.gif">
<div align="right">
<uc1:SearchInput id="SearchInput1" runat="server">
</uc1:SearchInput>
</div>
</td>
The HomePage.aspx template should now look similar to the following figure.
.gif)
Figure 5-15. Control added to existing HomePage.aspx template (click picture to see larger image)
5.7.3 The Advanced Search and Results Page
Once we have our search input control built, we need a page that will execute the search against the SharePoint Portal Server Query Service Web service and display the results. In addition, like all other search result pages, we need to add advanced searching options such as limiting our search to the Tropical Green plant catalog.
Before we can start building the results page, we need to add a Web reference to the SharePoint Portal Server Query Service Web service:
- In Visual Studio .NET, right-click the TropicalGreen project and select Add Web Reference.
- Enter the URL of the Web service that will retrieve the search results. The URL of the Query service is http://[portal]/_vti_bin/search.asmx. For this example, we'll use the portal created in Appendix A, http://portal.tropicalgreen.net/_vti_bin/search.asmx. Then click the Go button. You will likely be prompted for a user ID and password since this is part of the SharePoint portal virtual server, which isn't configured for anonymous access.
- Once the Web service loads and the available methods are shown in the Add Web Reference dialog box, click the Add Reference button to add the Web service to our project.
For simplicity, the search results page we will create will not be an MCMS template, rather it will be a standard ASP.NET Web Form in the root of the Tropical Green project.
- Right-click the project and select Add | Add Web Form.
- Give the new page the name SearchResults.aspx.
- In Design view, drag and drop the Styles.css file from Solution Explorer onto the form to apply the stylesheet to the page.
- Change the page layout to FlowLayout.
- Drag the following user controls into the designer:
- \UserControls\TopMenu.ascx
- \UserControls\RightMenu.ascx
- Switch to HTML view and modify the body tag as follows:
<body topmargin="0" leftmargin="0">
- Add the following HTML code to the page between the <form> tags, replacing the two controls we just added:
<form id="Form1" method="post" runat="server">
<table width="100%" border="0" cellspacing="0"
cellpadding="0">
<tr>
<td width="100%" colspan="2" valign="top"
bgcolor="#ffcc00">
<img src="/tropicalgreen/images/Logo.gif">
</td>
<td vAlign="top" rowSpan="10">
</td>
</tr>
<tr bgColor="#66cc33">
<td colSpan="2">
<uc1:TopMenu id="TopMenu1" runat="server">
</uc1:TopMenu>
</td>
</tr>
<tr>
<td vAlign="top" style="PADDING-RIGHT:30px;
PADDING-LEFT:30px; PADDING-BOTTOM:30px;
PADDING-TOP:10px">
<p> </p>
<table cellspacing="0" cellpadding="10" border="1"
bordercolor="#669900">
<tr vAlign="top">
<td>
<b>Tropical Green Search:<b/>
</td>
</tr>
<tr>
<td vAlign="top">
<b>Advanced Search</b>
<p>
<b>Search Results</b>
</td>
</tr>
</table>
</td>
<td class="RightMenuBar" width="20%" valign="top"
height="100%" align="center" rowspan="2"
bgcolor="#669900">
<uc1:RightMenu id="RightMenu1" runat="server">
</uc1:RightMenu>
</td>
</tr>
</table>
</form>
We now have the basic layout for our advanced search and search results page, which looks similar to the other templates in our site. Let's add some controls for our advanced search.
- In Design view, drag a TextBox from the Toolbox and place it under the Advanced Search text.
- In Design view, drag a Button from the Toolbox and place it to the right of the TextBox.
- The next thing we need to add is a DataGrid to contain the results of the search. In Design view, drag a DataGrid control from the Toolbox to just under the Add Search Results Here text. We'll worry about formatting this control later, for now we just need something to show us our data.
- Set the properties of the controls we just added as follows:
- TextBox: ID = txtAdvancedSearch
- Button: ID = btnAdvancedSearch, Text = Go
- DataGrid: ID = dgrSearchResults
Our advanced search page should now look like the following figure.
.gif)
Figure 5-16. Advanced search page (click picture to see larger image)
Now it's time to start coding our search logic. First, we need to add an event handler for our advanced search button.
- In Design view, double-click the btnAdvancedSearch button to create a click event handler. Visual Studio .NET will add an event handler method to the code-behind file.
- Add the following code to the btnAdvancedSearch_Click() event handler:
private void btnAdvancedSearch_Click(object sender, System.EventArgs e)
{
string keywords = this.txtAdvancedSearch.Text;
keywords = HttpUtility.UrlEncode(keywords);
Response.Redirect(Request.ApplicationPath
+ "/SearchResults.aspx?keywords=" + keywords);
}
Next, we need to check the querystring in the Page_Load() event handler to see if any keywords were passed from our SearchInput.ascx control or the txtAdvancedSearch TextBox.
Add the following code to check if there are any keywords supplied and execute the search if so:
private void Page_Load(object sender, System.EventArgs e)
{
if (Request.QueryString["keywords"] != null
&& Request.QueryString["keywords"] != String.Empty)
{
string keywords = Request.QueryString["keywords"];
DataSet ds = ExecuteSearch(keywords);
this.dgrSearchResults.Visible = true;
this.dgrSearchResults.DataSource = ds;
this.dgrSearchResults.DataBind();
// autofill the keyword input box with the search keywords
this.txtAdvancedSearch.Text = keywords;
}
else
{
this.dgrSearchResults.Visible = false;
}
}
Now we need to create the method that will execute the search against our SharePoint Portal Server content index. This method will:
- Create an instance of the Query Service Web service we just added to the project.
- Call a method that will build the MSQuery to submit to the Query service.
- Execute the search.
- Bind the search results to a DataGrid.
Import the following namespaces in the SearchResults.aspx.cs file:
using System.Security.Principal;
using System.Runtime.InteropServices;
using System.Net;
using System.Text;
using System.Text.RegularExpressions;
using Microsoft.ContentManagement.Publishing;
Add the following method to the SearchResults.aspx.cs file after the Page_Load() event handler. This method will ensure the current thread is running under the original security context regardless of any impersonations that may have been invoked previously:
// Get reference to the RevertToSelf method
[DllImport("ADVAPI32.DLL")]
public static extern int RevertToSelf();
/// <summary>
/// Builds the appropriate MSQuery,
/// submits the query to the SPS Query Service,
/// and returns the results as a DataGrid.
/// </summary>
/// <param name="keywords">String of keywords to search
/// for.</param>
/// <returns>DataSet of search results.</returns>
private DataSet ExecuteSearch(string keywords)
{
// decode the list of keywords
keywords = HttpUtility.UrlDecode(keywords);
// create reference to the Query Service Web service
net.tropicalgreen.portal.QueryService spsQueryService =
new net.tropicalgreen.portal.QueryService();
// use the current application pool identity to login
// to the SharePoint Query Service Web service
WindowsIdentity CurrentUser = WindowsIdentity.GetCurrent();
try
{
// use the Application Pool account to do access the
// SharePoint Search Services
RevertToSelf();
spsQueryService.Credentials =
CredentialCache.DefaultCredentials;
}
catch(System.Exception exception)
{
throw new System.Exception(exception.Message);
}
finally
{
// ensure that the original user is being impersonated again
CurrentUser.Impersonate();
CurrentUser = null;
}
// build MSQuery XML string to send to the Query Service
// - change the content source to "CMSChannels" if you used
// SearchSetup.exe
string msQuery = BuildMSQuery(keywords,
"Tropical Green website");
// execute the query and return the dataset
return spsQueryService.QueryEx(msQuery);
}
Note If you used the SearchSetup.exe program to create your content sources, you should use the content source group "CMSChannels" instead of "Tropical Green website" in the preceding code.
Our ExecuteSearch() method calls another method, called BuildMsQuery(), which constructs the MSQuery for sending to the QueryEx() Web method. An MSQuery is composed of XML tags that provide instructions to the Query service, such as the number of results to return in the request and a Microsoft SQL full-text (MSSQLFT) query. Building the MSSQLFT query and MSQuery is likely to be the most complicated task in implementing the SharePoint Portal Server search. We'll break it into two tasks: building the actual MSSQLFT query and building the MSQuery XML string. We'll first build the full-text query that our MSQuery will use in the construction of the XML string we'll send to the Query service.
5.8 Building the Microsoft SQL Full-Text Query
The SharePoint Portal Server search process uses full-text indexes and queries for fast keyword lookups in order to provide timely responses to the end user. Full-text queries are very similar to regular T-SQL queries in Microsoft SQL Server, but you have additional functions, or predicates, that you can use to give your query more power. One of these predicates that is useful when searching SharePoint Portal Server indexes is FREETEXT. FREETEXT takes a list of words separated by spaces, determines which words and phrases are significant, and uses that information to build an internal query to search the targeted data in an efficient manner.
First, you need to be aware of the various fields, or properties, available to you in your query. SharePoint Portal Server provides a list with this information in the Site Settings administration page of your portal.
- Open a new instance of Internet Explorer and navigate to our portal: http://portal.tropicalgreen.net.
- Click the Site Settings link in the upper right.
- Under the Search Settings and Indexed Content section, click the Manage properties from crawled documents link.
For each document crawled, the Manage Properties of Crawled Content page lists all properties that SharePoint Portal Server could potentially contain indexed data on. For our purposes, we're only going to look at the following two fields, as they contain information for our search results page:
- DAV:href
- DAV:getlastmodified
.gif)
Figure 5-17. Managed Properties of Crawled Content page
Notice that some of the fields listed under the urn:schemas.microsoft.com:htmlinfo:metainfo group are the same fields that we added to our template META tags using the SearchMetaTagGenerator user control from the MCMS Connector.
The other things we'll need are the name of the search scope we created, the name of the content index, and our keywords. Once we have all that information, it makes most sense to construct the MSSQLFT query in its own method for readability. We'll call this method BuildMssqlftQuery() and pass it a string containing our search keywords and the search scope to query. Add the following method at the end of the SearchResults.aspx.cs page:
/// <summary>
/// Builds the Microsoft SQL FullText query based on the parms.
/// </summary>
/// <param name="keywords">Keywords submitted for search.</param>
/// <param name="searchScope">SPS Search scope to filter.</param>
/// <returns>String of the MSSQLFT query.</returns>
private string BuildMSsqlftQuery(string keywords, string
searchScope)
{
StringBuilder mssqlftQuery = new StringBuilder();
ArrayList whereClause = new ArrayList();
#region FILTER: keywords
// list of keywords to include
if (keywords != null && keywords.Length >0)
{
// add the keyword filter, use a calculated weighted field
// just as SharePoint Portal Server does
whereClause.Add(string.Format(" {0} {1}",
"WITH (\"DAV:contentclass\":0,"
+ "\"urn:schemas.microsoft.com:fulltextqueryinfo:description\":0,"
+ "\"urn:schemas.microsoft.com:fulltextqueryinfo:sourcegroup\":0,"
+ "\"urn:schemas.microsoft.com:fulltextqueryinfo:cataloggroup\":0,"
+ "\"urn:schemas-microsoft-com:office:office#Keywords\":1.0,"
+ "\"urn:schemas-microsoft-com:office:office#Title\":0.9,"
+ "\"DAV:displayname\":0.9,"
+ "\"urn:schemas-microsoft-com:publishing:Category\":0.8,"
+ "\"urn:schemas-microsoft-com:office:office#Subject\":0.8,"
+ "\"urn:schemas-microsoft-com:office:office#Author\":0.7,"
+
"\"urn:schemas-microsoft-com:office:office#Description\":0.5,"
+ "\"urn:schemas-microsoft-com:sharepoint:portal:profile:"
+ "PreferredName\":0.2,contents:0.1,*:0.05) "
+ "AS #WeightedProps",
"FREETEXT(#WeightedProps,
'" +keywords.ToString().Trim() +"')")
);
}
#endregion
#region FILTER: sps source group
// filter source group
whereClause.Add(string.Format(" {0}",
"(\"urn:schemas.microsoft.com:fulltextqueryinfo:Sourcegroup\" =
'" + searchScope +"')"));
#endregion
//build search query
mssqlftQuery.Append("SELECT ");
mssqlftQuery.Append("\"DAV:href\",");
mssqlftQuery.Append("\"DAV:getlastmodified\"");
mssqlftQuery.Append(" FROM Non_Portal_Content..SCOPE()");
mssqlftQuery.Append(" WHERE ");
int i=0;
foreach (string s in whereClause)
{
if (i > 0)
mssqlftQuery.Append(" AND ");
mssqlftQuery.Append(s);
i++;
}
return mssqlftQuery.ToString();
}
Notice we added a calculated field, which we used to apply certain weight to some fields. This is how SharePoint Portal Server actually executes its own search. You could configure the property weighting to give more emphasis to specific properties in your query. For example, you may want to give more weight to the title of the page, or to the keywords stored in the HTML META tags, than to the contents of the page.
Now that we have this method, let's move on to creating the MSQuery string. We'll use this method in the construction of our MSQuery string.
5.9 Building the MSQuery XML String
We know that the SharePoint Portal Server Query Service Web service accepts a single parameter: an MSQuery string. This string is actually an XML document, but it's passed to the Query Service as a string. The XML tags in this string tell the Query Service the type of response it supports, how many records to return in the result, and the result index to start the search results at. The <StartAt></StartAt> element is what you can use in paging your result set. We won't be incorporating paging into our site as it is small, but you can see how easy it would be to do so.
Let's get started, by creating our BuildMsQuery() method that returns a complete MSQuery XML string containing all the information necessary to execute a query against a SharePoint index. Add the following method at the end of the SearchResults.aspx.cs page:
/// <summary>
/// Builds an MSQuery with an embedded MSSQLFT query embedded
/// for submission to SharePointPS Query Service.
/// </summary>
/// <param name="keywords">Keywords submitted for search.</param>
/// <param name="searchScope">SPS Search scope to filter.</param>
/// <returns>MSQuery</returns>
public string BuildMSQuery(string keywords, string searchScope)
{
StringBuilder msQuery = new StringBuilder();
// create the main header of the XML string
msQuery.Append("<?xml version=\"1.0\" encoding=\"utf-8\" ?>"
+ "<QueryPacket xmlns=\"urn:Microsoft.Search.Query\" "
+ "Revision=\"1000\">" + "<Query domain=\"QDomain\">"
+ "<SupportedFormats>"
+ "<Format>urn:Microsoft.Search.Response.Document.Document"
+ "</Format></SupportedFormats>");
// create the actual full-text query
msQuery.Append("<Context>"
+ "<QueryText language=\"en-US\" type=\"MSSQLFT\">"
+ "<![CDATA[" + this.BuildMSsqlftQuery(keywords, searchScope)
+ "]]></QueryText></Context>");
// create the range, page, and number of results
// to return
msQuery.Append("<Range><StartAt>1</StartAt><Count>20</Count>"
+ "</Range></Query></QueryPacket>");
return msQuery.ToString();
}
Note The two nodes of an MSQuery after the opening QueryPacket node (Query, and SupportedFormats) should not be modified. The Context node contains the actual search query, which you can change to suit your requirements. The last node, Range, contains directives used to tell the SharePoint Portal Server Query Service Web service how many results to return and at what index to start the result set.
For example, if you displayed 20 results per page and you wanted to show the third page of results, you'd set the StartAt node to 41 and leave the Count node at 20.
We now have a complete MSQuery string with an included full-text query.
Let's see if our search will now work. Build the Tropical Green project and navigate to http://www.tropicalgreen.net/. Enter ficus in the search box and click Go. You should see results similar to those in the following figure (we'll worry about making it more presentable in a moment).
.gif)
Figure 5-18. Tropical Green project search results for ficus (click picture to see larger image)
Every good search engine provides more than just keyword search. Some sites filter by topic and others by product. In our case, we could filter all our results to only the plant catalog, excluding the rest of the site. You would not be able to do this in a user-friendly manner using the controls provided by the MCMS Connector. While a knowledgeable guest could realize they could put in part of the MCMS path in one of the advanced search options, it's not straightforward to the typical guest of the site. This is where you can really start to leverage your custom search components.
Let's add a filter to search just our plant catalog.
- Open SearchResults.aspx in the Design view, and drag a CheckBox just below our advanced search text box and assign the following properties to it:
Checkbox: ID = chkFilterPlantCatalog, Text = Only Search Plant Catalog
- Open the code-behind file for the SearchResults.aspx page and add the following highlighted code to the btnAdvancedSearch_Click() event handler:
private void btnAdvancedSearch_Click(object sender, System.EventArgs e)
{
string keywords = this.txtAdvancedSearch.Text;
keywords = HttpUtility.UrlEncode(keywords);
string filter = string.Empty;
if (this.chkFilterPlantCatalog.Checked)
{
filter = "&filterPlantCatalog=1";
}
Response.Redirect(Request.ApplicationPath
+ "/SearchResults.aspx?keywords=" + keywords + filter);
}
- Add the highlighted code below to the BuildMSsqlftQuery() method:
private string BuildMSsqlftQuery(string keywords, string searchScope)
{
System.Text.StringBuilder mssqlftQuery =
new System.Text.StringBuilder();
ArrayList whereClause = new ArrayList();
#region FILTER: keywords
. . . code continues . . .
#endregion
#region FILTER: plant catalog
// list of keywords to include
if ( Request.QueryString["filterPlantCatalog"] != null
&& Request.QueryString["filterPlantCatalog"].ToString() == "1" )
{
whereClause.Add(
"(\"urn:schemas.microsoft.com:htmlinfo:metainfo:PATH"
+ "\" LIKE '/channels/tropicalgreen/plantcatalog/%')");
}
#endregion
. . . code continues . . .
Notice how we are using the urn:schemas.microsoft.com:htmlinfo:metainfo:PATH index property, which is mapped to the MCMS Channel Path thanks to the SearchPropertyCollection.xml file provided with the MCMS Connector.
- Let's see how the filter works. Save your changes and build the Tropical Green project. Once the build is complete, open your browser and navigate to http://www.tropicalgreen.net/TropicalGreen/SearchResults.aspx. Enter ficus in the text box, check the Only Search Plant Catalog CheckBox, and click the Go button.
.gif)
Figure 5-19. Advanced search page
Fantastic! We now only see records inside our Tropical Green plant catalog! This gives a good idea of what filtering brings to the table. We could filter by so many things, such as displaying only postings that have been updated in the last month or week. The possibilities are almost endless.
Let's see if we can't clean up those search results by getting rid of the DataGrid and replacing it with a Repeater. At the same time, we'll add filtering of the search results so users will only see postings that they have rights to access.
Although we listed numerous SharePoint index properties in our full text query, we will only use the DAV:href property when analyzing the results in our main results page to obtain a reference to the specified MCMS channel or posting to determine if the user has rights to browse the resource and also to determine and return the actual posting's name and description.
- Open the SearchResults.aspx page in Design view. Delete the DataGrid.
- Drag a Repeater object onto the page where the DataGrid was. Assign the Repeater an ID of rptSearchResults.
- While in Design view, select our new Repeater and open the Properties window. At the top of the window, click the Events button to show all possible events we can use. Double-click the box to the right of ItemDataBound to create an empty event handler that will fire every time an item is bound to the repeater.
- Switch back to HTML view for the SearchResults.aspx and scroll to our new Repeater.
- Add the following highlighted tags into the ItemTemplate of our Repeater:
<asp:repeater id="rptSearchResults" runat="server">
<ItemTemplate>
<asp:Placeholder ID="phdSearchResult" Runat="server"
visible="false">
<p>
<b><asp:HyperLink ID="hlkResultTitle" Runat="server" /></b>
<br><asp:Literal ID="litResultDescription" Runat="server" />
</p>
</asp:Placeholder>
</ItemTemplate>
</asp:repeater>
Notice the ASP.NET Placeholder we've added surrounding the search result. We'll use this to show and hide results that the user does or does not have permission to view.
Now that we have a Repeater filled with some placeholders for the content, we need to modify our data binding, which is still using a DataGrid.
- Open the code-behind file for SearchResults.aspx, find the Page_Load() event handler, and modify the code to bind the only DataTable in the DataSet to the Repeater as shown in the following code.
private void Page_Load(object sender, System.EventArgs e)
{
if (Request.QueryString["keywords"] != null
&& Request.QueryString["keywords"].Length >0)
{
string keywords = Request.QueryString["keywords"];
DataSet ds = ExecuteSearch(keywords);
this.rptSearchResults.Visible = true;
this.rptSearchResults.DataSource = ds.Tables[0].Rows;
this.rptSearchResults.DataBind();
}
else
{
this.rptSearchResults.Visible = false;
}
}
- Before we implement the ItemDataBound event, we need to create a method that will try to obtain the MCMS ChannellItem reference of the URL returned in the results. Add the following method after the Page_Load() event handler we just modified:
private HierarchyItem GetResult(string url)
{
try
{
// check if it's a GUID based URL
if (url.IndexOf("RDONLYRES") >= 0)
{
// try to get the GUID if it's a RDONLYRES URL
string guidRegEx =
"[a-fA-F0-9]{8}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-"
+ "[a-fA-F0-9]{4}-[a-fA-F0-9]{12}";
Regex regex = new Regex(guidRegEx);
Match m = regex.Match(url);
if (m.Success)
{
return
CmsHttpContext.Current.Searches.GetByGuid("{"+m.Value+"}");
}
}
else
{
// try to get the object via the URL
return CmsHttpContext.Current.Searches.GetByUrl(url);
}
// if this point reached, unknown URL
return null;
}
catch
{
return null;
}
}
- Now, find the rptSearchResults_ItemDataBound() event handler. We need to trap the event when it's binding a data item to the ItemTemplate or AlternateItemTemplate in the Repeater. Then, we'll get a reference to the data item being bound to the template, in our case a DataRow, and get references to the ASP.NET objects we added to the template. Finally, we'll use the data in the DataRow to populate the properties of our controls. Here's what our completed ItemDataBound() event handler will look like.
private void rptSearchResults_ItemDataBound(object sender, System.Web.UI.WebControls.RepeaterItemEventArgs e)
{
if ( (e.Item.ItemType == ListItemType.AlternatingItem)
|| (e.Item.ItemType == ListItemType.Item) )
{
// get a reference to the datarow being bound
DataRow row = e.Item.DataItem as DataRow;
HierarchyItem hi = GetResult(row[0].ToString());
// get references to all the ASP.NET objects
PlaceHolder resultContainer =
e.Item.FindControl("phdSearchResult") as PlaceHolder;
HyperLink resultTitle = e.Item.FindControl("hlkResultTitle")
as HyperLink;
Literal resultDesc =
e.Item.FindControl("litResultDescription") as Literal;
// if the URL doesn't resolve to an MCMS resource,
// output it to the results
if (hi != null)
{
// user has rights to this item so display it.
resultContainer.Visible = true;
// use values in DataRow to populate objects
resultDesc.Text = hi.Description;
if (hi is ChannelItem)
{
resultTitle.Text = (hi as ChannelItem).DisplayName;
resultTitle.NavigateUrl = (hi as ChannelItem).Url;
}
else
{
if (hi is Resource)
resultTitle.NavigateUrl = (hi as Resource).Url;
resultTitle.Text = hi.Name;
}
}
}
}
The final result looks something like the following figure.
.gif)
Figure 5-20. Final search page after modifying DataBinding (click picture to see larger image)
You'll see that the description field may not have exactly what we're looking for, but this technique lets us customize the search result list to our hearts' content. You could pull the description of the posting straight out of the indexed values, provided you exposed the page description using the SearchPropertyCollection.xml file. Or you could even have an HtmlPlaceholder called "Search Description" in all your templates that content owners could use to enter a description to show when the posting appears in search results.
5.10 Summary
In this chapter we discussed a few of the options available to MCMS developers for adding search functionality to their sites. We proceeded to take an in-depth look at the searching features built into SharePoint Portal Server and how they can be leveraged as a back-end search workhorse for an MCMS site. Before we could start adding the search functionality, we had to make a few changes to our site and templates, as well as build an index using SharePoint to crawl our site.
Once our site was configured for index crawls and SharePoint was configured to crawl our site and build an index, we explored in detail two options for adding search functionality to the Tropical Green site using the SharePoint crawler:
- First we implemented search using the MCMS Connector for SharePoint Technologies, an out-of-the-box solution.
- Then, we built our own solution using the SharePoint Portal Server Query Service Web service and custom full text T-SQL queries to provide search filters and customized results.
Read Chapter 5, Part 1