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
的添加步驟來講解整個流程。
1.命令設計
在實現新增 Binary 類型之前我們先想好要用怎麼樣的命令去使用這個類型,我們可以參考 NebulaGraph 已有的數據類型的使用
1.1 schema 創建命令
// 創建點表
create tag player(name string, image binary)
// 創建邊表
create edge team(name string, logo binary)
上面我們設計新建 schema 時使用 binary
關鍵字來表示設置二進制類型的屬性字段。
1.2 插入數據
這裏有一個問題就是,命令只能以字符串形式傳輸,所以我們如果通過命令來插入二進制數據的話,就需要轉碼。這裏我們以選用 Base64 編碼爲例。
insert vertex player values "p1":("jimmy", binary("0s=W9d...."))
我們在插入命令裏面同樣以一個 binary
關鍵字來表示插入的是二進制數據的字符串而不是普通的字符串。
1.3 查詢數據
其實正常的設計,或者現有的 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
的數據結構。
2. 定義數據結構
在服務端 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 等接口,都需要去適配。
好在這不是什麼難事,參考其他類型的實現就行。唯一要注意的是要細心!
2.1 定義 thrift 的數據結構
因爲我們的數據結構還需要進行網絡傳輸,所以我們還需要定義 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)
2.2 實現 Binary 的 thrift rpc 格式的序列化
這裏的代碼就不展示了,同樣可以參考其他類型的實現。最相近的可以參考 src/common/datatypes/ListOps-inl.h
的實現
3. 命令行實現
數據結構定義好之後,我們可以開始命令行的實現,首先打開 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 數據的命令。
4. storaged 服務的讀寫適配
上面我們搞定了數據結構定義和 rpc 序列化以及命令行適配,一個新增的數據結構通過命令創建後,由 grapd 服務接收到請求並傳輸給 storaged 服務端。然而 storaged 服務端存儲實際的數據是經過編碼之後的 string,我們需要爲這個新增的數據結構寫一個編解碼的代碼邏輯。
4.1 RowWriterV2 寫適配
在代碼文件 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()));
}
4.2 RowReaderV2 讀適配
在代碼文件 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 裏新增一個數據類型的流程就結束了。
看看效果
感謝你的閱讀 (///▽///)