How to execute a rather complicated RavenDB query and include the Lucene result in the results?

Let's say I have the following User

 public class User { // ... lots of other stuff public string Id{ get; set; } public double Relevance { get; set; } public bool IsMentor { get; set; } public string JobRole { get; set; } public bool IsUnavailable { get; set; } public List<string> ExpertiseAreas { get; set; } public List<string> OrganisationalAreas { get; set; } } 

Now I want to perform a search in which all users that fully meet the following criteria will be found:

  • IsMentor is true
  • IsUnavailable is false
  • Id not equal to one excluded user (the person performing the search)

I also want the results to fully or partially meet the following criteria, but only if the search terms are specified, otherwise I want the restriction to be ignored.

  • JobRole = [value]
  • ExpertiseAreas contains elements from [value-1, value-2, value-n]
  • OrganisationalAreas contains items from [value-1, value-2, value-n]

The list of users returned from this request may not match the criteria anyway. Some will be better than others. Therefore, I want to order my results, how well they correspond.

When I show my results, I want each result to be assigned a star rating (1-5), which indicates how well the user performed the search.

I spent a few days on how to do this. Therefore, I will now answer my question and hope that you save some effort. Of course, the answer will not be perfect, so please, if you can improve it, do it.

+8
jquery razor ravendb
source share
1 answer

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:

enter image description here

+16
source share

All Articles