首先,我需要一个包含我将要搜索的所有字段的 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>
就是这样。有很多需要咀嚼,有很多需要改进。如果您认为有更好/更有效的方法,请不要退缩。
这是一些测试数据的屏幕截图: