用圖技術搞定附近好友、時空交集等 7 個典型社交網絡應用

兩個月之前,我的同事拿了一張推特的互動關係圖(下圖,由 STRRL 授權)來問我能不能搞一篇圖技術來探索社交互動關係的文章,看看這些圖是如何通過技術實現的。

我想了想,自己玩推特以來也跟隨大部隊生成了不少的社交關係組圖,當中有複雜的社交羣體劃分:

我在技術圈,看在金融、數學圈的大佬在彼岸緊密貼貼。當然也有比較簡單的關係圖:

看誰和你互動比較多,而他們又和誰關係比較密切。那麼問題來了,像上面這種常見的社交關係圖,甚至是別的更復雜的基於社交網絡的圖是如何生成的呢?在本文我將用圖數據庫 NebulaGraph 來解決社交網絡問題,而上面的社交關係組圖也被包含在其中。btw,文中介紹的方法提供都了 Playground 供大家學習、玩耍。

簡單剖析社交網絡的選型

從上面的圖我們可以知道,一個典型的社交網絡拓撲圖便是用戶的點和關係的邊組成的網狀結構。

因此,我們可以用圖數據庫來表示用戶和他們的連接關係,來完成這個社交網絡的數據模型。基於圖數據庫,我們可以對用戶間的關係進行查詢,讓各類基於社交網絡連接關係的查找、統計、分析需求變得更便捷、高效。

例如,利用圖形數據庫來識別網絡中的“有影響力的用戶”;根據用戶之間的共同點對新的連接(好友關係、感興趣的信息)進行推薦;更甚者尋找社羣中聚集的不同人羣、社區,進行用戶畫像。

以 NebulaGraph 爲代表的圖數據庫不僅能支撐複雜的多跳查詢,同時也支持實時寫入、更新數據,因此非常適合用來探索用戶關係不斷變化的社交網絡系統。

圖建模

上文說過社交網絡天然就是一種網絡、圖的結構形態,爲了分析常見社交場景的應用示例,本文的例子採用了典型的小型社交網絡。因此,我在 NebulaGraph 官方數據集 basketballplayer 之上,增加了:

三種點:

  • 地址
  • 地點
  • 文章

四種邊:

  • 發文
  • 評論
  • 住在
  • 屬於(地點)

它的建模非常自然:

schema_sketch

數據導入

導入數據集

首先,我們加載默認的 basketballplayer 數據集,導入對應的 schema 和數據。

如果你使用的是命令行,那你在 console 之中執行 :play basketballplayer 就可以導入數據。如果你使用了可視化圖探索工具 NebulaGraph Studio / NebulaGraph Explorer,我們需要在歡迎頁點擊下載、部署這份基礎數據集。

basketballplayer_studio_starter

創建社交網絡 schema

通過下面語句創建新加入的點、邊類型 schema:

CREATE TAG IF NOT EXISTS post(title string NOT NULL);
CREATE EDGE created_post(post_time timestamp);
CREATE EDGE commented_at(post_time timestamp);
CREATE TAG address(address string NOT NULL, `geo_point` geography(point));
CREATE TAG place(name string NOT NULL, `geo_point` geography(point));
CREATE EDGE belong_to();
CREATE EDGE lived_in();

寫入新數據

在等待兩個心跳(20秒)以上時間之後,我們可以執行數據插入:

INSERT VERTEX post(title) values \
    "post1":("a beautify flower"), "post2":("my first bike"), "post3":("I can swim"), \
    "post4":("I love you, Dad"), "post5":("I hate coriander"), "post6":("my best friend, tom"), \
    "post7":("my best friend, jerry"), "post8":("Frank, the cat"), "post9":("sushi rocks"), \
    "post10":("I love you, Mom"), "post11":("Let's have a party!");

INSERT EDGE created_post(post_time) values \
    "player100"->"post1":(timestamp("2019-01-01 00:30:06")), \
    "player111"->"post2":(timestamp("2016-11-23 10:04:50")), \
    "player101"->"post3":(timestamp("2019-11-11 10:44:06")), \
    "player103"->"post4":(timestamp("2014-12-01 20:45:11")), \
    "player102"->"post5":(timestamp("2015-03-01 00:30:06")), \
    "player104"->"post6":(timestamp("2017-09-21 23:30:06")), \
    "player125"->"post7":(timestamp("2018-01-01 00:44:23")), \
    "player106"->"post8":(timestamp("2019-01-01 00:30:06")), \
    "player117"->"post9":(timestamp("2022-01-01 22:23:30")), \
    "player108"->"post10":(timestamp("2011-01-01 10:00:30")), \
    "player100"->"post11":(timestamp("2021-11-01 11:10:30"));

