Find It

Integrate Search Into Your Site With ASP.NET

Marco Bellinaso

Code download available at:Search2007_09.exe(164 KB)

This article discusses:

  • Choosing a search solution
  • Building a search framework for ASP.NET
  • Implementing a Live Search provider
  • Implementing a SharePoint provider
This article uses the following technologies:
ASP.NET, Live Search, SharePoint

Contents

Building a Search Infrastructure
Building the Live Search Provider
Building the Provider for SharePoint 2007
Building the Custom Search Page
A Look to the Future: the SearchDataSource Control

No matter how much content you provide or how good it is, if your customers can't find what they're looking for they'll go elsewhere. It's no wonder then that there are hundreds of search technology vendors. Which solution should you choose?

Windows Live™ Search crawls content on the Internet and also exposes search functionalities to developers through a Web service that allows the search to be scoped down to just your site. You can then render the results however you see fit.

Microsoft® Office SharePoint® Server 2007 is another good choice. It's a complete site-building and management solution that also provides robust search functionality. Figure 1 shows the results of a SharePoint Search integrated into a custom ASP.NET page.

Figure 1 SharePoint Search

Figure 1** SharePoint Search **(Click the image for a larger view)

If you're only interested in enterprise search, you may opt for the Microsoft Office SharePoint Server (MOSS) 2007 for Search edition (rather than the Standard or Enterprise versions of MOSS). The main differences are the maximum number of documents they can index and the ability to index advanced content such as external databases. However, even with the Standard version of the Search edition you can index file shares, Exchange public folders, Windows SharePoint Services (WSS) sites, and external Web sites.

Building a Search Infrastructure

With so many search providers to choose from, it's important that your site uses an abstracted implementation that doesn't rely on specifics of the underlying search engine. That way, you can decide later whether you'll use Live Search or SharePoint or some other provider as your search solution. In this article, we're going to build a pluggable search infrastructure and toolset that you can add to your Web site today (the code is available for download from the MSDN® Magazine Web site); as part of this, we'll implement search providers for both Live Search and SharePoint so you can take advantage of either in your own applications.

The implementation will include the following features:

  • Support for advanced queries with multiple words and AND/OR operators, exact phrases, words exclusion, and wildcard characters where supported by the query engine.
  • Pagination of results, with a configurable number of items per page.
  • Support for multiple languages, including the ability to filter or rank by language.
  • Highlighting of the searched words in the found items' summary or description text.

It will also include the ability to configure (in the web.config file) which search engine you want to run your query with. To do this we'll build an object model that implements a simplified provider model design pattern—that is, multiple provider classes that actually implement the search using a specific search engine (think of these as the data-access layer classes), and a SearchEngine class (the business-logic layer class) that dynamically instantiates and uses the provider specified in the configuration settings.

Figure 2 shows the code for the high-level SearchEngine class, which exposes properties such as ResultsPerPage, QueryText, and Highlight, and has a parameterless method called ExecuteQuery that delegates to the underlying ExecuteQuery method of the concrete provider class initialized in the class's static constructor (so that it's created only once for the application's lifetime).

Figure 2 SearchEngine Class

public class SearchEngine { private static ISearchProvider _provider = null; static SearchEngine() { if (!string.IsNullOrEmpty( WebConfigurationManager.AppSettings["Search.Provider"])) { _provider = (ISearchProvider) Activator.CreateInstance(Type.GetType( WebConfigurationManager.AppSettings[ "Search.Provider"])); } else _provider = new LiveSearchProvider(); } public SearchEngine() { if (!string.IsNullOrEmpty(WebConfigurationManager.AppSettings[ "Search.ResultsPerPage"])) this.ResultsPerPage = Convert.ToInt32( WebConfigurationManager.AppSettings[ "Search.ResultsPerPage"]); if (!string.IsNullOrEmpty( WebConfigurationManager.AppSettings[ "Search.Culture"])) this.Culture = WebConfigurationManager.AppSettings[ "Search.Culture"]; if (!string.IsNullOrEmpty( WebConfigurationManager.AppSettings[ "Search.HighlightEnabled"])) this.Highlight = Convert.ToBoolean( WebConfigurationManager.AppSettings[ "Search.HighlightEnabled"]); } public string ProviderName { get { return _provider.GetType().Name; } } private int _resultsPerPage = 10; public int ResultsPerPage { get { return _resultsPerPage; } set { _resultsPerPage = value; } } private int _pageIndex = 0; public int PageIndex { get { return _pageIndex; } set { _pageIndex = value; } } private string _culture = "en-US"; public string Culture { get { return _culture; } set { _culture = value; } } private string _queryText = ""; public string QueryText { get { return _queryText; } set { _queryText = value; } } private bool _highlight = true; public bool Highlight { get { return _highlight; } set { _highlight = value; } } public SearchResults ExecuteQuery() { return _provider.ExecuteQuery(this.QueryText, this.Culture, this.ResultsPerPage, this.PageIndex, this.Highlight); } }

