Nouveau service d'indices pour rechercher hh.ru

Les indices de recherche sont excellents. À quelle frĂ©quence tapons-nous l'adresse complĂšte du site dans la barre d'adresse? Et le nom du produit dans la boutique en ligne? Pour des requĂȘtes aussi courtes, il suffit gĂ©nĂ©ralement de taper quelques caractĂšres si les indices de recherche sont bons. Et si vous n'avez pas vingt doigts ou une vitesse de frappe incroyable, vous les utiliserez sĂ»rement.
Dans cet article, nous parlerons de notre nouveau service d'indices de recherche hh.ru, ce que nous avons fait dans le numéro précédent de la School of Programmers .

L'ancien service avait un certain nombre de problĂšmes:

  • il a travaillĂ© sur des requĂȘtes d'utilisateurs populaires sĂ©lectionnĂ©es Ă  la main;
  • n'a pas pu s'adapter Ă  l'Ă©volution des prĂ©fĂ©rences des utilisateurs;
  • n'a pas pu classer les requĂȘtes qui ne sont pas incluses dans le haut;
  • n'a pas corrigĂ© les fautes de frappe.


Dans le nouveau service, nous avons corrigé ces lacunes (tout en en ajoutant de nouvelles).

Dictionnaire des requĂȘtes populaires


Lorsqu'il n'y a aucun indice, vous pouvez sĂ©lectionner manuellement les requĂȘtes N des utilisateurs et gĂ©nĂ©rer des indices Ă  partir de ces requĂȘtes en utilisant l'occurrence exacte des mots (avec ou sans ordre). C'est une bonne option - elle est facile Ă  mettre en Ɠuvre, donne une bonne prĂ©cision des invites et ne rencontre pas de problĂšmes de performances. Pendant longtemps, notre conseil a fonctionnĂ© comme ça, mais un inconvĂ©nient important de cette approche est le caractĂšre incomplet de l'Ă©mission.

Par exemple, la demande "dĂ©veloppeur javascript" ne faisait pas partie de cette liste. Par consĂ©quent, lorsque nous saisissons "heures javascript", nous n'avons rien Ă  afficher. Si nous complĂ©tons la demande, en ne prenant en compte que le dernier mot, nous verrons "bricoleur javascript" en premier lieu. Pour la mĂȘme raison, il ne sera pas possible d'implĂ©menter une correction d'erreur plus difficile que l'approche standard avec la recherche des mots les plus proches par distance Damerau-Levenshtein.

ModĂšle de langage


Une autre approche consiste Ă  apprendre Ă  Ă©valuer les probabilitĂ©s de requĂȘtes et Ă  gĂ©nĂ©rer les suites les plus probables pour une requĂȘte utilisateur. Pour ce faire, utilisez des modĂšles de langage - une distribution de probabilitĂ© sur un ensemble de sĂ©quences de mots.

word_count

Étant donnĂ© que les demandes des utilisateurs sont gĂ©nĂ©ralement courtes, nous n'avons mĂȘme pas essayĂ© les modĂšles de langage de rĂ©seau neuronal, mais nous nous sommes limitĂ©s Ă  n-gram:

P(w1 dotswm)= prodmi=1P(wi|w1 dotswi−1) approx prodmi=1P(wi|wi−(n−1) pointswi−1)



En tant que modÚle le plus simple, nous pouvons prendre la définition statistique de la probabilité, puis

P(wi|w1 dotswi−1)= fraccount(w1 dotswi)count(w1 dotswi−1)



Cependant, un tel modĂšle ne convient pas pour Ă©valuer des requĂȘtes qui n'Ă©taient pas dans notre Ă©chantillon: si nous n'avons pas observĂ© le `` Java Developer Junior '', il s'avĂšre que

P( textdĂ©veloppeurjuniorjava)= fraccount( textdĂ©veloppeurjuniorjava)count( textdĂ©veloppeurjunior)=0

ééé



Pour résoudre ce problÚme, vous pouvez utiliser différents modÚles de lissage et d'interpolation. Nous avons utilisé Backoff:

Pbo(wn|w1 dotswn−1)= begincasesP(wn|w1 dotswn−1),count(w1 dotswn−1)>0 alphaPbo(wn|w2 dotswn−1),count(w1 dotswn−1)=0 endcases


 alpha= fracP(w1 dotswn−1)1− sumwPbo(w|w2 dotswn−1)


OĂč P est la probabilitĂ© lissĂ©e w1...wn−1 (nous avons utilisĂ© le lissage de Laplace):

P(wn|w1 pointswn−1)= fraccount(wn)+ deltacount(w1 dotswn−1)+ delta|V|


oĂč V est notre dictionnaire.

Génération d'options


