當前位置:首頁 » 數據倉庫 » 圖資料庫neo4j應用
擴展閱讀
webinf下怎麼引入js 2023-08-31 21:54:13
堡壘機怎麼打開web 2023-08-31 21:54:11

圖資料庫neo4j應用

發布時間: 2022-11-06 03:43:31

⑴ 比Redis好用的Nosql

實際上為了更好的描述實體之間的關系,我們要是再繼續使用Redis的話,是不是感覺實體之間的關系不夠那麼的明顯,雖然也是屬於NoSQL的一種,但是相對來說,Redis,表現實體之間的關系就沒有那麼清晰了,為了更好的描述實體之間的關系,就會使用圖形資料庫來進行了,那麼今天阿粉介紹的,就是一個圖形化的數據可,Neo4J。

Neo4j是一個世界領先的開源的基於圖的資料庫。 它是使用Java語言完全開發的。那麼什麼是圖資料庫呢?圖資料庫是以圖結構的形式存儲數據的資料庫。 它以節點,關系和屬性的形式存儲應用程序的數據。正如RDBMS以表的「行,列」的形式存儲數據,GDBMS以圖的形式存儲數據。

RDBMS與圖資料庫的區別

1.Tables 表Graphs 圖表

2.Rows 行Nodes 節點

3.Columns and Data 列和數據 Properties and its values屬性及其值

4.Constraints 約束Relationships 關系

5.Joins 加入Traversal 遍歷

說完了圖形資料庫,我們就來看看這個 Neo4J 資料庫吧

neo4j是用Java語言編寫的圖形資料庫,運行時需要啟動JVM進程,因此,需安裝JAVA SE的JDK。關於 Java 怎麼安裝,我就不用再多廢話了吧,到時候別忘了檢測一下 Java 的版本就好了, java -version

接下來我們就是要進行一個安裝了,我們先去官網,下載社區版,企業版要收費的,注意哈。

官網地址

下載完成,直接開始安裝,傻瓜式操作即可。

Neo4j應用程序有如下主要的目錄結構:

注意,如果你使用的是Zip的壓縮包來進行的使用的話,那麼你就需要注意一些地方,比如你如果是用 Zip 的包解壓之後,並且想要通過 bat 的命令啟動,直接在目錄下進行 cmd ,然後 neo4j.bat ,這時候可能會出現一個問題,就是版本可能會出現問題,你如果下載使用的是最新版的 Neo4J ,那麼就可能會讓你使用 JDK 11 ,而阿粉就是踩過了這個大坑之後,才發現,bat 閃退的原因。

這樣就是說明我們的 JDk 的版本對應的和 Neo4J 需要的 JDK 是不匹配的,我們就需要換一下我們的 JDK 了。把他換成 JDK 11 就好了,再次啟動。

這時候,我們就直接訪問 localhost:7474 的埠,直接就能看到如下的畫面, 1.jpg

剛進入的時候可能需要大家輸入帳號密碼,默認的帳號密碼就是,neo4j 修改成你想要的就行了。

這樣登錄進去我們就能開始正式學習 Neo4J 的所有內容了。

Neo4j - CQL語法

我們在講語法之前首先我們先得看看 Neo4J 的構建模塊,不然之後的查詢都是無意義的。

Neo4j圖資料庫主要有以下構建塊 -

節點是圖表的基本單位。 它包含具有鍵值對的屬性,如下所示

屬性是用於描述圖節點和關系的鍵值對

關系是圖形資料庫的另一個主要構建塊。 它連接兩個節點,如下所示。

Label將一個公共名稱與一組節點或關系相關聯。 節點或關系可以包含一個或多個標簽。 我們可以為現有節點或關系創建新標簽。 我們可以從現有節點或關系中刪除現有標簽。

Neo4j數據瀏覽器 一旦我們安裝Neo4j,我們可以訪問Neo4j數據瀏覽器使用以下URL

http:// localhost:7474 / browser /

CREATE 語法

CREATE ( : )

它是我們要創建的節點名稱。

它是一個節點標簽名稱

我們可以創建一個節點,然後給他安排上一個標簽

CREATE (emp:Employee)

當我們看到

Added 1 label, created 1 node, completed after 74 ms.

這就創建成功了,

那麼怎麼查看呢?

MATCH語法

MATCH ( : ) return xxx

是這個樣子的

但是看到裡面竟然沒有東西,就相當於是一個空的對象,那是不是就應該給裡面放入屬性的操作呢?沒錯,肯定有

CREATE (emp:Employee{ id : 1001 ,name :"lucy", age : 10})

Added 1 label, created 1 node, set 3 properties, completed after 163 ms. 創建成功。

我們再次查看就能看到

如果我們想只要其中的一些對象的屬性,而不是全部屬性,那應該怎麼操作呢?

RETURN語法

RETURN 可以返回的是一個對象,也可以是對象中的屬性,比如:

結果就是下面這個樣子的,大家看一下,是不是感覺還是挺好用的。

** WHERE語法**

WHERE

為什麼在前面的位置阿粉說,CQL 是和 SQL 類型的,這完全是因為很多東西和 SQL 是類似的。

結果如下:

相同的還有

布爾運算符 描述 AND 和 OR 或者 NOT 非 XOR 異或

比較運算符 描述 = 「等於」運算符 > 「不等於」運算符 < 「小於」運算符 > 「大於」運算符 <= 「小於或等於」運算符。 >= 「大於或等於」運算符。

DELETE語法

刪除語法必然是有的,因為有創建,肯定有刪除。

