如何像Freebase一样存储数据?
我承认这个问题基本上是重复的,之前有类似的提问:在本地服务器上使用Freebase数据?,但我需要更详细的答案。
我对Freebase爱得不可自拔。现在我想做的是创建一个非常简单的Freebase克隆,用来存储一些可能不适合放在Freebase上的内容,但这些内容可以用Freebase的结构来描述。简单来说,我想要一种简单优雅的方式来存储数据,就像Freebase那样,并且能够在一个Python(CherryPy)网络应用中轻松使用这些数据。
MQL参考指南的第二章提到:
Metaweb背后的数据库与您可能熟悉的关系型数据库有根本的不同。关系型数据库以表格的形式存储数据,而Metaweb数据库则以节点和这些节点之间关系的图形形式存储数据。
这大概意味着我应该使用三元组存储或者像Neo4j这样的图形数据库?这里有没有人有在Python环境中使用这些的经验?
(我目前尝试的是创建一个关系型数据库的结构,以便能够轻松存储Freebase主题,但在配置SQLAlchemy中的映射时遇到了一些问题。)
我正在研究的内容
更新 [28/12/2011]:
我在Freebase博客上找到了一篇文章,描述了Freebase自己使用的专有元组存储/数据库(graphd):http://blog.freebase.com/2008/04/09/a-brief-tour-of-graphd/
6 个回答
SPARQL是一种用来查询RDF数据的语言,它的写法和SQL很像。大多数RDF数据库都支持SPARQL接口。此外,Freebase允许你将数据导出为RDF格式,这样你就可以直接把这些数据放到RDF数据库中,并用SPARQL来查询。
如果你想更好地了解SPARQL,可以看看这个教程。
如果你要处理像Freebase这样的大数据集,我建议使用4store,并结合任何Python客户端。4store通过HTTP提供SPARQL接口,你可以发送HTTP请求来添加、删除和查询数据。它还以JSON格式处理查询结果,这在使用Python时非常方便。我在几个项目中使用过这个架构,虽然不是用CherryPy,而是用Django,但我觉得这个差别并不重要。
我想说说我的看法...
我用了一点Java代码,把Freebase的数据转成RDF格式:https://github.com/castagna/freebase2rdf
我使用Apache Jena的TDB存储来加载RDF数据,然后用Fuseki通过HTTP的SPARQL协议来提供这些数据。
另外,你也可以看看:
这是我用过的方法。它可以让你在标准的MySQL安装上,加载所有的Freebase数据,所需的磁盘空间不到100GB。关键在于理解数据在转储中的布局,然后进行转换(优化空间和速度)。
在尝试使用之前,你需要了解的Freebase概念(这些都来自文档):
- 主题 - 任何类型为'/common/topic'的东西,注意你可能会在Freebase中遇到的不同类型的ID,比如'id'、'mid'、'guid'、'webid'等。
- 域
- 类型 - '是一个'的关系
- 属性 - '有一个'的关系
- 模式
- 命名空间
- 键 - 在'/en'命名空间中是人类可读的
一些其他重要的Freebase细节:
- 查询编辑器是你的好帮手
- 理解'源'、'属性'、'目标'和'值'这些概念,详细信息可以在这里找到
- 每个实体都有一个mid,甚至像'/'、'/m'、'/en'、'/lang'、'/m/0bnqs_5'等这样的东西;可以使用查询编辑器测试:
[{'id':'/','mid':null}]
- 你不知道数据转储中的任何实体(即行)是什么,你必须找到它的类型才能知道(例如,如何知道
'/m/0cwtm'
是一个人); - 每个实体至少有一个类型(但通常会有更多)
- 每个实体至少有一个ID/键(但通常会有更多)
- 本体(即元数据)嵌入在与数据相同的转储和格式中(这与其他分发方式如DBPedia等不同)
- 转储中的'目标'列是比较混淆的,它可能包含一个mid或一个键(看看下面的转换是如何处理这个的)
- 域、类型、属性同时也是命名空间层级(我认为发明这个的人真是天才);
- 理解什么是主题,什么不是主题(绝对关键),例如这个实体
'/m/03lmb2f'
类型为'/film/performance'
的不是主题(我倾向于把这些看作是RDF中的空节点,尽管这可能在哲学上不准确),而类型为'/film/director'
的'/m/04y78wb'
(还有其他类型)是;
转换
(请查看底部的Python代码)
转换1(从shell中,分离命名空间中的链接,忽略notable_for和非/lang/en文本):
python parse.py freebase.tsv #end up with freebase_links.tsv and freebase_ns.tsv
转换2(从Python控制台,按freebase_ns_types.tsv和freebase_ns_props.tsv以及其他15个我们暂时忽略的文件分割freebase_ns.tsv)
import e
e.split_external_keys( 'freebase_ns.tsv' )
转换3(从Python控制台,将属性和目标转换为mids)
import e
ns = e.get_namespaced_data( 'freebase_ns_types.tsv' )
e.replace_property_and_destination_with_mid( 'freebase_links.tsv', ns ) #produces freebase_links_pdmids.tsv
e.replace_property_with_mid( 'freebase_ns_props.tsv', ns ) #produces freebase_ns_props_pmids.tsv
转换4(从MySQL控制台,加载freebase_links_mids.tsv、freebase_ns_props_mids.tsv和freebase_ns_types.tsv到数据库中):
CREATE TABLE links(
source VARCHAR(20),
property VARCHAR(20),
destination VARCHAR(20),
value VARCHAR(1)
) ENGINE=MyISAM CHARACTER SET utf8;
CREATE TABLE ns(
source VARCHAR(20),
property VARCHAR(20),
destination VARCHAR(40),
value VARCHAR(255)
) ENGINE=MyISAM CHARACTER SET utf8;
CREATE TABLE types(
source VARCHAR(20),
property VARCHAR(40),
destination VARCHAR(40),
value VARCHAR(40)
) ENGINE=MyISAM CHARACTER SET utf8;
LOAD DATA LOCAL INFILE "/data/freebase_links_pdmids.tsv" INTO TABLE links FIELDS TERMINATED BY '\t' LINES TERMINATED BY '\n';
LOAD DATA LOCAL INFILE "/data/freebase_ns_props_pmids.tsv" INTO TABLE ns FIELDS TERMINATED BY '\t' LINES TERMINATED BY '\n';
LOAD DATA LOCAL INFILE "/data/freebase_ns_base_plus_types.tsv" INTO TABLE types FIELDS TERMINATED BY '\t' LINES TERMINATED BY '\n';
CREATE INDEX links_source ON links (source) USING BTREE;
CREATE INDEX ns_source ON ns (source) USING BTREE;
CREATE INDEX ns_value ON ns (value) USING BTREE;
CREATE INDEX types_source ON types (source) USING BTREE;
CREATE INDEX types_destination_value ON types (destination, value) USING BTREE;
代码
将此保存为e.py:
import sys
#returns a dict to be used by mid(...), replace_property_and_destination_with_mid(...) bellow
def get_namespaced_data( file_name ):
f = open( file_name )
result = {}
for line in f:
elements = line[:-1].split('\t')
if len( elements ) < 4:
print 'Skip...'
continue
result[(elements[2], elements[3])] = elements[0]
return result
#runs out of memory
def load_links( file_name ):
f = open( file_name )
result = {}
for line in f:
if len( result ) % 1000000 == 0:
print len(result)
elements = line[:-1].split('\t')
src, prop, dest = elements[0], elements[1], elements[2]
if result.get( src, False ):
if result[ src ].get( prop, False ):
result[ src ][ prop ].append( dest )
else:
result[ src ][ prop ] = [dest]
else:
result[ src ] = dict([( prop, [dest] )])
return result
#same as load_links but for the namespaced data
def load_ns( file_name ):
f = open( file_name )
result = {}
for line in f:
if len( result ) % 1000000 == 0:
print len(result)
elements = line[:-1].split('\t')
src, prop, value = elements[0], elements[1], elements[3]
if result.get( src, False ):
if result[ src ].get( prop, False ):
result[ src ][ prop ].append( value )
else:
result[ src ][ prop ] = [value]
else:
result[ src ] = dict([( prop, [value] )])
return result
def links_in_set( file_name ):
f = open( file_name )
result = set()
for line in f:
elements = line[:-1].split('\t')
result.add( elements[0] )
return result
def mid( key, ns ):
if key == '':
return False
elif key == '/':
key = '/boot/root_namespace'
parts = key.split('/')
if len(parts) == 1: #cover the case of something which doesn't start with '/'
print key
return False
if parts[1] == 'm': #already a mid
return key
namespace = '/'.join(parts[:-1])
key = parts[-1]
return ns.get( (namespace, key), False )
def replace_property_and_destination_with_mid( file_name, ns ):
fn = file_name.split('.')[0]
f = open( file_name )
f_out_mids = open(fn+'_pdmids'+'.tsv', 'w')
def convert_to_mid_if_possible( value ):
m = mid( value, ns )
if m: return m
else: return None
counter = 0
for line in f:
elements = line[:-1].split('\t')
md = convert_to_mid_if_possible(elements[1])
dest = convert_to_mid_if_possible(elements[2])
if md and dest:
elements[1] = md
elements[2] = dest
f_out_mids.write( '\t'.join(elements)+'\n' )
else:
counter += 1
print 'Skipped: ' + str( counter )
def replace_property_with_mid( file_name, ns ):
fn = file_name.split('.')[0]
f = open( file_name )
f_out_mids = open(fn+'_pmids'+'.tsv', 'w')
def convert_to_mid_if_possible( value ):
m = mid( value, ns )
if m: return m
else: return None
for line in f:
elements = line[:-1].split('\t')
md = convert_to_mid_if_possible(elements[1])
if md:
elements[1]=md
f_out_mids.write( '\t'.join(elements)+'\n' )
else:
#print 'Skipping ' + elements[1]
pass
#cPickle
#ns=e.get_namespaced_data('freebase_2.tsv')
#import cPickle
#cPickle.dump( ns, open('ttt.dump','wb'), protocol=2 )
#ns=cPickle.load( open('ttt.dump','rb') )
#fn='/m/0'
#n=fn.split('/')[2]
#dir = n[:-1]
def is_mid( value ):
parts = value.split('/')
if len(parts) == 1: #it doesn't start with '/'
return False
if parts[1] == 'm':
return True
return False
def check_if_property_or_destination_are_mid( file_name ):
f = open( file_name )
for line in f:
elements = line[:-1].split('\t')
#if is_mid( elements[1] ) or is_mid( elements[2] ):
if is_mid( elements[1] ):
print line
#
def split_external_keys( file_name ):
fn = file_name.split('.')[0]
f = open( file_name )
f_out_extkeys = open(fn+'_extkeys' + '.tsv', 'w')
f_out_intkeys = open(fn+'_intkeys' + '.tsv', 'w')
f_out_props = open(fn+'_props' + '.tsv', 'w')
f_out_types = open(fn+'_types' + '.tsv', 'w')
f_out_m = open(fn+'_m' + '.tsv', 'w')
f_out_src = open(fn+'_src' + '.tsv', 'w')
f_out_usr = open(fn+'_usr' + '.tsv', 'w')
f_out_base = open(fn+'_base' + '.tsv', 'w')
f_out_blg = open(fn+'_blg' + '.tsv', 'w')
f_out_bus = open(fn+'_bus' + '.tsv', 'w')
f_out_soft = open(fn+'_soft' + '.tsv', 'w')
f_out_uri = open(fn+'_uri' + '.tsv', 'w')
f_out_quot = open(fn+'_quot' + '.tsv', 'w')
f_out_frb = open(fn+'_frb' + '.tsv', 'w')
f_out_tag = open(fn+'_tag' + '.tsv', 'w')
f_out_guid = open(fn+'_guid' + '.tsv', 'w')
f_out_dtwrld = open(fn+'_dtwrld' + '.tsv', 'w')
for line in f:
elements = line[:-1].split('\t')
parts_2 = elements[2].split('/')
if len(parts_2) == 1: #the blank destination elements - '', plus the root domain ones
if elements[1] == '/type/object/key':
f_out_types.write( line )
else:
f_out_props.write( line )
elif elements[2] == '/lang/en':
f_out_props.write( line )
elif (parts_2[1] == 'wikipedia' or parts_2[1] == 'authority') and len( parts_2 ) > 2:
f_out_extkeys.write( line )
elif parts_2[1] == 'm':
f_out_m.write( line )
elif parts_2[1] == 'en':
f_out_intkeys.write( line )
elif parts_2[1] == 'source' and len( parts_2 ) > 2:
f_out_src.write( line )
elif parts_2[1] == 'user':
f_out_usr.write( line )
elif parts_2[1] == 'base' and len( parts_2 ) > 2:
if elements[1] == '/type/object/key':
f_out_types.write( line )
else:
f_out_base.write( line )
elif parts_2[1] == 'biology' and len( parts_2 ) > 2:
f_out_blg.write( line )
elif parts_2[1] == 'business' and len( parts_2 ) > 2:
f_out_bus.write( line )
elif parts_2[1] == 'soft' and len( parts_2 ) > 2:
f_out_soft.write( line )
elif parts_2[1] == 'uri':
f_out_uri.write( line )
elif parts_2[1] == 'quotationsbook' and len( parts_2 ) > 2:
f_out_quot.write( line )
elif parts_2[1] == 'freebase' and len( parts_2 ) > 2:
f_out_frb.write( line )
elif parts_2[1] == 'tag' and len( parts_2 ) > 2:
f_out_tag.write( line )
elif parts_2[1] == 'guid' and len( parts_2 ) > 2:
f_out_guid.write( line )
elif parts_2[1] == 'dataworld' and len( parts_2 ) > 2:
f_out_dtwrld.write( line )
else:
f_out_types.write( line )
将此保存为parse.py:
import sys
def parse_freebase_quadruple_tsv_file( file_name ):
fn = file_name.split('.')[0]
f = open( file_name )
f_out_links = open(fn+'_links'+'.tsv', 'w')
f_out_ns = open(fn+'_ns' +'.tsv', 'w')
for line in f:
elements = line[:-1].split('\t')
if len( elements ) < 4:
print 'Skip...'
continue
#print 'Processing ' + str( elements )
#cases described here http://wiki.freebase.com/wiki/Data_dumps
if elements[1].endswith('/notable_for'): #ignore notable_for, it has JSON in it
continue
elif elements[2] and not elements[3]: #case 1, linked
f_out_links.write( line )
elif not (elements[2].startswith('/lang/') and elements[2] != '/lang/en'): #ignore languages other than English
f_out_ns.write( line )
if len(sys.argv[1:]) == 0:
print 'Pass a list of .tsv filenames'
for file_name in sys.argv[1:]:
parse_freebase_quadruple_tsv_file( file_name )
注意事项:
- 根据机器的不同,索引创建可能需要几小时到12小时以上(不过要考虑你处理的数据量)。
- 为了能够双向遍历数据,你还需要在links.destination上建立索引,我发现这在时间上是昂贵的,并且从未完成。
- 这里还有许多其他优化的可能性。例如,'types'表足够小,可以在Python字典中加载(见
e.get_namespaced_data( 'freebase_ns_types.tsv' )
)
最后的标准免责声明。这已经是几个月前的事情了。我相信这些内容大部分是正确的,但如果我的笔记遗漏了什么,我深感抱歉。不幸的是,我需要的项目没有继续下去,但希望这能帮助到其他人。如果有什么不清楚的地方,请在这里留言。