First I need a RavenDB Index, which includes all the fields that I will look for. It is easy.
Index
public class User_FindMentor : AbstractIndexCreationTask<User> { public User_FindMentor() { Map = users => users.Select(user => new { user.Id, user.IsUnavailable, user.IsMentor, user.OrganisationalAreas, user.ExpertiseAreas, user.JobRole }); } }
Next, I need a service method to execute the request. All magic happens here.
Search service
public static Tuple<List<User>, RavenQueryStatistics> FindMentors( IDocumentSession db, string excludedUserId = null, string expertiseAreas = null, string jobRoles = null, string organisationalAreas = null, int take = 50) { RavenQueryStatistics stats; var query = db .Advanced .LuceneQuery<User, RavenIndexes.User_FindMentor>() .Statistics(out stats) .Take(take) .WhereEquals("IsMentor", true).AndAlso() .WhereEquals("IsUnavailable", false).AndAlso() .Not.WhereEquals("Id", excludedUserId); if (expertiseAreas.HasValue()) query = query .AndAlso() .WhereIn("ExpertiseAreas", expertiseAreas.SafeSplit()); if (jobRoles.HasValue()) query = query .AndAlso() .WhereIn("JobRole", jobRoles.SafeSplit()); if (organisationalAreas.HasValue()) query = query .AndAlso() .WhereIn("OrganisationalAreas", organisationalAreas.SafeSplit()); var mentors = query.ToList(); if (mentors.Count > 0) { var max = db.GetRelevance(mentors[0]); mentors.ForEach(mentor => mentor.Relevance = Math.Floor((db.GetRelevance(mentor)/max)*5)); } return Tuple.Create(mentors, stats); }
Note in the code snippet below, I did not write my own Lucene Query line generator. In fact, I wrote this and it was beautiful, but then I discovered that RavenDB has a much smoother interface for building dynamic queries. So keep your tears and use your own query interface from the very beginning.
RavenQueryStatistics stats; var query = db .Advanced .LuceneQuery<User, RavenIndexes.User_FindMentor>() .Statistics(out stats) .Take(take) .WhereEquals("IsMentor", true).AndAlso() .WhereEquals("IsUnavailable", false).AndAlso() .Not.WhereEquals("Id", excludedUserId);
Further you can see that I am checking if the search passed in any values ββfor the conditional elements of the query, for example:
if (expertiseAreas.HasValue()) query = query .AndAlso() .WhereIn("ExpertiseAreas", expertiseAreas.SafeSplit());
This uses several extension methods that I found generally useful:
public static bool HasValue(this string candidate) { return !string.IsNullOrEmpty(candidate); } public static bool IsEmpty(this string candidate) { return string.IsNullOrEmpty(candidate); } public static string[] SafeSplit(this string commaDelimited) { return commaDelimited.IsEmpty() ? new string[] { } : commaDelimited.Split(','); }
Then we get a bit that processes the Relevance each result. Remember that I want my results to display from 1 to 5 stars, so I want the Relevance value to be normalized in this range. To do this, I must find out the maximum relevance, which in this case is the value of the first user in the list. This is due to the fact that the raven automatically adjusts the results according to relevance, if you did not specify the sort order - it is very convenient.
if (mentors.Count > 0) { var max = db.GetRelevance(mentors[0]); mentors.ForEach(mentor => mentor.Relevance = Math.Floor((db.GetRelevance(mentor)/max)*5)); }
The relevancy extraction is based on yet another extension method, which draws the lucene score from the metadata of the ravendb document, for example:
public static double GetRelevance<T>(this IDocumentSession db, T candidate) { return db .Advanced .GetMetadataFor(candidate) .Value<double>("Temp-Index-Score"); }
Finally, we return a list of results along with query statistics using the new Tuple widget. If you, like me, have not used Tuple before, this is an easy way to send more than one value from a method without using out parameters. This is it. Therefore, determine the type of the returned method, and then use "Tuple.Create ()", for example:
public static Tuple<List<User>, RavenQueryStatistics> FindMentors(...) { ... return Tuple.Create(mentors, stats); }
And this is for the request.
But what about the cool star rating that I mentioned? Well, since I'm such an encoder that wants a moon on a stick, I used a beautiful jQuery plugin called raty , which worked great for me. Here are some HTML5 + razor + jQuery to give you an idea:
<div id="find-mentor-results"> @foreach (User user in Model.Results) { ...stuff <div class="row"> <img id="headshot" src="@user.Headshot" alt="headshot"/> <h5>@user.DisplayName</h5> <div class="star-rating" data-relevance="@user.Relevance"></div> </div> ...stuff } </div> <script> $(function () { $('.star-rating').raty({ readOnly: true, score: function () { return $(this).attr('data-relevance'); } }); }); </script>
And indeed it is. A lot to chew, a lot to improve. Do not hold back if you think there is a better / more efficient way.
Here is a screenshot of some test data:
