Comment fonctionne la recherche

Salut, nom d'utilisateur! Chaque jour, nous sommes confrontés à une recherche de données diverses. Presque tous les sites Web contenant de nombreuses informations ont désormais une recherche. La recherche se fait dans les ordinateurs personnels, dans les téléphones portables, dans divers types de logiciels. Bien sûr, si vous interrogez un développeur sur la recherche en termes de technologie, elasticsearch, lucene ou sphinx vous viendra immédiatement à l'esprit. Aujourd'hui, je veux regarder avec vous «sous le capot» d'une recherche en texte intégral et trouver une premiÚre approximation de son fonctionnement, en utilisant hh.ru comme exemple.

image

Avertissement: cet article n'est pas le seul vrai point de vue et ne sert que de point d'introduction pour une introduction initiale au travail de recherche de texte et quelques options pour la mise en Ɠuvre de ses diffĂ©rentes parties.

Si vous regardez les détails de la recherche, en plus de la partie évidente sous la forme d'une chaßne de recherche, vous pouvez en voir beaucoup plus:

  • indice (elle suggĂšre)
  • compteur de rĂ©sultats de recherche (compteur),
  • diffĂ©rents types de tri (tri),
  • facettes - caractĂ©ristiques groupĂ©es des documents, par exemple, le mĂ©tro sur lequel se trouve une vacance, remplissant Ă©galement la fonction de filtres (filtres),
  • synonymes
  • pagination
  • snippet (snippet) - une petite description du document dans l'Ă©mission,
  • etc.

image

Et tout cela sert un seul objectif - satisfaire le besoin de l'utilisateur de trouver les bonnes informations aussi rapidement et de maniĂšre pertinente que possible. Par exemple, le filtrage est important pour affiner les rĂ©sultats de recherche, dans notre cas, il peut s'agir d'un filtre par expĂ©rience, emplacement ou emploi du candidat. Les facettes sont utiles pour afficher le nombre de postes vacants dans chaque tranche de salaire. Il est Ă©galement important de complĂ©ter les requĂȘtes et les documents par des synonymes afin qu'Ă  la demande du «dĂ©veloppeur Java», ils puissent trouver des documents «DĂ©veloppeur Java».

En plus de la recherche elle-mĂȘme, il y a toujours beaucoup de composants qui facilitent la vie de l'utilisateur: un tuteur qui est responsable de la correction des erreurs, ou un sajest qui invite des requĂȘtes plus appropriĂ©es lorsque vous interagissez avec la barre de recherche. Dans certains cas, il est important de pouvoir reformuler la demande. Par exemple, dĂ©placez une partie de la demande vers des filtres: Ă  partir de la demande «programmeur de Moscou», Moscou peut ĂȘtre retirĂ© dans le filtre par ville.

1. Basique


