PostgreSQL:匹配通用名称变体昵称

问题描述

场景

我有许多企业数据集,我必须找到它们之间缺失的链接,我用来查找潜在匹配项的方法之一是连接名字和姓氏。复杂的是,我们有很多人在一个数据集(员工记录)中使用他们的法定姓名,但他们在其他数据集中使用昵称或(更糟糕的是)他们的中间名(即 EAD、培训、PIV 卡、等等。)。我正在寻找一种方法来匹配各种数据集中这些可能不同的名称

简化示例

这是我尝试做的一个过于简化的例子,但我认为它传达了我的思考过程。我从员工表开始:

员工表

employee_id first_name last_name
052451 罗伯特 阿姆斯登
442896 雅各 克拉克斯福德
054149 授予 基廷
025747 加布里埃尔 伦顿
071238 玛格丽特 塞芬马赫

并尝试从 PIV 卡片数据集中找到匹配的数据:

卡片表

card_id first_name last_name
1008571527 鲍比 阿姆斯登
1009599982 杰克 克拉克斯福德
1004786477 加比 伦顿
1000628540 玛姬 塞芬马赫

想要的结果

尝试在名字和姓氏上匹配这些数据集后,我想得到以下结果:

Employees_Cards 表

emp_employee_id emp_first_name emp_last_name crd_card_id crd_first_name crd_last_name
052451 罗伯特 阿姆斯登 1008571527 鲍比 阿姆斯登
442896 雅各 克拉克斯福德 1009599982 杰克 克拉克斯福德
054149 授予 基廷 NULL NULL NULL
025747 加布里埃尔 伦顿 1004786477 加比 伦顿
071238 玛格丽特 塞芬马赫 1000628540 玛姬 塞芬马赫

如您所见,我想进行以下匹配:

加布里埃尔 -> 加比
雅各布 -> 雅各布
玛格丽特 -> 玛姬
罗伯特 -> 鲍比

我最初的想法是找到一个通用名称数据集:

Name_Aliases 表

name1 name2 name3 name4
加布里埃尔 加比 NULL NULL
雅各 杰克 NULL NULL
玛格丽特 玛姬 玛姬 梅格
迈克尔 迈克 Mikey 米克
罗伯特 鲍比 鲍勃 罗布

并在 JOIN 中使用类似的东西:

CREATE TABLE employee_cards AS
    SELECT 
        employees.employee_id AS emp_employee_ID,employees.first_name AS emp_first_name,employees.last_name AS emp_last_name,cards.card_id AS crd_card_id,cards.first_name AS crd_first_name,cards.last_name AS crd_last_name
    FROM employees
    LEFT OUTER JOIN name_aliases
    LEFT OUTER JOIN cards
        ON employees.first_name IN (
            nane_aliases.name1,nane_aliases.name2,nane_aliases.name3,nane_aliases.name4
        )
        AND employees.last_name = cards.last_name;

这就是我卡住的地方,因为我不知道如何将第一个 ON 条件的结果与卡片表中的名字联系起来。

一些问题

当我更深入地思考这个问题时,我知道我不是第一个遇到这种常见名称变体匹配需求的人。我最初的搜索向我指出了 fuzzysearchsoundex 之类的东西,但这些并不是我当前场景所需要的(尽管它们可能会派上用场)。鉴于此,我有几个问题要问社区:

可下载的通用名称变体数据集?

是否有人按照我上面的 name-aliases 表的内容编译或众包了一个综合名称变体数据集?我的搜索让我找到了几个似乎有此类数据的网站,但它们都无法下载以导入到我的本地数据库中。

我确实发现这个 SO 讨论已经有十多年的历史了,但它似乎不是最新的:Database of common name aliases / nicknames of people

另外,我无法为此支付任何费用,所以我希望github上可能有一个隐藏的人。

构造 Name_Aliases 表的更好方法

既然 name_aliases 表中的每条记录都可以有两个或多个条目,那么有没有更好的方法来设置该表的结构,使其具有无限的灵活性?

匹配 Name_Aliases 表中的任何列?

如何设置 JOIN 查询以将员工.first_name 与 name_alises 中的任何列匹配,然后最终将其与 card.first_name 匹配?

更好的解决方案?

我是否采取了错误方法解决这个问题?有人使用 Postgresql 提出了更灵活、更优雅的方法吗?

解决方法

解决方案

这有点像一场战斗,但我的所有问题都得到了解答,我很高兴终于让这个项目的一切工作顺利进行。详情如下。

昵称数据集

我在 Github 上发现了几个很有前途的昵称数据集,这个看起来是维护最积极的:https://github.com/carltonnorthern/nickname-and-diminutive-names-lookup。我将names.csv 文件下载到我的计算机,并使用以下代码将其导入到我的数据库中:

导入脚本

DROP TABLE IF EXISTS names_aliases_temp;
CREATE TABLE names_aliases_temp
(
   names_data text
);

COPY names_aliases_temp
FROM '~/Downloads/names.csv';

DROP TABLE IF EXISTS names_aliases;
CREATE TABLE names_aliases
(
   id serial,nicknames text[]
);

INSERT INTO names_aliases (nicknames)
SELECT string_to_array(names_data,',') FROM names_aliases_temp;

DROP TABLE IF EXISTS names_aliases_temp;

CREATE INDEX idx_gin_names ON names_aliases USING GIN(nicknames);

Names_Aliases 数据集

以下是导入数据库后的示例:

id 昵称
1 {aaron,erin,ronnie,ron}
2 {abbigail,nabby,abby,gail}
3 {abednego,bedney}
4 {abel,ebbie,ab,abe,eb}
5 {abiel,ab}
6 {abigail,gail}
7 {abijah,bige}
8 {abner,ab}
9 {abraham,abe}
10 {abram,abe}