Nous sommes donc en mesure d'Ă©valuer la probabilitĂ© d'une demande particuliĂšre, mais comment gĂ©nĂ©rer ces mĂȘmes demandes? Il est judicieux de procĂ©der comme suit: laisser l'utilisateur entrer une requĂȘte w1...wn , les requĂȘtes qui nous conviennent peuvent ĂȘtre trouvĂ©es Ă  partir de la condition

w1 dotswm= undersetwn+1 dotswm inVargmaxP(w1 dotswnwn+1 dotswm)



Bien sĂ»r, le tri |V|m−n,m=1 pointsM Il n'est pas possible de sĂ©lectionner les meilleures options pour chaque demande entrante, nous utiliserons donc la recherche de faisceaux . Pour notre modĂšle de langage n-gram, cela se rĂ©sume Ă  l'algorithme suivant:

def beam(initial, vocabulary): variants = [initial] for i in range(P): candidates = [] for variant in variants: candidates.extends(generate_candidates(variant, vocabulary)) variants = sorted(candidates)[:N] return candidates def generate_candidates(variant, vocabulary): top_terms = [] #         1, 2, ... n  for n0 in range(n): top_n = sorted(vocabulary, key=lambda c: P(|variant[-n0:]) top_terms.extends(top_n) candidates = [variant + [term] for term in top_terms] #       candidates = sorted(candidates, key=lambda v: P(variant))[:N] return candidates 

recherche de faisceau

Ici, les nƓuds surlignĂ©s en vert sont les derniĂšres options sĂ©lectionnĂ©es, le nombre devant le nƓud wn - probabilitĂ© P(wn|wn−1) , aprĂšs le nƓud - P(w1...wn) .

C'est devenu beaucoup mieux, mais dans generate_candidates, vous devez obtenir rapidement N meilleurs termes pour un contexte donnĂ©. Dans le cas du stockage uniquement des probabilitĂ©s de n-grammes, nous devons parcourir l'intĂ©gralitĂ© du dictionnaire, calculer les probabilitĂ©s de toutes les phrases possibles, puis les trier. De toute Ă©vidence, cela ne dĂ©collera pas pour les requĂȘtes en ligne.

Bore pour les probabilités


Pour obtenir rapidement le meilleur N dans les variantes de probabilitĂ© conditionnelle de la continuation de la phrase, nous avons utilisĂ© le bore en termes. Dans le nƓud w1 Ă w2 coefficient enregistrĂ©  alpha , valeur P(w2|w1) et triĂ©s par probabilitĂ© conditionnelle P( bullet|w1w2) liste des termes w3 avec P(w3|w1w2) . Le terme spĂ©cial eos marque la fin d'une phrase.
trie

Mais il y a une nuance


Dans l'algorithme dĂ©crit ci-dessus, nous supposons que tous les mots de la requĂȘte ont Ă©tĂ© complĂ©tĂ©s. Cependant, ce n'est pas vrai pour le dernier mot que l'utilisateur saisit en ce moment. Nous devons Ă  nouveau parcourir tout le dictionnaire pour continuer le mot en cours de saisie. Pour rĂ©soudre ce problĂšme, nous utilisons un bore symbolique, dans les nƓuds dont nous stockons M termes triĂ©s par la probabilitĂ© unigramme. Par exemple, cela ressemblera Ă  notre bor pour java, junior, jupyter, javascript avec M = 3:

trie

Ensuite, avant de commencer la recherche de faisceaux, nous trouvons les M meilleurs candidats pour continuer le mot actuel wn et sĂ©lectionner les N meilleurs candidats pour P(w1 pointswn) .

Typos


Eh bien, nous avons construit un service qui vous permet de donner de bons conseils pour une demande d'utilisateur. Nous sommes mĂȘme prĂȘts pour de nouveaux mots. Et tout irait bien ... Mais les utilisateurs font attention et ne changent pas les claviers hfcrkflre.

Comment résoudre ça? La premiÚre chose qui me vient à l'esprit est la recherche de corrections en trouvant les options les plus proches pour la distance Damerau-Levenshtein, qui est définie comme le nombre minimum d'insertion / suppression / remplacement d'un caractÚre ou transposition de deux caractÚres voisins nécessaires pour en obtenir un autre à partir d'une ligne. Malheureusement, cette distance ne prend pas en compte la probabilité d'un remplacement particulier. Ainsi, pour le mot saisi «sapeur», nous obtenons que les options «collecteur» et «soudeur» sont équivalentes, bien qu'il semble intuitivement qu'elles signifient le deuxiÚme mot.

Le deuxiĂšme problĂšme est que nous ne prenons pas en compte le contexte dans lequel l'erreur s'est produite. Par exemple, dans la requĂȘte «sapeur de commandes», nous prĂ©fĂ©rons toujours l'option «collecteur» plutĂŽt que «soudeur».

