2012-11-07 8 views
8

Dire che ho il seguente UserCome posso eseguire una query RavenDB ragionevolmente complessa e includere il punteggio Lucene nei risultati?

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

Ora voglio eseguire una ricerca che troverà tutti gli Utenti che corrispondono pienamente i seguenti criteri:

  • IsMentor uguale vero
  • IsUnavailable uguale a false
  • Id non è uguale a un singolo, l'utente escluso (la persona che fa la ricerca )

voglio anche i risultati per abbinare tutto o in parte i seguenti criteri, ma solo se i termini di ricerca sono forniti, altrimenti io voglio il vincolo essere ignorato

  • JobRole = [valore]
  • ExpertiseAreas contiene articoli di [valore-1, valore-2, valore-n]
  • OrganisationalAreas contiene articoli di [a valore 1, valore-2, valore-n]

L'elenco di utenti restituiti da questa query potrebbe non corrispondere tutti ugualmente ai criteri. Alcuni saranno migliori di altri. Quindi voglio ordinare i miei risultati per quanto bene corrispondono.

Quando visualizzo i risultati, desidero che a ogni risultato venga assegnata una valutazione a stella (1-5) che indichi quanto l'utente ha adattato la ricerca.

Ho trascorso alcuni giorni a capire come farlo. Quindi ora risponderò alla mia domanda e spero che vi risparmi qualche sforzo. Ovviamente la risposta non sarà perfetta, quindi per favore, se puoi migliorarla, fallo.

risposta

16

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:

enter image description here

+1

Grazie per mettere la ricerca in questo ed inviarla a pila. L'ho trovato abbastanza utile. – jamesamuir

Problemi correlati