DELETE

但是這個命令也不是單獨使用的哈,

MATCH (e: Employee) DELETE e

直接刪除成功。

基礎的東西講完了,阿粉就得說說這個比較重要的內容了,關系,

我們之前創建節點的時候,那叫一個簡單舒適加愉快,但是創建關系就比較復雜了,因為需要考慮如何匹配到有關系的兩個節點,以及關系本身的屬性如何設置。這里我們就簡單學一下如何建立節點之間的關系。

由於Neo4j CQL語法是以人類可讀的格式。 Neo4j CQL也使用類似的箭頭標記來創建兩個節點之間的關系。

每個關系( )包含兩個節點

在Neo4j中,兩個節點之間的關系是有方向性的。 它們是單向或雙向的。

如果我們嘗試創建一個沒有任何方向的關系,那麼就會報錯。

關系創建語法

CREATE ( )-[ ]->( )

我們這里直接使用創建新的節點來創建關系。

提示創建成功

這里關系名稱是「CONTAINS」

關系標簽是「contains」。

這么看是看不出有啥關系的,但是,我們可以從另外的一個位置

這樣看下來,這個 Neo4J 簡單操作是不是就學會了,阿粉接下來的文章中講怎麼使用 Java 來操作 Neo4J 資料庫。歡迎大家來觀看。

⑵ 為什麼選擇圖形資料庫,為什麼選擇Neo4j

圖形資料庫每個對象是一個節點,之間的關系是一條邊。相對於關系資料庫來說,圖形資料庫善於處理大量復雜、互連接、低結構化的數據,這些數據變化迅速,需要頻繁的查詢——在關系資料庫中,由於這些查詢會導致大量的表連接,從而導致性能問題,而且在設計使用上也不方便。
圖形資料庫適合用於社交網路,推薦系統等專注於構建關系圖譜的系統。
圖資料庫的代表有Neo4J、FlockDB、InfoGrid、AllegroGraph、GraphDB等。

⑶ 什麼是圖資料庫

圖資料庫(Graph database) 並非指存儲圖片的資料庫,而是以「圖」這種數據結構存儲和查詢數據。目前比較典型的代表產品是Neo4j。

⑷ Neo4j類似的軟體有哪些

GraphScope、NetworkX、JanusGraph、TigerGraph、Dgraph這些都是,比如GraphScope的代碼可以在GitHub上面查看,它是阿里達摩院研發的一站式圖計算系統,應該還是比較權威。

⑸ 漫談工業大數據9:開源工業大數據軟體簡介(上)

今天真是一個美好的時代,有無數的開源系統可以為我們提供服務,現在有許多開發軟體可以用到工業大數據中,當然很多系統還不成熟,應用到工業中還需要小心,並且需要開發人員對其進行一定的優化和調整。下面就簡單介紹一些開源的大數據工具軟體,看看有哪些能夠應用到工業大數據領域。

下面這張圖是我根據網上流傳的一張開源大數據軟體分類圖整理的:

我們可以把開源大數據軟體分成幾類,有一些可以逐步應用到工業大數據領域,下面就一一介紹一下這些軟體。(以下系統介紹大都來源於網路)

1、數據存儲類

(1)關系資料庫MySQL

這個就不用太多介紹了吧,關系型資料庫領域應用最廣泛的開源軟體,目前屬於 Oracle 旗下產品。

(2)文件資料庫Hadoop

Hadoop是大數據時代的明星產品,它最大的成就在於實現了一個分布式文件系統(Hadoop Distributed FileSystem),簡稱HDFS。HDFS有高容錯性的特點,並且設計用來部署在低廉的硬體上,而且它提供高吞吐量來訪問應用程序的數據,適合那些有著超大數據集的應用程序。

Hadoop可以在工業大數據應用中用來作為底層的基礎資料庫,由於它採用了分布式部署的方式,如果是私有雲部署,適用於大型企業集團。如果是公有雲的話,可以用來存儲文檔、視頻、圖像等資料。

(3)列資料庫Hbase

HBase是一個分布式的、面向列的開源資料庫,HBase是Apache的Hadoop項目的子項目。HBase不同於一般的關系資料庫,它是一個適合於非結構化數據存儲的資料庫。另一個不同的是HBase基於列的而不是基於行的模式。

基於Hbase開發的OpenTSDB,可以存儲所有的時序(無須采樣)來構建一個分布式、可伸縮的時間序列資料庫。它支持秒級數據採集所有metrics,支持永久存儲,可以做容量規劃,並很容易的接入到現有的報警系統里。

這樣的話,它就可以替代在工業領域用得最多的實時資料庫。

(4)文檔資料庫MongoDB

MongoDB是一個介於關系資料庫和非關系資料庫之間的產品,是非關系資料庫當中功能最豐富,最像關系資料庫的。他支持的數據結構非常鬆散,是類似json的bson格式,因此可以存儲比較復雜的數據類型。Mongo最大的特點是他支持的查詢語言非常強大,其語法有點類似於面向對象的查詢語言,幾乎可以實現類似關系資料庫單表查詢的絕大部分功能,而且還支持對數據建立索引。

MongoDB適合於存儲工業大數據中的各類文檔,包括各類圖紙、文檔等。

(5)圖資料庫Neo4j/OrientDB

圖資料庫不是存放圖片的,是基於圖的形式構建的數據系統。

