I have had countless people tell me that they want to be able to do wildcard search in MOSS Enterprise Search using the existing Search Center site templates. The Search Center site template uses keyword query syntax which does not support wildcards. To get wildcards you need to use a full text SQL query. Until now the only solutions to getting wildcard search was either use Ontolica's Wildcard Search or write your own search page. Neither option has ever been appealing to me. Ontolica replaces the entire search center and provides an extra layer of abstraction to your managed properties. I didn't want to write my own search page because the search center already produces great looking results and it would be a lot of effort to reinvent everything on the search page. The reason why you would have had to write your own search page is that you could not get access via conventional means to the objects to change the query.
This is why I finally decided to take matters into my own hand. I really just wanted to just inherit from CoreResultsWebPart, change out the keyword query with a FullTextSqlQuery and call it good. Anyone who may have looked at this before knows it is not that simple. The class that does all the work SearchResultsHiddenObject is marked internal. Right now this seems an impossible task unless you bend the rules a little. That's right. I decided I am going to break OO rules and use reflection to get to the properties I needed. Some people say you should never do this and that its a hack. I agree to some extent, but when you are programming against an API and the provider of said API doesn't give you the tools you need to do your job, sometimes you have to bend the rules. Let's face it. Microsoft should have given us this support out of the box and at the minimum should have allowed us to change the query that the CoreResultsWebPart executes through the API. They did neither so here we are.
So how does the code work? Well, CoreResultsWebPart happens to be the one class in all of the Enterprise Search controls that isn't marked sealed. That is good news. Through the use of Reflector, I discovered the method I need to inherit from is SetPropertiesOnHiddenObject. Microsoft was even nice enough to mark this method as virtual for me. I then got access to the type and then used the base type to get a FieldInfo object for the private field srho (which is the SearchResultsHiddenObject).
// get the type of the current object
Type coreResultsWebPartType = this.GetType();
// get the private field containing the searchResultsHiddenObject
FieldInfo searchResultsHiddenObjectField = coreResultsWebPartType.BaseType.GetField("srho", BindingFlags.NonPublic | BindingFlags.Instance);
Once, I got access to the hidden object, I read the value of the KeywordQuery property to get what the user searched for. I then had to set this value to null, because I was replacing the keyword query with a FullTextSqlQuery.
// get the actual internal srho object attached to CoreResultsWebPart
object searchResultsHiddenObject = searchResultsHiddenObjectField.GetValue(this);
// get the type of the srho
Type searchResultsHiddenObjecType = searchResultsHiddenObject.GetType();
// get the keyword query property
PropertyInfo keywordQueryProperty = searchResultsHiddenObjecType.GetProperty("KeywordQuery", BindingFlags.Instance | BindingFlags.Public);
// read what the user searched for
string keywordQuery = (string)keywordQueryProperty.GetValue(searchResultsHiddenObject, null);
// set the keywordProperty to null so we can change it to a fullTextQuery
keywordQueryProperty.SetValue(searchResultsHiddenObject, null, null);
It was then just a matter of forming a new SQL query string (check the code on how I did that), and setting some additional fields _IsFullTextQuerySetFromForm and m_bIsKeywordQuery.
// get the fullTextQuery field
PropertyInfo fullTextQueryProperty = searchResultsHiddenObjecType.GetProperty("FullTextQuery", BindingFlags.Instance | BindingFlags.Public);
// create a new query and set it
string fullTextQueryString = GetFullTextQuery(keywordQuery, keywordsAsQuery);
fullTextQueryProperty.SetValue(searchResultsHiddenObject, fullTextQueryString, null);
// this field needs to be set to true to use a full text query
FieldInfo fullTextQuerySetField = searchResultsHiddenObjecType.GetField("_IsFullTextQuerySetFromForm", BindingFlags.NonPublic | BindingFlags.Instance);
// tell the srho that it is not a keyword query any more
FieldInfo isKeywordQueryField = searchResultsHiddenObjecType.GetField("m_bIsKeywordQuery", BindingFlags.NonPublic | BindingFlags.Instance);
The code for this is really pretty simple (aside from the reflection). Had the SearchResultsHiddenObject been marked public, we could have had this functionality over a year ago, but oh well.
Installation is relatively simple and instructions are included in a readme file in the document. A solution package has been provided for ease of installation. Once the package has been installed activate the Wildcard Search Web Part feature on your site collection. I went with a site collection feature because the search center site does not have a web part gallery in it. Now that the feature is activated, go to your results page in your Search Center, edit the page, and add the Wildcard Search Core Results Web Part to the Bottom Zone. You can then remove the old CoreResultsWebPart. Also note that this web part requires .NET Framework 3.5 because I used LINQ to XML to parse through the SelectColumns property.
Once you have the web part installed, you can perform a wildcard search by just adding an asterisk to whatever you type in the search box. For example app* would return mataches on app, apple, and application. You can also set the Always Use Wildcard property in the Miscellaneous property settings to always perform a wildcard search.
One thing to note. Wildcard searches reek havoc on your search relevance. Where you might be used to having nice clean looking results with your keyword searches, your wildcard searches will look different. It might not always be obvious why a particular item was returned in the search results. The best thing to do is try and see if it works for your particular situation.
This type of web part probably could have been sold, but I thought it was more important to give it to the community. This feature gets asked for by MOSS customers all the time. Finally there is an easy solution to implementing it. Since this web part is new, I am sure there are going to be issues with it. Please, log any issues you run into in installation or in us on the issue tracker of the CodePlex site. Currently, it only supports simple keyword queries. I still need to implement support for passing scopes and managed properties, so look for that in an update soon.
You can find the release files at CodePlex.
Corey Roth is a MOSS consultant for Stonebridge.