如何像Freebase一样存储数据?

7 投票
6 回答
4466 浏览
提问于 2025-04-17 09:02

我承认这个问题基本上是重复的,之前有类似的提问:在本地服务器上使用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 个回答

3

SPARQL是一种用来查询RDF数据的语言,它的写法和SQL很像。大多数RDF数据库都支持SPARQL接口。此外,Freebase允许你将数据导出为RDF格式,这样你就可以直接把这些数据放到RDF数据库中,并用SPARQL来查询。

如果你想更好地了解SPARQL,可以看看这个教程

如果你要处理像Freebase这样的大数据集,我建议使用4store,并结合任何Python客户端。4store通过HTTP提供SPARQL接口,你可以发送HTTP请求来添加、删除和查询数据。它还以JSON格式处理查询结果,这在使用Python时非常方便。我在几个项目中使用过这个架构,虽然不是用CherryPy,而是用Django,但我觉得这个差别并不重要。

6

我想说说我的看法...

我用了一点Java代码,把Freebase的数据转成RDF格式:https://github.com/castagna/freebase2rdf

我使用Apache Jena的TDB存储来加载RDF数据,然后用Fuseki通过HTTP的SPARQL协议来提供这些数据。

另外,你也可以看看:

10

这是我用过的方法。它可以让你在标准的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' )

最后的标准免责声明。这已经是几个月前的事情了。我相信这些内容大部分是正确的,但如果我的笔记遗漏了什么,我深感抱歉。不幸的是,我需要的项目没有继续下去,但希望这能帮助到其他人。如果有什么不清楚的地方,请在这里留言。

撰写回答