最近在搞知识图谱,知识图谱的模型比较统一——就是实体和关系。其中实体和关系可以挂载关联的属性,属性以key-value键值对存在。称之为Property Graph Model。
-
属性:
Property: key->value
-
实体
Entity: (id) => (type, property*)
其中: type决定该实体的schema,如Person, Product,一个实体必须且只能挂在一个type下面。type这里相当于行业。
-
关系: SPO三元组,附加可选的k-v属性对
Assoc: (id1, atype, id2) => (time, property*)
其中atype是关系的类型,起到labeled Edge的作用。
根据这个定义我们很容易定义出如下实体类:
package me.arganzheng.kg.model;
import java.util.ArrayList;
import java.util.List;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
public class Entity {
String id;
String type;
String name;
List<String> aliases;
String description;
String image;
String url;
List<Property> properties = new ArrayList<Property>();
/**
* Add or set a property value for the {@code Element} given its key.
*/
public <V> Property<V> addProperty(final String key, final V value, float confidence) {
Property<V> p = new Property<V>();
p.key = key;
p.value = value;
p.confidence = confidence;
this.properties.add(p);
return p;
}
public <V> Property<V> getProperty(final String key) {
for (Property<V> p : this.properties) {
if (p.key.equals(key)) {
return p;
}
}
return null;
}
public static void main(String[] args) {
Entity entity = new Entity();
entity.id = "1";
entity.name = "argan";
List<String> aliases = new ArrayList<String>();
aliases.add("Forrest");
aliases.add("Gump");
entity.aliases = aliases;
entity.type = "Person";
entity.description = "arganzheng test KG model";
entity.addProperty("age", 35, 10.0f);
entity.addProperty("nickname", "magi", 10.0f);
Gson gson = new GsonBuilder().create();
System.out.println(gson.toJson(entity));
Property<Integer> age = entity.getProperty("age");
System.out.println(age.value);
}
static class Property<V> {
String key;
V value;
float confidence;
}
}
List
public class Entity {
String id;
String type;
String name;
List<String> aliases;
String description;
String image;
String url;
Map<String, Property> properties = new HashMap<String, Property>();
/**
* Add or set a property value for the {@code Element} given its key.
*/
public <V> Property<V> addProperty(final String key, final V value, float confidence) {
Property<V> p = new Property<V>();
p.value = value;
p.confidence = confidence;
this.properties.put(key, p);
return p;
}
public <V> Property<V> getProperty(final String key) {
return this.properties.get(key);
}
public static void main(String[] args) {
Entity entity = new Entity();
entity.id = "1";
entity.name = "argan";
List<String> aliases = new ArrayList<String>();
aliases.add("Forrest");
aliases.add("Gump");
entity.aliases = aliases;
entity.type = "Person";
entity.description = "arganzheng test KG model";
entity.addProperty("age", 35, 10.0f);
entity.addProperty("nickName", "magi", 10.0f);
Gson gson = new GsonBuilder().create();
System.out.println(gson.toJson(entity));
Property<Integer> age = entity.getProperty("age");
Property<String> nickName = entity.getProperty("nickName");
System.out.println(age.value);
System.out.println(nickName.value);
}
static class Property<V> {
V value;
float confidence;
}
}
当然,如果觉得Property太过于动态,也可以采用静态类继承的方式:
比如定义一个Entity基类:
public class Entity {
String id;
String type;
String name;
List<String> aliases;
String description;
String image;
String url;
}
然后各个行业的类型定义成子类,如Person:
public class Person extends Entity {
int age;
Date birthdate;
float height;
float weight;
// ...
public static void main(String[] args) {
Person entity = new Person();
entity.id = "1";
entity.name = "argan";
List<String> aliases = new ArrayList<String>();
aliases.add("Forrest");
aliases.add("Gump");
entity.aliases = aliases;
entity.type = "Person";
entity.description = "arganzheng test KG model";
entity.age = 10;
entity.birthdate = new Date();
Gson gson = new GsonBuilder().create();
System.out.println(gson.toJson(entity));
System.out.println(entity.age);
}
}
用起来比较确定,就是模型可能会变动,需要客户端更新。
然后我们需要提供RPC接口给上层业务使用,假设我们要提供的接口如下:
List<Entity> entities_search(List<String> ids);
这时候需要对这些对象进行序列化了,然后问题来了。凡是语言无关的序列化方案都没有没有泛型、继承的概念。以protobuf为例子,泛型只能用types表示;没有继承,只有组合,类似于C的struct。有几种方式可以选择,每种都有各自的优缺点。
总体来说有两种方式:
- 分散式
- 集中式
1、分散式
每个继承的子类都使用一个独立的类,在消息中的第一个字段为父类的消息,并且名称可以固定成为super。例如我们上面的例子:
message Entity { // 基类
// 实体id,需要稳定且唯一
required string id = 1;
// 实体类型(一个实体只能属于一个类型)
required enum type = 2;
// 实体名称
required string name = 3;
// 实体别名
repeated string aliases = 4;
// 实体描述
optional string description = 5;
// An image to help identify the entity.
optional string image = 6;
// The official website URL of the entity, if available.
optional string url = 7;
}
message Person {
required Entity super = 1;
/// 子类自己的属性..
optional int age = 2;
...
}
message Product {
required Entity super = 1;
/// 子类自己的属性..
optional float price = 2;
...
}
这种方式使用方式如下:
写:
Entity super;
Person person;
super.set_id(..);
super.set_name(..);
..
person.set_entity(super);
person.set_age(..);
读:
String id = person.get_super().get_id();
int age = person.get_age();
是不是觉得使用起来特别别扭?一点继承的意思都没有,完全就是组合嘛。。
为了解决这种别扭,干脆就把基类在各个子类展开定义算了:
message Person {
// 实体id,需要稳定且唯一
required string id = 1;
// 实体类型(一个实体只能属于一个类型)
required enum type = 2;
// 实体名称
required string name = 3;
// 实体别名
repeated string aliases = 4;
// 实体描述
optional string description = 5;
// An image to help identify the entity.
optional string image = 6;
// The official website URL of the entity, if available.
optional string url = 7;
/// 子类自己的属性..
optional int age = 2;
...
}
message Product {
// 实体id,需要稳定且唯一
required string id = 1;
// 实体类型(一个实体只能属于一个类型)
required enum type = 2;
// 实体名称
required string name = 3;
// 实体别名
repeated string aliases = 4;
// 实体描述
optional string description = 5;
// An image to help identify the entity.
optional string image = 6;
// The official website URL of the entity, if available.
optional string url = 7;
/// 子类自己的属性..
optional float price = 2;
...
}
这样使用起来就顺畅的多了:
写:
Person person;
person.set_id(..);
person.set_name(..);
..
person.set_age(..);
读:
String id = person.get_id();
int age = person.get_age();
缺点就是每个子类都要重复定义一次,如果有修改的话需要改动多处,而且没有一个通用的父类接受。这里其实Protobuf是可以提供类似于 C 的宏定义的,一处定义,到处展开。可以达到代码复用的效果。
法二、集中式
另外一种方式是集中式,整个继承体系使用一个独立的类,把所有的子类都封装在一起,类似于C中的union。
message Entity { // 基类
// 实体id,需要稳定且唯一
required string id = 1;
// 实体类型(一个实体只能属于一个类型)
required enum type = 2;
// 实体名称
required string name = 3;
// 实体别名
repeated string aliases = 4;
// 实体描述
optional string description = 5;
// An image to help identify the entity.
optional string image = 6;
// The official website URL of the entity, if available.
optional string url = 7;
// 子类作为父类的一个optional字段:
optional Person person = 60;
optional Product product = 61;
// .. 其他的子类 ..
}
message Person {
/// 子类自己的属性..
optional int age = 2;
...
}
message Product {
/// 子类自己的属性..
optional float price = 2;
...
}
这种方式的使用方式如下:
写:
Entity entity;
Person person;
entity.set_id(..);
entity.set_name(..);
..
person.set_age(..);
entity.set_person(person);
读:
string id = entity.get_id();
if (entity.has_person()) {
int age = entity.get_person().get_age();
...
}
子类变成父类的一个属性了。。感觉更别扭。。
而且这种方式还需要检查哪个子类存在,写入的时候还可以写入多个子类。不过Protobuf提供了一个语法可以防止这种情况发生——Oneof,达到了真正类似于union的效果:
message Entity { // 基类
// 实体id,需要稳定且唯一
required string id = 1;
// 实体类型(一个实体只能属于一个类型)
required enum type = 2;
// 实体名称
required string name = 3;
// 实体别名
repeated string aliases = 4;
// 实体描述
optional string description = 5;
// An image to help identify the entity.
optional string image = 6;
// The official website URL of the entity, if available.
optional string url = 7;
// 子类作为父类的一个optional字段,使用oneof语法:
oneof extension {
Person person = 60;
Product product = 61;
// .. 其他的子类 ..
}
}
然后可以这种使用:
写:
Entity entity;
Person person;
Product product;
entity.set_id(..);
person.set_age(..);
product.set_price(..);
entity.set_person(person);
entity.set_product(product); // will clear person field!
读:
switch(entityoneof_name_case()) {
case Entity::kPerson: {
Person person = entity.get_person();
int age = person.get_age();
...
break;
}
case Entity::kProduct: {
Product product = entity.get_product();
float price = product.get_price();
...
break;
}
...
default:
break;
}
可惜子类的名字并不是统一的,看起来也是跟上面的optional差不多,只是会保证只有一个子类会被设置成功。另外,读取的时候,需要同时用到子类和父类(子类和父类看起来像是毫无关系的两个类一样)。
跟这种方式类似,有一种称之为 Embedded Serialized Messages的方法,就是把子类序列化成父类的一个字段,然后用一个type字段表明子类类型,这样就可以根据这个类型对子类进行反序列化了:
message Entity { // 基类
// 实体id,需要稳定且唯一
required string id = 1;
// 实体类型(一个实体只能属于一个类型)
required enum type = 2;
// 实体名称
required string name = 3;
// 实体别名
repeated string aliases = 4;
// 实体描述
optional string description = 5;
// An image to help identify the entity.
optional string image = 6;
// The official website URL of the entity, if available.
optional string url = 7;
enum Type {
Person = 1;
Product = 2;
// ...
}
// 子类序列化成bytes
required Type type = 100;
required bytes subclass = 101;
}
然后每个子类还是自己定义:
message Person {
/// 子类自己的属性..
optional int age = 2;
...
}
message Product {
/// 子类自己的属性..
optional float price = 2;
...
}
使用方式跟上面的oneof也是非常类似:
写:
Entity entity;
Person person;
entity.set_id(..);
entity.set_name(..);
..
person.set_age(..);
// 序列化为二进制,保存在父类中
entity.set_type(Type::Person);
person.SerializeToString(entity.add_subclass());
读:
string id = entity.get_id();
if (entity.get_type() == Entity::Person) {
Person person;
person.ParseFromString(entity.get_subclass());
int age = person.get_age();
...
}
protobuf还有另一种语法,可以把所有子类的名字都统一,就是extensions。 这个名称其实有点诡异,并不是java的继承的意思。就是纯粹的扩展父类:
message Entity { // 基类
// 实体id,需要稳定且唯一
required string id = 1;
// 实体类型(一个实体只能属于一个类型)
required enum type = 2;
// 实体名称
required string name = 3;
// 实体别名
repeated string aliases = 4;
// 实体描述
optional string description = 5;
// An image to help identify the entity.
optional string image = 6;
// The official website URL of the entity, if available.
optional string url = 7;
// 父类预留一些字段作为扩展:
extensions 100 to max;
}
然后“子类”可以定义扩展属性:
person.proto:
import "entity.proto";
extend Entity {
optional int age = 100;
...
}
product.proto:
import "entity.proto";
extend Entity {
optional float price = 200;
...
}
看起来像是直接在entity中增加了新的字段,但是使用起来还是有区别的:所有的扩展属性都放在一个叫做extension的属性中:
Entity entity;
entity.set_id(..);
entity.SetExtension(age, 30); // 不能直接 entity.set_age(30);
entity.SetExtension(price, 30.0); // 不能直接 entity.set_price(30.0);
int age = entity.GetExtension(age); // 不能直接 entity.get_age();
float price = entity.GetExtension(price); // 不能直接 entity.get_price();
NOTE
1、注意tag数字不要冲突,否则会发生类型冲突。
2、可以看到这种方式并没有子类的概念,所有的子类字段都打散存放在extension字段中了。相当于一个超级扁平化的父类。protobuf提供了一个Nested Extensions,对新增字段增加了一个命名空间的概念,可以相对缓解这个问题:
person.proto:
import "entity.proto";
message Person {
extend Entity {
optional int age = 100;
...
}
}
product.proto:
import "entity.proto";
message Product {
extend Entity {
optional float price = 200;
...
}
}
然后使用的时候可以这么搞:
Entity entity;
entity.set_id(..);
entity.SetExtension(Person::age, 30); // 不能直接 entity.set_age(30);
entity.SetExtension(Product::price, 30.0); // 不能直接 entity.set_price(30.0);
int age = entity.GetExtension(Person::age); // 不能直接 entity.get_age();
float price = entity.GetExtension(Product::price); // 不能直接 entity.get_price();
相对来说看起来清晰一些。
还可以稍微变换一下,通过在子类中扩展父类,让其指向自己,而不是一个个打散的属性:
message Entity { // 基类
// 实体id,需要稳定且唯一
required string id = 1;
// 实体类型(一个实体只能属于一个类型)
required enum type = 2;
// 实体名称
required string name = 3;
// 实体别名
repeated string aliases = 4;
// 实体描述
optional string description = 5;
// An image to help identify the entity.
optional string image = 6;
// The official website URL of the entity, if available.
optional string url = 7;
// 父类预留一些字段作为扩展:
extensions 100 to max;
enum Type {
Person = 1;
Product = 2;
}
required Type type = 8;
}
然后“子类”可以定义扩展属性:
person.proto:
import "entity.proto";
message Person {
extend Entity {
required Person entity = 100;
}
optional int age = 1;
...
}
product.proto:
import "entity.proto";
message Product {
extend Entity {
required Product entity = 100;
}
optional float price = 200;
...
}
使用起来大概是这个样子:
写:
Entity entity;
entity.set_id(..);
entity.MutableExtension(Person::entity)->set_age(30);
entity.set_type(Entity::Person);
// product类似
entity.MutableExtension(Product::entity)->set_price(30.0);
entity.set_type(Entity::Product);
读: switch(entity->type()) {
case Entity::Person: {
int age = entity.MutableExtension(Person::entity)->age();
...
break;
}
case Entity::Person: {
float price = entity.MutableExtension(Product::entity)->price();
...
break;
}
...
default:
break;
}
不过使用起来也是怪怪的。。
总结
protobuf中没有继承的概念,只能使用类似于组合的方式来达到类似的目的,每种方式都有各自优缺点,使用起来都没有继承优雅,需要自己根据实际情况做权衡:
- 分散式
- 子类包含父类
- 直接将父类在子类展开
- 集中式
- 所有子类作为父类的一个 optional 字段,类似于C的 union
- 所有子类作为父类的一个 optional字 段,通过 protobuf 的 oneof 语法,类似于C的 union
- Embedded Serialized Messages:将子类序列化成父类的一个 bytes 字段,然后用一个 type 字段表明子类类型
- 使用protobuf的 extensions 语法,扩展”父”类,所有扩展字段存放在“父”类的一个 extensions 字段。
- 使用protobuf的 Nested Extensions 语法,扩展”父”类,所有扩展字段存放在“父”类的一个 extensions 字段,但是扩展属性有各自的命名空间。
- Nested Extensions 的一个特殊例子:扩展属性就是子类本身。
从对用户的使用友好度来说,1.2 和 2.5/2.6 是相对比较友好的。其他的情况使用起来多多少少有点别扭。。