Neo4j是一個高性能的,NOSQL圖形資料庫,它將結構化數據存儲在網路上而不是表中。它是一個嵌入式的、基於磁碟的、具備完全的事務特性的Java持久化引擎,但是它將結構化數據存儲在網路(從數學角度叫做圖)上而不是表中。Neo4j也可以被看作是一個高性能的圖引擎,該引擎具有成熟資料庫的所有特性。程序員工作在一個面向對象的、靈活的網路結構下而不是嚴格、靜態的表中——但是他們可以享受到具備完全的事務特性、 企業級 的資料庫的所有好處。

OrientDB是兼具文檔資料庫的靈活性和圖形資料庫管理 鏈接 能力的可深層次擴展的文檔-圖形資料庫管理系統。可選無模式、全模式或混合模式下。支持許多高級特性,諸如ACID事務、快速索引,原生和SQL查詢功能。可以JSON格式導入、導出文檔。若不執行昂貴的JOIN操作的話,如同關系資料庫可在幾毫秒內可檢索數以百記的鏈接文檔圖。

這些資料庫都可以用來存儲非結構化數據。

2、數據分析類

(1)批處理MapRece/Spark

MapRece是一種編程模型,用於大規模數據集(大於1TB)的並行運算。概念"Map(映射)"和"Rece(歸約)",是它們的主要思想,都是從函數式編程語言里借來的,還有從矢量編程語言里借來的特性。它極大地方便了編程人員在不會分布式並行編程的情況下,將自己的程序運行在分布式系統上。 當前的軟體實現是指定一個Map(映射)函數,用來把一組鍵值對映射成一組新的鍵值對,指定並發的Rece(歸約)函數,用來保證所有映射的鍵值對中的每一個共享相同的鍵組。

Apache Spark 是專為大規模數據處理而設計的快速通用的計算引擎。Spark 是一種與 Hadoop 相似的開源集群計算環境,但是兩者之間還存在一些不同之處,這些有用的不同之處使 Spark 在某些工作負載方面表現得更加優越,換句話說,Spark 啟用了內存分布數據集,除了能夠提供互動式查詢外,它還可以優化迭代工作負載。盡管創建 Spark 是為了支持分布式數據集上的迭代作業,但是實際上它是對 Hadoop 的補充,可以在 Hadoop 文件系統中並行運行。

這些大數據的明星產品可以用來做工業大數據的處理。

(2)流處理Storm

Storm是一個開源的分布式實時計算系統,可以簡單、可靠的處理大量的數據流。Storm有很多使用場景:如實時分析,在線機器學習,持續計算,分布式RPC,ETL等等。Storm支持水平擴展,具有高容錯性,保證每個消息都會得到處理,而且處理速度很快(在一個小集群中,每個結點每秒可以處理數以百萬計的消息)。Storm的部署和運維都很便捷,而且更為重要的是可以使用任意編程語言來開發應用。

(3)圖處理Giraph

Giraph是什麼?Giraph是Apache基金會開源項目之一,被定義為迭代式圖處理系統。他架構在Hadoop之上,提供了圖處理介面,專門處理大數據的圖問題。

Giraph的存在很有必要,現在的大數據的圖問題又很多,例如表達人與人之間的關系的有社交網路,搜索引擎需要經常計算網頁與網頁之間的關系,而map-rece介面不太適合實現圖演算法。

Giraph主要用於分析用戶或者內容之間的聯系或重要性。

(4)並行計算MPI/OpenCL

OpenCL(全稱Open Computing Language,開放運算語言)是第一個面向 異構系統 通用目的並行編程的開放式、免費標准,也是一個統一的編程環境,便於軟體開發人員為高性能計算 伺服器 、桌面計算系統、手持設備編寫高效輕便的代碼,而且廣泛適用於多核心處理器(CPU)、圖形處理器(GPU)、Cell類型架構以及數字信號處理器(DSP)等其他並行處理器,在 游戲 、 娛樂 、科研、醫療等各種領域都有廣闊的發展前景。

(5)分析框架Hive

Hive是基於Hadoop的一個數據倉庫工具,可以將結構化的數據文件映射為一張資料庫表,並提供簡單的sql查詢功能,可以將sql語句轉換為MapRece任務進行運行。 其優點是學習成本低,可以通過類SQL語句快速實現簡單的MapRece統計,不必開發專門的MapRece應用,十分適合數據倉庫的統計分析。

(6)分析框架Pig

Apache Pig 是apache平台下的一個免費開源項目,Pig為大型數據集的處理提供了更高層次的抽象,很多時候數據的處理需要多個MapRece過程才能實現,使得數據處理過程與該模式匹配可能很困難。有了Pig就能夠使用更豐富的數據結構。[2]

Pig LatinPig Latin 是一個相對簡單的語言,一條語句 就是一個操作,與資料庫的表類似,可以在關系資料庫中找到它(其中,元組代錶行,並且每個元組都由欄位組成)。

Pig 擁有大量的數據類型,不僅支持包、元組和映射等高級概念,還支持簡單的數據類型,如 int、long、float、double、chararray 和 bytearray。並且,還有一套完整的比較運算符,包括使用正則表達式的豐富匹配模式。

⑹ 圖計算引擎Neo4j和Graphscope有什麼區別

Neo4j是單機系統,主要做圖資料庫。GraphScope是由阿里巴巴達摩院智能計算實驗室研發的圖計算平台,是全球首個一站式超大規模分布式圖計算平台,並且還入選了中 國科學技術協會「科創中 國」平台。Graphscope的代碼在github.com/alibaba/graphscope上開源。SSSP演算法上,GraphScope單機模式下平均要比Neo4j快176.38倍,最快在datagen-9.2_zf數據集上快了292.2倍。

