模糊去重值

9

我有一个房地产列表的数据库,需要返回一个社区的列表。现在我正在使用mysql的DISTINCT函数,它返回所有不同的值。我的问题是有很多社区名称相似:例如:

Park View Sub 1
Park View
Park View Sub 2
Park View Sub 3
Great Lake Sub 1
Great Lake Sub 2
Great Lake 
Great Lake Sub 3

我正在寻找一种简单的php或mysql解决方案,它可以识别“Park View”和“Great Lake”已经存在,并且只返回“Park View”和“Great Lake”。
我的初始想法是以长度为排序顺序,这样短的值就在顶部,然后使用strstr循环。听起来像一个庞大的任务,我想知道是否有一个函数,在mysql或php中可以轻松完成这个任务。

你能否在问题中添加所需的输出以便更好地理解吗? - heretolearn
“Sub X” 是唯一会出现在结尾的字符串吗?还是那段文本是可变的? - Chris Baker
@sshekhar: “仅返回“Park View”和“Great Lake”。” - 这是预期的输出结果。 - Travesty3
谢谢Travesty3。关于Sub x - no.,那只是一个例子。它可以是任何东西,比如sub,flg,unit,bldg等等。 - user982853
1
@user982853 你将如何知道哪些是相关的字符串文本,哪些不是?换句话说,解决方案应该如何确定文本的重要部分和不重要部分?是否有一个“附加”文本的绝对列表?是否有字符限制?我只是不明白你的代码应该如何确定在“Park View Sub”中,“Sub”不相关,但在“Yellow Sub”中,“sub”应该保留。 - Chris Baker
显示剩余2条评论
4个回答

2

以下是您可以尝试的一些方法,假设您正在寻找完全匹配和近似匹配。

首先查找完全匹配。 然后在反转名称上查找LIKE匹配项。 然后查找具有最少额外字符的匹配项。

这是一个可以执行所有这些操作的查询。请注意,如果要使此过程高效,您需要将反转的地名存储在索引列中。

select name 
  from (
   select name, 0 ordinal
     from place 
    where name = 'Park View'
  union
  select name, 1 ordinal
    from place 
   where Reverse(Name) like concat(Reverse('Park View'),'%')
  union
  select name, 2+length(name)
    from place
   where name like concat('Park View','%')
 ) a 
order by ordinal
   limit 1

请注意,这个UNION查询使用ordinal来查找最佳匹配。
在这里查看:http://sqlfiddle.com/#!2/76a97/9/0

它只返回了公园景观,但它还应该返回绿色湖作为另一个不同的值。 - heretolearn

0
如果您始终有一个没有“Sub#”部分的条目,可以尝试以下方法:
SELECT DISTINCT neighborhood FROM table WHERE neighborhood NOT LIKE '% Sub %';

按字符串长度排序:
SELECT DISTINCT neighborhood FROM table ORDER BY LENGTH(neighborhood);

3
排除 Sub 的唯一问题在于,如果“Park View Sub 1”是仅有的附近社区,我希望它返回这一个。只有当已经存在包含 Sub 的社区时,我才想要排除它们。 - user982853

0

您可以使用PHP的similar_text来实现一个简单的解决方案。如果您预先对数据进行排序,使得较短、期望的地址排在前面,那么它应该能够很好地工作。此外,如果“不同”的地址不太相似,它将会更好地工作(但您始终可以提高阈值):

// if an address is 70% (or more) similar to another, it is not unique
$threshold = 70;

// list of addresses (and sorting them); this is done through the DB in your code
$addresses = array('Park View Sub 1', 'Park View', 'Park View Sub 2', 'Park View Sub 3', 'Great Lake Sub 1', 'Great Lake Sub 2', 'Great Lake', 'Great Lake Sub 3');
sort($addresses);

$unique = array();
foreach ($addresses as $address) {
    $isUnique = true;
    foreach ($unique as $u) {
        // get the similarity between the current address and each unique address
        similar_text($address, $u, $percent);
        if ($percent > $threshold) {
            // not unique; drop it
            $isUnique = false;
            break;
        }
    }
    if ($isUnique) $unique[] = $address;
}

对于其他选择,您还可以查看PHP的{{link1:levenshtein}}和{{link2:soundex}},以及MySQL的{{link3:SOUNDEX()}}。