Si vous abordez la tĂąche de corriger les fautes de frappe d'un point de vue probabiliste, il est tout Ă  fait naturel d'arriver Ă  un modĂšle de canal bruyant :

  1. ensemble alphabet  Sigma ;
  2. ensemble de toutes les lignes de fuite  Sigma∗ sur lui;
  3. beaucoup de lignes qui sont des mots corrects D subseteq Sigma∗ ;
  4. distributions donnĂ©es P(s|w) oĂč s in Sigma∗,w inD .

Ensuite, la tĂąche de correction est dĂ©finie comme la recherche du mot correct w pour l'entrĂ©e s. Selon la source de l'erreur, mesurez P il peut ĂȘtre construit de diffĂ©rentes maniĂšres, dans notre cas, il est sage d'essayer d'estimer la probabilitĂ© de fautes de frappe (appelons-les remplacements Ă©lĂ©mentaires) Pe(t|r) , oĂč t, r sont des n-grammes symboliques, puis Ă©valuent P(s|w) comme la probabilitĂ© d'obtenir s de w par les remplacements Ă©lĂ©mentaires les plus probables.

Soit Partn(x) - fractionner la chaßne x en n sous-chaßnes (éventuellement zéro). Le modÚle de Brill-Moore implique le calcul de la probabilité P(s|w) comme suit:

P (s | w) \ approx \ max_ {R \ in Part_n (s)} T \ in Part_n (s)} \ prod_ {i = 1} ^ {n} P_e (T_i | R_i)


Mais nous devons trouver P(w|s) :

P(w|s)= fracP(s|w)P(w)P(s)=const cdotP(s|w) cdotP(w)


En apprenant Ă  Ă©valuer P (w | s), nous rĂ©soudrons Ă©galement le problĂšme des options de classement avec la mĂȘme distance Damerau-Levenshtein et pouvons prendre en compte le contexte lors de la correction d'une faute de frappe.

Calcul Pe(Ti|Ri)


Pour calculer les probabilitĂ©s de substitutions Ă©lĂ©mentaires, les requĂȘtes des utilisateurs nous aideront Ă  nouveau: nous composerons des paires de mots (s, w) qui

  1. fermer Ă  Damerau-Levenshtein;
  2. l'un des mots est plus courant que les autres N fois.

Pour de telles paires, nous considérons l'alignement optimal selon Levenshtein:


Nous composons toutes les partitions possibles de s et w (nous nous sommes limitĂ©s aux longueurs n = 2, 3): n → n, pr → rn, pro → rn, ro → po, m → ``, mm → m, etc. Pour chaque n-gramme, on trouve

Pe(t|r)= fraccount(r tot)count(r)



Calcul P(s|w)


Calcul P(s|w) prend directement O(2|w|+|s|) : nous devons trier toutes les partitions possibles de w avec toutes les partitions possibles de s. Cependant, la dynamique sur le prĂ©fixe peut donner une rĂ©ponse pour O(|w|∗|s|∗n2) oĂč n est la longueur maximale des substitutions Ă©lĂ©mentaires:

d [i, j] = \ begin {cases} d [0, j] = 0 & j> = k \\ d [i, 0] = 0 & i> = k \\ d [0, j] = P (s [0: j] \ espace | \ espace w [0]) & j <k \\ d [i, 0] = P (s [0] \ espace | \ espace w [0: i]) & i <k \\ d [i, j] = \ underset {k, l \ le n, k \ lt i, l \ lt j} {max} (P (s [jl: j] \ space | \ space w [ik: i]) \ cdot d [ik-1, jl-1]) \ end {cases}


Ici, P est la probabilitĂ© de la ligne correspondante dans le modĂšle k-gramme. Si vous regardez de plus prĂšs, il est trĂšs similaire Ă  l'algorithme de Wagner-Fisher avec Ă©crĂȘtage Ukkonen. À chaque Ă©tape, nous obtenons P(w[0:i]|s[0:j]) en Ă©numĂ©rant toutes les corrections w[i−k:i] dans s[j−l:j] sous rĂ©serve de k,l len et le choix du plus probable.

Retour Ă  P(w|s)


