【问题标题】:How to find similar results and sort by similarity?如何找到相似的结果并按相似度排序?
【发布时间】:2011-03-21 07:32:22
【问题描述】:

如何查询按相似度排序的记录?

例如。搜索“Stock Overflow”会返回

  1. 堆栈溢出
  2. SharePoint 溢出
  3. 数学溢出
  4. 政治溢出
  5. 视觉特效溢出

例如。搜索“LO”将返回:

  1. 毕加索毕加索
  2. 米开朗琪罗
  3. 杰克逊波洛克

我需要什么帮助:

  1. 使用搜索引擎对 MySQL 表进行索引和搜索,以获得更好的结果

    • 使用Sphinx 搜索引擎和PHP

    • 在 PHP 中使用 Lucene 引擎

  2. 使用全文索引,查找相似/包含字符串


有什么不好的地方

  • Levenshtein distance 非常不稳定。 (UDF, Query)
    搜索“狗”给了我:
    1. 沼泽
    2. 以前
    3. 回声
  • LIKE 返回更好的结果,但对于长查询不返回任何内容,尽管确实存在类似的字符串
    1. 狗狗
    2. 狗狗
    3. 教条

【问题讨论】:

    标签: mysql sql string sorting similarity


    【解决方案1】:

    我发现,当您针对另一个完整字符串搜索完整字符串时,Levenshtein 距离可能很好,但是当您在字符串中查找关键字时,此方法不会(有时)返回所需的结果。而且,SOUNDEX 功能不适合英语以外的其他语言,所以它是相当有限的。您可以使用 LIKE,但它确实适用于基本搜索。您可能需要查看其他搜索方法以了解您想要实现的目标。例如:

    您可以使用Lucene 作为您项目的搜索库。它已在大多数主要编程语言中实现,并且速度非常快且用途广泛。这种方法可能是最好的,因为它不仅搜索子字符串,还搜索字母换位、前缀和后缀(全部组合)。但是,您需要保留一个单独的索引(尽管偶尔使用 CRON 从独立脚本更新它是可行的)。

    或者,如果您想要一个 MySQL 解决方案,全文功能非常好,而且肯定比存储过程快。如果您的表不是 MyISAM,您可以创建一个临时表,然后执行全文搜索:

    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 ;
    

    如果您不想自己创建一些随机数据,请使用data generator 生成一些随机数据...

    ** 注意 **:列类型应为latin1_bin 以执行区分大小写的搜索,而不是使用latin1 不区分大小写。对于 unicode 字符串,我建议 utf8_bin 用于区分大小写,utf8_general_ci 用于不区分大小写的搜索。

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

    MySQL API reference page了解更多信息

    这样做的缺点是它不会寻找字母换位或“相似,听起来像”的词。

    ** 更新 **

    使用 Lucene 进行搜索,您只需要创建一个 cron 作业(所有 Web 主机都有这个“功能”),该作业将简单地执行一个 PHP 脚本(ig "cd /path/to/script; php searchindexer .php") 将更新索引。原因是索引数千个“文档”(行、数据等)可能需要几秒钟甚至几分钟,但这是为了确保尽可能快地执行所有搜索。因此,您可能希望创建一个延迟作业以由服务器运行。它可能是一夜之间,或者在接下来的一个小时内,这取决于你。 PHP 脚本应如下所示:

    $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
    

    那么,这基本上就是你的搜索方式(基本搜索):

    $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.
    }
    

    JavaPHP.Net 中有很多关于 Lucene 的网站。

    总结每种搜索方法都有自己的优缺点:

    • 您提到了Sphinx search,它看起来非常好,只要您可以让守护程序在您的虚拟主机上运行。
    • Zend Lucene 需要一个 cron 作业来重新索引数据库。虽然它对用户非常透明,但这意味着任何新数据(或已删除的数据!)并不总是与数据库中的数据同步,因此不会立即显示在用户搜索中。
    • MySQL FULLTEXT 搜索既好又快,但不会为您提供前两个搜索的所有功能和灵活性。

    如果我忘记/遗漏了什么,请随时发表评论。

    【讨论】:

    • 我已经在您的问题中添加了区分大小写/不区分大小写的部分,但是恐怕仅 SQL 的解决方案不如 Lucene 解决方案好。但这只是恕我直言。也许有一天,有人会为 MySQL 实现一个 Lucene 搜索功能,坦率地说,我很想看到那一天,但与此同时,这是我现在能找到的最佳解决方案。
    • 我会附和。仅 mysql 的解决方案不会很快推出。
    • 你能帮我解决一下 Lucene 的问题吗?我如何开始使用它来查询具有相似性的记录?类似搜索引擎的东西?如果你能告诉我如何让它工作,我会给你赏金。
    • 斯芬克斯看起来不错。您可以从 Zend 的网站找到有关 Lucene 的信息(您不需要整个 Zend 框架结构来使用 Zend_Search_Lucene 类),一切都非常详细。如果你不想打扰 Zend,Sphynx 看起来也不错!并且它似乎不需要保留数据的单独索引的开销......我将自己进一步挖掘那个。谢谢你分享这个。 :) 祝你好运!
    • 非常感谢亚尼克!你的答案很棒,但我还需要一些帮助:1)你能告诉我一个简单的 MySQL 查询,其中包含全文列来搜索相似的记录吗?看我的问题。 2) 搜索相似记录的 Lucene 查询字符串是什么,最相关的“匹配”或“包含”记录在顶部,“相似”或“相似”记录在其下方。
    【解决方案2】:

    1.相似度

    对于 MySQL 中的 Levenshtein,我发现了这个,来自 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。包含,不区分大小写

    使用 MySQL 的LIKE 语句,默认不区分大小写。 % 是通配符,所以search_string 前后可以有任意字符串。

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

    3.包含,区分大小写

    MySQL Manual 有帮助:

    默认字符集和排序规则是 latin1 和 latin1_swedish_ci,因此默认情况下非二进制字符串比较不区分大小写。这意味着如果您使用 col_name LIKE 'a%' 进行搜索,您将获得所有以 A 或 a 开头的列值。要使此搜索区分大小写,请确保其中一个操作数具有区分大小写或二进制排序规则。例如,如果您要比较的列和字符串都具有 latin1 字符集,则可以使用 COLLATE 运算符使任一操作数具有 latin1_general_cs 或 latin1_bin 排序规则...

    我的 MySQL 设置不支持 latin1_general_cslatin1_bin,但使用排序规则 utf8_bin 对我来说效果很好,因为二进制 utf8 区分大小写:

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

    2。 / 3.按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
    

    【讨论】:

    • 在检查搜索的字符串是否出现在列中时如何定义相似度?有 2 种可能性:TRUE 和 FALSE,两者之间没有任何关系。实际上,您可以通过将搜索字符串的字符串长度除以列的字符串长度来获得一个因子,但是您总是会得到最短的字符串——您想按实际列中的出现次数排序吗?为什么不全文搜索?
    • 不,我的意思是你可以使用#2 和#3 进行搜索并使用 Levenshtein 或类似工具按相似度排序吗?因此,您会得到最相似的结果。请参阅我的问题中给出的示例。
    • 你去吧,但我认为在使用 LIKE 时按 Levenshtein 排序没有意义。为什么你会在你的例子中这样排序(1. Adopt / 2. Adore / 3. Adorn)?使用 levenshtein,它们具有相同的值(3,因为您总是必须添加 3 个字符)
    • MySQL Dam-Lev 的实现很好,但它产生的结果非常不稳定,因为 Lev 的理念是“测量编辑”而不是“测量差异”。请参阅上面我更新的问题。
    • @opatut 是的 Levenshtein 是一个不错的选择。但是,当我有一组要与另一组字符串匹配的字符串时,如何找到 Levenshtein 距离的最小值?
    【解决方案3】:

    看来您对相似性的定义是语义相似性。因此,为了构建这样的相似度函数,您应该使用语义相似度度量。 请注意,该问题的工作范围可能从几小时到几年不等,因此建议在开始工作之前确定范围。 我没有弄清楚你有哪些数据来建立相似关系。我假设您可以访问文档数据集和查询数据集。 您可以从单词的共现开始(例如,条件概率)。 你会很快发现你得到了stop words 列表,因为它们非常受欢迎。 使用条件概率的提升将处理停用词,但会使关系在少数情况下容易出错(大多数情况下)。 您可以尝试Jacard,但由于它是对称的,因此将找不到许多关系。 然后你可能会考虑只出现在离基本词很近的地方的关系。您可以(并且应该)考虑基于一般语料库(例如,维基百科)和特定用户(例如,他的电子邮件)的关系。

    很快你就会有大量的相似性度量,当所有度量都很好并且比其他度量有一些优势时。

    为了结合这些措施,我喜欢将问题简化为分类问题。

    您应该建立一个单词 paris 的数据集并将它们标记为“相关”。 为了构建大型标记数据集,您可以:

    • 使用已知相关词的来源(例如,良好的旧维基百科类别)作为肯定词
    • 大部分不称为相关的词都是不相关的。

    然后使用您拥有的所有度量作为配对的特征。 现在您处于监督分类问题的领域。 在数据集上构建分类器,根据您的需求进行评估,并获得适合您需求的相似性度量。

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2015-01-23
      • 1970-01-01
      相关资源
      最近更新 更多