1. 首先如何判断内容的相似度,PostgreSQL中提供了中文分词,pg_trgm(将字符串切成多个不重复的token,计算两个字符串的相似度) .

对于本题,我建议采取中文分词的方式,首先将内容拆分成词组。

2. 在拆分成词组后,首先分组聚合,去除完全重复的数据。

3. 然后自关联生成笛卡尔(矩阵),计算出每条记录和其他记录的相似度。相似度的算法很简单,重叠的token数量除以集合的token去重后的数量。

4. 根据相似度,去除不需要的数据。

这里如果数据量非常庞大,使用专业的分析编程语言会更好例如 PL/R。

首先要安装PostgreSQL 中文分词插件

(阿里云AliCloudDB PostgreSQL已包含这个插件,用法参考官方手册)

  1. mv pg_jieba $PGSRC/contrib/
  2. export PATH=/home/digoal/pgsql9.5/bin:$PATH
  3. cd $PGSRC/contrib/pg_jieba
  4. make clean;make;make install
  5. git clone https://github.com/jaiminpan/pg_scws.git
  6. mv pg_jieba $PGSRC/contrib/
  7. export PATH=/home/digoal/pgsql9.5/bin:$PATH
  8. cd $PGSRC/contrib/pg_scws
  9. make clean;make;make install

创建插件

  1. psql
  2. # create extension pg_jieba;
  3. # create extension pg_scws;

创建测试CASE

  1. create table tdup1 (id int primary key, info text);
  2. create extension pg_trgm;
  3. insert into tdup1 values (1, '银屑病怎么治?');
  4. insert into tdup1 values (2, '银屑病怎么治疗?');
  5. insert into tdup1 values (3, '银屑病怎么治疗好?');
  6. insert into tdup1 values (4, '银屑病怎么能治疗好?');

这两种分词插件,可以任选一种。

创建三个函数,

计算2个数组的集合(去重后的集合)

  1. postgres=# create or replace function array_union(text[], text[]) returns text[] as $$
  2. select array_agg(c1) from (select c1 from unnest($1||$2) t(c1) group by c1) t;
  3. $$ language sql strict;
  4. CREATE FUNCTION

数组去重

  1. postgres=# create or replace function array_dist(text[]) returns text[] as $$
  2. select array_agg(c1) from (select c1 from unnest($1) t(c1) group by c1) t;
  3. $$ language sql strict;
  4. CREATE FUNCTION

计算两个数组的重叠部分(去重后的重叠部分)

  1. postgres=# create or replace function array_share(text[], text[]) returns text[] as $$
  2. $$ language sql strict;
  3. CREATE FUNCTION

笛卡尔结果是这样的:

regexp_split_to_array((regexp_replace(to_tsvector('jiebacfg',info)::text,'(:\d+)', '', 'g')),' ') 用于将info转换成数组。

以上生成的实际上是一个矩阵,simulate就是矩阵中我们需要计算的相似度:

我们在去重计算时不需要所有的笛卡尔积,只需要这个矩阵对角线的上部分或下部分数据即可。

所以加个条件就能完成。

  1. (select id,info,array_dist(regexp_split_to_array((regexp_replace(to_tsvector('jiebacfg',info)::text,'(:\d+)', '', 'g')),' ')) from tdup1)
  2. select * from (select t1.c1 t1c1,t2.c1 t2c1,t1.c2 t1c2,t2.c2 t2c2,t1.c3 t1c3,t2.c3 t2c3,round(array_length(array_share(t1.c3,t2.c3),1)::numeric/array_length(array_union(t1.c3,t2.c3),1),2)
  3. simulate from t t1,t t2 where t1.c1<>t2.c1 and t1.c1<t2.c1) t;
  4. t1c1 | t2c1 | t1c2 | t2c2 | t1c3 | t2c3 | simulate
  5. ------+------+--------------------+----------------------+-------------------+-------------------+----------
  6. 1 | 2 | 银屑病怎么治? | 银屑病怎么治疗? | {'银屑病','治'} | {'银屑病','治疗'} | 0.33
  7. 1 | 3 | 银屑病怎么治? | 银屑病怎么治疗好? | {'银屑病','治'} | {'银屑病','治疗'} | 0.33
  8. 1 | 4 | 银屑病怎么治? | 银屑病怎么能治疗好? | {'银屑病','治'} | {'银屑病','治疗'} | 0.33
  9. 2 | 3 | 银屑病怎么治疗? | 银屑病怎么治疗好? | {'银屑病','治疗'} | {'银屑病','治疗'} | 1.00
  10. 2 | 4 | 银屑病怎么治疗? | 银屑病怎么能治疗好? | {'银屑病','治疗'} | {'银屑病','治疗'} | 1.00
  11. 3 | 4 | 银屑病怎么治疗好? | 银屑病怎么能治疗好? | {'银屑病','治疗'} | {'银屑病','治疗'} | 1.00
  12. (6 rows)

开始对这些数据去重,去重的第一步,明确simulate, 例如相似度大于0.5的,需要去重。

  1. postgres=# with t(c1,c2,c3) as
  2. (select id,info,array_dist(regexp_split_to_array((regexp_replace(to_tsvector('jiebacfg',info)::text,'(:\d+)', '', 'g')),' ')) from tdup1)
  3. select * from (select t1.c1 t1c1,t2.c1 t2c1,t1.c2 t1c2,t2.c2 t2c2,t1.c3 t1c3,t2.c3 t2c3,round(array_length(array_share(t1.c3,t2.c3),1)::numeric/array_length(array_union(t1.c3,t2.c3),1),2)
  4. simulate from t t1,t t2 where t1.c1<>t2.c1 and t1.c1<t2.c1) t where simulate>0.5;
  5. t1c1 | t2c1 | t1c2 | t2c2 | t1c3 | t2c3 | simulate
  6. ------+------+--------------------+----------------------+-------------------+-------------------+----------
  7. 2 | 3 | 银屑病怎么治疗? | 银屑病怎么治疗好? | {'银屑病','治疗'} | {'银屑病','治疗'} | 1.00
  8. 2 | 4 | 银屑病怎么治疗? | 银屑病怎么能治疗好? | {'银屑病','治疗'} | {'银屑病','治疗'} | 1.00
  9. 3 | 4 | 银屑病怎么治疗好? | 银屑病怎么能治疗好? | {'银屑病','治疗'} | {'银屑病','治疗'} | 1.00
  10. (3 rows)

去重第二步,将t2c1列的ID对应的记录删掉即可。

  1. delete from tdup1 where id in (with t(c1,c2,c3) as
  2. (select id,info,array_dist(regexp_split_to_array((regexp_replace(to_tsvector('jiebacfg',info)::text,'(:\d+)', '', 'g')),' ')) from tdup1)
  3. select t2c1 from (select t1.c1 t1c1,t2.c1 t2c1,t1.c2 t1c2,t2.c2 t2c2,t1.c3 t1c3,t2.c3 t2c3,round(array_length(array_share(t1.c3,t2.c3),1)::numeric/array_length(array_union(t1.c3,t2.c3),1),2)
  4. simulate from t t1,t t2 where t1.c1<>t2.c1 and t1.c1<t2.c1) t where simulate>0.5);
  5. 例如 :
  6. postgres=# insert into tdup1 values (11, '白血病怎么治?');
  7. INSERT 0 1
  8. INSERT 0 1
  9. INSERT 0 1
  10. postgres=# insert into tdup1 values (24, '白血病怎么能治疗好?');
  11. INSERT 0 1
  12. postgres=#
  13. postgres=# with t(c1,c2,c3) as
  14. (select id,info,array_dist(regexp_split_to_array((regexp_replace(to_tsvector('jiebacfg',info)::text,'(:\d+)', '', 'g')),' ')) from tdup1)
  15. select * from (select t1.c1 t1c1,t2.c1 t2c1,t1.c2 t1c2,t2.c2 t2c2,t1.c3 t1c3,t2.c3 t2c3,round(array_length(array_share(t1.c3,t2.c3),1)::numeric/array_length(array_union(t1.c3,t2.c3),1),2)
  16. simulate from t t1,t t2 where t1.c1<>t2.c1 and t1.c1<t2.c1) t where simulate>0.5;
  17. t1c1 | t2c1 | t1c2 | t2c2 | t1c3 | t2c3 | simulate
  18. ------+------+--------------------+----------------------+-------------------+-------------------+----------
  19. 2 | 3 | 银屑病怎么治疗? | 银屑病怎么治疗好? | {'银屑病','治疗'} | {'银屑病','治疗'} | 1.00
  20. 2 | 4 | 银屑病怎么治疗? | 银屑病怎么能治疗好? | {'银屑病','治疗'} | {'银屑病','治疗'} | 1.00
  21. 3 | 4 | 银屑病怎么治疗好? | 银屑病怎么能治疗好? | {'银屑病','治疗'} | {'银屑病','治疗'} | 1.00
  22. 22 | 24 | 白血病怎么治疗? | 白血病怎么能治疗好? | {'治疗','白血病'} | {'治疗','白血病'} | 1.00
  23. 13 | 22 | 白血病怎么治疗好? | 白血病怎么治疗? | {'治疗','白血病'} | {'治疗','白血病'} | 1.00
  24. 13 | 24 | 白血病怎么治疗好? | 白血病怎么能治疗好? | {'治疗','白血病'} | {'治疗','白血病'} | 1.00
  25. (6 rows)
  26. postgres=# begin;
  27. BEGIN
  28. postgres=# delete from tdup1 where id in (with t(c1,c2,c3) as
  29. postgres(# (select id,info,array_dist(regexp_split_to_array((regexp_replace(to_tsvector('jiebacfg',info)::text,'(:\d+)', '', 'g')),' ')) from tdup1)
  30. postgres(# select t2c1 from (select t1.c1 t1c1,t2.c1 t2c1,t1.c2 t1c2,t2.c2 t2c2,t1.c3 t1c3,t2.c3 t2c3,round(array_length(array_share(t1.c3,t2.c3),1)::numeric/array_length(array_union(t1.c3,t2.c3),1),2)
  31. postgres(# simulate from t t1,t t2 where t1.c1<>t2.c1 and t1.c1<t2.c1) t where simulate>0.5);
  32. DELETE 4
  33. postgres=# select * from tdup1 ;
  34. id | info
  35. ----+--------------------
  36. 1 | 银屑病怎么治?
  37. 2 | 银屑病怎么治疗?
  38. 11 | 白血病怎么治?

用数据库解会遇到的问题, 因为我们的JOIN filter是<>和<,用不上hashjoin。

其他更优雅的方法,使用PLR或者R进行矩阵运算,得出结果后再进行筛选。

PLR

R

或者使用MPP数据库例如Greenplum加上R和madlib可以对非常庞大的数据进行处理。

MADLIB

MPP

这里用到了PG的什么特性?

1. 中文分词

2. 窗口查询功能

(本例中没有用到,但是如果你的数据没有主键时,则需要用ctid和row_number来定位到一条唯一记录)

《PostgreSQL 全文检索 - 词频统计》

《PostgreSQL UDF实现tsvector(全文检索), array(数组)多值字段与scalar(单值字段)类型的整合索引(类分区索引) - 单值与多值类型复合查询性能提速100倍+ 案例 (含,单值+多值列合成)》

《多流实时聚合 - 记录级实时快照 - JSON聚合与json全文检索的功能应用》

《用PostgreSQL 做实时高效 搜索引擎 - 全文检索、模糊查询、正则查询、相似查询、ADHOC查询》

《HTAP数据库 PostgreSQL 场景与性能测试之 7 - (OLTP) 全文检索 - 含索引实时写入》

《PostgreSQL 中英文混合分词特殊规则(中文单字、英文单词) - 中英分明》

《多国语言字符串的加密、全文检索、模糊查询的支持》

《PostgreSQL 中如何找出记录中是否包含编码范围内的字符,例如是否包含中文》

《如何解决数据库分词的拼写纠正问题 - PostgreSQL Hunspell 字典 复数形容词动词等变异还原》

《聊一聊双十一背后的技术 - 分词和搜索》

《PostgreSQL 如何高效解决 按任意字段分词检索的问题 - case 1》

《中文模糊查询性能优化 by PostgreSQL trgm》

《使用阿里云PostgreSQL zhparser中文分词时不可不知的几个参数》

《PostgreSQL Greenplum 结巴分词(by plpython)》

《PostgreSQL chinese full text search 中文全文检索》

《PostgreSQL USE plpythonu get Linux FileSystem usage》

https://github.com/jaiminpan/pg_jieba

http://joeconway.com/plr/

http://madlib.apache.org/