另一种伪模糊方法是通过按字母顺序排序地址(通过MySQL或PHP),并逐个循环遍历它们;如果当前地址以已找到的唯一地址的文本开头,则将其删除。这与使用实际模糊方法非常相似,但更加直截了当:

// list of addresses (and sorting them); this is done through the DB in your code
$addresses = array('Park View Sub 1', 'Park View', 'Park View Sub 2', 'Park View Sub 3', 'Great Lake Sub 1', 'Great Lake Sub 2', 'Great Lake', 'Great Lake Sub 3');
sort($addresses);

$unique = array();
foreach ($addresses as $address) {
    $isUnique = true;
    foreach ($unique as $u) {
        if (substr($address, 0, strlen($u)) == $u) {
            $isUnique = false;
            break;
        }
    }
    if ($isUnique) $unique[] = $address;
}

这种方法仅在地址排序后有效,因为较短的地址Park View需要在Park View Sub 1之前找到。如果您的地址过于相似,上述similar_text方法会漏掉太多,那么可以尝试使用后面的函数,因为它更加严格。


0
下面的示例查询将使用MySQL获取指定的结果集,但它并不真正执行“模糊匹配”,至少我不会这样描述算法。(这实现了您描述的算法-按值排序,然后检查每个值,以查看前导部分是否与先前检索到的值“匹配”)。
这会找到邻域值的前导部分的“精确匹配”,与先前检索到的行的值进行比较,匹配没有任何“模糊性”。
当查询遇到一个“不匹配”的值时,它标记该值为“不匹配”。对于下一个检索到的值,它检查该值是否以先前的“不匹配”值开头;如果字符串的前导部分是精确匹配,则该值被丢弃。否则,该值被标记为“不匹配”值,并保留。
此方法使用内联视图(或MySQL所称的“派生表”)。最内层的内联视图(别名为s)为我们提供了按邻域排序的不同值列表。下一个内联视图(别名为“t”)的“技巧”(如果您想这样称呼它)在于我们利用MySQL用户变量引用先前检索到的值。
为避免与“特殊字符”相关的任何问题,我们对前导字符进行相等比较。
以下是整个查询:
SELECT t.neighborhood
  FROM (
         SELECT IF(IFNULL(LEFT(s.neighborhood,CHAR_LENGTH(@match)) <> @match,1),@match := s.neighborhood,NULL) AS neighborhood
           FROM (SELECT RTRIM(neighborhood) AS neighborhood
                   FROM mytable
                   JOIN (SELECT @match := NULL) r
                  GROUP BY neighborhood
                  ORDER BY neighborhood
                ) s
       ) t
 WHERE t.neighborhood IS NOT NULL  

这一切都非常简单,除了@match变量的初始化和执行当前值与先前值比较的表达式。

如果我们不担心值中引入特殊字符带来的边缘情况,我们可以使用更简单的LIKE或REGEXP进行比较:

s.neighborhood NOT LIKE CONCAT(@match,'%')

s.neighborhood NOT REGEXP CONCAT('^',@match)

LIKE运算符受到下划线和百分号字符的影响,而REGEXP则受到正则表达式中使用的特殊字符的影响。为了避免这些问题,上面的查询使用了一个看起来有点笨拙的比较方式:

LEFT(s.neighborhood,CHAR_LENGTH(@match)) <> @match

这段代码的作用是将先前的值(例如@match:='Park View')与下一个值的前导部分(长度为'Park View')进行比较,以确定它是否匹配。


使用此查询的好处之一是返回的值保证在后续查询的谓词中“匹配”。假设您正在使用此查询获取社区列表,并且用户已选择了一个社区。这将返回一组值,这些值将“匹配”每一行。

随后的查询可以在简单谓词(WHERE子句)中使用任何返回的值来返回匹配的行。例如,如果用户选择了值“Great Lake”:

SELECT t.*
  FROM mytable t
 WHERE LEFT(t.neighborhood,CHAR_LENGTH('Great Lake') = 'Great Lake'

在使用LIKE或REGEXP谓词进行匹配的情况下,我们希望在后续查询的谓词中使用相应的匹配。
SELECT t.*
  FROM mytable t
 WHERE t.neighborhood LIKE CONCAT('Great Lake','%')

SELECT t.*
  FROM mytable t
 WHERE t.neighborhood REGEXP CONCAT('^','Great Lake')

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