⑺ 有哪些輕型的非關系型資料庫

常見的非關系型資料庫有:1、mongodb;2、cassandra;3、redis;4、hbase;5、neo4j。其中mongodb是非常著名的NoSQL資料庫,它是一個面向文檔的開源資料庫。
常見的幾種非關系型資料庫:
1、MongoDB
MongoDB是最著名的NoSQL資料庫。它是一個面向文檔的開源資料庫。MongoDB是一個可伸縮和可訪問的資料庫。它在c++中。MongoDB同樣可以用作文件系統。在MongoDB中,JavaScript可以作為查詢語言使用。通過使用sharding MongoDB水平伸縮。它在流行的JavaScript框架中非常有用。
人們真的很享受分片、高級文本搜索、gridFS和map-rece功能。驚人的性能和新特性使這個NoSQL資料庫在我們的列表中名列第一。
特點:提供高性能;自動分片;運行在多個伺服器上;支持主從復制;數據以JSON樣式文檔的形式存儲;索引文檔中的任何欄位;由於數據被放置在碎片中,所以它具有自動負載平衡配置;支持正則表達式搜索;在失敗的情況下易於管理。
優點:易於安裝MongoDB;MongoDB Inc.為客戶提供專業支持;支持臨時查詢;高速資料庫;無模式資料庫;橫向擴展資料庫;性能非常高。
缺點:不支持連接;數據量大;嵌套文檔是有限的;增加不必要的內存使用。
2、Cassandra
Cassandra是Facebook為收件箱搜索開發的。Cassandra是一個用於處理大量結構化數據的分布式數據存儲系統。通常,這些數據分布在許多普通伺服器上。您還可以添加數據存儲容量,使您的服務保持在線,您可以輕松地完成這項任務。由於集群中的所有節點都是相同的,因此不需要處理復雜的配置。
Cassandra是用Java編寫的。Cassandra查詢語言(CQL)是查詢Cassandra資料庫的一種類似sql的語言。因此,Cassandra在最佳開源資料庫中排名第二。Facebook、Twitter、思科(Cisco)、Rackspace、eBay、Twitter、Netflix等一些最大的公司都在使用Cassandra。
特點:線性可伸縮;;保持快速響應時間;支持原子性、一致性、隔離性和耐久性(ACID)等屬性;使用Apache Hadoop支持MapRece;分配數據的最大靈活性;高度可伸縮;點對點架構。
優點:高度可伸縮;無單點故障;Multi-DC復制;與其他基於JVM的應用程序緊密集成;更適合多數據中心部署、冗餘、故障轉移和災難恢復。
缺點:對聚合的有限支持;不可預知的性能;不支持特別查詢。
3、Redis
Redis是一個鍵值存儲。此外,它是最著名的鍵值存儲。Redis支持一些c++、PHP、Ruby、Python、Perl、Scala等等。Redis是用C語言編寫的。此外,它是根據BSD授權的。
特點:自動故障轉移;將其資料庫完全保存在內存中;事務;Lua腳本;將數據復制到任意數量的從屬伺服器;鑰匙的壽命有限;LRU驅逐鑰匙;支持發布/訂閱。
優點:支持多種數據類型;很容易安裝;非常快(每秒執行約11萬組,每秒執行約81000次);操作都是原子的;多用途工具(在許多用例中使用)。
缺點:不支持連接;存儲過程所需的Lua知識;數據集必須很好地適應內存。
4、HBase
HBase是一個分布式的、面向列的開源資料庫,該技術來源於 Fay Chang 所撰寫的Google論文「Bigtable:一個結構化數據的分布式存儲系統」。就像Bigtable利用了Google文件系統(File System)所提供的分布式數據存儲一樣,HBase在Hadoop之上提供了類似於Bigtable的能力。
HBase是Apache的Hadoop項目的子項目。HBase不同於一般的關系資料庫,它是一個適合於非結構化數據存儲的資料庫。另一個不同的是HBase基於列的而不是基於行的模式。
5、neo4j
Neo4j被稱為原生圖資料庫,因為它有效地實現了屬性圖模型,一直到存儲層。這意味著數據完全按照白板的方式存儲,資料庫使用指針導航和遍歷圖。Neo4j有資料庫的社區版和企業版。企業版包括Community Edition必須提供的所有功能,以及額外的企業需求,如備份、集群和故障轉移功能。
特點:它支持唯一的約束;Neo4j支持完整的ACID(原子性、一致性、隔離性和持久性)規則;Java API: Cypher API和本機Java API;使用Apache Lucence索引;簡單查詢語言Neo4j CQL;包含用於執行CQL命令的UI: Neo4j Data Browser。
優點:容易檢索其相鄰節點或關系細節,無需連接或索引;易於學習Neo4j CQL查詢語言命令;不需要復雜的連接來檢索數據;非常容易地表示半結構化數據;大型企業實時應用程序的高可用性;簡化的調優。
缺點:不支持分片

⑻ neo4j是什麼怎麼配置能單獨使用嗎

