【问题标题】:fuzzy DISTINCT Values模糊 DISTINCT 值
【发布时间】:2012-08-23 07:45:48
【问题描述】:

我有一个房地产列表数据库,需要返回一个社区列表。现在我正在使用 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 中是否有一个函数可以轻松执行此操作。

【问题讨论】:

  • 您能否将所需的输出添加到您的问题中以便更好地理解..?
  • 是“Sub X”唯一的字符串,还是那个文本变量?
  • @sshekhar: "ONLY return "Park View" and "Great Lake"." - 这是预期的输出。
  • 谢谢 Travesty3。关于 Sub x - 没有。那只是一个例子。可以是 sub、flg、unit、bldg 等。
  • @user982853 那么你怎么知道什么是相关的字符串文本,什么不是相关的字符串文本呢?重申一下,解决方案应如何确定文本的哪些部分重要,哪些部分不重要?是否有“插件”文本的绝对列表?有字数限制吗?我只是不明白你的代码应该如何确定在“Park View Sub”中,“Sub”不相关,但在“Yellow Sub”中,“sub”应该保留。

标签: php mysql loops distinct strstr


【解决方案1】:

您可以尝试以下方法;大概您正在寻找完全匹配和紧密匹配。

首先查找完全匹配。 然后在 REVERSED 名称上查找 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

【讨论】:

  • 它只返回公园景观,但它也应该返回绿色湖泊,因为这也是一个独特的价值。
【解决方案2】:

如果你总是有一个没有“Sub #”部分的条目,你可以这样做:

SELECT DISTINCT neighborhood FROM table WHERE neighborhood NOT LIKE '% Sub %';

按字符串长度排序:

SELECT DISTINCT neighborhood FROM table ORDER BY LENGTH(neighborhood);

【讨论】:

  • 排除 Sub 的唯一问题是,如果“Park View Sub 1”是唯一的社区,我希望它返回那个。我希望他们排除的唯一时间是是否已经有一个包含它的社区。​​span>
【解决方案3】:

您可以使用 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 的 levenshteinsoundex,以及 MySQL 的 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 方法丢弃了太多,您可以尝试后一种功能,因为它更严格。

【讨论】:

    【解决方案4】:

    下面的示例查询将使用 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')
    

    【讨论】:

      猜你喜欢
      • 2012-07-28
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2014-08-13
      • 2021-11-05
      • 1970-01-01
      • 2020-03-17
      • 1970-01-01
      相关资源
      最近更新 更多