INSERT EDGE commented_at(post_time) values \
    "player105"->"post1":(timestamp("2019-01-02 00:30:06")), \
    "player109"->"post1":(timestamp("2016-11-24 10:04:50")), \
    "player113"->"post3":(timestamp("2019-11-13 10:44:06")), \
    "player101"->"post4":(timestamp("2014-12-04 20:45:11")), \
    "player102"->"post1":(timestamp("2015-03-03 00:30:06")), \
    "player103"->"post1":(timestamp("2017-09-23 23:30:06")), \
    "player102"->"post7":(timestamp("2018-01-04 00:44:23")), \
    "player101"->"post8":(timestamp("2019-01-04 00:30:06")), \
    "player106"->"post9":(timestamp("2022-01-02 22:23:30")), \
    "player105"->"post10":(timestamp("2011-01-11 10:00:30")), \
    "player130"->"post1":(timestamp("2019-01-02 00:30:06")), \
    "player131"->"post2":(timestamp("2016-11-24 10:04:50")), \
    "player131"->"post3":(timestamp("2019-11-13 10:44:06")), \
    "player133"->"post4":(timestamp("2014-12-04 20:45:11")), \
    "player132"->"post5":(timestamp("2015-03-03 00:30:06")), \
    "player134"->"post6":(timestamp("2017-09-23 23:30:06")), \
    "player135"->"post7":(timestamp("2018-01-04 00:44:23")), \
    "player136"->"post8":(timestamp("2019-01-04 00:30:06")), \
    "player137"->"post9":(timestamp("2022-01-02 22:23:30")), \
    "player138"->"post10":(timestamp("2011-01-11 10:00:30")), \
    "player141"->"post1":(timestamp("2019-01-03 00:30:06")), \
    "player142"->"post2":(timestamp("2016-11-25 10:04:50")), \
    "player143"->"post3":(timestamp("2019-11-14 10:44:06")), \
    "player144"->"post4":(timestamp("2014-12-05 20:45:11")), \
    "player145"->"post5":(timestamp("2015-03-04 00:30:06")), \
    "player146"->"post6":(timestamp("2017-09-24 23:30:06")), \
    "player147"->"post7":(timestamp("2018-01-05 00:44:23")), \
    "player148"->"post8":(timestamp("2019-01-05 00:30:06")), \
    "player139"->"post9":(timestamp("2022-01-03 22:23:30")), \
    "player140"->"post10":(timestamp("2011-01-12 10:01:30")), \
    "player141"->"post1":(timestamp("2019-01-04 00:34:06")), \
    "player102"->"post2":(timestamp("2016-11-26 10:06:50")), \
    "player103"->"post3":(timestamp("2019-11-15 10:45:06")), \
    "player104"->"post4":(timestamp("2014-12-06 20:47:11")), \
    "player105"->"post5":(timestamp("2015-03-05 00:32:06")), \
    "player106"->"post6":(timestamp("2017-09-25 23:31:06")), \
    "player107"->"post7":(timestamp("2018-01-06 00:46:23")), \
    "player118"->"post8":(timestamp("2019-01-06 00:35:06")), \
    "player119"->"post9":(timestamp("2022-01-04 22:26:30")), \
    "player110"->"post10":(timestamp("2011-01-15 10:00:30")), \
    "player111"->"post1":(timestamp("2019-01-06 00:30:06")), \
    "player104"->"post11":(timestamp("2022-01-15 10:00:30")), \
    "player125"->"post11":(timestamp("2022-02-15 10:00:30")), \
    "player113"->"post11":(timestamp("2022-03-15 10:00:30")), \
    "player102"->"post11":(timestamp("2022-04-15 10:00:30")), \
    "player108"->"post11":(timestamp("2022-05-15 10:00:30"));

INSERT VERTEX `address` (`address`, `geo_point`) VALUES \
    "addr_0":("Brittany Forge Apt. 718 East Eric  WV 97881", ST_Point(1,2)),\
    "addr_1":("Richard Curve Kingstad  AZ 05660", ST_Point(3,4)),\
    "addr_2":("Schmidt Key Lake Charles  AL 36174", ST_Point(13.13,-87.65)),\
    "addr_3":("5 Joanna Key Suite 704 Frankshire  OK 03035", ST_Point(5,6)),\
    "addr_4":("1 Payne Circle Mitchellfort  LA 73053", ST_Point(7,8)),\
    "addr_5":("2 Klein Mission New Annetteton  HI 05775", ST_Point(9,10)),\
    "addr_6":("1 Vanessa Stravenue Suite 184 Baileyville  NY 46381", ST_Point(11,12)),\
    "addr_7":("John Garden Port John  LA 54602", ST_Point(13,14)),\
    "addr_8":("11 Webb Groves Tiffanyside  MN 14566", ST_Point(15,16)),\
    "addr_9":("70 Robinson Locks Suite 113 East Veronica  ND 87845", ST_Point(17,18)),\
    "addr_10":("24 Mcknight Port Apt. 028 Sarahborough  MD 38195", ST_Point(19,20)),\
    "addr_11":("0337 Mason Corner Apt. 900 Toddmouth  FL 61464", ST_Point(21,22)),\
    "addr_12":("7 Davis Station Apt. 691 Pittmanfort  HI 29746", ST_Point(23,24)),\
    "addr_13":("1 Southport Street Apt. 098 Westport  KY 85907", ST_Point(120.12,30.16)),\
    "addr_14":("Weber Unions Eddieland  MT 64619", ST_Point(25,26)),\
    "addr_15":("1 Amanda Freeway Lisaland  NJ 94933", ST_Point(27,28)),\
    "addr_16":("2 Klein HI 05775", ST_Point(9,10)),\
    "addr_17":("Schmidt Key Lake Charles AL 13617", ST_Point(13.12, -87.60)),\
    "addr_18":("Rodriguez Track East Connorfort  NC 63144", ST_Point(29,30));

