如何执行相当复杂的RavenDB查询并在结果中包含Lucene分数?

时间:2012-11-07 13:40:56

标签: jquery razor ravendb

说我有以下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; }
}

现在我想执行搜索,找到完全符合以下条件的所有用户:

  • IsMentor等于 true
  • IsUnavailable等于 false
  • Id不等于单个被排除的用户(执行此操作的用户) 搜索)

我还希望结果完全或部分匹配以下条件,但仅在提供搜索词的情况下,否则我希望忽略约束。

  • JobRole = [ value ]
  • ExpertiseAreas包含[ value-1 value-2 value-n ]中的项目
  • OrganisationalAreas包含[ value-1 value-2 中的项目, 值正

从此查询返回的用户列表可能并非都符合条件。有些人会比其他人更好。所以我想根据它们的匹配程度来订购我的结果。

当我显示结果时,我希望每个结果都有一个星级(1-5),表示用户与搜索匹配的程度。

我花了几天时间研究如何做到这一点。所以我现在回答我自己的问题,希望能为你节省一些力气。答案当然不是完美的,所以,如果你能改进它,请这样做。

1 个答案:

答案 0 :(得分:17)

首先,我需要一个包含我将要搜索的所有字段的RavenDB索引。这很容易。

<强>索引

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
        });
    }
}

接下来我需要一个服务方法来执行查询。这就是所有魔法发生的地方。

搜索服务

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);
}

请注意,在下面的代码段中,我编写了我自己的Lucene Query字符串生成器。事实上,我确实写了这个,这很美,但后来我发现RavenDB有一个很多更好的流畅界面来构建动态查询。因此,请保存您的眼泪,并从一开始就使用本机查询界面。

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());

这使用了一些我认为通常有用的扩展方法:

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(',');
}

然后我们得到了每个结果的Relevance。请记住,我希望我的结果显示1到5颗星,因此我希望我的相关性值在此范围内进行标准化。为此,我必须找出最大相关性,在这种情况下是列表中第一个用户的值。这是因为如果您没有指定排序顺序,Raven会自动神奇地按相关性对结果进行排序 - 非常方便。

if (mentors.Count > 0)
{
    var max = db.GetRelevance(mentors[0]);
    mentors.ForEach(mentor =>
                    mentor.Relevance = Math.Floor((db.GetRelevance(mentor)/max)*5));
}

提取相关性依赖于另一种扩展方法,该方法从ravendb文档的元数据中提取lucene得分,如下所示:

public static double GetRelevance<T>(this IDocumentSession db, T candidate)
{
    return db
        .Advanced
        .GetMetadataFor(candidate)
        .Value<double>("Temp-Index-Score");
}

最后,我们使用新的Tuple小部件返回结果列表以及查询统计信息。如果你和我一样,之前没有使用Tuple,那么结果是一种简单的方法,可以在不使用out参数的情况下从方法中发回多个值。而已。因此,定义您的方法返回类型,然后使用'Tuple.Create()',如下所示:

public static Tuple<List<User>, RavenQueryStatistics> FindMentors(...)
{
    ...
    return Tuple.Create(mentors, stats);
}

这就是查询。

但是我提到的这个很酷的星级呢?好吧,因为我是那种想要棒上月球的编码器,我使用了一个名为raty的好jQuery插件,对我来说效果很好。这里有一些HTML5 + razor + jQuery给你的想法:

<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>

真的就是这样。有很多可以咀嚼,有很多需要改进的地方。如果您认为有更好/更有效的方式,请不要退缩。

以下是一些测试数据的屏幕截图:

enter image description here