NebulaGraph 核心所自帶的資料結構其實已經很豐富了,比如 List、Set、Map、Duration、DataSet 等等,但是我們平時在建表和資料寫入的時候,可以用到的資料結構其實比較有限,複雜結構目前僅支援以下幾種:
enum PropertyType {
UNKNOWN = 0,
... // 基礎型別
TIMESTAMP = 21,
DURATION = 23,
DATE = 24,
DATETIME = 25,
TIME = 26,
GEOGRAPHY = 31,
} (cpp.enum_strict)
所以,有時候因為業務需求,我們需要能存入一些客製化化的資料型別,比如機器學習中經常用到的 Embedding 的資料型別,工程上經常會有直接儲存二進位制資料 Binary 的需求,這就需要開發者自己去新增一個資料型別來滿足自己的業務開發需求。
本文將手把手教你如何在 NebulaGraph 中增加一種資料型別,直到可以建表使用並插入對應資料以及查詢。
下面我們以一個簡單的二進位制型別 Binary
的新增步驟來講解整個流程。
在實現新增 Binary 型別之前我們先想好要用怎麼樣的命令去使用這個型別,我們可以參考 NebulaGraph 已有的資料型別的使用
// 建立點表
create tag player(name string, image binary)
// 建立邊表
create edge team(name string, logo binary)
上面我們設計新建 schema 時使用 binary
關鍵字來表示設定二進位制型別的屬性欄位。
這裡有一個問題就是,命令只能以字串形式傳輸,所以我們如果通過命令來插入二進位制資料的話,就需要轉碼。這裡我們以選用 Base64 編碼為例。
insert vertex player values "p1":("jimmy", binary("0s=W9d...."))
我們在插入命令裡面同樣以一個 binary
關鍵字來表示插入的是二進位制資料的字串而不是普通的字串。
其實正常的設計,或者現有的 NebulaGraph 程式碼上面來看,查詢語句並不需要做改變,直接按照像讀其他資料一樣讀取 Binary
欄位就可以了,只是這裡我們需要考慮一個問題,使用者端沒適配的話怎麼辦?像 nebula-console、nebula-java、nebula-cpp 這些使用者端,我們暫時沒法一一去適配新增的型別,所以為了測試的時候使用 nebula-console 能夠正常讀取到資料,我們需要提供轉換函數,將新增的 Binary
型別轉換為現有使用者端能讀取的資料格式。
fetch prop on player "p1" yield base64(player.image) as img_base64
這裡我們定義了一個 base64()
的轉換函數,將儲存的二進位制資料再以 Base64的格式輸出。(兜兜轉轉回到原點了(:≡)
定義好命令之後,我們來看看怎麼實現這些內容,首先我們需要實現這個 Binary
的資料結構。
在伺服器端 C++ 程式碼中,我們可以以一個 Bytes 陣列來表示二進位制的資料結構
struct Binary {
std::vector<std::byte> values;
Binary() = default;
Binary(const Binary &) = default;
Binary(Binary &&) noexcept = default;
explicit Binary(std::vector<std::byte> &&vals);
explicit Binary(const std::vector<std::byte> &l);
// 用於直接從命令列的字串中解析出二進位制
explicit Binary(const std::string &str);
... // 其他介面
};
一個簡單的資料結構定義好之後,我們需要將這個結構新增到 Value
的 union 中
Value 這個資料結構在 Value.cpp 中定義,它是 nebula 中所有資料結構的一個基礎類別表示,每個新增的資料結構想要和之前其他資料結構一起混用的話,需要在 Value.cpp 裡面對各個介面做適配。
這個 Value 的資料結構裡面有很多的介面定義,像賦值構造、符號過載、toString、toJson、hash 等介面,都需要去適配。
好在這不是什麼難事,參考其他型別的實現就行。唯一要注意的是要細心!
因為我們的資料結構還需要進行網路傳輸,所以我們還需要定義 thrift
檔案裡面的結構型別並實現序列化能力。
// 新增的資料型別
struct Binary {
1: list<byte> values;
} (cpp.type = "nebula::Binary")
// 在Value union中增加Binary型別
union Value {
1: NullType nVal;
2: bool bVal;
3: i64 iVal;
4: double fVal;
5: binary sVal;
6: Date dVal;
7: Time tVal;
8: DateTime dtVal;
9: Vertex (cpp.type = "nebula::Vertex") vVal (cpp.ref_type = "unique");
10: Edge (cpp.type = "nebula::Edge") eVal (cpp.ref_type = "unique");
11: Path (cpp.type = "nebula::Path") pVal (cpp.ref_type = "unique");
12: NList (cpp.type = "nebula::List") lVal (cpp.ref_type = "unique");
13: NMap (cpp.type = "nebula::Map") mVal (cpp.ref_type = "unique");
14: NSet (cpp.type = "nebula::Set") uVal (cpp.ref_type = "unique");
15: DataSet (cpp.type = "nebula::DataSet") gVal (cpp.ref_type = "unique");
16: Geography (cpp.type = "nebula::Geography") ggVal (cpp.ref_type = "unique");
17: Duration (cpp.type = "nebula::Duration") duVal (cpp.ref_type = "unique");
18: Binary (cpp.type = "nebula::Binary") btVal (cpp.ref_type = "unique");
} (cpp.type = "nebula::Value")
另外我們還需要在 common.thrift
檔案中的 PropertyType
該列舉中增加一個 BINARY
型別。
enum PropertyType {
UNKNOWN = 0,
... // 基礎型別
TIMESTAMP = 21,
DURATION = 23,
DATE = 24,
DATETIME = 25,
TIME = 26,
GEOGRAPHY = 31,
BINARY = 32,
} (cpp.enum_strict)
這裡的程式碼就不展示了,同樣可以參考其他型別的實現。最相近的可以參考 src/common/datatypes/ListOps-inl.h
的實現
資料結構定義好之後,我們可以開始命令列的實現,首先開啟 src/parser/scanner.lex
,我們需要新增一個關鍵字 Binary
:
"BINARY" { return TokenType::KW_BINARY; }
接著開啟 src/parser/parser.yy
檔案,將關鍵字宣告一下:
$token KW_BINARY
為了儘量減少命令列的影響,我們將 Binary
關鍵字新增到非保留關鍵字的集合中:
unreserved_keyword
...
| KW_BINARY { $$ = new std::string("binary"); }
接下來我們要將Binary
關鍵字新增到建表命令的詞法樹中:
type_spec
...
| KW_BINARY {
$$ = new meta::cpp2::ColumnTypeDef();
$$->type_ref() = nebula::cpp2::PropertyType::BINARY;
}
最後我們實現插入命令:
constant_expression
...
| KW_BINARY L_PAREN STRING R_PAREN {
$$ = ConstantExpression::make(qctx->objPool(), Value(Binary(*$3)));
delete $3;
}
就這樣,我們就簡單實現了上面命令設計裡面的建立 binary schema 和插入 binary 資料的命令。
上面我們搞定了資料結構定義和 rpc 序列化以及命令列適配,一個新增的資料結構通過命令建立後,由 grapd 服務接收到請求並傳輸給 storaged 伺服器端。然而 storaged 伺服器端儲存實際的資料是經過編碼之後的 string,我們需要為這個新增的資料結構寫一個編解碼的程式碼邏輯。
在程式碼檔案 src/codec/RowWriterV2.cpp
中,有以下幾個函數需要適配的。
RowWriterV2::RowWriterV2(RowReader& reader) // 建構函式中適配新增的類似
WriteResult RowWriterV2::write(ssize_t index, const Binary& v) // 新增一個Binary的編碼寫入函數
這裡我直接將 Bytes 陣列寫入 String 中
WriteResult RowWriterV2::write(ssize_t index, const Binary& v) noexcept {
return write(index, folly::StringPiece(reinterpret_cast<const char*>(v.values.data()), v.values.size()));
}
在程式碼檔案 src/codec/RowReaderV2.cpp
中,同樣有以下函數需要適配
Value RowReaderV2::getValueByIndex(const int64_t index) const {
...
case PropertyType::VID: {
// This is to be compatible with V1, so we treat it as
// 8-byte long string
return std::string(&data_[offset], sizeof(int64_t));
}
case PropertyType::FLOAT: {
float val;
memcpy(reinterpret_cast<void*>(&val), &data_[offset], sizeof(float));
return val;
}
case PropertyType::DOUBLE: {
double val;
memcpy(reinterpret_cast<void*>(&val), &data_[offset], sizeof(double));
return val;
}
...
// code here
case PropertyType::BINARY: {
...
}
}
需要注意的是:讀和寫必須對映上,怎麼寫的就怎麼讀。
至此,在 NebulaGraph 裡新增一個資料型別的流程就結束了。
感謝你的閱讀 (///▽///)