INSERT VERTEX `place` (`name`, `geo_point`) VALUES \
    "WV":("West Virginia", ST_Point(1,2.5)),\
    "AZ":("Arizona", ST_Point(3,4.5)),\
    "AL":("Alabama", ST_Point(13.13,-87)),\
    "OK":("Oklahoma", ST_Point(5,6.1)),\
    "LA":("Louisiana", ST_Point(7,8.1)),\
    "HI":("Hawaii", ST_Point(9,10.1)),\
    "NY":("New York", ST_Point(11,12.1)),\
    "MN":("Minnesota", ST_Point(15,16.1)),\
    "ND":("North Dakota", ST_Point(17,18.1)),\
    "FL":("Florida", ST_Point(21,22.1)),\
    "KY":("Kentucky", ST_Point(120.12,30)),\
    "MT":("Montana", ST_Point(25,26.1)),\
    "NJ":("New Jersey", ST_Point(27,28.1)),\
    "NC":("North Carolina", ST_Point(29,30.1));

INSERT EDGE `belong_to`() VALUES \
    "addr_0"->"WV":(),\
    "addr_1"->"AZ":(),\
    "addr_2"->"AL":(),\
    "addr_3"->"OK":(),\
    "addr_4"->"LA":(),\
    "addr_5"->"HI":(),\
    "addr_6"->"NY":(),\
    "addr_7"->"LA":(),\
    "addr_8"->"MN":(),\
    "addr_9"->"ND":(),\
    "addr_10"->"MD":(),\
    "addr_11"->"FL":(),\
    "addr_12"->"HI":(),\
    "addr_13"->"KY":(),\
    "addr_14"->"MT":(),\
    "addr_15"->"NJ":(),\
    "addr_16"->"HI":(),\
    "addr_17"->"AL":(),\
    "addr_18"->"NC":();

INSERT EDGE `lived_in`() VALUES \
    "player100"->"addr_4":(),\
    "player101"->"addr_7":(),\
    "player102"->"addr_2":(),\
    "player103"->"addr_3":(),\
    "player104"->"addr_0":(),\
    "player105"->"addr_5":(),\
    "player106"->"addr_6":(),\
    "player107"->"addr_1":(),\
    "player108"->"addr_8":(),\
    "player109"->"addr_9":(),\
    "player110"->"addr_10":(),\
    "player111"->"addr_11":(),\
    "player112"->"addr_12":(),\
    "player113"->"addr_13":(),\
    "player114"->"addr_14":(),\
    "player115"->"addr_15":(),\
    "player116"->"addr_16":(),\
    "player117"->"addr_17":(),\
    "player118"->"addr_18":();

數據初探

完成 schema 創建和數據導入之後,我們來看看數據統計:

[basketballplayer]> SUBMIT JOB STATS;

+------------+
| New Job Id |
+------------+
| 10         |
+------------+
[basketballplayer]> SHOW STATS;
+---------+----------------+-------+
| Type    | Name           | Count |
+---------+----------------+-------+
| "Tag"   | "address"      | 19    |
| "Tag"   | "place"        | 14    |
| "Tag"   | "player"       | 51    |
| "Tag"   | "post"         | 10    |
| "Tag"   | "team"         | 30    |
| "Edge"  | "belong_to"    | 19    |
| "Edge"  | "commented_at" | 40    |
| "Edge"  | "created_post" | 10    |
| "Edge"  | "follow"       | 81    |
| "Edge"  | "lived_in"     | 19    |
| "Edge"  | "serve"        | 152   |
| "Space" | "vertices"     | 124   |
| "Space" | "edges"        | 321   |
+---------+----------------+-------+
Got 13 rows (time spent 1038/51372 us)

查一下所有的數據:

MATCH ()-[e]->() RETURN e LIMIT 10000

因爲數據量太小了,這裏可以把所有數據在 NebulaGraph Explorer 中進行渲染出來:

match_all

社交網絡典型應用

找出網絡中的關鍵人物

要識別社交網絡中的有影響力的關鍵人物們(Influencer)需要使用上各種指標和方法,而 KOL 的識別對很多業務場景都有幫助,比如:用於營銷或研究網絡中的信息傳播。

識別他們的方法有很多,具體的方法和考量信息、關係、角度也取決於這些關鍵人物的類型和獲取他們的目的。

識別 Influencer 常見的方法包括看他們的粉絲數、內容瀏覽量,內容(包括帖子、視頻)上讀者的參與度,以及他們的內容影響力(轉發、引用)。這些思路如果用圖數據庫實現也沒啥問題,不過太普通太普通了,我們搞點高科技的——用評估、計算節點重要性的圖算法,在圖上得出這些關鍵人物。

PageRank

PageRank 是一個非常“古老的”圖算法,它計算圖上點之間的關係數量得到每個點的得分(Rank),最初由 Google 的創始人 Larry Page 和 Sergey Brin 提出並應用在早期的 Google 搜索引擎中,用來排序搜索結果,這裏的 Page 可以是 Larry Page 的姓和 Web Page 的雙關了。

在現代、複雜的搜索引擎中,PageRank 早就因爲其過於簡單的設計而被棄用,但是在部分圖結構網絡場景中,PageRank 算法仍然在發光發熱。在社交網絡中,我們姑且粗略地認爲所有連接的重要程度相同,去運行 PageRank 算法找到那些 Influencer。

在 NebulaGraph 中,我們可以利用圖計算工具 NebulaGraph Algorithm、NebulaGraph Analytics 在全圖上運行 PageRank,這是因爲數據量小。但在日常的分析、驗證、設計截斷時,我們不需要在全量數據上跑結果,在一些很小的子圖上(最多上萬個節點),我們可以輕鬆地在瀏覽器裏邊運行各種圖算法得到業務可用的數據。

這裏,我們就用 NebulaGraph Explorer 內置的圖算法功能,在瀏覽器上點擊一下鼠標執行 PageRank 看看,具體方法這裏略去,可以參考文檔

PageRank