The provider name and other settings are located in the web.config file's <appSettings> section, as follows:

<add key="Search.Provider" value="LiveSearchProvider" /> <add key="Search.ResultsPerPage" value="5"/> <add key="Search.Culture" value="en-US"/> <add key="Search.HighlightEnabled" value="True"/>

The provider object returned by the Activator.CreateInstance call is saved into a static private reference of type ISearchProvider, the interface that all provider classes implement. The interface has a single method, ExecuteQuery, which takes as input all the information needed by the providers to run the query. It is defined as follows:

public interface ISearchProvider { SearchResults ExecuteQuery(string queryText, string culture, int resultsPerPage, int resultsPageIndex, bool hightlight); }

The ExecuteQuery method returns an instance of the SearchResults class, which includes static fields to return the total number of results (TotalResults), the list of results (WebResults), and the list of spelling suggestions for the query text. Here's the class code:

public class SearchResults { public int TotalResults = 0; public List<string> Suggestions = new List<string>(); public List<ResultItem> WebResults = new List<ResultItem>(); }

Suggestions is a collection of strings and WebResults is a collection of ResultItem objects. The ResultItem class exposes properties that return the result's URL, Title, and Description. You can see the actual code in Figure 3.

Figure 3 ResultItem Class

public class ResultItem { public ResultItem(string url, string title, string description) { this.Url = url; this.Title = title; this.Description = description; } private string _url; public string Url { get { return _url; } set { _url = value; } } private string _title; public string Title { get { return _title; } set { _title = value; } } private string _description; public string Description { get { return _description; } set { _description = value; } } }

Now that the basic architecture is ready we can start implementing provider classes.

Building the Live Search Provider

The Live Search Web service allows you to programmatically run the same kinds of search queries you can run on live.com. The Web service is located at soap.search.msn.com/webservices.asmx (with the WSDL for the service available at soap.search.msn.com/webservices.asmx?wsdl), and by using the Visual Studio® 2005 Add Web Reference option, you can create a proxy class that you use in your code to execute a search. Here's how you instantiate it:

MSNSearchService searchEngine = new MSNSearchService();

After creating an instance of the search engine class, you create the search request like so:

SearchRequest searchRequest = new SearchRequest();

Be aware that with the free service you're limited to 25,000 queries a day. Live counts your queries by tracking you through the unique "application ID" that you must get at search.msn.com/developer and use as the value for the SearchRequest's AppID property:

searchRequest.AppID = "185A8FADA7931D06D119C2FC1FF104";

The other essential property to set for the SearchRequest object is Query, which contains the search text as you would specify it on live.com. You can specify multiple words (the AND operator is implicit; you can explicitly use OR to search for any of the specified words), you can search for an exact phrase by wrapping it in quotation marks, you can use parentheses to group words, and you can use the minus sign to exclude words. The following example looks for documents that contain the word "ajax", the exact phrase "code example", either "Marco" or "Francesco", and that do not contain the word "xml" or the phrase "Visual Basic":

searchRequest.Query = "ajax AND \"code example\" AND (Marco OR Francesco)" + " –(xml Visual Basic)";

This query searches the Web as a whole, just as it would if you ran the query from live.com. If you want to restrict the results to only pages on your site, you can add site:www.yoursite.com to the end of the query, like so:

searchRequest.Query = "ajax site:msdn.microsoft.com/msdnmag";

Please refer to help.live.com for a complete list of search keywords. The best way to experiment with search strings and combining multiple filters and keywords, however, is to use the query builder tool you'll find at dev.live.com/livesearch/sdk (see Figure 4). This tool even generates the C# code necessary to invoke the Web service with the query, so it's really a must for your toolbox.

Figure 4 Live Search SDK's Query Builder

Figure 4** Live Search SDK's Query Builder **(Click the image for a larger view)

