如何查找相似结果并按相似度排序?

80

如何按相似度查询记录?

例如,搜索“Stock Overflow”将返回:

  1. Stack Overflow
  2. SharePoint Overflow
  3. Math Overflow
  4. Politic Overflow
  5. VFX Overflow

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

  1. pabLO picasso
  2. michelangeLO
  3. jackson polLOck

我需要帮助处理的问题:

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

    • 使用PHP和Sphinx搜索引擎

    • 使用PHP和Lucene搜索引擎

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


不太有效的方法:

  • Levenshtein distance 很不稳定。(UDFQuery
    搜索“dog”会返回:
    1. dog
    2. bog
    3. ago
    4. big
    5. echo
  • LIKE 返回更好的结果,但对于较长的查询没有返回值,即使存在类似的字符串
    1. dog
    2. dogid
    3. dogaral
    4. dogma
3个回答

96
我发现当您在一个字符串中寻找关键字时,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 ;

使用数据生成器来生成一些随机数据,如果您不想自己创建的话...
** 注意 **:列类型应为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参考页面了解更多相关信息。

这样做的缺点是它不会查找字母转换或"类似,发音相似"的单词。

** 更新 **

使用Lucene进行搜索,您只需要创建一个cron作业(所有Web主机都具有此"功能"),其中此作业将仅执行一个PHP脚本(例如"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.
}

以下是关于Lucene的优秀站点,包括JavaPHP.Net

总之,每种搜索方法都有其优缺点:

  • 你提到了Sphinx搜索,看起来非常好,只要你能让守护程序在Web主机上运行。
  • Zend Lucene需要一个cron作业来重新索引数据库。虽然对用户而言相当透明,但这意味着任何新数据(或删除的数据!)不一定与您的数据库中的数据同步,因此不会立即显示在用户搜索中。
  • MySQL FULLTEXT搜索很快,但无法提供前两者的所有功能和灵活性。

如果我遗漏/错过了什么,请随时评论。


1
我已经添加了您问题中区分大小写/不区分大小写的部分,但是恐怕仅使用SQL的解决方案可能不如Lucene的解决方案好。但这只是我的个人意见。也许有一天,有人会为MySQL实现一个Lucene搜索功能,坦白说,我非常希望看到那一天的到来,但同时,这是我现在能找到的最佳解决方案。 - Yanick Rochon
你将使用哪种语言来编写Lucene?Java,PHP,还是其他的? - Yanick Rochon
1
Sphynx看起来非常不错。您可以在Zend的网站上找到有关Lucene的信息(您无需使用整个Zend Framework结构即可使用Zend_Search_Lucene类),一切都相当详细。如果您不想麻烦使用Zend,Sphynx也很不错!而且似乎不需要额外的索引维护开销…我会自己深入挖掘一下。谢谢分享这个。 :) 祝好运! - Yanick Rochon
我找到了关于Zend_Search_Lucene和PHP的教程。如果您能提供更多有关使用Lucene等内容的具体帮助,我将授予您奖励。请参见更新的问题。http://devzone.zend.com/article/91 - Robin Rodricks
1
非常感谢你,Yanick!你的回答非常棒,但我还需要帮助解决以下几个问题:1)你能给我展示一个简单的MySQL查询语句,用于搜索相似记录的全文列吗?请看我的问题。2)搜索相似记录的Lucene查询字符串是什么,最相关的“匹配”或“包含”记录在顶部,而“相似”或“类似”的记录在其下方。 - Robin Rodricks
显示剩余5条评论

26

1. 相似度计算

我在stackoverflow找到了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 手册 给出了帮助:

默认的字符集和排序规则是 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

当检查搜索字符串是否出现在列中时,如何定义相似性?有两种可能性:TRUE和FALSE,中间没有任何东西。实际上,您可以通过将搜索字符串的字符串长度除以列的字符串长度来获得因子,但您始终会得到最短的字符串 - 您想按实际列中出现次数排序吗?为什么不使用全文搜索? - opatut
不好意思,我的意思是你能否使用#2和#3进行搜索,并使用Levenshtein或类似算法按相似度排序吗?这样你就可以得到最相似的结果放在前面...请参考我问题中给出的示例。 - Robin Rodricks
这就是你要的,但我认为在使用LIKE时按Levenshtein排序没有意义。为什么在你的例子中要这样排序(1.采用/2.崇拜/3.装饰)?使用Levenshtein,它们具有相同的值(3,因为您总是需要添加3个字符)。 - opatut
MySQL的Dam-Lev实现很不错,但它产生的结果相当不稳定,因为Lev的哲学是“测量编辑”,而不是“测量差异”...请参见我上面更新的问题。 - Robin Rodricks
@opatut 是的,Levenshtein是一个不错的选择。但是当我有一组字符串想要与另一组字符串匹配时,如何找到Levenshtein距离的最小值呢? - Walter Schrabmair

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

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

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

您应该构建一个由单词对组成的数据集,并将它们标记为“相关”。

  • 使用已知相关词汇的来源(例如,古老的维基百科类别)作为正面示例
  • 大多数不被认为是相关的单词都不是相关的。

然后将你拥有的所有度量用作配对的特征。

现在你处于监督分类问题的领域。在数据集上构建一个分类器,根据你的需求进行评估,获得适合你需求的相似度测量。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接