如圖所示,PageRank 計算之後所有綠色的 player(人)中,"player.name: Tim Duncan" 是最大的一個點。與之相關聯的關係看起來的確不少,我們在圖上選擇他,再右鍵反選,選擇除了 Tim Duncan 之外的所有點,用退格鍵刪除所有其他的點,再以他作爲起點雙向探索出 1 到 5 步,可以得到 Tim Duncan 的子圖:

TimDuncan

除了可以看到 Tim Duncan 是個 Influener 之外,我們還可以看到其他受歡迎的隊員和他一樣服役過知名球隊的熱刺(Spurs),這些都印證了 PageRank 的評估方式。

現在,我們再看看其他判定維度下的算法會不會得出一樣的結論呢?

Betweenness Centrality

作爲另一個流行的節點重要性算法,通過計算一個節點在圖中起到多少橋樑作用來衡量節點的重要性。這裏的橋樑作用是有數學定義的量化算法,這裏就不展開說了。不過從感官上,可以看出它是另一個符合直覺去評估節點重要性的方法。

我們重新在畫布上查詢所有的點邊之後,在瀏覽器裏運行 Betweenness Centrality 算法,這次的結果是:

Betweeness_centrality

從它的五跳內子圖可以看出,與之前 PageRank 所得的關鍵人物 Tim Duncan 呈現的星狀不同,Dejounte Murray 的子圖呈現簇狀,在感官、直覺上可以想象 Dejounte Murray 真的在很多節點之間的最小路徑上,而 Tim Duncan 則似乎和更多的重要連接者產生了關聯。

DejounteMurray

在實際的應用場景中,針對不同需求我們會選擇不同的算法。一般來說,要嘗試各種定義、試驗各種執行結果,以便分析去找到我們關注的關鍵人物產生影響的結構特徵。

找出社區、聚集羣體

社交網絡中的社區檢測是一種通過分析社交關係來發現社區結構的技術。社區結構是指在社交網絡、圖譜中互相聯繫密切的一組節點,這些節點通常具有相似的特徵。例如,社區結構其中一種表現形式爲:都對某類的話題或內容感興趣而聚集的一組用戶。

社區檢測的目的是通過對社交網絡進行分析,找出不同社區的邊界,並確定每個社區中的節點。這個過程可以通過各種算法來完成,例如:標籤傳播算法、弱聯通分量算法和 Louvain 算法等。通過發現社區結構,可以更好地瞭解社交網絡的結構和特徵,並有助於社交網絡服務方更好地推斷和預測社交網絡中的行爲,協助治理社交網絡、廣告投放、市場營銷等行爲。

由於我們的數據集是模擬的,在不同的算法之下得出的結果未必能展現其真實的面貌,所以本章只是簡單地展示下利用若干圖算法進行社區識別之後的結果。在真實世界的案例中,我們應該在此基礎之上結合領域知識或者其他技術手段協同給出不同羣體、社區的畫像、標籤。

標籤傳播算法效果:

LPA

Louvain 算法效果:

Louvain

弱聯通分量算法效果:

WCC

在後面的章節,我們會在更小、更簡單的子圖上再次驗證這幾個算法,結果會更具有可解釋性。

好友親密度

通過社區識別算法,其實能在一定程度上在全局計算中獲得興趣相近、關聯緊密的好友。那麼,如何獲得一個指定用戶的其他親密好友呢?

我們可以通過計算得到這個用戶的好友,根據該好友和用戶擁有的共同好友數進行排序,最後得到結果。

這裏以 "Tim Duncan" 舉例,他的兩度好友(好友的好友)相關語句是 (:player{name: "Tim Duncan"})-[:follow]-(f:player)-[:follow]-(fof:player)。如果 Tim Duncan 的兩度好友同時也是他的好友的話,那麼這個中間好友就是 Tim Duncan 和這個兩度好友的共同好友 Mutual Friend。我們有理由相信那些和 Tim Duncan 有更多共同好友的人可能跟他有更高的親密度:

MATCH (start:`player`{name: "Tim Duncan"})-[:`follow`]-(f:`player`)-[:`follow`]-(fof:`player`),
(start:`player`)-[:`follow`]-(fof:`player`)
RETURN fof.`player`.name, count(DISTINCT f) AS NrOfMutualF ORDER BY NrOfMutualF DESC;

這個計算結果是,"Tony Parker" 和 Tim 有 5 個共同好友,最爲親密。

fof.player.name NrOfMutualF
Tony Parker 5
Dejounte Murray 4
Manu Ginobili 3
Marco Belinelli 3
Danny Green 2
Boris Diaw 1
LaMarcus Aldridge 1
Tiago Splitter 1

下面,咱們通過可視化來驗證一下這個結果吧!

先看看每一個好友的共同好友 (f:) 都是誰?

MATCH (start:player{name: "Tim Duncan"})-[:`follow`]-(f:player)-[:`follow`]-(fof:player),
(start:player)-[:`follow`]-(fof:player)
RETURN fof.player.name, collect(DISTINCT f.player.name);

結果如下:

fof.player.name collect(distinct f.player.name)
Boris Diaw ["Tony Parker"]
Manu Ginobili ["Dejounte Murray", "Tiago Splitter", "Tony Parker"]
LaMarcus Aldridge ["Tony Parker"]
Tiago Splitter ["Manu Ginobili"]
Tony Parker ["Dejounte Murray", "Boris Diaw", "Manu Ginobili", "Marco Belinelli", "LaMarcus Aldridge"]
Dejounte Murray ["Danny Green", "Tony Parker", "Manu Ginobili", "Marco Belinelli"]
Danny Green ["Dejounte Murray", "Marco Belinelli"]
Marco Belinelli ["Dejounte Murray", "Danny Green", "Tony Parker"]

下面,我們上可視化工具——NebulaGraph Explorer 搞一搞這個結果:

首先,我們把 Tim 的兩度好友路徑全查出來

MATCH p=(start:player{name: "Tim Duncan"})-[:`follow`]-(f:player)-[:follow]-(fof:player)
RETURN p

再按照出入度去渲染節點大小,並選中 Tim 和 Tony,並在兩者之間查詢 follow 類型邊、雙向、最多 2 跳的全部路徑:

可以看出 Tim 和 Tony 是最親密的朋友沒跑了,而且他們的共同好友也在路徑之中:

["Dejounte Murray", "Boris Diaw", "Manu Ginobili", "Marco Belinelli", "LaMarcus Aldridge"]

closest_friend

朋友圈子裏的小羣體

上面提過由於這份數據集非真實,使得社區發現算法的結果不能得到其中洞察的內涵。現在我們可以接着這個小的子圖,來看看 Tim 的好友中可以如何區分羣組、社區呢?

咱們先跑一個 Louvain 、弱聯通分量、標籤傳播看看:

弱聯通分量

弱聯通分量,可以把 Tim 的朋友們大體分割出兩、三個相互不連通的部分,非常符合連通分量的直觀理解和定義。

Tim_wcc

標籤傳播

標籤傳播,我們可以通過控制迭代次數按需去通過隨機的傳播劃定出不同的劃分度,結果可以有一定的區分度:

這是 20 次迭代的圖:

Tim_LPA

這是 1,000 次迭代

Tim_LPA_1000

Louvain

Louvain,是一個比較高效、穩定的算法,基本上在這個子圖下我們可以在很小的迭代次數下得到很符合直覺的劃分:

Tim_Louvain

新朋友推薦

接着前面兩度朋友(朋友的朋友)的思路,我們可以很容易把那些還不是朋友的兩度朋友作爲推薦添加的好友,而排序規則是他們之間的共同好友數量:

MATCH (start:player{name: "Tim Duncan"})-[:`follow`]-(f:player)-[:`follow`]-(fof:player)
WHERE NOT (start:player)-[:`follow`]-(fof:player) AND fof != start
RETURN fof.player.name, count(DISTINCT f) AS NrOfMutualF ORDER BY NrOfMutualF DESC;
fof.player.name NrOfMutualF
LeBron James 2
James Harden 1
Chris Paul 1
Yao Ming 1
Damian Lillard 1
JaVale McGee 1
Kevin Durant 1
Kyle Anderson 1
Rudy Gay 1
Russell Westbrook 1

顯然,LeBron 最值得推薦!再看看這些共同好友都是誰?

fof.player.name collect(distinct f.player.name)
James Harden ["Dejounte Murray"]
LeBron James ["Danny Green", "Dejounte Murray"]
Chris Paul ["Dejounte Murray"]
Yao Ming ["Shaquille O'Neal"]
Damian Lillard ["LaMarcus Aldridge"]
JaVale McGee ["Shaquille O'Neal"]
Kevin Durant ["Dejounte Murray"]
Kyle Anderson ["Dejounte Murray"]
Rudy Gay ["LaMarcus Aldridge"]
Russell Westbrook ["Dejounte Murray"]

同樣,我們在剛纔的子圖裏找找 LeBron James 吧!我們把他倆之間的兩步、雙向路徑找出來,果然只會經過 ["Danny Green", "Dejounte Murray"] 並且,沒有直接的連接:

Tim_newFriend

現在,系統應該給兩邊發提醒:“hey,也許你們應該交個朋友!”

共同鄰居

查找共同鄰居是一個很常見的圖查詢,它的場景可能根據不同的鄰居關係、節點類型,同構、異構,帶來不同的場景。前面兩個場景的共同好友本質上是兩點之間的共同鄰居,直接查詢這樣的關係用 openCypher 的表達非常簡單。

兩點之間的共同鄰居

這個表達可以查詢兩個用戶之間的共性、交集,結果可能是共同團隊、去過的地方、興趣愛好、共同參與的帖子回覆等等:

MATCH p = (`v0`)--()--(`v1`)
WHERE id(`v0`) == "player100" AND id(`v1`) == "player104"
RETURN p

而限定了邊的類型之後,這個查詢就限定在共同好友的查詢了。

MATCH p = (v0)--(:`follow`)--(v1)
WHERE id(v0) == "player100" AND id(v1) == "player104"
RETURN p

多點之間的共同鄰居:內容推送

下面,我們給出一個多點共同鄰居的場景:我們從一個文章出發,查出所有在這個文章上有互動的用戶,找到這一羣體中的共同鄰居。

這個共同鄰居有什麼用處呢?很自然,如果這個共同鄰居還沒有和這個文章有任何交互,我們可以把這個文章推薦給他

這個查詢的實現很有意思:

  • 第一個 MATCH 是查到所有 post11 文章下留言、點贊之類的同作者交互的總人數
  • 第二個 MATCH 裏我們查詢這羣人的一度好友中哪些人同互動用戶的共同好友數量剛好等於文章互動人數,即這些所有參與互動的用戶的共同好友。
MATCH (blog:post)<-[e]-(:player) WHERE id(blog) == "post11"
WITH blog, count(e) AS invoved_user_count
MATCH (blog:post)<-[]-(users:player)-[:`follow`]-(common_neighbor:player)
WITH toSet(collect(users)) AS users, common_neighbor, invoved_user_count
WHERE size(users) == invoved_user_count
RETURN common_neighbor

而這個人就是...Tony!

+-----------------------------------------------------+
| common_neighbor                                     |
+-----------------------------------------------------+
| ("player101" :player{age: 36, name: "Tony Parker"}) |
+-----------------------------------------------------+

而我們可以很容易在可視化中驗證它:

MATCH p=(blog:post)<-[]-(users:player)-[:`follow`]-(common_neighbor:player)
WHERE id(blog) == "post11"
RETURN p

渲染這個查詢結果,再在這篇叫做 "Let's have a party!" 的文章與 Tony 之間查找評論、發帖、關注三類邊的雙向、兩跳查詢,就可以看到這些參與文章的人們無一例外,都是 Tony 的好友,而只有 Tony 自己還沒去文章裏留言!

而 Party 怎麼可以少了 Tony 呢?難道是他的驚喜生日 Party,Opps,我們是不是不應該告訴他?

common_nbrs_tony

信息流

我在之前寫過《基於圖技術的推薦系統實現方法》,其中講述了在圖譜上實現現代推薦系統中基於內容和協同的過濾,而類似的原理應用在社交網絡可以實現個性推薦信息流。

好友參與的內容

最簡單的信息流,可能就是朋友圈、微博 feed 上刷到的關注的人創建、參與的內容。先不考慮排序的問題,這些內容一定是:

  • 一定時間段內好友創建的內容
  • 一定時間段內好友評論的內容

我們可以用 Cypher 表達這個查詢用戶 id 爲 player100 的信息流:

MATCH (feed_owner:player)-[:`follow`]-(friend:player) WHERE id(feed_owner) == "player100"
OPTIONAL MATCH (friend:player)-[newly_commented:commented_at]->(:post)<-[:created_post]-(feed_owner:player)
    WHERE newly_commented.post_time > timestamp("2010-01-01 00:00:00")
OPTIONAL MATCH (friend:player)-[newly_created:created_post]->(po:post)
    WHERE newly_created.post_time > timestamp("2010-01-01 00:00:00")
WITH DISTINCT friend,
    collect(DISTINCT po.post.title) + collect("comment of " + dst(newly_commented))
        AS feeds WHERE size(feeds) > 0
RETURN friend.player.name, feeds
friend.player.name feeds
Boris Diaw ["I love you, Mom", "comment of post11"]
Marco Belinelli ["my best friend, tom", "comment of post11"]
Danny Green ["comment of post1"]
Tiago Splitter ["comment of post1"]
Dejounte Murray ["comment of post11"]
Tony Parker ["I can swim"]
LaMarcus Aldridge ["I hate coriander", "comment of post11", "comment of post1"]
Manu Ginobili ["my best friend, jerry", "comment of post11", "comment of post11"]

於是,我們可以把這些評論、文章呈現到用戶的 feed。

一樣的,我們來看看可視化效果。輸出所有查到的路徑:

MATCH p=(feed_owner:player)-[:`follow`]-(friend:player) WHERE id(feed_owner) == "player100"
OPTIONAL MATCH p_comment=(friend:player)-[newly_commented:commented_at]->(:post)<-[:created_post]-(feed_owner:player)
    WHERE newly_commented.post_time > timestamp("2010-01-01 00:00:00")
OPTIONAL MATCH p_post=(friend:player)-[newly_created:created_post]->(po:post)
    WHERE newly_created.post_time > timestamp("2010-01-01 00:00:00")
RETURN p, p_comment, p_post

在 Explorer 上進行渲染,選擇“神經網絡”這個佈局,可以很清晰地看出這些紅色的文章節點,還有代表評論的邊。

feed_from_friends

附近好友的內容

我們再進一步,把地理信息考慮進來,獲取那些在指定距離範圍內的朋友的相關內容。

這裏,我們用到了 NebulaGraph 的 GeoSpatial 地理功能,ST_Distance(home.address.geo_point, friend_addr.address.geo_point) AS distance WHERE distance < 1000000 的約束條件幫我們表達了距離的限制。

MATCH (home:address)-[:lived_in]-(feed_owner:player)-[:`follow`]-(friend:player)-[:lived_in]-(friend_addr:address)
    WHERE id(feed_owner) == "player100"
WITH feed_owner, friend, ST_Distance(home.address.geo_point, friend_addr.address.geo_point) AS distance WHERE distance < 1000000

OPTIONAL MATCH (friend:player)-[newly_commented:commented_at]->(:post)<-[:created_post]-(feed_owner:player)
    WHERE newly_commented.post_time > timestamp("2010-01-01 00:00:00")
OPTIONAL MATCH (friend:player)-[newly_created:created_post]->(po:post)
    WHERE newly_created.post_time > timestamp("2010-01-01 00:00:00")
WITH DISTINCT friend,
    collect(DISTINCT po.post.title) + collect("comment of " + dst(newly_commented))
        AS feeds WHERE size(feeds) > 0
RETURN friend.player.name, feeds
friend.player.name feeds
Marco Belinelli ["my best friend, tom", "comment of post11"]
Tony Parker ["I can swim"]
Danny Green ["comment of post1"]

這時候,從可視化這個結果也可以看到住址,以及它們的經緯度信息。我手動按經緯度把地址的節點在畫布上進行排列(下圖右側橙色部分),可以看到這個 feed 的主人 Tim(player100) 的住址(7,8)剛好在其他好友住址的中間位置,這些鄰近好友的相關的文章和參與評論的內容將被作爲信息流推送給 Tim:

geo_feed

時空關係追蹤

時空關係追蹤,這個圖譜應用是在公共安全、物流、疫情防控等場景下,利用圖遍歷將繁雜、凌亂的信息充分利用起來的典型應用。當我們建立起這樣的圖譜之後,往往只需要簡單的圖查詢就可以獲得非常有用的洞察。本章節我給大家近距離講下這個應用場景。

數據集

我創建了一個虛擬數據集來構建時空關係圖譜,數據集的生成程序和拿來即用的文件都放在了 GitHub 上,倉庫地址是:https://github.com/wey-gu/covid-track-graph-datagen

它的數據建模如下:

data_schema

在一個全新的環境裏,僅用下面的 3 行命令就能準備好這個圖譜:

# 安裝 NebulaGraph + NebulaGraph Studio
curl -fsSL nebula-up.siwei.io/install.sh | bash -s -- v3
# 下載數據集
git clone https://github.com/wey-gu/covid-track-graph-datagen && cd covid-track-graph-datagen
# 導入數據集
docker run --rm -ti \
    --network=nebula-net \
    -v ${PWD}/:/root \
    vesoft/nebula-importer:v3.2.0 \
    --config /root/nebula-importer-config.yaml

我們在 console 裏查看一下數據:

~/.nebula-up/console.sh
# 進入 console 了,進到 covid_trace 圖空間(剛纔創建的)
USE covid_trace;
# 執行數據統計的任務
SHOW JOB STATS

結果:

(root@nebula) [covid_trace]> SHOW STATS
+---------+------------+--------+
| Type    | Name       | Count  |
+---------+------------+--------+
| "Tag"   | "人"       | 10000  |
| "Tag"   | "地址"     | 1000   |
| "Tag"   | "城市"     | 341    |
| "Tag"   | "村鎮"     | 42950  |
| "Tag"   | "省份"     | 32     |
| "Tag"   | "聯繫方式" | 0      |
| "Tag"   | "行政區"   | 3134   |
| "Tag"   | "街道"     | 667911 |
| "Edge"  | "住址"     | 0      |
| "Edge"  | "到訪"     | 19986  |
| "Edge"  | "同住"     | 19998  |
| "Edge"  | "屬於"     | 715336 |
| "Space" | "vertices" | 725368 |
| "Space" | "edges"    | 755320 |
+---------+------------+--------+
Got 14 rows (time spent 1087/46271 us)

兩人之間的關聯

很自然,利用路徑查詢就可以了:

# 最短
FIND SHORTEST PATH FROM "p_100" TO "p_101" OVER * BIDIRECT YIELD PATH AS paths

# 所有路徑
FIND ALL PATH FROM "p_100" TO "p_101" OVER * BIDIRECT YIELD PATH AS paths | LIMIT 10

最短路徑結果:

paths
<("p_100")<-[:同住@0 {}]-("p_2136")<-[:同住@0 {}]-("p_3708")-[:到訪@0 {}]->("a_125")<-[:到訪@0 {}]-("p_101")>

所有路徑結果:

paths
<("p_100")<-[:同住@0 {}]-("p_2136")<-[:同住@0 {}]-("p_3708")-[:到訪@0 {}]->("a_125")<-[:到訪@0 {}]-("p_101")>
<("p_100")-[:到訪@0 {}]->("a_328")<-[:到訪@0 {}]-("p_6976")<-[:同住@0 {}]-("p_261")-[:到訪@0 {}]->("a_352")<-[:到訪@0 {}]-("p_101")>
<("p_100")-[:同住@0 {}]->("p_8709")-[:同住@0 {}]->("p_9315")-[:同住@0 {}]->("p_261")-[:到訪@0 {}]->("a_352")<-[:到訪@0 {}]-("p_101")>
<("p_100")-[:到訪@0 {}]->("a_328")<-[:到訪@0 {}]-("p_6311")-[:同住@0 {}]->("p_3941")-[:到訪@0 {}]->("a_345")<-[:到訪@0 {}]-("p_101")>
<("p_100")-[:到訪@0 {}]->("a_328")<-[:到訪@0 {}]-("p_5046")-[:同住@0 {}]->("p_3993")-[:到訪@0 {}]->("a_144")<-[:到訪@0 {}]-("p_101")>
<("p_100")-[:同住@0 {}]->("p_3457")-[:到訪@0 {}]->("a_199")<-[:到訪@0 {}]-("p_6771")-[:到訪@0 {}]->("a_458")<-[:到訪@0 {}]-("p_101")>
<("p_100")<-[:同住@0 {}]-("p_1462")-[:到訪@0 {}]->("a_922")<-[:到訪@0 {}]-("p_5869")-[:到訪@0 {}]->("a_345")<-[:到訪@0 {}]-("p_101")>
<("p_100")<-[:同住@0 {}]-("p_9489")-[:到訪@0 {}]->("a_985")<-[:到訪@0 {}]-("p_2733")-[:到訪@0 {}]->("a_458")<-[:到訪@0 {}]-("p_101")>
<("p_100")<-[:同住@0 {}]-("p_9489")-[:到訪@0 {}]->("a_905")<-[:到訪@0 {}]-("p_2733")-[:到訪@0 {}]->("a_458")<-[:到訪@0 {}]-("p_101")>
<("p_100")-[:到訪@0 {}]->("a_89")<-[:到訪@0 {}]-("p_1333")<-[:同住@0 {}]-("p_1683")-[:到訪@0 {}]->("a_345")<-[:到訪@0 {}]-("p_101")>

我們把所有路徑進行可視化渲染,標記出起點、終點的兩人,並在其中查到他們的最短路徑。他們之間的千絲萬縷關係就一目瞭然了,無論是商業洞察、公共安全還是疫情防控,有了這個信息,相應的工作都可以如虎添翼了。

find_path_two_people

當然,在真實的系統上,可能我們只需要關心兩個用戶之間的關係遠近,得出量化的評估:

FIND SHORTEST PATH FROM "p_100" TO "p_101" OVER * BIDIRECT YIELD PATH AS paths |
    YIELD collect(length($-.paths)) AS len | YIELD coalesce($-.len[0], -1) AS len

比如,下面只關心兩點之間最短路徑的長度爲:4。

len
4

時空相交的人

再深入一點,我們可以用圖語義勾勒出任何帶有時間與空間信息的模式,在圖譜中實時查詢出來。比如,找尋 id 爲 p_101 的指定用戶在特定時間內有時空相交的人,這意味着那些人在 p_101 訪問某個地方的某段時間內也逗留過:

MATCH (p:人)-[`visit0`:到訪]->(`addr`:地址)<-[`visit1`:到訪]-(p1:人)
    WHERE id(p) == "p_101" AND `visit0`.`start_time` < `visit1`.`end_time` AND `visit0`.`end_time` > `visit1`.`start_time`
    RETURN paths;

我們得到了用戶每一個到訪地點,與他時空相交的人的列表。如下:

addr.地址.name collect(p1.人.name)
閔行仇路q座 255960 ["徐暢", "王佳", "曾亮", "薑桂香", "邵秀英", "韋婷婷", "陶玉", "馬坤", "黃想", "張秀芳", "顏桂芳", "張洋"]
豐都北京路J座 725701 ["陳春梅", "施婷婷", "井成", "範文", "王楠", "尚明", "薛秀珍", "宋金鳳", "楊雪", "鄧麗華", "李楊", "溫佳", "葉玉", "周明", "王桂珍", "段玉華", "金成", "黃鑫", "鄔兵", "魏柳", "王蘭英", "楊柳"]
普陀潛江路P座 210730 ["儲平", "洪紅霞", "沈玉英", "王潔", "董玉英", "鄧鳳英", "謝海燕", "梁雷", "張暢", "任玉蘭", "賈宇", "汪成", "孫琴", "紀紅梅", "王欣", "陳兵", "張成", "王東", "谷霞", "林成"]
普陀武街f座 706352 ["邢成", "張建軍", "張鑫", "戴濤", "蔡洋", "汪燕", "尹亮", "何利", "何玉", "周波", "金秀珍", "楊波", "張帥", "周柳", "馬雲", "張建華", "王麗麗", "陳麗", "萬萍"]
城東貴陽街O座 110567 ["李潔", "陳靜", "王建國", "方淑華", "古想", "漆萍", "詹桂花", "王成", "李慧", "孫娜", "馬偉", "謝傑", "王鵬", "鞠桂英", "莫桂英", "汪雷", "黃彬", "李玉梅", "祝紅梅"]

現在,我們在圖上可視化這個結果看看:

MATCH (p:人)-[`visit0`:到訪]->(`addr`:地址)<-[`visit1`:到訪]-(p1:人)
    WHERE id(p) == "p_101" AND `visit0`.`start_time` < `visit1`.`end_time` AND `visit0`.`end_time` > `visit1`.`start_time`
RETURN paths;

結果中我們標記 p_101 爲不同的圖標,再用標籤傳播算法識別一下聚集社區,是不是一圖勝千言呢?

time_and_space

最近去過的省份

最後,我們再用簡單的查詢模式表達出一個人在給定時間內的行爲路徑。比如:從一個時間點開始,到訪過的所有省份

MATCH (p:人)-[visit:到訪]->(`addr`:地址)-[:屬於*5]-(province:省份)
    WHERE id(p) == "p_101" AND visit.start_time > 1625469000
    RETURN province.省份.name, collect(addr.地址.name);

看起來他/她去過不少地方呢:

province.省份.name collect(addr.地址.name)
四川省 ["閔行仇路q座 255960"]
山東省 ["城東貴陽街O座 110567"]
雲南省 ["豐都北京路J座 725701"]
福建省 ["普陀潛江路P座 210730"]
內蒙古自治區 ["普陀武街f座 706352"]

老規矩,我們在圖上看看這個結果吧。這次,我們選擇 Dagre-LR 這個佈局渲染,結果是不是非常清晰呢?

visited_provinces

總結

社交網絡作爲天然的圖結構,真的非常適合用圖技術來存儲、查詢、計算、分析與可視化去解決各式各樣的問題。NebulaGraph 強大的數據處理和可視化能力,使得它被百家企業採用來處理社交網絡問題,這其中包括:網易遊戲、微信、Line、Soul、快手和知乎等等很多行業領先的團隊。希望大家通過本章能對社交領域的圖技術應有有一個初步的認識。


謝謝你讀完本文 (///▽///)

NebulaGraph Desktop,Windows 和 macOS 用戶安裝圖數據庫的綠色通道,10s 拉起搞定海量數據的圖服務。通道傳送門:http://c.nxw.so/c0svX

想看源碼的小夥伴可以前往 GitHub 閱讀、使用、(з)-☆ star 它 -> GitHub;和其他的 NebulaGraph 用戶一起交流圖數據庫技術和應用技能,留下「你的名片」一起玩耍呢~

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章