Ainsi, nous pouvons calculer P(s|w) . Maintenant, nous devons sĂ©lectionner plusieurs options en maximisant P(w|s) . Plus prĂ©cisĂ©ment, pour la demande d'origine s1s2 dotssn tu dois choisir w1 dotswn oĂč P(w1 pointswn|s1 pointssn) maximum. Malheureusement, un choix honnĂȘte d'options ne correspondait pas Ă  nos exigences de temps de rĂ©ponse (et la date limite du projet touchait Ă  sa fin), nous avons donc dĂ©cidĂ© de nous concentrer sur l'approche suivante:

  1. Ă  partir de la requĂȘte d'origine, nous obtenons plusieurs options en changeant les k derniers mots:
    1. nous corrigeons la disposition du clavier si le terme résultant a une probabilité plusieurs fois supérieure à celle d'origine;
    2. on trouve des mots dont la distance Damerau-Levenshtein ne dépasse pas d;
    3. choisissez parmi les options top-N pour P(s|w) ;
  2. envoyer BeamSearch à l'entrée avec la demande d'origine;
  3. lors du classement des rĂ©sultats, nous actualisons les options obtenues sur  prodk−1i=0P(sn−i|wn−i) .

Pour l'élément 1.2, nous avons utilisé l'algorithme FB-Trie (tri avant et arriÚre), basé sur une recherche floue dans les arbres de préfixe avant et arriÚre. Cela s'est avéré plus rapide que l'évaluation de P (s | w) dans le dictionnaire.

Statistiques de requĂȘte


Avec la construction du modĂšle de langage, tout est simple: nous collectons des statistiques sur les requĂȘtes des utilisateurs (combien de fois nous avons fait une demande pour une phrase donnĂ©e, combien d'utilisateurs, combien d'utilisateurs enregistrĂ©s), nous avons divisĂ© les demandes en n-grammes et construit des fraises. Plus compliquĂ© avec le modĂšle d'erreur: au minimum, un dictionnaire des bons mots est nĂ©cessaire pour le construire. Comme mentionnĂ© ci-dessus, pour sĂ©lectionner les paires d'entraĂźnement, nous avons utilisĂ© l'hypothĂšse que ces paires devraient ĂȘtre proches dans la distance Damerau-Levenshtein, et l'une devrait se produire plus souvent que l'autre plusieurs fois.

Mais les donnĂ©es sont encore trop bruyantes: tentatives d'injection xss, mise en page incorrecte, texte alĂ©atoire du presse-papiers, utilisateurs expĂ©rimentĂ©s avec des requĂȘtes "programmeur c pas 1c", requĂȘtes du chat qui sont passĂ©es par le clavier .

Par exemple, qu'avez-vous essayé de trouver par une telle demande?


Par conséquent, pour effacer les données source, nous avons exclu:

  • termes de basse frĂ©quence;
  • Contenant des opĂ©rateurs de langage de requĂȘte
  • vocabulaire obscĂšne.

Ils ont également corrigé la disposition du clavier, vérifié les mots des textes des postes vacants et ouvert les dictionnaires. Bien sûr, il n'a pas été possible de tout réparer, mais ces options sont généralement soit complÚtement coupées, soit situées en bas de la liste.

En prod


Juste avant la protection du projet, ils ont lancĂ© un service en production pour les tests internes, et aprĂšs quelques jours - pour 20% des utilisateurs. Dans hh.ru, toutes les modifications importantes pour les utilisateurs passent par un systĂšme de tests AB , ce qui nous permet non seulement d'ĂȘtre sĂ»r de la signification et de la qualitĂ© des modifications, mais aussi de trouver des erreurs .

métrique

La mesure du nombre moyen de recherches Ă  partir de la requĂȘte pour les candidats s'est amĂ©liorĂ©e (passant de 0,959 Ă  1,1355), et la part des recherches Ă  partir de toutes les requĂȘtes de recherche est passĂ©e de 12,78% Ă  15,04%. Malheureusement, les principales mesures du produit n'ont pas augmentĂ©, mais les utilisateurs ont certainement commencĂ© Ă  utiliser plus de conseils.

En fin de compte


Il n'y avait pas de place pour une histoire sur les processus de l'Ă©cole, les autres modĂšles testĂ©s, les outils que nous avons Ă©crits pour les comparaisons de modĂšles et les rĂ©unions oĂč nous avons dĂ©cidĂ© quelles fonctionnalitĂ©s dĂ©velopper afin de rĂ©aliser une dĂ©mo intermĂ©diaire. Regardez les dossiers de l'Ă©cole passĂ©e , laissez une demande sur https://school.hh.ru , remplissez des tĂąches intĂ©ressantes et venez Ă©tudier. Soit dit en passant, le service de vĂ©rification des tĂąches a Ă©galement Ă©tĂ© effectuĂ© par les diplĂŽmĂ©s de l'ensemble prĂ©cĂ©dent.

Que lire?


  1. Introduction au modĂšle de langage
  2. ModĂšle Brill-Moore
  3. Fb-trie
  4. Qu'advient-il de votre requĂȘte de recherche

Source: https://habr.com/ru/post/fr464415/


All Articles