The search system that is built into the starting point application is pretty simple. However, it does provide a foundation for a modular search system in which each module is responsible for providing a search service, but results are collated to provide a community-wide search. This means that as new modules are added, they can plug into the search system without the need to change the infrastructure code to include them.
Every module business service class includes a GetSearchResults(String) method. This is required by the ModuleBase base class from which they all inherit. This method returns a SearchResultCollection object. This is a custom collection that contains SearchResult objects.
Each SearchResult object has the following properties:
name (String)A few words that describe the result
description (String)A one-paragraph description
relevance (Single)A number between 0 and 1 that indicates how relevant the search result is to the search terms
link (String)The URL to which the user should be directed when she clicks on the search result
CoreModule, being derived from ModuleBase, also provides a GetSearchResults(String) method. It is ModuleBase.GetSearchResults that calls the SearchResults method for each installed module and collates the results.
The process for dealing with a search request is as follows:
The user enters search terms in the search control.
default.aspx is loaded, with the search URL parameter containing the search terms.
A new SearchResults user control is created.
The Results property of the control is set to the result of CoreModule.GetSearchResults.
CoreModule.GetSearchResults calls GetSearchResults for each installed module, passing the search terms.
Each module returns its search results.
CoreModule.GetSearchResults collates the results and returns them.
The results are displayed in the SearchResults control.
Each module can create its own set of search results in any way we like, so each module can work in the way that makes sense for its content.
Let's take a look at the GetSearchResults method for the News module. Open Modules/News/NewsModule.vb and find the method:
Public Overrides Function GetSearchResults(ByVal pSearchTerms As String) _ As SearchResultCollection Dim results As New SearchResultCollection Dim crit1 As New Criteria crit1.AddLike("_Body", "%" & pSearchTerms & "%") Dim crit2 As New Criteria crit2.AddLike("_Title", "%" & pSearchTerms & "%") Dim crit3 As New Criteria crit3.AddLike("_Summary", "%" & pSearchTerms & "%") crit1.AddOrCriteria(crit2) crit1.AddOrCriteria(crit3) Dim crit4 As New Criteria crit4.AddEqualTo("_ModuleInstanceID", Me.ModuleInstance.PrimaryKey1) crit1.AddAndCriteria(crit4) Dim newsItems As IList =
QueryFacade.Find(Type.GetType("Community.NewsItem"), crit1) Dim ni As NewsItem For Each ni In newsItems results.add(New SearchResult(ni.Title, ni.Summary, 0.5, "default.aspx?Module=1&Item=" & ni.PrimaryKey1,
ni.ModuleInstance)) Next Return results End Function
First, we create a SearchResultCollection object. This is a custom collection class we will look at shortly.
We build a Criteria tree that will return a result if the search terms are found in the title, summary, or body of the news item. We also include a Criteria to require that the item is from the instance that is currently performing the search.
We then retrieve the items and create a SearchResult object for each of them in turn before returning the complete set of results.
The SearchResult class is a simple class, not a persistent object. We don't need to save SearchResult objects to the database, so they do not need to interact with the persistence service.
You can find the SearchResult class in Global/Search/SearchResult.vb. It is a simple combination of fields and properties to carry the data about the search result.
SearchResultCollection, found in Global/Search/SearchResultCollection.vb, is more complicated. Open that file now to take a look at it.
There are actually two classes in this file. To work properly, a custom collection class needs a matching custom enumerator class. Because they are so closely linked, we have put them in the same file.
The enumerator is important because, by providing an enumerator, our collection class can be used just like the standard collections. We can, therefore, use For Each loops and other standard looping techniques to access the members. The enumerator does the job of looping through the members of the collection.
Let's start at the top with SearchResultEnumerator.
It starts by stating that the class will implement the IEnumerator interface:
Public Class SearchResultEnumerator Implements IEnumerator
It is this interface that enables our custom enumerator to be treated as an enumerator by outside code. (We will see why this is important shortly.)
Next come the private fields:
Private _index As Integer = -1 Private _searchResultCollection As SearchResultCollection
We store the current index and the SearchResultCollection object to which the enumerator is attached.
Next, we provide a method to reset the enumerator back to the beginning of the collection:
Public Sub reset() Implements IEnumerator.Reset _index = -1 End Sub
In fact, we have to implement this method. As shown by the inclusion of the Implements statement, it is one of the methods that IEnumerator requires us to include.
IEnumerator also requires us to provide a method that returns the item to which the enumerator is currently pointing:
Public ReadOnly Property Current() As Object Implements
IEnumerator.Current Get If _index > -1 Then Return _searchResultCollection(_index) Else Return -1 End If End Get End Property
We check whether the current index is above 1. If it is, we return the relevant item from the collection. If it is not, we cannot return an item because the enumerator has not yet started enumerating the collection. We return -1 instead.
The final method we have to implement moves the enumerator to the next item in the collection:
Public Function MoveNext() As Boolean Implements IEnumerator.MoveNext _index = _index + 1 If _index < _searchResultCollection.count Then Return True Else Return False End If End Function
We move the index on and then check that we have not moved outside of the size of the collection. If we have, we return False to let the calling code know that the end of the collection has been reached.
So, we have seen the enumerator. Let's now look at the collection it enumerates.
We start by implementing the IEnumerable interface:
Public Class SearchResultCollection Implements IEnumerable
This is the key to a custom collection classby implementing this interface, the calling code knows that it can get an enumerator to perform For Each loops and so on.
Then, we have a private field to hold the actual results:
Private results As ArrayList = New ArrayList
We provide a this property that will enable members of the collection to be accessed by their index:
Default Public Property this(ByVal index As Integer) As SearchResult Get Return CType(results(index), SearchResult) End Get Set(ByVal Value As SearchResult) results(index) = Value End Set End Property
By including this property with the specific name this, we allow calling code to use standard indexed access syntax to access the members.
Then, we have a method to add items to the collection and a property to get the current length of the collection:
Public Sub add(ByVal item As SearchResult) results.Add(item) End Sub Public ReadOnly Property count() As Integer Get Return results.Count End Get End Property
Finally, we have a method, required by IEnumerable, to get an enumerator for the collection:
Public Function GetEnumerator() As IEnumerator Implements IEnumerable.GetEnumerator Return New SearchResultEnumerator(Me) End Function
You might be wondering what the point of using a custom collection class is, as opposed to simply putting the search results into an ArrayList or similar.
By using our custom class, we guarantee that only SearchResult objects can be added to the collection (because the add method will only accept SearchResult objects) and we also get the opportunity to include custom logic if we want to. For example, we might decide to have the SearchResultCollection automatically sort the search results by priority. We might be able to do that more efficiently within the collection, as items are added, than calling code would be able to.