This was one of unanswered items in my recent two presentations on Search at TSPUG and MSPUG, so I was driven to figure it out and eventually did get it to work although not without some controversial steps. In this post I chose to also describe other approaches I tried and things I learned along the way, which didn’t necessarily get me to the end goal but may still be useful in your specific scenario. If you are looking for just extending CoreResultsWebPart so that it can “understand” FQL then you may want to scroll down a bit. You can download the complete source code here. I was testing my code on a SharePoint Server 2010 with December 2010 CU installed.
As you may know search web parts in SharePoint 2010 are no longer sealed, which gives you lots of flexibility via extending them. CoreResultsWebPart is probably the most important of all and therefore it is a great candidate for being extended. I wanted to take a search phrase passed as a query string argument to a search results page and write my own FQL query using this phrase as a parameter. My FQL query would do something interesting with it, for example use XRANK to boost documents created by a particular author. I certainly wanted to leverage all the goodness of CoreResultsWebPart, just use my FQL query instead of a plain search phrase. Contrary to my expectations it turned out to be not trivial to accomplish. So let’s dive into the details.
The story started with it completely not working, so I was forced to write a custom web part that used query object model and in particular the KeywordQuery class to deal with queries written in FAST Query Language (FQL). This is the one I have demonstrated at MSPUG and (with little success) at TSPUG. Below is a fragment showing how the query is submitted and results are rendered (BTW here is related MSDN reference with example).
using (KeywordQuery q = new KeywordQuery(ssaProxy))
{
q.QueryText = fql;
q.EnableFQL = true;
q.ResultTypes = ResultType.RelevantResults;
ResultTableCollection tables = q.Execute();
if (tables.Count == 0)
{
return;
}
using (ResultTable table = tables[ResultType.RelevantResults])
{
while (table.Read())
{
int ordinal = table.GetOrdinal("Title");
string title = table.GetString(ordinal);
TitleLabel.Controls.Add(
new LiteralControl(
String.Format("<br>{0}</br>\r\n", title)));
}
table.Close();
}
}
public class ExtendedCoreResultsWebPartWithKeywordSyntax : CoreResultsWebPart
{
protected override void ConfigureDataSourceProperties()
{
const string LongQueryFormat = "(FileExtension=\"doc\" OR FileExtension=\"docx\") AND (Author:\"{0}\")";
const string ShortQuery = "(FileExtension=\"doc\" OR FileExtension=\"docx\")";
string query = null;
if (String.IsNullOrEmpty(AuthorToFilterBy))
{
query = ShortQuery;
}
else
{
query = String.Format(LongQueryFormat, AuthorToFilterBy);
}
this.FixedQuery = query;
base.ConfigureDataSourceProperties();
}
[SPWebCategoryName("Custom"),
Personalizable(PersonalizationScope.Shared),
WebPartStorage(Storage.Shared),
WebBrowsable(true),
WebDisplayName("Author to filter by"),
Description("First and last name of the author to filter results by.")]
public string AuthorToFilterBy { get; set; }
}
The web part actually filters results by a given author. It uses keyword query syntax and not the FQL so we are far from being done yet. Remember how in the previous code fragment there was a line q.EnableFQL = true;? If just we could set it somewhere we would be essentially done! Well right but the KeywordQuery object is not directly accessible from the CoreResultsWebPart because it uses federation object model on top of the query object model (as do other search web parts). Purpose of the federation object model is sending same query to multiple search results providers and aggregating results later either in different spots on results page or in the same list. This is done by abstracting each search results provider by means of a Location class. Important classes in federation object model are shown on the diagram below.
class CoreFqlResultsDataSourceView : CoreResultsDatasourceView
{
public CoreFqlResultsDataSourceView(SearchResultsBaseDatasource dataSourceOwner, string viewName)
: base(dataSourceOwner, viewName)
{
CoreFqlResultsDataSource fqlDataSourceOwner = base.DataSourceOwner as CoreFqlResultsDataSource;
if (fqlDataSourceOwner == null)
{
throw new ArgumentOutOfRangeException();
}
}
public override void SetPropertiesOnQdra()
{
base.SetPropertiesOnQdra();
// At this point the query has not yet been dispatched to a search
// location and we can set properties on that location, which will
// let it understand the FQL syntax.
UpdateFastSearchLocation();
}
private void UpdateFastSearchLocation()
{
if (base.LocationList == null || 0 == base.LocationList.Count)
{
return;
}
foreach (Location location in base.LocationList)
{
// We examine the contents of an internal
// location.LocationRuntime property using Reflection. This is
// the key step, which is also controversial since there is
// probably a reason for not exposing the runtime publically.
Type locationType = location.GetType();
PropertyInfo info = locationType.GetProperty(
"LocationRuntime",
BindingFlags.NonPublic | BindingFlags.Instance,
null,
typeof(ILocationRuntime),
new Type[0],
null);
object value = info.GetValue(location, null);
FASTSearchRuntime runtime = value as FASTSearchRuntime;
if (null != runtime)
{
// This is a FAST Search runtime. We can now enable FQL.
runtime.EnableFQL = true;
break;
}
}
}
}
By the way, another limitation of my approach is that using Reflection requires full trust CAS policy level. That said, finally we have arrived at our objective – we can set the flag on the FASTSearchRuntime, and it will understand our FQL queries. Our extended search results web part will show results as directed by the query (in the attached source code it uses XRANK) and leverage presentation richness of the CoreResultsWebPart.
Another solution is to create your own Search Location which wraps logic for the fql. You could for example use the static query as a template for the actual query term. All in all more code, but maybe cleaner architectural wise.
ReplyDeleteThis has other side issues with the oob webparts as mentioned in my blog post (http://techmikael.blogspot.com/2010/11/why-enterprise-search-web-parts-are.html), but you get away with not modifying the web parts.
Amazing post..I like this kind of blog because it was very interesting and informative.Thank you.
ReplyDeleteEnterprise search
Thanks a lot! Ivan for this great post, it helped a lot. Have a query, created this Extended CoreResult Web part as you described, its working fine for 'Wildcard' but while combining those Advanced Search keywords ( for eg ALL() ) its is not handling.
ReplyDeleteIs this possible to use by using both the wildcard search and Advanced Search filters together ? It would be great for me if you can reply early as you can. Thanks once again
Thanks & Regards
Siji.S
connectsiji@gmail.com
We are running into an issue where we have extended the OOB coresearchresults webpart to use FQL & now our "did you mean" are screwed up..
ReplyDeleteThe did you means come back as "Did you mean? string("abc",mode="simpleall",annotation_class="user")" with the FQL part as hyper link.
Have you happen to see this kind of behaviour & do you have any suggestions to counter this ?
Any help would be greatly appreciated!
Thanks,
Ashwani
Hi Ashwani,
ReplyDeletewe face the same problem with scrwed query suggestions, did you mean and search statistics due to usage of extended query which are not user generated. I've deeply analysed how and where logging of queries and clicks occurs, it seems not feasible to influence on it and modify query string at logging time.
What I will try is to write a database "clean up" utility that will exclude "bad" terms from query logs.
Hi,
ReplyDeleteI got a problem . I have implemented this post but my webpart is not showing anything.
Although i see search statistics and pagination displaying numbers that the result has been feteched.
Please reply
Same issue here. No results...
DeleteSame thing here, no results.
Delete