Thursday, November 22, 2007

Introducing SolrNet

UPDATE 2/19/2009: by now most of this is obsolete, please check out more recent releases

Last month I've been working my a** off integrating Solr to our main site. The first step was to find out how to communicate with the Solr server. Naturally, I came to SolrSharp. But I found it to be really IoC-unfriendly: lots of inheritance, no interfaces, no unit-tests, so it would have been a real PITA to integrate it to Castle. So, instead of wrapping it, I built SolrNet.

Before explaining how it works, a disclaimer: I'm a complete newbie to Solr, Lucene and full-text searching in general. The code works on my machine and does what I need it to do for the task that I have at hand. This project is not, and might never be, feature complete like SolrSharp. Currently it doesn't support facets (UPDATE 8/20/08: I added facet support) or highlights, and maybe some other stuff. If you absolutely need those features right now, either use SolrSharp or write a patch for SolrNet. However, the next step in the integration is implementing faceted search, so I will definitely implement facets sooner or later.

Usage

First we have to map the Solr document to a class (Solr supports only one document type per instance at the moment). Let's use a subset of the default schema that comes with the Solr distribution:

 

public class TestDocument : ISolrDocument {
    private ICollection<string> cat;
    private ICollection<string> features;
    private string id;
    private bool inStock;
    private string manu;
    private string name;
    private int popularity;
    private double price;
    private string sku;

    [SolrField("cat")]
    public ICollection<string> Cat {
        get { return cat; }
        set { cat = value; }
    }

    [SolrField("features")]
    public ICollection<string> Features {
        get { return features; }
        set { features = value; }
    }

    [SolrUniqueKey]
    [SolrField("id")]
    public string Id {
        get { return id; }
        set { id = value; }
    }

    [SolrField("inStock")]
    public bool InStock {
        get { return inStock; }
        set { inStock = value; }
    }

    [SolrField("manu")]
    public string Manu {
        get { return manu; }
        set { manu = value; }
    }

    [SolrField("name")]
    public string Name {
        get { return name; }
        set { name = value; }
    }

    [SolrField("popularity")]
    public int Popularity {
        get { return popularity; }
        set { popularity = value; }
    }

    [SolrField("price")]
    public double Price {
        get { return price; }
        set { price = value; }
    }

    [SolrField("sku")]
    public string Sku {
        get { return sku; }
        set { sku = value; }
    } 
}

 

It's just a POCO with a marker interface (ISolrDocument)[1] and some attributes: SolrField maps the attribute to a Solr field and SolrUniqueKey (optional) maps an attribute to a Solr unique key field. Let's add a document (make sure you have a running Solr instance first):

[Test]
public void AddOne() {
    ISolrOperations<TestDocument> solr = new SolrServer<TestDocument>("http://localhost:8983/solr");
    TestDocument doc = new TestDocument();
    doc.Id = "123456";
    doc.Name = "some name";
    doc.Cat = new string[] {"cat1", "cat2"};
    solr.Add(doc);
    solr.Commit();
}

Let's see if the document is there:

[Test]
public void QueryAll() {
    ISolrOperations<TestDocument> solr = new SolrServer<TestDocument>("http://localhost:8983/solr");
    ISolrQueryResults<TestDocument> r = solr.Query("*:*");
    Assert.AreEqual("123456", r[0].Id);
}

For more examples, see the tests.

DSL

Since DSLs are such a hot topic nowadays, I decided to give it a try to see what happened. I just defined the syntax I wanted in a test, then wrote the interfaces to comply to the syntax and chain the methods, then built the implementations for those interfaces. The result is pretty much self-explanatory:

[SetUp]
public void setup() {
    Solr.Connection = new SolrConnection("http://localhost:8983/solr");
}

[Test]
public void QueryById() {    
    ISolrQueryResults<TestDocument> r = Solr.Query<TestDocument>().By("id").Is("123456").Run();
}

[Test]
public void QueryByRange() {
    ISolrQueryResults<TestDocument> r = Solr.Query<TestDocument>().By("id").Between(123).And(456).OrderBy("id", Order.ASC).Run();
}

[Test]
public void DeleteByQuery() {
    Solr.Delete.ByQuery<TestDocument>("id:123456");
}

Run() is the explicit kicker method [1]. The DSL is defined in a separate DLL, in case you don't want/need it. There are some more examples in the tests.

I TDDd most of the project, so the code coverage is near 75%. I'll add the remaining tests if/when I have the time. Of course, as usual, patches/bugfixes are more than welcome :-)

[1] I might drop this requirement in the future.

Wednesday, November 21, 2007

Poor man's javascript testing

I'm sure someone else has done this, being so simple and old tech... But I couldn't find it anywhere, soo...

Let's say you have just finished writing a great, I mean really ground-breaking javascript library, and you call it lib.js:

Array.prototype.clear = function(){
  this.length=0;
}

And you want to test it. But how? Well, you can forget all about JsUnit, Script.aculo.us BDD, Crosscheck or the others, because now you can test your javascript with... cscript!! Yes, just write a little WSF that includes your great library:

<job>
    <script src="lib.js" language="jscript"/>
    <script language="jscript">
        var a = [1,2,3];
        a.clear();
        if (a.length != 0) {
            WScript.Echo("test failed");
            WScript.Quit(1);
        } else {
            WScript.Echo("test passed");
            WScript.Quit(0);
        }
    </script>
</job>

To execute the test, just invoke cscript on the WSF. Isn't it cool? Ok, ok, it's not cool at all, it only works for non-DOM javascript and only tests for internet explorer, BUT it's really simple and you get the potential to write the tests in another language [1], AND it can be easily integrated to a nant build with a <exec> task. In any case, it beats having no tests at all.

PS: Now seriously, go and check out the tools I mentioned above. GO GO!

[1] Doesn't really work most of the times. I tried ActivePython and ActiveRuby, but they don't see the functions exposed in jscript. Not to mention prototype modifications like the example above, since it doesn't make sense to other languages... VBScript seems to cooperate nicely but only for the most basic scenarios.