Neo4j是一個嵌入式,基於磁碟的,支持完整事務的Java持久化引擎,它在圖像中而不是表中存儲數據。Neo4j提供了大規模可擴展性,在一台機器上可以處理數十億節點/關系/屬性的圖像,可以擴展到多台機器並行運行。相對於關系資料庫來說,圖形資料庫善於處理大量復雜、互連接、低結構化的數據,這些數據變化迅速,需要頻繁的查詢——在關系資料庫中,這些查詢會導致大量的表連接,因此會產生性能上的問題。Neo4j重點解決了擁有大量連接的傳統RDBMS在查詢時出現的性能衰退問題。通過圍繞圖形進行數據建模,Neo4j會以相同的速度遍歷節點與邊,其遍歷速度與構成圖形的數據量沒有任何關系。此外,Neo4j還提供了非常快的圖形演算法、推薦系統和OLAP風格的分析,而這一切在目前的RDBMS系統中都是無法實現的。

⑼ 如何用 Python 實現一個圖資料庫(Graph Database)

本文章是 重寫 500 Lines or Less 系列的其中一篇,目標是重寫 500 Lines or Less 系列的原有項目:Dagoba: an in-memory graph database。

Dagoba 是作者設計用來展示如何從零開始自己實現一個圖資料庫( Graph Database )。該名字似乎來源於作者喜歡的一個樂隊,另一個原因是它的前綴 DAG 也正好是有向無環圖 ( Directed Acyclic Graph ) 的縮寫。本文也沿用了該名稱。

圖是一種常見的數據結構,它將信息描述為若干獨立的節點( vertex ,為了和下文的邊更加對稱,本文中稱為 node ),以及把節點關聯起來的邊( edge )。我們熟悉的鏈表以及多種樹結構可以看作是符合特定規則的圖。圖在路徑選擇、推薦演算法以及神經網路等方面都是重要的核心數據結構。

既然圖的用途如此廣泛,一個重要的問題就是如何存儲它。如果在傳統的關系資料庫中存儲圖,很自然的做法就是為節點和邊各自創建一張表,並用外鍵把它們關聯起來。這樣的話,要查找某人所有的子女,就可以寫下類似下面的查詢:

還好,不算太復雜。但是如果要查找孫輩呢?那恐怕就要使用子查詢或者 CTE(Common Table Expression) 等特殊構造了。再往下想,曾孫輩又該怎麼查詢?孫媳婦呢?

這樣我們會意識到,SQL 作為查詢語言,它只是對二維數據表這種結構而設計的,用它去查詢圖的話非常笨拙,很快會變得極其復雜,也難以擴展。針對圖而言,我們希望有一種更為自然和直觀的查詢語法,類似這樣:

為了高效地存儲和查詢圖這種數據結構,圖資料庫( Graph Database )應運而生。因為和傳統的關系型資料庫存在極大的差異,所以它屬於新型資料庫也就是 NoSql 的一個分支(其他分支包括文檔資料庫、列資料庫等)。圖資料庫的主要代表包括 Neo4J 等。本文介紹的 Dagoba 則是具備圖資料庫核心功能、主要用於教學和演示的一個簡單的圖資料庫。

原文代碼是使用 JavaScript 編寫的,在定義調用介面時大量使用了原型( prototype )這種特有的語言構造。對於其他主流語言的用戶來說,原型的用法多少顯得有些別扭和不自然。

考慮到本系列其他資料庫示例大多是用 Python 實現的,本文也按照傳統,用 Python 重寫了原文的代碼。同樣延續之前的慣例,為了讓讀者更好地理解程序是如何逐步完善的,我們用迭代式的方法完成程序的各個組成部分。

原文在 500lines 系列的 Github 倉庫中只包含了實現代碼,並未包含測試。按照代碼注釋說明,測試程序位於作者的另一個代碼庫中,不過和 500lines 版本的實現似乎略有不同。

本文實現的代碼參考了原作者的測試內容,但跳過了北歐神話這個例子——我承認確實不熟悉這些神祇之間的親緣關系,相信中文背景的讀者們多數也未必了解,雖然作者很喜歡這個例子,想了想還是不要徒增困惑吧。因此本文在編寫測試用例時只參考了原文關於家族親屬的例子,放棄了神話相關的部分,盡管會減少一些趣味性,相信對於入門級的代碼來說這樣也夠用了。

本文實現程序位於代碼庫的 dagoba 目錄下。按照本系列程序的同意規則,要想直接執行各個已完成的步驟,讀者可以在根目錄下的 main.py 找到相應的代碼位置,取消注釋並運行即可。

本程序的所有步驟只需要 Python3 ,測試則使用內置的 unittest , 不需要額外的第三方庫。原則上 Python3.6 以上版本應該都可運行,但我只在 Python3.8.3 環境下完整測試過。

本文實現的程序從最簡單的案例開始,通過每個步驟逐步擴展,最終形成一個完整的程序。這些步驟包括:

接下來依次介紹各個步驟。

回想一下,圖資料庫就是一些點( node )和邊( edge )的集合。現在我們要做出的一個重大決策是如何對節點/邊進行建模。對於邊來說,必須指定它的關聯關系,也就是從哪個節點指向哪個節點。大多數情況下邊是有方向的——父子關系不指明方向可是要亂套的!

考慮到擴展性及通用性問題,我們可以把數據保存為字典( dict ),這樣可以方便地添加用戶需要的任何數據。某些數據是為資料庫內部管理而保留的,為了明確區分,可以這樣約定:以下劃線開頭的特殊欄位由資料庫內部維護,類似於私有成員,用戶不應該自己去修改它們。這也是 Python 社區普遍遵循的約定。

此外,節點和邊存在互相引用的關系。目前我們知道邊會引用到兩端的節點,後面還會看到,為了提高效率,節點也會引用到邊。如果僅僅在內存中維護它們的關系,那麼使用指針訪問是很直觀的,但資料庫必須考慮到序列化到磁碟的問題,這時指針就不再好用了。

