web-dev-qa-db-fra.com

Comment trouver des résultats similaires et trier par similitude?

Comment rechercher des enregistrements classés par similarité?

Par exemple. la recherche de "Stock Overflow" reviendrait

  1. Débordement de pile
  2. Débordement SharePoint
  3. Débordement mathématique
  4. Débordement politique
  5. VFX Overflow

Par exemple. la recherche de "LO" retournerait:

  1. pablo Picasso
  2. michelangeLO
  3. jackson polLOck

Avec quoi j'ai besoin d'aide:

  1. Utiliser un moteur de recherche pour indexer et rechercher une table MySQL, pour de meilleurs résultats

    • Utilisation du moteur de recherche Sphinx , avec PHP

    • Utilisation du moteur Lucene avec PHP

  2. Utilisation de l'indexation de texte intégral pour rechercher des chaînes similaires/contenant


Ce qui ne fonctionne pas bien

  • distance Levenshtein est très erratique. ( UDF , Requête )
    La recherche de "chien" me donne:
    1. chien
    2. tourbière
    3. depuis
    4. gros
    5. écho
  • LIKE renvoie de meilleurs résultats, mais ne renvoie rien pour les longues requêtes bien que des chaînes similaires existent
    1. chien
    2. dogid
    3. dogaral
    4. dogme
67
Robinicks

J'ai découvert que la distance Levenshtein peut être bonne lorsque vous recherchez une chaîne complète par rapport à une autre chaîne complète, mais lorsque vous recherchez des mots clés dans une chaîne, cette méthode ne renvoie pas (parfois) les résultats souhaités. De plus, la fonction SOUNDEX ne convient pas aux langues autres que l'anglais, elle est donc assez limitée. Vous pourriez vous en tirer avec LIKE, mais c'est vraiment pour les recherches de base. Vous voudrez peut-être examiner d'autres méthodes de recherche pour savoir ce que vous voulez réaliser. Par exemple:

Vous pouvez utiliser Lucene comme base de recherche pour vos projets. Il est implémenté dans la plupart des principaux langages de programmation et il est assez rapide et polyvalent. Cette méthode est probablement la meilleure, car elle recherche non seulement les sous-chaînes, mais aussi la transposition des lettres, les préfixes et les suffixes (tous combinés). Cependant, vous devez conserver un index séparé (utiliser CRON pour le mettre à jour à partir d'un script indépendant de temps en temps fonctionne cependant).

Ou, si vous voulez une solution MySQL, la fonctionnalité de texte intégral est assez bonne, et certainement plus rapide qu'une procédure stockée. Si vos tables ne sont pas MyISAM, vous pouvez créer une table temporaire, puis effectuer votre recherche plein texte:

CREATE TABLE IF NOT EXISTS `tests`.`data_table` (
  `id` int(10) unsigned NOT NULL AUTO_INCREMENT,
  `title` varchar(2000) CHARACTER SET latin1 NOT NULL,
  `description` text CHARACTER SET latin1 NOT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB  DEFAULT CHARSET=utf8 COLLATE=utf8_bin AUTO_INCREMENT=1 ;

Utilisez un générateur de données pour générer des données aléatoires si vous ne voulez pas vous embêter à les créer vous-même ...

** [~ # ~] note [~ # ~] **: le type de colonne doit être latin1_bin pour effectuer une casse recherchez au lieu de ne pas respecter la casse avec latin1. Pour les chaînes unicode, je recommanderais utf8_bin Pour les recherches sensibles à la casse et utf8_general_ci Pour les recherches qui ne respectent pas la casse.

DROP TABLE IF EXISTS `tests`.`data_table_temp`;
CREATE TEMPORARY TABLE `tests`.`data_table_temp`
   SELECT * FROM `tests`.`data_table`;

ALTER TABLE `tests`.`data_table_temp`  ENGINE = MYISAM;

ALTER TABLE `tests`.`data_table_temp` ADD FULLTEXT `FTK_title_description` (
  `title` ,
  `description`
);

SELECT *,
       MATCH (`title`,`description`)
       AGAINST ('+so* +nullam lorem' IN BOOLEAN MODE) as `score`
  FROM `tests`.`data_table_temp`
 WHERE MATCH (`title`,`description`)
       AGAINST ('+so* +nullam lorem' IN BOOLEAN MODE)
 ORDER BY `score` DESC;

DROP TABLE `tests`.`data_table_temp`;

En savoir plus à ce sujet sur la page de référence de l'API MySQL

L'inconvénient est qu'il ne recherchera pas la transposition de lettres ou des mots "similaires, comme des".

** [~ # ~] mise à jour [~ # ~] **

En utilisant Lucene pour votre recherche, vous aurez simplement besoin de créer un travail cron (tous les hôtes Web ont cette "fonctionnalité") où ce travail exécutera simplement un script PHP (ig "cd/path/to/script; php searchindexer.php ") qui mettra à jour les index. La raison en est que l'indexation de milliers de "documents" (lignes, données, etc.) peut prendre plusieurs secondes, voire quelques minutes, mais c'est pour s'assurer que toutes les recherches sont effectuées aussi rapidement que possible. Par conséquent, vous souhaiterez peut-être créer un travail de retard à exécuter par le serveur. Cela peut être du jour au lendemain, ou dans l'heure suivante, cela dépend de vous. Le script PHP devrait ressembler à ceci:

$indexer = Zend_Search_Lucene::create('/path/to/lucene/data');

Zend_Search_Lucene_Analysis_Analyzer::setDefault(
  // change this option for your need
  new Zend_Search_Lucene_Analysis_Analyzer_Common_Utf8Num_CaseInsensitive()
);

$rowSet = getDataRowSet();  // perform your SQL query to fetch whatever you need to index
foreach ($rowSet as $row) {
   $doc = new Zend_Search_Lucene_Document();
   $doc->addField(Zend_Search_Lucene_Field::text('field1', $row->field1, 'utf-8'))
       ->addField(Zend_Search_Lucene_Field::text('field2', $row->field2, 'utf-8'))
       ->addField(Zend_Search_Lucene_Field::unIndexed('someValue', $someVariable))
       ->addField(Zend_Search_Lucene_Field::unIndexed('someObj', serialize($obj), 'utf-8'))
  ;
  $indexer->addDocument($doc);
}

// ... you can get as many $rowSet as you want and create as many documents
// as you wish... each document doesn't necessarily need the same fields...
// Lucene is pretty flexible on this

$indexer->optimize();  // do this every time you add more data to you indexer...
$indexer->commit();    // finalize the process

Ensuite, voici essentiellement comment vous effectuez une recherche (recherche de base):

$index = Zend_Search_Lucene::open('/path/to/lucene/data');

// same search options
Zend_Search_Lucene_Analysis_Analyzer::setDefault(
   new Zend_Search_Lucene_Analysis_Analyzer_Common_Utf8Num_CaseInsensitive()
);

Zend_Search_Lucene_Search_QueryParser::setDefaultEncoding('utf-8');

$query = 'php +field1:foo';  // search for the Word 'php' in any field,
                                 // +search for 'foo' in field 'field1'

$hits = $index->find($query);

$numHits = count($hits);
foreach ($hits as $hit) {
   $score = $hit->score;  // the hit weight
   $field1 = $hit->field1;
   // etc.
}

Voici d'excellents sites sur Lucene en Java , PHP et . Net .

En conclusion chaque méthode de recherche a ses avantages et ses inconvénients:

  • Vous avez mentionné recherche Sphinx et cela semble très bien, tant que vous pouvez faire fonctionner le démon sur votre hébergeur.
  • Zend Lucene nécessite un travail cron pour réindexer la base de données. Bien qu'il soit assez transparent pour l'utilisateur, cela signifie que toutes les nouvelles données (ou données supprimées!) Ne sont pas toujours synchronisées avec les données de votre base de données et n'apparaîtront donc pas immédiatement lors de la recherche d'utilisateurs.
  • La recherche MySQL FULLTEXT est bonne et rapide, mais ne vous donnera pas toute la puissance et la flexibilité des deux premières.

N'hésitez pas à commenter si j'ai oublié/oublié quelque chose.

83
Yanick Rochon

1. Similitude

Pour Levenshtein dans MySQL, je l'ai trouvé, à partir de www.codejanitor.com/wp/2007/02/10/levenshtein-distance-as-a-mysql-stored-function

SELECT 
    column, 
    LEVENSHTEIN(column, 'search_string') AS distance 
FROM table 
WHERE 
    LEVENSHTEIN(column, 'search_string') < distance_limit
ORDER BY distance DESC

2. Contenant, insensible à la casse

Utilisez l'instruction LIKE de MySQL, qui est insensible à la casse par défaut. Le % est un caractère générique, il peut donc y avoir n'importe quelle chaîne avant et après search_string.

SELECT 
    *
FROM 
    table
WHERE 
    column_name LIKE "%search_string%"

3. Contenant, sensible à la casse

Le MySQL Manual aide:

Le jeu de caractères et le classement par défaut sont latin1 et latin1_swedish_ci, donc les comparaisons de chaînes non binaires ne respectent pas la casse par défaut. Cela signifie que si vous recherchez avec col_name LIKE 'a%', vous obtenez toutes les valeurs de colonne qui commencent par A ou a. Pour rendre cette recherche sensible à la casse, assurez-vous que l'un des opérandes a un classement sensible à la casse ou binaire. Par exemple, si vous comparez une colonne et une chaîne qui ont toutes deux le jeu de caractères latin1, vous pouvez utiliser l'opérateur COLLATE pour que l'un ou l'autre opérande ait le classement latin1_general_cs ou latin1_bin ...

Ma configuration MySQL ne prend pas en charge latin1_general_cs ou latin1_bin, mais cela a bien fonctionné pour moi d'utiliser le classement utf8_bin car l'utf8 binaire est sensible à la casse:

SELECT 
    *
FROM 
    table
WHERE 
    column_name LIKE "%search_string%" COLLATE utf8_bin

2./3. triés par distance de Levenshtein

SELECT 
    column, 
    LEVENSHTEIN(column, 'search_string') AS distance // for sorting
FROM table 
WHERE 
    column_name LIKE "%search_string%"
    COLLATE utf8_bin // for case sensitivity, just leave out for CI
ORDER BY
    distance
    DESC
21
opatut

Il semble que votre définition de la similitude soit une similitude sémantique. Donc, pour construire une telle fonction de similitude, vous devez utiliser des mesures de similitude sémantique. Notez que la portée du travail sur la question peut varier de quelques heures à plusieurs années, il est donc recommandé de décider de la portée avant de commencer à travailler. Je n'ai pas compris de quelles données disposez-vous pour établir la relation de similitude. Je suppose que vous avez accès à un ensemble de données de documents et à un ensemble de données de requêtes. Vous pouvez commencer par la co-occurrence des mots (par exemple, probabilité conditionnelle). Vous découvrirez rapidement que vous obtenez la liste des mots vides comme liés le plus des mots simplement parce qu'ils sont très populaires. L'utilisation de la levée de la probabilité conditionnelle prendra soin des mots vides mais rendra la relation sujette à erreur en petit nombre (la plupart de vos cas). Vous pourriez essayer Jacard mais comme il est symétrique, il y aura de nombreuses relations qu'il ne trouvera pas. Ensuite, vous pourriez envisager des relations qui n'apparaissent qu'à courte distance du mot de base. Vous pouvez (et devriez) envisager des relations basées sur des corpus généraux (par exemple, Wikipedia) et spécifiques à l'utilisateur (par exemple, ses e-mails).

Très bientôt, vous aurez de nombreuses mesures de similitude, lorsque toutes les mesures sont bonnes et ont un certain avantage sur les autres.

Afin de combiner de telles mesures, j'aime réduire le problème en un problème de classification.

Vous devez créer un ensemble de données de paris de mots et les étiqueter comme "est lié". Pour créer un grand ensemble de données étiqueté, vous pouvez:

  • Utilisez des sources de mots apparentés connus (par exemple, de bonnes vieilles catégories Wikipedia) pour les positifs
  • La plupart des mots non connus comme liés ne sont pas liés.

Utilisez ensuite toutes les mesures que vous avez comme caractéristiques des paires. Vous êtes maintenant dans le domaine des problèmes de classification supervisée. Construisez un classificateur sur l'ensemble de données, évalué en fonction de vos besoins et obtenez une mesure de similitude qui correspond à vos besoins.

3
DaL