使用数组的注意事项

我对进入数组格式的难易感到兴奋,但更让我兴奋的是我能够在 JOIN 查询中使用数组格式!由于每行的条目数差异很大,我发现 array 数据类型使其非常适合此数据,这也使得使用 ILIKE ANY() 运算符匹配记录变得非常容易。

JOIN 查询脚本

DROP TABLE IF EXISTS employee_cards;

CREATE TABLE employee_cards AS
WITH joined_data AS (
    SELECT
        employees.employee_id AS emp_id,employees.first_name  AS emp_first_name,employees.last_name   AS emp_last_name,cards.first_name      AS crd_first_name,cards.last_name       AS crd_last_name,cards.card_id         AS crd_id
    FROM employees
        -- Attempt to match first names with nicknames
        LEFT JOIN names_aliases
            ON employees.first_name ILIKE ANY(names_aliases.nicknames)
        LEFT JOIN cards
            -- First match records where name is the same between `employees` and `cards`
            ON (employees.last_name ILIKE cards.last_name
               AND employees.first_name ILIKE cards.first_name)
            -- Then bring in nicknames where no matches are found
            OR (employees.last_name ILIKE cards.last_name
                AND cards.first_name ILIKE ANY(names_aliases.nicknames))
)
-- Put successful matches at the top for each employee and retain only the first row
SELECT DISTINCT ON (emp_id)
    emp_id,emp_first_name,emp_last_name,crd_first_name,crd_last_name,crd_id
FROM joined_data
ORDER BY
    emp_id,crd_id NULLS LAST;

使用的表格

为方便起见,本练习中使用的三个表格如下。

员工表

employee_id first_name last_name
052451 罗伯特 阿姆斯登
022448 迈克尔 棕色
442896 雅各 克拉克斯福德
054149 授予 基廷
025747 加布里埃尔 伦顿
425972 Consorcia 雷亚斯
071238 玛格丽特 塞芬马赫
insert into public.employees (employee_id,first_name,last_name)
values  ('052451','Robert','Armsden'),('022448','Michael','Brown'),('442896','Jacob','Craxford'),('054149','Grant','Keeting'),('025747','Gabrielle','Renton'),('425972','Consorcia','Reyas'),('071238','Margaret','Seifenmacher');

卡片表

card_id first_name last_name
1008571527 鲍勃 阿姆斯登
1000594085 迈克尔 棕色
1009599982 杰克 克拉克斯福德
1004786477 Gabby 伦顿
1009481574 Consorcia 雷亚斯
1000628540 玛姬 塞芬马赫
insert into public.cards (card_id,last_name)
values  ('1008571527','Bob',('1000594085',('1009599982','Jake',('1004786477','Gabby',('1009481574',('1000628540','Maggy','Seifenmacher');

员工卡(联接)表

emp_id emp_first_name emp_last_name crd_first_name crd_last_name crd_id
052451 罗伯特 阿姆斯登 鲍勃 阿姆斯登 1008571527
022448 迈克尔 棕色 迈克尔 棕色 1000594085
442896 雅各 克拉克斯福德 杰克 克拉克斯福德 1009599982
054149 授予 基廷 NULL NULL NULL
025747 加布里埃尔 伦顿 Gabby 伦顿 1004786477
425972 Consorcia 雷亚斯 Consorcia 雷亚斯 1009481574
071238 玛格丽特 塞芬马赫 玛姬 塞芬马赫 1000628540
insert into public.employee_cards (emp_id,crd_id)
values  ('052451','Armsden','1008571527'),'Brown','1000594085'),'Craxford','1009599982'),'Keeting',null,null),'Renton','1004786477'),'Reyas','1009481574'),'Seifenmacher','1000628540');
,

如何构造和查询别名表是一个有趣的问题。我建议成对组织它而不是更宽的行,因为您事先不知道在一组连接的名称中最终可能需要多少变体,而两列结构使您能够添加到给定的组无限期:

姓名1 name2
雅各 杰克
玛格丽特 玛姬
玛格丽特 玛姬
玛格丽特 梅格
玛姬 玛姬
玛姬 梅格
玛姬 梅格

然后您只需检查查询中每个 JOIN 中的两列,如下所示:

SELECT DISTINCT
    employees.employee_id AS emp_employee_id,employees.first_name AS emp_first_name,employees.last_name  AS emp_last_name,cards.card_id        AS card_id,cards.first_name     AS crd_first_name,cards.last_name      AS crd_last_name
FROM
    employees
    INNER JOIN name_aliases ON
        name_aliases.name1    = employees.first_name
        OR name_aliases.name2 = employee.first_name
    LEFT JOIN cards ON
        (
            cards.first_name    = name_aliases.name1
            OR cards.first_name = name_aliases.name2
        )
        AND cards.last_name = employees.last_name

这假设一些员工不在卡片表中,并且没有两个人有相同的名字。如果这些假设是正确的,您将需要 DISTINCT 来消除部分外连接上的重复项。

您可以通过在全外连接中配对名称来进一步简化查询逻辑:

姓名1 name2
雅各 雅各
雅各 杰克
杰克 雅各
杰克 杰克
玛格丽特 玛格丽特
玛格丽特 玛姬
玛姬 玛格丽特
玛姬 玛姬
梅格 梅格
玛格丽特 梅格
梅格 玛格丽特
玛姬 梅格
梅格 玛姬

依此类推,然后您可以像对待多对多连接表一样对待别名表:

SELECT DISTINCT
    employees.employee_id AS emp_employee_id,cards.last_name      AS crd_last_name
FROM
    employees
    INNER JOIN name_aliases ON name_aliases.name1 = employees.first_name
    LEFT JOIN cards ON
        cards.first_name    = name_aliases.name2
        AND cards.last_name = employees.last_name