為此,最好按照資料庫的一般要求,為每個節點維護一個主鍵( _id ),用主鍵來描述它們之間的關聯關系。

我們第一步要把資料庫的模型建立起來。為了測試目的,我們使用一個最簡單的資料庫模型,它只包含兩個節點和一條邊,如下所示:

按照 TDD 的原則,首先編寫測試:

與原文一樣,我們把資料庫管理介面命名為 Dagoba 。目前,能夠想到的最簡單的測試是確認節點和邊是否已經添加到資料庫中:

assert_item 是一個輔助方法,用於檢查字典是否包含預期的欄位。相信大家都能想到該如何實現,這里就不再列出了,讀者可參考 Github 上的完整源碼。

現在,測試是失敗的。用最簡單的辦法實現資料庫:

需要注意的是,不管添加節點還是查詢,程序都使用了拷貝後的數據副本,而不是直接使用原始數據。為什麼要這樣做?因為字典是可變的,用戶可以在任何時候修改其中的內容,如果資料庫不知道數據已經變化,就很容易發生難以追蹤的一致性問題,最糟糕的情況下會使得數據內容徹底混亂。

拷貝數據可以避免上述問題,代價則是需要佔用更多內存和處理時間。對於資料庫來說,通常查詢次數要遠遠多於修改,所以這個代價是可以接受的。

現在測試應該正常通過了。為了讓它更加完善,我們可以再測試一些邊緣情況,看看資料庫能否正確處理異常數據,比如:

例如,如果用戶嘗試添加重復主鍵,我們預期應拋出 ValueError 異常。因此編寫測試如下:

為了滿足以上測試,代碼需要稍作修改。特別是按照 id 查找主鍵是個常用操作,通過遍歷的方法效率太低了,最好是能夠通過主鍵直接訪問。因此在資料庫中再增加一個字典:

完整代碼請參考 Github 倉庫。

在上個步驟,我們在初始化資料庫時為節點明確指定了主鍵。按照資料庫設計的一般原則,主鍵最好是不具有業務含義的代理主鍵( Surrogate key ),用戶不應該關心它具體的值是什麼,因此讓資料庫去管理主鍵通常是更為合理的。當然,在部分場景下——比如導入外部數據——明確指定主鍵仍然是有用的。

為了同時支持這些要求,我們這樣約定:欄位 _id 表示節點的主鍵,如果用戶指定了該欄位,則使用用戶設置的值(當然,用戶有責任保證它們不會重復);否則,由資料庫自動為它分配一個主鍵。

如果主鍵是資料庫生成的,事先無法預知它的值是什麼,而邊( edge )必須指定它所指向的節點,因此必須在主鍵生成後才能添加。由於這個原因,在動態生成主鍵的情況下,資料庫的初始化會略微復雜一些。還是先寫一個測試:

為支持此功能,我們在資料庫中添加一個內部欄位 _next_id 用於生成主鍵,並讓 add_node 方法返回新生成的主鍵:

接下來,再確認一下邊是否可以正常訪問:

運行測試,一切正常。這個步驟很輕松地完成了,不過兩個測試( DbModelTest 和 PrimaryKeyTest )出現了一些重復代碼,比如 get_item 。我們可以把這些公用代碼提取出來。由於 get_item 內部調用了 TestCase.assertXXX 等方法,看起來應該使用繼承,但從 TestCase 派生基類容易引起一些潛在的問題,所以我轉而使用另一個技巧 Mixin :

實現資料庫模型之後,接下來就要考慮如何查詢它了。

在設計查詢時要考慮幾個問題。對於圖的訪問來說,幾乎總是由某個節點(或符合條件的某一類節點)開始,從與它相鄰的邊跳轉到其他節點,依次類推。所以鏈式調用對查詢來說是一種很自然的風格。舉例來說,要知道 Tom 的孫子養了幾只貓,可以使用類似這樣的查詢:

可以想像,以上每個方法都應該返回符合條件的節點集合。這種實現是很直觀的,不過存在一個潛在的問題:很多時候用戶只需要一小部分結果,如果它總是不計代價地給我們一個巨大的集合,會造成極大的浪費。比如以下查詢:

為了避免不必要的浪費,我們需要另外一種機制,也就是通常所稱的「懶式查詢」或「延遲查詢」。它的基本思想是,當我們調用查詢方法時,它只是把查詢條件記錄下來,而並不立即返回結果,直到明確調用某些方法時才真正去查詢資料庫。

如果讀者比較熟悉流行的 Python ORM,比如 SqlAlchemy 或者 Django ORM 的話,會知道它們幾乎都是懶式查詢的,要調用 list(result) 或者 result[0:10] 這樣的方法才能得到具體的查詢結果。

在 Dagoba 中把觸發查詢的方法定義為 run 。也就是說,以下查詢執行到 run 時才真正去查找數據:

和懶式查詢( Lazy Query )相對應的,直接返回結果的方法一般稱作主動查詢( Eager Query )。主動查詢和懶式查詢的內在查找邏輯基本上是相同的,區別只在於觸發機制不同。由於主動查詢實現起來更加簡單,出錯也更容易排查,因此我們先從主動查詢開始實現。

還是從測試開始。前面測試所用的簡單資料庫數據太少,難以滿足查詢要求,所以這一步先來創建一個更復雜的數據模型:

此關系的復雜之處之一在於反向關聯:如果 A 是 B 的哥哥,那麼 B 就是 A 的弟弟/妹妹,為了查詢到他們彼此之間的關系,正向關聯和反向關聯都需要存在,因此在初始化資料庫時需要定義的邊數量會很多。