Maintenant au point. La recherche elle-mĂȘme est divisĂ©e en deux grandes Ă©tapes:

  • indexation (traitement des documents et mise en page selon des structures d'index particuliĂšres, pour que vous puissiez ensuite effectuer rapidement la recherche elle-mĂȘme),
  • recherche (application de filtres, recherche boolĂ©enne, classement, etc.).

1.1. Indexation


Une lĂ©gĂšre digression lyrique. Je prĂ©senterai plus loin le concept de terme - car il est d'usage d'appeler l'unitĂ© minimale d'indexation et de requĂȘte. C'est l'unitĂ© mĂȘme qui sera stockĂ©e dans le dictionnaire d'index. Il peut s'agir d'un mot rĂ©duit Ă  sa forme ou base de mot normale, Ă  son numĂ©ro, Ă  son adresse Ă©lectronique, Ă  sa lettre n-grammes ou Ă  autre chose. En rĂšgle gĂ©nĂ©rale, un terme comprend un champ dans lequel il est indexĂ© ou dans lequel une recherche est effectuĂ©e.

Tout d'abord, vous devez transformer les documents d'entrĂ©e en un ensemble de termes et filtrer les mots vides. Ils peuvent ĂȘtre comme des mots frĂ©quents - prĂ©positions, conjonctions, interjections et autres choses, par exemple, des caractĂšres spĂ©ciaux que nous ne voulons pas rechercher. Pour que la recherche fonctionne avec diffĂ©rentes formes de mots, au cours du processus d'indexation, nous amenons gĂ©nĂ©ralement tous les mots Ă  un Ă©tat de base. Habituellement, l'une des deux procĂ©dures est utilisĂ©e: soit la racine - le processus d'isolement de la base d'un mot (dĂ©veloppement -> dĂ©veloppement), soit la lemmatisation - le processus de mise en forme normale d'un mot (compĂ©tences -> compĂ©tences).

1.2 Structures d'index


La façon la plus courante de reprĂ©senter un index est un index inversĂ©. En fait, il s'agit d'une sorte de table de hachage, oĂč la clĂ© est un terme, et la valeur est une liste de documents (gĂ©nĂ©ralement une liste d'ID de document appelĂ©e liste de publications) dans laquelle ce terme est prĂ©sent. Habituellement, un index inversĂ© se compose de deux parties - un dictionnaire (dictionnaire de termes) et des listes de documents pour chaque terme (liste de publication):

image

De plus, l'index peut contenir des informations sur les positions des termes dans le document (indice de position), qui seront utiles lors de la recherche de termes Ă  une certaine distance, en particulier avec des requĂȘtes phrasales, sur la frĂ©quence des termes, ce qui aidera Ă  classer et Ă  construire un plan de requĂȘte. Mais plus Ă  ce sujet ci-dessous.

1.2.1 Dictionnaire des termes


Le dictionnaire stocke tous les termes qui existent dans l'index et est conçu pour trouver rapidement des liens vers une liste de documents. Il existe plusieurs options pour stocker un dictionnaire:

  • Une table de hachage, oĂč le terme est la clĂ© et la valeur est un lien vers une liste de documents de ce terme.
  • Une liste ordonnĂ©e par laquelle vous pouvez effectuer une recherche par recherche binaire.
  • Arbre de prĂ©fixe (trie).

Le moyen le plus optimal est la derniĂšre option, car Elle prĂ©sente plusieurs avantages. PremiĂšrement, sur un grand nombre de termes, l'arbre des prĂ©fixes occupera beaucoup moins de mĂ©moire, car les parties rĂ©pĂ©tĂ©es des prĂ©fixes ne seront stockĂ©es qu'une seule fois. DeuxiĂšmement, nous avons immĂ©diatement la possibilitĂ© de faire des demandes de prĂ©fixe. Et troisiĂšmement, un tel arbre peut ĂȘtre compressĂ© en combinant des parties non ramifiĂ©es.

image

Bien sĂ»r, un arbre de prĂ©fixe peut ne pas ĂȘtre la seule structure pour stocker des termes dans un index. Par exemple, un arbre de suffixes peut Ă©galement ĂȘtre Ă  proximitĂ©, ce qui Ă  son tour sera plus optimal pour les requĂȘtes avec jokers (requĂȘtes de la forme po * sql).

1.2.2 Liste de diffusion


La liste des documents est une liste ordonnĂ©e d'identifiants de documents, qui permet de faire quelques optimisations avec elle. Habituellement, il stocke en lui-mĂȘme non seulement une liste de documents dans lesquels le terme apparaĂźt, mais aussi les positions (publications) auxquelles il se produit. Cela rĂ©sout plusieurs problĂšmes Ă  la fois: nous savons immĂ©diatement combien de fois un mot apparaĂźt dans un document, nous pouvons faire des phrases et des requĂȘtes avec une certaine distance entre les termes, traverser plusieurs listes de documents Ă  la fois et regarder les positions des termes.

image

Par exemple, dans cette liste par le terme scala dans le 6Ăšme document du titre, le mot apparaĂźt 4 fois, aux positions 2, 15, 18 et 25.

1.2.3 Documents avec plusieurs champs


La plupart du document se compose de plusieurs champs, au moins Ă  partir du nom du document et de son corps. Cela aide lors de la recherche de parties individuelles du document et lorsque le terme est significatif lors du classement (par exemple, le terme apparaissant dans le titre peut ĂȘtre considĂ©rĂ© comme plus significatif).

De plus, dans l'index, généralement non seulement les champs de texte sont stockés, mais aussi les signes de documents, certaines valeurs numériques, etc. Le stockage dans l'index prend généralement la forme de {champ-terme}.

image

Par exemple, si vous prenez un poste vacant, alors il aura plusieurs champs Ă  la fois: nom, description, entreprise, paie, ville et l'expĂ©rience nĂ©cessaire. Cela est nĂ©cessaire pour que l'utilisateur puisse rechercher facilement non seulement par le nom et le texte de l'entreprise, mais aussi filtrer par salaire et expĂ©rience, voir combien de postes vacants sont dans sa ville et dans les villes voisines, ou mĂȘme rechercher des postes vacants pour une entreprise particuliĂšre.

1.3 Compression et optimisation d'index


La vitesse de travail est importante pour la recherche, par conséquent, la plupart des opérations de recherche d'index sont généralement effectuées dans la RAM. Pour ce faire, il est trÚs important d'appliquer un certain nombre d'optimisations à l'index, qui l'adapteront à une taille de mémoire limitée. En plus de cela, un certain nombre d'optimisations sont généralement appliquées, ce qui vous permet de vous déplacer autour de l'index à une vitesse plus élevée lors de la recherche, en ignorant les éléments inutiles.

1.3.1 Compression Delta


Comme nous nous souvenons que la liste des documents par terme (aka liste des publications) est ordonnée, la premiÚre idée de la façon de la compresser est de créer une liste avec les décalages d'ID au lieu de la liste avec l'ID du document par rapport aux précédents. Sur une liste spécifique de 6 identifiants, cela ressemblera à ceci:

image

Ainsi, en parcourant la liste, nous calculerons toujours l'identifiant actuel à partir de la valeur précédente obtenue. Par exemple, au deuxiÚme décalage 3, nous ajoutons la premiÚre valeur 2 et obtenons l'id 5, au troisiÚme 4 nous ajoutons 5, et obtenons 9 et ainsi de suite. Avec un grand nombre de documents, cela fonctionne trÚs bien, en particulier en conjonction avec une autre méthode de compression - l'enregistrement de nombres au format variable.

1.3.2 VarByte et VarInt


Il s'agit d'un moyen de stocker chaque Ă©lĂ©ment de liste individuel en mĂ©moire afin qu'il occupe le minimum d'espace. Par exemple, si les trois premiers dĂ©calages ne tiennent que sur 1 octet, il n'est pas nĂ©cessaire d'en prendre plus. Étant donnĂ© que notre liste ne contient pas de documents d'identitĂ©, mais des deltas, la compression sera trĂšs efficace. Dans cette reprĂ©sentation des nombres, le premier bit de chaque octet est le drapeau indiquant si la reprĂ©sentation du numĂ©ro courant se termine sur cet octet.

image

Si le premier bit de l'octet 0 est alors le dernier octet du nombre, si 1 ne l'est pas.

1.3.3 Sauter la liste / Sauter la table


Une liste de sauts est l'une des structures permettant de parcourir rapidement une liste de documents d'un certain terme, en sautant une partie inutile de la liste. L'idĂ©e est de stocker des liens vers des Ă©lĂ©ments distants de la liste sur le disque en face de la liste elle-mĂȘme, car aprĂšs la compression, nous ne pouvons pas dire exactement oĂč le document 100 ou 200 sera situĂ©. Par exemple, cela est pratique lorsqu'il y a une requĂȘte de deux termes, oĂč un terme sera frĂ©quemment trouvĂ©, et le second, au contraire, sera rare, et la liste des documents ne commencera qu'avec le 200e identifiant du document. Ensuite, s'il existe une liste avec des passes pour la premiĂšre liste, nous pouvons gagner du temps sur le dĂ©placement et ignorer immĂ©diatement le bloc d'identifiants inutiles.

image

1.4 Demandes


1.4.0 RequĂȘte de terme


Le type de demande le plus simple dans lequel il suffit de trouver la liste appropriée des documents et d'émettre des documents triés aprÚs classement.
Par exemple, de cette façon, nous trouvons une liste de positions pour java :

image

1.4.1 RequĂȘtes boolĂ©ennes (et, ou, non)


La recherche boolĂ©enne est l'une des parties les plus importantes de la recherche d'informations que nous trouvons partout. La recherche boolĂ©enne entiĂšre est basĂ©e sur une combinaison de ET, OU et NON. Par exemple, lorsque nous recherchons une requĂȘte en deux mots: java android , puis en fait, dans une recherche simple, elle sera convertie en java AND android . Et cela signifie que nous voulons trouver tous les documents qui contiennent les deux mots.

Il convient de mentionner tout de suite comment vous vous dĂ©placez dans la liste des documents. Étant donnĂ© que les listes de documents pour chaque terme sont triĂ©es, il existe gĂ©nĂ©ralement deux façons de parcourir les listes: parcourir les documents de maniĂšre sĂ©quentielle, en les passant un par un, ou passer immĂ©diatement Ă  un document spĂ©cifique, en ignorant ceux qui ne sont pas nĂ©cessaires (par exemple, lorsque la premiĂšre liste est beaucoup plus petite) , et nous n'avons pas besoin de parcourir un grand bloc de documents dans la deuxiĂšme liste). Dans ce cas, nous utilisons d'abord le pointeur des pointeurs de saut pour que la deuxiĂšme liste se rapproche le plus possible de l'ID de document souhaitĂ©, puis nous y dĂ©plaçons linĂ©airement.

Au moment de la recherche, les événements suivants se produisent: dans l'index des termes java et android sont des listes de documents, puis une intersection est faite sur eux - c'est-à-dire que nous trouvons des documents qui contiennent les deux termes. Avec cette recherche, les deux méthodes de déplacement dans les listes sont utilisées pour un croisement plus rapide.

image

Avec les requĂȘtes OR du formulaire java OR scala, oĂč nous devons trouver tous les documents contenant au moins un des termes, la situation est diffĂ©rente - ici, nous n'avons pas besoin que le terme soit dans toutes les listes de documents Ă  la fois. Mais il existe des requĂȘtes avec plusieurs opĂ©rateurs OR, puis la condition pour un nombre minimum de correspondances peut se produire, par exemple, il peut y avoir une requĂȘte java OU scala OU cotlin OU clojure avec au moins deux correspondances, puis nous devons afficher tous les documents qui contiennent au moins deux mots de la requĂȘte .

Dans ce cas, le tas fonctionne plus efficacement. Nous pouvons y stocker des liens vers les itĂ©rateurs de chacune des listes et obtenir l'Ă©lĂ©ment minimum pour un temps constant. AprĂšs avoir pris l'Ă©lĂ©ment minimum, nous supprimons l'itĂ©rateur du tas, faisons un pas en avant et l'ajoutons Ă  nouveau au tas. Vous pouvez stocker sĂ©parĂ©ment le candidat actuel Ă  ajouter au rĂ©sultat et au compteur, combien de fois il s'est rencontrĂ© et au moment oĂč le candidat diffĂ©rera de l'Ă©lĂ©ment minimum dans le tas, voyez si nous passons par le nombre minimum de correspondances dans l'opĂ©ration. Et soit ajouter Ă  la liste finale des rĂ©sultats, soit jeter le document.

image

1.4.2 Préfixe / Jokers


Parfois, nous voulons trouver tous les documents qui contiennent un mot qui commence par un préfixe spécifique. Dans de tels cas, la demande de préfixe nous aidera, qui ressemblera à jav * . Une demande de préfixe fonctionne trÚs bien lorsque le dictionnaire est implémenté sur une arborescence de préfixes, puis nous arrivons à l'imbrication du préfixe et prenons tous les termes qui se trouvent ci-dessous.

1.4.3 RequĂȘtes sur des phrases et requĂȘte proche


Il y a des moments oĂč vous devez trouver la phrase entiĂšre, par exemple, "dĂ©veloppeur Java", ou trouver des mots entre lesquels il n'y aura pas plus de quelques mots, par exemple, "Java" et "dĂ©veloppeur", entre lesquels pas plus de 2 mots, afin que vous puissiez trouver documents contenant java android kotlin developer. Pour cela, des listes de positions de mots dans chaque document sont Ă©galement utilisĂ©es.

image

Au moment de traverser les listes de documents, tout est identique à l'opération ET. Mais une fois le document trouvé dans les deux listes, une vérification supplémentaire est effectuée - que les termes sont à la bonne distance l'un de l'autre, par la différence de position (position).

1.4.4 Plan de demande


Habituellement, avant l'exĂ©cution de la demande elle-mĂȘme, son plan est construit. Cela se produit afin d'optimiser l'exĂ©cution de la demande et de faire fonctionner des optimisations telles qu'une liste avec omissions pour la liste des documents.

Le moyen le plus simple d'optimiser votre requĂȘte est de parcourir les listes de documents par ordre croissant de taille. Ainsi, nous ne gaspillerons pas le gaspillage de documents de grandes listes qui ne sont pas dans de petites listes.

Par exemple, analysons la requĂȘte android AND java AND sql . Disons qu'il y a 10 documents dans la liste android, en sql - 20, et en java - 100. Dans ce cas, il est prĂ©fĂ©rable de traverser d'abord les plus petites listes, et la requĂȘte optimisĂ©e ressemblera Ă  (android AND sql) AND java .

Dans le cas de OR, le plus simple est de compter le nombre de documents à l'intersection comme la somme de deux listes potentiellement intersectées.

1.4.5 Extension de requĂȘte - Synonymes


En plus de ce que l'utilisateur entre dans la barre de recherche, la recherche essaie gĂ©nĂ©ralement d'Ă©tendre la requĂȘte elle-mĂȘme pour trouver des documents plus pertinents. Beaucoup peut ĂȘtre utilisĂ© pour Ă©tendre la recherche: l'historique des requĂȘtes de l'utilisateur, certaines donnĂ©es personnalisĂ©es le concernant, etc. Mais en plus de tout cela, il existe Ă©galement un moyen universel d'Ă©largir la demande - les synonymes.

Dans ce cas, lors de l'écriture de documents dans l'index, le terme est remplacé par un «lien» dans le dictionnaire des synonymes:

image

La mĂȘme chose se produit lors de la conversion d'une demande. Par exemple, lorsque nous demandons un directeur des ventes , la demande ressemble en fait Ă :

image

Ainsi, dans la réponse, nous recevrons non seulement les documents qui contiennent le directeur des ventes, mais aussi ceux qui contiennent l'agent de vente et les ventes.

1.5 Filtrage


1.5.1 Filtre Ă  plage rapide


Parfois, nous voulons filtrer quelque chose par une gamme de valeurs, par exemple, par l'expérience des années. Supposons que nous voulons trouver tous les postes vacants avec l'expérience requise de 3 à 11 ans. La premiÚre décision est de faire une demande avec toutes les options de la gamme, en les combinant via OR. Mais le problÚme est qu'il peut y avoir trop de valeurs. Une façon de résoudre ce problÚme consiste à enregistrer la valeur avec plusieurs précisions à la fois:

image

Dans ce cas, nous enregistrerons 5 valeurs de précision: un an (nous considérerons cela comme la valeur initiale), deux, quatre, huit et seize.

Ensuite, lors de l'enregistrement, les événements suivants se produiront: par exemple, lors de l'enregistrement d'un document avec une exigence d'expérience de 6 ans, nous enregistrons immédiatement la valeur en toute précision:

image

Lors du filtrage «de 3 Ă  11 ans», il se produit ce qui suit: nous sĂ©lectionnons uniquement les valeurs dont nous avons besoin avec la prĂ©cision requise, et nous n'obtenons que 3 valeurs au lieu de 8 et nous obtenons la requĂȘte (valeur rĂ©elle == 3) OU (prĂ©cision 4 == 4) OU (prĂ©cision 4 == 8)

image

1.5.2 Masques de bits


Les masques de bits font partie intĂ©grante d'un index. L'utilisation la plus importante est le filtrage des documents supprimĂ©s. Lorsque vous supprimez un document de l'index, la suppression physique ne se produit pas immĂ©diatement. Une structure spĂ©ciale est Ă©crite Ă  cĂŽtĂ© de l'index, oĂč chaque bit signifie l'id du document dans l'index, et lorsqu'il est supprimĂ©, un bit est levĂ© et lors de la recherche, ces documents sont filtrĂ©s et ne tombent pas dans la sortie.

Vous pouvez également utiliser des masques de bits pour définir des autorisations pour chaque utilisateur sur certains documents et pour mettre en cache des filtres populaires individuels. Ensuite, les masques de bits sont généralement stockés séparément de l'index.

Par exemple, nous avons des filtres populaires: la ville de Moscou, uniquement à temps partiel, sans expérience de travail. Ensuite, avant la demande, nous pouvons obtenir les masques de bits déjà enregistrés pour ces documents, les ajouter et obtenir le masque de bits final - quels documents passent par ces trois filtres, ce qui permet de gagner du temps sur le filtrage.

image

2. Classement


Comme nous nous en souvenons, la tĂąche principale de la recherche est d'obtenir les informations les plus pertinentes en un minimum de temps. Et en cela, nous serons aidĂ©s par le classement des documents aprĂšs avoir filtrĂ© les documents par requĂȘte de texte et appliquĂ© les filtres et les droits nĂ©cessaires.

Le moyen le plus simple et le moins cher de faire un classement est de simplement trier les documents par date. Dans certains systÚmes, cela se faisait auparavant, par exemple, dans les actualités ou dans les annonces immobiliÚres, de sorte que l'utilisateur a d'abord été informé des derniers documents.

Parfois, un modĂšle de classement par le nombre de mots trouvĂ©s dans un document peut ĂȘtre utilisĂ©, par exemple, quand il n'y a pas autant de documents, et nous voulons trouver tous les documents dans lesquels au moins un des mots de la requĂȘte est trouvĂ©. Dans ce cas, les documents dans lesquels tous les mots de la requĂȘte ou plusieurs d'entre eux sont trouvĂ©s seront plus pertinents.

Bien sĂ»r, Ă  l'heure actuelle, ces mĂ©thodes sont dĂ©jĂ  devenues sans objet, et elles peuvent plus probablement ĂȘtre attribuĂ©es Ă  l'histoire du problĂšme.

2.1 TF-IDF


TF-IDF (terme frĂ©quence - frĂ©quence inverse des documents) est l'une des formules de classement les plus Ă©lĂ©mentaires et les plus utilisĂ©es. L'essence de la formule est de rĂ©duire les termes utilisĂ©s partout, par exemple, les prĂ©positions et les interjections, et de rendre les termes plus significatifs qui sont rares, montrant ainsi les premiers documents avec des termes plus rares et plus significatifs de la requĂȘte. Maintenant, dĂ©composons la formule en plusieurs parties:

TF (terme fréquence) est la fréquence du terme apparaissant dans le document. Il est calculé simplement:

TF terme `java` = nombre de termes` java` dans le document / nombre de tous les termes dans le document

IDF (fréquence de document inverse) - l'inverse de la fréquence à laquelle le mot apparaßt dans la collection de documents. Aide à réduire le poids des mots couramment utilisés.

IDF (`java`) = log (nombre de documents dans la collection / nombre de documents dans lesquels le terme` java` apparaĂźt)

De plus, pour obtenir le TF-IDF du terme java, il suffit de multiplier les valeurs TF et IDF obtenues. , , . , , developer , , java developer .

2.2


, , . , , , .

2.3 BM25 BM*


BM25 (Okapi best match 25) TF-IDF . BM25F, ( ).

BM25=∑i=1w(1+k)⋅TFi⋅IDFiTFi+k(1−b+bDLavgDL)IDFi=log(N−n+1n)logN



2.4


, :

  • DFR (divergence from randomness),
  • IBS (information-based models),
  • LM Dirichlet,
  • Jelinek-Mercer.

2.5


, , . , .

2.6 Top k


, . , , .

, . top k .

— . k, k * . heap. n*log(n) k.

. , , , 10 12, score 10 score . , n — (n*page size) .

3.


3.1


— . , .

, : , , . , . . ( , ). (merge).

, , :

image

. , , , - .

3.2 (megre)


— . «» — . , , ( ).

image

, , , . :

  • ,
  • (, 2 ),
  • (, ).

4.


, , . , - (, . .). , , , .

image

4.1


(, hh , ), . .

, , . -, , . -, , , .

4.2


, . , .

, hh, , , - :

image

5. 



, , , . , : , , , , , (highlight), . , , , top k .

:


C'est tout, merci à tous pour votre attention, il est intéressant d'entendre vos commentaires et vos questions.

PS


Je voudrais exprimer ma gratitude à gdanschin pour m'avoir aidé à écrire cet article.

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


All Articles