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.

Ă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 = []

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.

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:

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 :
- ensemble alphabet Sigma ;
- ensemble de toutes les lignes de fuite Sigmaâ sur lui;
- beaucoup de lignes qui sont des mots corrects D subseteq Sigmaâ ;
- 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
- fermer Ă Damerau-Levenshtein;
- 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:
- Ă partir de la requĂȘte d'origine, nous obtenons plusieurs options en changeant les k derniers mots:
- nous corrigeons la disposition du clavier si le terme résultant a une probabilité plusieurs fois supérieure à celle d'origine;
- on trouve des mots dont la distance Damerau-Levenshtein ne dépasse pas d;
- choisissez parmi les options top-N pour P(s|w) ;
- envoyer BeamSearch à l'entrée avec la demande d'origine;
- 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 .

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?
- Introduction au modĂšle de langage
- ModĂšle Brill-Moore
- Fb-trie
- Qu'advient-il de votre requĂȘte de recherche