8

说我有以下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等于
  • IsUnavailable等于
  • Id不等于单个排除用户(进行搜索的人)

我还希望结果完全或部分匹配以下条件,但前提是提供了搜索词,否则我希望忽略约束。

  • JobRole= [价值]
  • ExpertiseAreas包含来自 [ value-1 , value-2 , value-n ]的项目
  • OrganisationalAreas包含来自 [ value-1 , value-2 , value-n ]的项目

从该查询返回的用户列表可能并不完全符合条件。有些会比其他匹配更好。所以我想根据它们的匹配程度来排序我的结果。

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

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

4

1 回答 1

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 查询字符串生成器。事实上,我确实写了这个,它很漂亮,但后来我发现 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);
}

这就是查询。

但是我提到的那个很酷的星级呢?好吧,因为我是那种想要moon-on-a-stick 的编码员,所以我使用了一个不错的jQuery 插件,叫做raty,它对我来说很好用。这是一些 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>

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

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

在此处输入图像描述

于 2012-11-07T13:40:56.140 回答