Wednesday, August 20, 2008

SolrNet with faceting support

I finally added support for facets in SolrNet. There are basically two kinds of facet queries:

  1. querying by field
  2. arbitrary facet queries

Querying by field

ISolrOperations<TestDocument> solr = ...
ISolrQueryResults<TestDocument> r = solr.Query("brand:samsung", new QueryOptions {
    FacetQueries = new ISolrFacetQuery[] {
        new SolrFacetFieldQuery("category")
    }
});

Yeah, kind of verbose, right? The DSL makes it shorter:

ISolrQueryResults<TestDocument> r = Solr.Query<TestDocument>().By("brand").Is("samsung").WithFacetField("id").Run();

To get the facet results, you get a property FacetFields in ISolrQueryResults<T> that is a IDictionary<string, ICollection<KeyValuePair<string, int>>>. The key of this dictionary is the facet field you have queried. The value is a collection of pairs where the key is the value found and the value is the count of ocurrences. Sounds complex? It's not. Let's see an example:

Let's assume that eBay used SolrNet to do its queries (please bear with me :-) ). Let's say a user enters the category Maps, Atlases & Globes, so you want the items within that category, as well as the item count on each subcategory ("Europe", "India", etc) that shows up as "Narrow your results". You could express such a query like this:

var results = Solr.Query<EbayItem>().By("category").Is("maps-atlases-globes")
    .WithFacetField("subcategory")
    .Run();

Now to print the subcategory count:

foreach (var facet in results.FacetFields["subcategory"]) {
    Console.WriteLine("{0}: {1}", facet.Key, facet.Value);
}

Which would print something like this:

United States (Pre-1900): 2123
Europe: 916
World Maps: 650
...

and so on. See? Told you it wasn't hard :-)

Note that by default, Solr orders facet field results by count (descending), which makes sense since most of the time you want the most populated/important terms first. If you want to override that:

ISolrQueryResults<EbayItem> results = Solr.Query<EbayItem>().By("category").Is("maps-atlases-globes")
    .WithFacetField("subcategory").DontSortByCount()
    .Run();

There are other options for facet field queries, I copied the docs from the official Solr documentation.

Arbitrary facet queries

Support for arbitrary queries is not very nice at the moment, but it works:

var priceLessThan500 = "price:[* TO 500]";
var priceMoreThan500 = "price:[500 TO *]";
var results = Solr.Query<TestDocument>().By("category").Is("maps-atlases-globes")
    .WithFacetQuery(priceLessThan500)
    .WithFacetQuery(priceMoreThan500)
    .Run();
Then results.FacetQueries[priceLessThan500] and results.FacetQueries[priceMoreThan500] get you the respective result counts.

Code is hosted at googlecode

Sunday, July 20, 2008

Facebook login WTF

Yesterday I was about to log into Facebook and I noticed it was taking too long to load the page... I opened Firebug and saw this:

 

facebook_login

 

Wow. Nice stairs, but it really hurts performance.

Tuesday, July 15, 2008

Castle Facility for SolrSharp

Some months ago I wrote SolrNet, an interface to Solr, mainly because I thought SolrSharp (the standard Solr interface for .net) was too verbose and IoC-unfriendly. I still think that way, but I always wondered what it would take to integrate it to Windsor (or any other IoC container, for that matter).

And the answer is... 241 lines of code (as per "wc -l *.cs"). That's it. That's all it took to write some interfaces and adapters and then wrap them in a Castle facility. That should teach me to try a bit harder next time! :-)

SolrNet was designed with IoC in mind, so integration is just a matter of registering a component:

<component id="solr" service="SolrNet.ISolrOperations`1, SolrNet" type="SolrNet.SolrServer`1, SolrNet">
    <parameters>
        <serverURL>http://localhost:8983/solr</serverURL>
    </parameters>
</component>

Code is here. Note that it's just a proof of concept, I barely tested it!

Monday, July 14, 2008

Y-combinator and LINQ

Today I finally found an excuse to use Wes Dyer's implementation of the Y-combinator in C#:

public delegate Func<A, R> Recursive<A, R>(Recursive<A, R> r)

public static Func<A, R> Y<A, R>(Func<Func<A, R>, Func<A, R>> f) {
Recursive<A, R> rec = r => a => f(r(r))(a);
return rec(rec);
}

I used it to recursively get all the files in a given directory, in a one-liner:
var RecGetFiles = Y<string, IEnumerable<string>>(f => d => Directory.GetFiles(d).Concat(Directory.GetDirectories(d).SelectMany(f)))
Nice, isn't it? Sample usage:
foreach (var f in RecGetFiles(Directory.GetCurrentDirectory()))
Console.WriteLine(f);

It's probably not a good idea, though, to use this in production code, as it's not really necessary (just use regular recursion) and it harms readability.

A few less strings in MonoRail


In the same spirit as my previous post, I wrote a couple of extensions methods to avoid strings when redirecting in Castle MonoRail.
Let's say you have this controller:
public class HomeController : SmartDispatcherController
{
public void Index()
{
}

public void SaveInformation(String name, int age, DateTime dob)
{
// work work work

// Send the user back to the index
RedirectToAction("index");
}
}

Now you can write the redirect like this:

this.RedirectToAction(c => c.Index());

It also works with parameters, i.e. if you have an action like this:

public void Index(int a) {}

you can do:

this.RedirectToAction(c => Index(1));

and it will redirect to /home/index.castle?a=1 (or whatever extension you're using)

It even works with objects and [DataBind] (with some limitations, see below):

public class Properties {
public int SomeValue { get; set; }
}

public class SmartTest2Controller : SmartDispatcherController {
public void DataBinding([DataBind("prefi")] Properties prop) {}
public void Index() {
this.RedirectToAction(c => c.DataBinding(new Properties {SomeValue = 22}));
}
}

Which will redirect to /SmartTest2/DataBinding.castle?prefi.SomeValue=22

There's also support for [ARDataBind] and [ARFetch]:

    public class ARController : ARSmartDispatcherController {
public void Index() {
this.RedirectToAction(c => c.ARFetchAction(new Entity { PK = 5, Name = "Robert Duvall" }));
this.RedirectToAction(c => c.ARAction(new Entity { PK = 4, Name = "John Q" }));
}
public void ARAction([ARDataBind("pre")] Entity e) {}
public void ARFetchAction([ARFetch("pre")] Entity e) { }
}

Which would respectively redirect to /AR/ARFetchAction.castle?pre=5 and /AR/ARAction.castle?pre.PK=4&pre.Name=John+Q
(I was watching John Q while writing this :-) )

ActiveRecord support is implemented in another assembly, to cleanly separate the dependencies. When using the ActiveRecord extensions, you have to initialize it in your Application_Start():

Setup.ForAll(new DefaultExtensionsProvider {ExtUtils = new ARUtils()});
This instructs the extension methods to use the ActiveRecord helpers.

Code is here, let me know your opinion...

Caveats / limitations (for now):
  • Doesn't work with nested object databinding
  • Doesn't work with arrays of objects
  • Works on Castle RC3. Trunk has been refactored a lot regarding controllers since RC3, so this code would have to be adapted (extending IRedirectSupport instead of Controller and such)

UPDATE 5/16/2009: I just found out there already was a similar project on castle contrib.