當然,父子之間也存在反向關聯的問題,為了讓問題稍微簡化一些,我們目前只需要向下(子孫輩)查找,可以稍微減少一些關聯數量。

因此,我們定義數據模型如下。為了減少重復工作,我們通過 _backward 欄位定義反向關聯,而資料庫內部為了查詢方便,需要把它維護成兩條邊:

然後,測試一個最簡單的查詢,比如查找某人的所有孫輩:

這里 outcome/income 分別表示從某個節點出發、或到達它的節點集合。在原作者的代碼中把上述方法稱為 out/in 。當然這樣看起來更加簡潔,可惜的是 in 在 Python 中是個關鍵字,無法作為函數名。我也考慮過加個下劃線比如 out_.in_ 這種形式,但看起來也有點怪異,權衡之後還是使用了稍微啰嗦一點的名稱。

現在我們可以開始定義查詢介面了。在前面已經說過,我們計劃分別實現兩種查詢,包括主動查詢( Eager Query )以及延遲查詢( Lazy Query )。

它們的內在查詢邏輯是相通的,看起來似乎可以使用繼承。不過遵循 YAGNI 原則,目前先不這樣做,而是只定義兩個新類,在滿足測試的基礎上不斷擴展。以後我們會看到,與繼承相比,把共同的邏輯放到資料庫本身其實是更為合理的。

接下來實現訪問節點的方法。由於 EagerQuery 調用查詢方法會立即返回結果,我們把結果記錄在 _result 內部欄位中。雖然 node 方法只返回單個結果,但考慮到其他查詢方法幾乎都是返回集合,為統一起見,讓它也返回集合,這樣可以避免同時支持集合與單結果的分支處理,讓代碼更加簡潔、不容易出錯。此外,如果查詢對象不存在的話,我們只返回空集合,並不視為一個錯誤。

查詢輸入/輸出節點的方法實現類似這樣:

查找節點的核心邏輯在資料庫本身定義:

以上使用了內部定義的一些輔助查詢方法。用類似的邏輯再定義 income ,它們的實現都很簡單,讀者可以直接參考源碼,此處不再贅述。

在此步驟的最後,我們再實現一個優化。當多次調用查詢方法後,結果可能會返回重復的數據,很多時候這是不必要的。就像關系資料庫通常支持 unique/distinct 一樣,我們也希望 Dagoba 能夠過濾重復的數據。

假設我們要查詢某人所有孩子的祖父,顯然不管有多少孩子,他們的祖父應該是同一個人。因此編寫測試如下:

現在來實現 unique 。我們只要按照主鍵把重復數據去掉即可:

在上個步驟,初始化資料庫指定了雙向關聯,但並未測試它們。因為我們還沒有編寫代碼去支持它們,現在增加一個測試,它應該是失敗的:

運行測試,的確失敗了。我們看看要如何支持它。回想一下,當從邊查找節點時,使用的是以下方法:

這里也有一個潛在的問題:調用 self.edges 意味著遍歷所有邊,當資料庫內容較多時,這是巨大的浪費。為了提高性能,我們可以把與節點相關的邊記錄在節點本身,這樣要查找邊只要看節點本身即可。在初始化時定義出入邊的集合:

在添加邊時,我們要同時把它們對應的關系同時更新到節點,此外還要維護反向關聯。這涉及對字典內容的部分復制,先編寫一個輔助方法:

然後,將添加邊的實現修改如下:

這里的代碼同時添加正向關聯和反向關聯。有的朋友可能會注意到代碼略有重復,是的,但是重復僅出現在該函數內部,本著「三則重構」的原則,暫時不去提取代碼。

實現之後,前面的測試就可以正常通過了。

在這個步驟中,我們來實現延遲查詢( Lazy Query )。

延遲查詢的要求是,當調用查詢方法時並不立即執行,而是推遲到調用特定方法,比如 run 時才執行整個查詢,返回結果。

延遲查詢的實現要比主動查詢復雜一些。為了實現延遲查詢,查詢方法的實現不能直接返回結果,而是記錄要執行的動作以及傳入的參數,到調用 run 時再依次執行前面記錄下來的內容。

如果你去看作者的實現,會發現他是用一個數據結構記錄執行操作和參數,此外還有一部分邏輯用來分派對每種結構要執行的動作。這樣當然是可行的,但數據處理和分派部分的實現會比較復雜,也容易出錯。

本文的實現則選擇了另外一種不同的方法:使用 Python 的內部函數機制,把一連串查詢變換成一組函數,每個函數取上個函數的執行結果作為輸入,最後一個函數的輸出就是整個查詢的結果。由於內部函數同時也是閉包,盡管每個查詢的參數形式各不相同,但是它們都可以被閉包「捕獲」而成為內部變數,所以這些內部函數可以採用統一的形式,無需再針對每種查詢設計額外的數據結構,因而執行過程得到了很大程度的簡化。

首先還是來編寫測試。 LazyQueryTest 和 EagerQueryTest 測試用例幾乎是完全相同的(是的,兩種查詢只在於內部實現機制不同,它們的調用介面幾乎是完全一致的)。

因此我們可以把 EagerQueryTest 的測試原樣不變拷貝到 LazyQueryTest 中。當然拷貝粘貼不是個好注意,對於比較冗長而固定的初始化部分,我們可以把它提取出來作為兩個測試共享的公共函數。讀者可參考代碼中的 step04_lazy_query/tests/test_lazy_query.py 部分。

