Innanzitutto ho bisogno di un indice RavenDB che includa tutti i campi su cui cercherò. Questo è facile.
Indice
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
});
}
}
Poi ho bisogno di un metodo di servizio per eseguire la query. È qui che avviene tutta la magia.
servizio di ricerca
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);
}
Nota nel frammento di codice di seguito, non ho non scritto il mio proprio generatore stringa Lucene Query. In effetti, ho scritto questo, ed era una cosa di bellezza, ma poi ho scoperto che RavenDB ha un'interfaccia fluida molto migliore per la creazione di query dinamiche.Quindi salva le tue lacrime e usa l'interfaccia di query nativa fin dall'inizio.
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);
Poi si può vedere che sto controllando se la ricerca ha passato in tutti i valori per gli elementi condizionali della query, ad esempio:
if (expertiseAreas.HasValue())
query = query
.AndAlso()
.WhereIn("ExpertiseAreas", expertiseAreas.SafeSplit());
Questo utilizza un paio di metodi di estensione che ho trovato generalmente utile:
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(',');
}
Poi abbiamo il bit che funziona la Relevance
di ogni risultato. Ricorda che voglio che i miei risultati visualizzino da 1 a 5 stelle, quindi voglio che il mio valore di Rilevanza sia normalizzato all'interno di questo intervallo. Per fare questo devo scoprire la massima pertinenza, che in questo caso è il valore del primo utente nella lista. Questo perché Raven ordina automaticamente i risultati in base alla pertinenza se non si specifica diversamente un ordinamento: molto utile.
if (mentors.Count > 0)
{
var max = db.GetRelevance(mentors[0]);
mentors.ForEach(mentor =>
mentor.Relevance = Math.Floor((db.GetRelevance(mentor)/max)*5));
}
Estrazione del Rilevanza si basa su un altro metodo di estensione che tira il punteggio Lucene dai metadati del documento RavenDB, in questo modo:
public static double GetRelevance<T>(this IDocumentSession db, T candidate)
{
return db
.Advanced
.GetMetadataFor(candidate)
.Value<double>("Temp-Index-Score");
}
Infine ritorniamo alla lista dei risultati insieme con le statistiche delle query usando il nuovo widget Tuple
. Se, come me, non hai mai utilizzato lo Tuple
in precedenza, risulta essere un modo semplice per inviare più di un valore da un metodo senza utilizzare i parametri out
. Questo è tutto. Quindi, definire il tipo di ritorno del metodo e poi usare 'Tuple.Create()', in questo modo:
public static Tuple<List<User>, RavenQueryStatistics> FindMentors(...)
{
...
return Tuple.Create(mentors, stats);
}
E cioè che per la query.
Ma che dire di quella fredda stella che ho menzionato? Bene visto che sono il tipo di programmatore che vuole il moon-on-a-stick, ho usato un bel plugin jQuery chiamato raty che ha funzionato bene per me. Ecco alcuni HTML5 + rasoio + jQuery per dare l'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>
E che in realtà è. Molto da masticare, molto da migliorare. Non trattenere se pensi che ci sia un modo migliore/più efficiente.
Ecco uno screenshot di alcuni dati di test:
Grazie per mettere la ricerca in questo ed inviarla a pila. L'ho trovato abbastanza utile. – jamesamuir