There are other SearchRequest properties that allow you to set the SafeSearch level (use Moderate if you want to exclude lewd images and texts from the results), flags (such as whether you want to highlight query words in the results), and the CultureInfo for the current culture:

searchRequest.SafeSearch = SafeSearchOptions.Moderate; searchRequest.Flags = SearchFlags.MarkQueryWords; searchRequest.CultureInfo = "en-US";

Now you must specify which type of results you want to have returned by Live Search: Web pages, spelling suggestions (for example, to suggest "membership" when you type "membershipa"), addresses and phone numbers, news, inline answers or images. For a Web search that returns pages from your site, you usually just want to include Web results and spelling suggestions; you do that by creating an array of two SourceRequest objects as the value for the SearchRequest's property:

SourceRequest[] sourceRequests = new SourceRequest[2]; searchRequest.Requests = sourceRequests;

Then you actually define the two source requests, creating the SourceRequest objects, specifying their type (Web and Spelling), the fields you want to get in the results (only the title for the spelling suggestions, and all typical fields such as title, Url and description for the Web results). You can also specify the number of results you want (three is often sufficient for spelling suggestions, while 10-20 is typically appropriate for Web results) and the offset of the first result (the index to start from among the total results, which is important for pagination):

sourceRequests[0] = new SourceRequest(); sourceRequests[0].Source = SourceType.Web; sourceRequests[0].ResultFields = ResultFieldMask.All | ResultFieldMask.SearchTagsArray; sourceRequests[0].Count = 20; sourceRequests[0].Offset = 0; sourceRequests[1] = new SourceRequest(); sourceRequests[1].Source = SourceType.Spelling; sourceRequests[1].ResultFields = ResultFieldMask.Title; sourceRequests[1].Count = 3; sourceRequests[1].Offset = 0;

You run the query by calling the MSNSearchService Search method with the SearchRequest object defined in input:

SearchResponse searchResponse = searchEngine.Search(searchRequest);

The call to Search returns an object of type SearchResponse. Its Responses property is an array of objects of type SourceResponse, and there's one for each SourceRequest defined above. In the sample, this means there's an array of two objects, one for Web results and the other for spelling suggestions. The SourceResponse object has a property named Total that's the total number of results found for the query executed. You should note that Total isn't the number of returned results (which is usually a subset according to the current index of a pageable report), but rather the overall total count. The actual results are accessible through the SourceResponse's Results property, which is a collection of Result objects. The Result object has several properties including Title, Url, and Description, which may be filled or empty according to what you've specified to return in the corresponding SourceRequest definition:

// Process the Web results int total = searchResponse.Responses[0].Total; foreach (Result result in searchResponse.Responses[0].Results) { // access the result.Title, result. //Url, and other values... } // Process the spelling results foreach (Result result in searchResponse.Responses[1].Results) { // print the result.Title value... }

That's all there is to it. To recap, you defined a SearchRequest with two SourceRequest objects, specifying to return Web results and spelling suggestions, with highlighting and moderation enabled; then you called the search engine's Search method for that SearchRequest and processed the resulting SourceResponses' Results collections. Figure 5 lists the complete code for the LiveSearchProvider class, which implements the ISearchProvider interface defined earlier. The code is similar to what you've seen so far in this section almost line by line, but of course much of the required information is not hardcoded into the class but is instead taken as input parameters or read from the configuration file.

public class LiveSearchProvider : ISearchProvider { public SearchResults ExecuteQuery(string queryText, string culture, int resultsPerPage, int resultsPageIndex, bool highlight) { SearchResults results = new SearchResults(); // create the search engine and search request MSNSearchService searchEngine = new MSNSearchService(); SearchRequest searchRequest = new SearchRequest(); // define the search requests: one for web results... SourceRequest[] sourceRequests = new SourceRequest[2]; sourceRequests[0] = new SourceRequest(); sourceRequests[0].Source = SourceType.Web; sourceRequests[0].ResultFields = ResultFieldMask.All | ResultFieldMask.SearchTagsArray; sourceRequests[0].Count = resultsPerPage; sourceRequests[0].Offset = resultsPageIndex * resultsPerPage; // ...and one for spelling suggestions sourceRequests[1] = new SourceRequest(); sourceRequests[1].Source = SourceType.Spelling; sourceRequests[1].ResultFields = ResultFieldMask.Title; sourceRequests[1].Count = 3; sourceRequests[1].Offset = 0; // set the text to be searched, the App ID, and more searchRequest.Requests = sourceRequests; string siteFilter = " site:" + WebConfigurationManager.AppSettings[ "LiveSearch.TargetSite"]; searchRequest.Query = queryText; if (!string.IsNullOrEmpty( WebConfigurationManager.AppSettings[ "LiveSearch.TargetSite"])) searchRequest.Query += siteFilter; searchRequest.SafeSearch = SafeSearchOptions.Moderate; searchRequest.AppID = WebConfigurationManager.AppSettings["LiveSearch.AppID"]; searchRequest.Flags = highlight ? SearchFlags.MarkQueryWords : SearchFlags.None; searchRequest.CultureInfo = culture; // run the query and get the response SearchResponse searchResponse = searchEngine.Search(searchRequest); // get the spelling suggestions foreach (Result result in searchResponse.Responses[1].Results) { results.Suggestions.Add(result.Title.Trim() .Replace(siteFilter, "") .Replace("\xe000", "").Replace("\xe001", "")); } // get the web results from the Web SourceResponse results.TotalResults = searchResponse.Responses[0].Total; foreach (Result result in searchResponse.Responses[0].Results) { results.WebResults.Add(new ResultItem(result.Url, result.Title.Replace("\xe000", "") .Replace("\xe001", ""), result.Description.Replace("\xe000", "<b>") .Replace("\xe001", "</b>"))); } return results; } }

Building the Provider for SharePoint 2007

SharePoint Portal 2007 search capabilities are very powerful. We'll explore just the features that are most important for our specific goal, but you may want to deepen your knowledge by reading some of the many guides available for system administrators and developers.

Let's start with the SharePoint Web-based administrative application, in the Search Settings section under Shared Services, and create a new search Content Source. This defines what you want to index, and how, and when. SharePoint Portal allows you to index typical Internet Web sites, SharePoint Web sites (which it handles in a specific way because it takes the content directly from the database), documents in a file share, custom databases, and more. In this case, you'll select the first option and specify the root URL of the site you want to index. SharePoint will start indexing from that URL and continue by following the links recursively (you can specify how deep it can go or that it should index all pages it finds on the same domain). Refer to Figure 6 to see the page where you create the content source.

Figure 6 Adding a Search Content Source

Figure 6** Adding a Search Content Source **(Click the image for a larger view)

While creating the content source you can also tell SharePoint (at the bottom of the page) to start the indexing process immediately after confirming the new source, and set up a schedule for the incremental indexing (for example, to run the process every day at midnight, when there's a lighter load on the servers). You can see these configurations in Figure 7.

Figure 7 Configuring the Indexing Schedule

Figure 7** Configuring the Indexing Schedule **(Click the image for a larger view)

The last configuration step consists of creating a search scope so that when you run the search you can limit it to the desired source. This is particularly important if you have multiple Web sites, each indexed separately, and you want to search content from one site and not from the others. To complete this operation, just fill out the fields of the wizard in Figure 8 (note that you may also create a scope that includes everything except a specific URL or content source).

Figure 8 Search Scope Wizard

Figure 8** Search Scope Wizard **(Click the image for a larger view)

When the new content source is defined, from the Search Settings page click the command to update the content sources so that the number of documents they include is recalculated. This useful information can then be reviewed from the page that lists all content sources.

Now it's time to write some code to query the index. In SharePoint this can be done in one of two ways: through its local object model (the SharePoint DLL assemblies) or through SharePoint Web services. The first approach is only possible when your site runs on the same server as SharePoint, which is quite rare. Therefore, it's much better to use the Web service, which you can call whether or not you're running on the same server. Add a Web Reference in Visual Studio to YourSharePointServerName/_vti_bin/search.asmx so that it creates the QueryService proxy class. The most important part of the coding work is crafting a correct query, which uses a combination of a SQL-like language in combination with XML. Here's an example of the SQL portion of a query:

string sqlQuery = "SELECT Path, Rank, Title, HitHighlightedSummary FROM MSDNMagazine..scope() WHERE FREETEXT('ajax asynchronous') ORDER BY Rank DESC";

You see that it's very similar to a normal SQL query that uses the SQL Server Full Text Search, even though indexes are stored in files and not in a SQL Server database. The FROM clause does not specify a table name, but rather the search scope name, followed by "..scope()". The WHERE filter uses the FREETEXT predicate, so you can specify advanced multiple conditions. The query will return the path, title, rank, and description of the found results, as specified in the SELECT. The SQL is not passed in to the query engine as is, but rather is enclosed as part of an XML request that defines other information, such as the number of results you want to retrieve, the starting offset (1 being the minimum, instead of 0 like it is for Live Search), the culture, and more. Figure 9 shows a complete request (notice how the SQL query is inserted into the surrounding XML).

Figure 9 SharePoint XML Search Request

string xmlQuery = string.Format(@" <QueryPacket urn:Microsoft.Search.Query""> <Query> <SupportedFormats> <Format>urn:Microsoft.Search.Response.Document:Document</Format> </SupportedFormats> <Context> <QueryText type=""MSSQLFT"" language=""en-US"">{0}</QueryText> </Context> <Range> <StartAt>1</StartAt> <Count>20</Count> </Range> <EnableStemming>true</EnableStemming> <TrimDuplicates>true</TrimDuplicates> <IgnoreAllNoiseQuery>true</IgnoreAllNoiseQuery> <ImplicitAndBehavior>true</ImplicitAndBehavior> <IncludeRelevanceResults>true</IncludeRelevanceResults> <IncludeSpecialTermResults>true</IncludeSpecialTermResults> <IncludeHighConfidenceResults>true</IncludeHighConfidenceResults> </Query> </QueryPacket>", sqlQuery);

Actually running the query and retrieving the results is just a matter of instantiating a QueryService object, settings its credentials to appropriate credentials (SharePoint filters the results according to the permissions associated with these credentials, but since we're targeting a public Web site's search scope, no result will be filtered out), and calling the Query method with the XML request in input:

QueryService queryService = new QueryService(); queryService.UseDefaultCredentials = true; string xml = queryService.Query(xmlQuery);

Figure 10 shows a partial XML response.

Figure 10 Sample XML Response Returned by the SharePoint Search Web Service

<ResponsePacket xmlns="urn:Microsoft.Search.Response"> <Response> <Range> <StartAt>1</StartAt> <Count>10</Count> <TotalAvailable>11</TotalAvailable> <Results> <Document xmlns="urn:Microsoft.Search.Response.Document"> <Action> <LinkUrl> https://msdn.microsoft.com/msdnmag/issues/07/02/cuttingedge </LinkUrl> </Action> <Properties xmlns= "urn:Microsoft.Search.Response.Document.Document"> <Property> <Name>PATH</Name> <Type>String</Type> <Value> https://msdn.microsoft.com/msdnmag/issues/07/02/cuttingedge </Value> </Property> <Property> <Name>RANK</Name> <Type>Int64</Type> <Value>419</Value> </Property> <Property> <Name>TITLE</Name> <Type>String</Type> <Value>Cutting Edge: Perspectives on ASP.NET AJAX – MSDN Magazine, February 2007</Value> </Property> <Property> <Name>HITHIGHLIGHTEDSUMMARY</Name> <Type>String</Type> <Value>Ways to &lt;c0&gt;AJAX&lt;/c0&gt; in ASP.NET &lt;ddd/&gt; First, there's the acronym-it stands for &lt;c1&gt;Asynchronous&lt;/c1&gt; JavaScript and XML. &lt;ddd/&gt; with ASP.NET &lt;c0&gt;AJAX&lt;/c0&gt;, but it can be used as the core code to add basic &lt;c0&gt;AJAX&lt;/c0&gt; capabilities to ASP.NET 1. &lt;ddd/&gt; </Value> </Property> </Properties> </Document> <Document> ... </Document> <!-- more Document nodes --> </Results> </Range> <Status>SUCCESS</Status> </Response> </ResponsePacket>

The rest of the code would just parse the XML text through an XmlDocument. In Figure 11 you find the complete implementation of the SharePointSearchProvider.

Figure 11 ISearchProvider for SharePoint

public class SharePointSearchProvider : ISearchProvider { public SearchResults ExecuteQuery(string queryText, string culture, int resultsPerPage, int resultsPageIndex, bool highlight) { SearchResults results = new SearchResults(); // prepare the SharePoint XML query string string sqlQuery = string.Format(@" SELECT Path, Rank, Title, HitHighlightedSummary FROM {0}..scope() WHERE FREETEXT('{1}') ORDER BY Rank DESC", WebConfigurationManager.AppSettings[ "SharePointSearch.TargetScope"], queryText.Replace("'","''").Replace("\"","\"\"")); string xmlQuery = string.Format(@" <QueryPacket urn:Microsoft.Search.Query""> <Query> <SupportedFormats> <Format>urn:Microsoft.Search.Response.Document:Document</Format> </SupportedFormats> <Context> <QueryText type=""MSSQLFT"" language=""{1}"">{0}</QueryText> </Context> <Range> <StartAt>{2}</StartAt> <Count>{3}</Count> </Range> <EnableStemming>true</EnableStemming> <TrimDuplicates>true</TrimDuplicates> <IgnoreAllNoiseQuery>true</IgnoreAllNoiseQuery> <ImplicitAndBehavior>true</ImplicitAndBehavior> <IncludeRelevanceResults>true</IncludeRelevanceResults> <IncludeSpecialTermResults>true</IncludeSpecialTermResults> <IncludeHighConfidenceResults>true</IncludeHighConfidenceResults> </Query> </QueryPacket>", sqlQuery, culture, (resultsPageIndex * resultsPerPage) + 1, resultsPerPage); // run the query and get the response QueryService queryService = new QueryService(); queryService.UseDefaultCredentials = true; string xml = queryService.Query(xmlQuery); // if there were no errors in the query, parse the results if (xml.IndexOf("<Status>SUCCESS</Status>") > -1) { XmlDocument xmlResults = new XmlDocument(); xmlResults.LoadXml(xml.Replace( " xmlns=\"urn:Microsoft.Search.Response\"", "").Replace("xmlns=" + "\"urn:Microsoft.Search.Response.Document.Document\"", "").Replace(" xmlns=" + "\"urn:Microsoft.Search.Response.Document\"", "")); results.TotalResults = Convert.ToInt32( xmlResults.SelectSingleNode( "//Response/Range/TotalAvailable").InnerText); foreach (XmlNode resNode in xmlResults.SelectNodes( "//Response/Range/Results/Document/Properties")) { string url = ""; string title = ""; string description = ""; foreach (XmlNode propNode in resNode.ChildNodes) { if (propNode.ChildNodes[0].InnerText == "PATH") url = propNode.ChildNodes[2].InnerText; else if (propNode.ChildNodes[0].InnerText == "TITLE") title = propNode.ChildNodes[2].InnerText; else if (propNode.ChildNodes[0].InnerText == "HITHIGHLIGHTEDSUMMARY") description = propNode.ChildNodes[2].InnerText; } // if hightlighting is enabled, replace all // <c?></c?> instances with <b></b>, otherwise // with an empty string if (highlight) { description = Regex.Replace( description, @"<c\d+>", "<b>"); description = Regex.Replace( description, @"</c\d+>", "</b>"); } results.WebResults.Add( new ResultItem(url, title, description)); } } return results; } }

It's worth noting that the QueryService also exposes a QueryEx method in addition to Query. This method returns the results in a DataTable instead of as raw XML. This makes things easier for the developer because no parsing would be required. However, it doesn't return the total number of results, which is necessary to implement the pagination feature in our search page (unless you want to always retrieve all results, and then do the pagination in-memory, but I don't recommend this approach), so we'll use the basic Query method instead.

Building the Custom Search Page

Now that we have the high-level interface and the underlying provider code complete, what's left is just the custom Search.aspx. You'll find the full implementation in the downloadable source code, so I'll simply highlight the most important part. The search box is made up by a textbox and a submit button. For the results reporting section of the page, a Repeater control lists the spelling suggestions as hyperlinks that point to the page itself, with the suggestion text passed in the querystring's "q" parameter:

<asp:Repeater runat="server" ID="rptSuggestions"> <ItemTemplate> <asp:HyperLink runat="server" ID="lnkSuggestionUrl" NavigateUrl='<%# "~/Search.aspx?q=" + DataBinder.Eval(Container, "DataItem") %>' Text='<%# DataBinder.Eval(Container, "DataItem") %>' /> </ItemTemplate> <SeparatorTemplate>, </SeparatorTemplate> </asp:Repeater>

In case you're wondering why LinkButton controls were not used in place of the hyperlinks, it's because they would have caused postbacks instead of again requesting the page with an updated querystring, with the result that the user could not save a shortcut to the page and later come back to get the same results.

Likewise, the same principle is true for pagination bars that have hyperlinks that point to the same page, with the same "q" parameter for the query text, and with a "p" parameter that specifies the index of the page of results to show. The actual Web results are displayed through a DataList control that renders each result's title and a hyperlink to the actual found page, along with some description text like so:

<asp:DataList ID="dlstResults" runat="server" ...styles here...> <ItemTemplate> <asp:HyperLink runat="server" ID="lnkResultUrl" NavigateUrl='<%# Eval("Url") %>' Text='<%# Eval("Title") %>' /><br /><%# Eval("Description") %> </ItemTemplate> </asp:DataList>

The codebehind is also simple. The developer of the UI doesn't need to know anything about the search engine that's used under the covers. In fact, she actually doesn't even need to know which one is used. The reason for this is because all this logic is wrapped up in the SearchEngine class. The following code shows how the class is used, and the results are bound to the UI controls:

SearchEngine search = new SearchEngine(); search.PageIndex = this.PageIndex; search.QueryText = txtQueryText.Text; SearchResults results = search.ExecuteQuery(); rptSuggestions.DataSource = results.Suggestions; lblTotal.Text = results.TotalResults.ToString(); dlstResults.DataSource = results.WebResults; Page.DataBind();

The result is the same as you saw in Figure 1.

A Look to the Future: the SearchDataSource Control

The May 2007 release of ASP.NET Futures (asp.net/downloads/futures) introduces a new data source control named SearchDataSource that—like all other data source controls, such as ObjectDataSource and SqlDataSource)—can be bound to controls such as Repeater, DataList or GridView, and automatically provide the data to be rendered. Of course, the provided data is the result of a Web search, run by one of the provider classes registered and configured in the web.config file:

<search enabled="true"> <providers> <add name="WindowsLiveSearchProvider" type="Microsoft.Web.Preview.Search.WindowsLiveSearchProvider, Microsoft.Web.Preview" appID="185A8FADA7931D06D119C2FC1FF104" siteDomainName="msdn.microsoft.com/msdnmag" /> </providers> </search>

The data source control itself can be added to the page either visually through the Visual Studio designer, or with the following markup:

<asp:TextBox ID="txtQuery" runat="server" /> <asp:Button ID="btnSearch" runat="server" Text="Search" /> <asp:SearchDataSource ID="SearchDataSource1" runat="server" > <SelectParameters> <asp:ControlParameter ControlID="txtQuery" Name="query" PropertyName="Text" /> </SelectParameters> </asp:SearchDataSource>

You see that there's a control parameter, query, that's bound to the Text property of the txtQuery TextBox. During postbacks, the data source control is automatically refreshed (you can also force that programmatically by calling the control's DataBind method), the query is run with the value contained in that TextBox, and the results are bound to the associated data control (the one with DataSourceID="SearchDataSource1"). If you used a DataList or Repeater control to display the results, its template definition would be the same as the code shown earlier for our custom solution, since the results bound to the control are of type SearchResult and contain the Title, Url and Description properties, as our WebResult class does. So the binding expressions would be identical.

If you want to create your own provider, you define a class that inherits from SearchProviderBase, overrides its Search method to provide the specific search engine's implementation, and the Initialize method to read the configuration settings:

public class SharePointSearchProvider : SearchProviderBase { public SharePointSearchProvider() : base() { } public override SearchResult[] Search(SearchQuery searchQuery) { ... } public override void Initialize(string name, NameValueCollection config) { ... } }

The SearchDataSource control approach is very elegant, since it fully implements the provider model design pattern (not a simplified version as the solution developed in this article), and the ability to visually drop this control on the form and bind it to a data control is appealing also since it doesn't require you to write any code. However, at the time of this writing the control is also very limited. I expect that Microsoft will soon extend the base provider class and the input SearchQuery type with more features and members. If this happens, you will be able to easily port the solution presented here to the SearchDataSource model: all you will need to do is move the search implementation from the class that implements the ISearchProvider interface to a class that inherits from SearchProviderBase, and read the input parameters from the provider's properties or the SearchQuery's properties, rather than from the ExecuteQuery's input values. The markup code will remain practically unchanged, since the SearchResult and the WebResult types expose the same properties.

Marco Bellinaso is a frequent speaker at Microsoft conferences in Italy and is the author of the Wrox book, ASP.NET 2.0 Website Programming / Problem – Design – Solution. Marco works for Code Architects Srl, consulting for clients and developing commercial developer tools. You can reach him at mbellinaso@gmail.com.