程序把查詢函數的串列執行稱為管道( pipeline ),用一個變數來記錄它:

然後依次實現各個調用介面。每種介面的實現都是類似的:用內部函數執行真正的查詢邏輯,再把這個函數添加到 pipeline 調用鏈中。比如 node 的實現類似下面:

其他介面的實現也與此類似。最後, run 函數負責執行所有查詢,返回最終結果;

完成上述實現後執行測試,確保我們的實現是正確的。

在前面我們說過,延遲查詢與主動查詢相比,最大的優勢是對於許多查詢可以按需要訪問,不需要每個步驟都返回完整結果,從而提高性能,節約查詢時間。比如說,對於下面的查詢:

以上查詢的意思是從孫輩中找到一個符合條件的節點即可。對該查詢而言,主動查詢會在調用 outcome('son') 時就遍歷所有節點,哪怕最後一步只需要第一個結果。而延遲查詢為了提高效率,應在找到符合條件的結果後立即停止。

目前我們尚未實現 take 方法。老規矩,先添加測試:

主動查詢的 take 實現比較簡單,我們只要從結果中返回前 n 條記錄:

延遲查詢的實現要復雜一些。為了避免不必要的查找,返回結果不應該是完整的列表( list ),而應該是個按需返回的可迭代對象,我們用內置函數 next 來依次返回前 n 個結果:

寫完後運行測試,確保它們是正確的。

從外部介面看,主動查詢和延遲查詢幾乎是完全相同的,所以用單純的數據測試很難確認後者的效率一定比前者高,用訪問時間來測試也並不可靠。為了測試效率,我們引入一個節點訪問次數的概念,如果延遲查詢效率更高的話,那麼它應該比主動查詢訪問節點的次數更少。

為此,編寫如下測試:

我們為 Dagoba 類添加一個成員來記錄總的節點訪問次數,以及兩個輔助方法,分別用於獲取和重置訪問次數:

然後瀏覽代碼,查找修改點。增加計數主要在從邊查找節點的時候,因此修改部分如下:

此外還有 income/outcome 方法,修改都很簡單,這里就不再列出。

實現後再次運行測試。測試通過,表明延遲查詢確實在效率上優於主動查詢。

不像關系資料庫的結構那樣固定,圖的形式可以千變萬化,查詢機制也必須足夠靈活。從原理上講,所有查詢無非是從某個節點出發按照特定方向搜索,因此用 node/income/outcome 這三個方法幾乎可以組合出任意所需的查詢。

但對於復雜查詢,寫出的代碼有時會顯得較為瑣碎和冗長,對於特定領域來說,往往存在更為簡潔的名稱,例如:母親的兄弟可簡稱為舅舅。對於這些場景,如果能夠類似 DSL (領域特定語言)那樣允許用戶根據專業要求自行擴展,從而簡化查詢,方便閱讀,無疑會更為友好。

如果讀者去看原作者的實現,會發現他是用一種特殊語法 addAlias 來定義自己想要的查詢,調用方法時再進行查詢以確定要執行的內容,其介面和內部實現都是相當復雜的。

而我希望有更簡單的方法來實現這一點。所幸 Python 是一種高度動態的語言,允許在運行時向類中增加新的成員,因此做到這一點可能比預想的還要簡單。

為了驗證這一點,編寫測試如下:

無需 Dagoba 的實現做任何改動,測試就可以通過了!其實我們要做的就是動態添加一個自定義的成員函數,按照 Python 對象機制的要求,成員函數的第一個成員應該是名為 self 的參數,但這里已經是在 UnitTest 的內部,為了和測試類本身的 self 相區分,新函數的參數增加了一個下劃線。

此外,函數應返回其所屬的對象,這是為了鏈式調用所要求的。我們看到,動態語言的靈活性使得添加新語法變得非常簡單。

到此,一個初具規模的圖資料庫就形成了。

和原文相比,本文還缺少一些內容,比如如何將資料庫序列化到磁碟。不過相信讀者都看到了,我們的資料庫內部結構基本上是簡單的原生數據結構(列表+字典),因此序列化無論用 pickle 或是 JSON 之類方法都應該是相當簡單的。有興趣的讀者可以自行完成它們。

我們的圖資料庫實現為了提高查詢性能,在節點內部存儲了邊的指針(或者說引用)。這樣做的好處是,無論資料庫有多大,從一個節點到相鄰節點的訪問是常數時間,因此數據訪問的效率非常高。

但一個潛在的問題是,如果資料庫規模非常大,已經無法整個放在內存中,或者出於安全性等原因要實現分布式訪問的話,那麼指針就無法使用了,必須要考慮其他機制來解決這個問題。分布式資料庫無論採用何種數據模型都是一個棘手的問題,在本文中我們沒有涉及。有興趣的讀者也可以考慮 500lines 系列中關於分布式和集群演算法的其他一些文章。

本文的實現和系列中其他資料庫類似,採用 Python 作為實現語言,而原作者使用的是 JavaScript ,這應該和作者的背景有關。我相信對於大多數開發者來說, Python 的對象機制比 JavaScript 基於原型的語法應該是更容易閱讀和理解的。

當然,原作者的版本比本文版本在實現上其實是更為完善的,靈活性也更好。如果想要更為優雅的實現,我們可以考慮使用 Python 元編程,那樣會更接近於作者的實現,但也會讓程序的復雜性大為增加。如果讀者有興趣,不妨對照著去讀讀原作者的版本。