上一次说到,使用三元组可以使数据自身描述模式关系,下面就继续挖掘一下它的潜能。
上篇文章:初探语义Web - 孙泽辉 (foggy.shop)
构建
下面我将介绍如何构建一个简单的三元组存储。
索引
首先创建一个类,这个类代表整个数据结构
class simpleGraph {
private _spo: spoGraph;
private _pos: posGraph;
private _osp: ospGraph;
constructor() {
this._spo = {};
this._pos = {};
this._osp = {};
}
}
类中每个索引存放了三元组的不同排列
type sub = string | Object;
type pred = string | Object;
type obj = string | Object;
// 主谓宾 { 我: { 吃: Set<饭> } }
interface spoGraph {
sub?: {
prod?: Set<obj>;
};
}
// 谓宾主 { 吃: { 饭: Set<我> } }
interface posGraph {
pred?: {
obj?: Set<sub>;
};
}
// 宾主谓 { 饭: { 我: Set<吃> } }
interface ospGraph {
obj?: {
sub?: Set<pred>;
};
}
通过不同排列,每个三元组都出现在对应的索引中
向索引中添加元组
class simpleGraph{
add([sub, pred, obj]) {
this._addToIndex(this._spo, sub, pred, obj);
this._addToIndex(this._pos, pred, obj, sub);
this._addToIndex(this._osp, obj, sub, pred);
}
private _addToIndex(index, a, b, c): void {
if (!(a in index)) {
index[a] = { [b]: new Set([c]) };
} else {
if (!(b in index[a])) {
index[a][b] = new Set([c]);
} else {
index[a][b].add(c);
}
}
}
}
对着三种不同的排列插入元组,见缝插针
实现查询
基本的查询方法需要提供某个(主语,谓语,宾语)的模式,并返回与该模式匹配的所有三元组,三元组中设置为 null 的项被视为通配符。
class simpleGraph {
*triples([sub = null, pred = null, obj = null]) {
try {
if (sub != null) {
if (pred != null) {
if (obj != null) {
// sub pred obj
if (this._spo[sub][pred].has(obj)) yield [sub, pred, obj];
} else {
// sub pred null
for (const retObj of this._spo[sub][pred]) {
yield [sub, pred, retObj];
}
}
} else {
if (obj != null) {
// sub null obj
for (const retPred of this._osp[obj][sub])
yield [sub, retPred, obj];
} else {
// sub null null
for (const [key, value] of Object.entries(this._spo[sub]) as [
string,
any
]) {
const [retPred, objSet] = [key, value];
for (const retObj of objSet) yield [sub, retPred, retObj];
}
}
}
} else {
if (pred !== null) {
if (obj !== null) {
// null pred obj
for (const retSub of this._pos[pred][obj]) {
yield [retSub, pred, obj];
}
} else {
// null pred null
for (const [key, value] of Object.entries(this._pos[pred]) as [
string,
any
]) {
const [retObj, subSet] = [key, value];
for (const retSub of subSet) yield [retSub, pred, retObj];
}
}
} else {
if (obj !== null) {
// null null obj
for (const [key, value] of Object.entries(this._osp[obj]) as [
string,
any
]) {
const [retSub, predSet] = [key, value];
for (const retPred of predSet) yield [retSub, retPred, obj];
}
} else {
// null null null
for (const [key, value] of Object.entries(this._osp) as [
string,
any
]) {
const [retSub, predSet] = [key, value];
for (const [key, value] of Object.entries(predSet) as [
string,
any
]) {
const [retPred, objSet] = [key, value];
for (const retObj of objSet) yield [retSub, retPred, retObj];
}
}
}
}
}
} catch (e) {
// maybe no-exist
}
}
}
为了更方便的拿到单个结果,实现 value 方法
class simpleGraph{
value([sub = null, pred = null, obj = null]): string {
const result = [...this.triples([sub, pred, obj])].flat();
if (sub === null) {
return result[0];
} else if (pred === null) {
return result[1];
} else {
return result[2];
}
}
}
最佳实践
// 导入写好的 simpleGrapth 类
import simpleGraph from "./tools/simplegraph";
let graph: simpleGraph = new simpleGraph();
// 添加知识
graph.add(["sunzehui", "like", "coding"]);
graph.add(["wangdefa", "like", "eat"]);
// 查询 sunzehui like 什么?
const result = graph.value(["sunzehui", "like", null]);
console.log(result); // coding
// 查询 sunzehui 和 coding 的关系?
const result = graph.value(["sunzehui", null, "coding"]);
console.log(result); // coding
// 查询 谁 like coding?
const result = graph.value([null, "like", "coding"]);
console.log(result); // sunzehui
// 查询 所有知识
const result = [...graph.triples([null, null, null])];
console.log(result);
/*
[
[ 'sunzehui', 'like', 'coding' ],
[ 'wangdefa', 'like', 'eat' ]
]
*/
现在知识库都是在内存里的,不方便持久化存储,下篇文章继续完善一下。