Как хранить данные, как это делает Freebase?

Я признаю, что это в основном повторяющийся вопрос Использовать данные Freebase на локальном сервере? но мне нужны более подробные ответы, чем там уже дано

Я абсолютно влюбился в Freebase. Теперь я хочу создать очень простой клон Freebase для хранения контента, который может не принадлежать самой Freebase, но может быть описан с использованием схемы Freebase. По сути, я хочу простой и элегантный способ хранения данных, как это делает сама Freebase, и иметь возможность легко использовать эти данные в веб-приложении Python (CherryPy).

Глава 2 справочника 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/


person Bruce van der Kooij    schedule 26.12.2011    source источник
comment
Учитывая первое предложение, правильно было бы открыть вознаграждение за исходный вопрос.   -  person Matt Ball    schedule 27.12.2011
comment
возможный дубликат Использовать данные Freebase на локальном сервере?   -  person Matt Ball    schedule 27.12.2011
comment
Ну, AFAIK, как третье лицо, вы не можете создать награду и добавить уточнение относительно того, какой информации пока не хватает в существующих ответах. Так что просто назначать награду было бы не очень полезно, мне кажется.   -  person Bruce van der Kooij    schedule 27.12.2011


Ответы (6)


Это то, что сработало для меня. Это позволяет вам загрузить весь дамп Freebase в стандартной установке MySQL менее чем на 100 ГБ диска. Ключевым моментом является понимание расположения данных в дампе, а затем его преобразование (оптимизация по размеру и скорости).

Понятия Freebase, которые вы должны понять, прежде чем пытаться использовать это (все взято из документации):

  • Тема — что угодно типа «/common/topic», обратите внимание на различные типы идентификаторов, которые вы можете встретить в Freebase — «id», «mid», «guid», «webid» и т. д.
  • Домен
  • Тип - отношение "является"
  • Свойства - "имеет" связь
  • Схема
  • Пространство имен
  • Ключ - человекочитаемый в пространстве имен '/en'

Некоторые другие важные особенности Freebase:

  • редактор запросов — ваш друг
  • понимать понятия «источник», «свойство», «назначение» и «значение», описанные здесь
  • у всего есть середина, даже у таких вещей, как '/', '/m', '/en', '/lang', '/m/0bnqs_5' и т. д.; Протестируйте с помощью редактора запросов: [{'id':'/','mid':null}]​
  • вы не знаете, что представляет собой какая-либо сущность (т. е. строка) в дампе данных, вам нужно добраться до ее типов, чтобы сделать это (например, откуда я знаю, что '/m/0cwtm' является человеком);
  • каждая сущность имеет по крайней мере один тип (но обычно гораздо больше)
  • у каждой сущности есть хотя бы один идентификатор/ключ (но обычно их намного больше)
  • онтология (т. е. метаданные) встраивается в тот же дамп и в том же формате, что и данные (не с другими дистрибутивами, такими как DBPedia и т. д.)
  • столбец «назначение» в дампе сбивает с толку, он может содержать промежуточное значение или ключ (см., как с этим справляются преобразования ниже)
  • домены, типы, свойства являются уровнями пространства имен одновременно (кто бы это ни придумал, ИМХО гений);
  • понять, что является Темой, а что не является Темой (абсолютно важно), например, этот объект '/m/03lmb2f' типа '/film/performance' НЕ является Темой (я предпочитаю думать об этом как о том, что Пустые узлы в RDF, хотя это может быть и неточно с философской точки зрения), а '/m/04y78wb' типа '/film/director' (среди прочих);

Превращает

(см. код Python внизу)

ПРЕОБРАЗОВАНИЕ 1 (из оболочки, разделить ссылки из пространств имен, игнорируя текст notable_for и не /lang/en):

python parse.py freebase.tsv  #end up with freebase_links.tsv and freebase_ns.tsv

TRANSFORM 2 (из консоли Python, разделите файл freebase_ns.tsv на файл freebase_ns_types.tsv, freebase_ns_props.tsv и еще 15 других, которые мы пока игнорируем)

import e
e.split_external_keys( 'freebase_ns.tsv' )

ПРЕОБРАЗОВАНИЕ 3 (из консоли Python преобразовать свойство и место назначения в средние значения)

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, который, как я обнаружил, был дорогим по времени и никогда не заканчивался.
  • Здесь возможны многие другие оптимизации. Например, таблица «типы» достаточно мала, чтобы ее можно было загрузить в память в словаре Python (см. e.get_namespaced_data( 'freebase_ns_types.tsv' ))

И стандартный отказ от ответственности здесь. Прошло несколько месяцев с тех пор, как я это сделал. Я считаю, что это в основном правильно, но я прошу прощения, если мои заметки что-то упустили. К сожалению, проект, для которого мне это было нужно, провалился, но надеюсь, что это поможет кому-то еще. Если что-то непонятно, напишите сюда.

person ivaylo_iliev    schedule 14.09.2012

Мои 2 цента...

Я использую немного кода Java для преобразования дампа данных Freebase в RDF: https://github.com/castagna/freebase2rdf

Я использую хранилище TDB Apache Jena для загрузки данных RDF и Fuseki для обслуживания данных по протоколу SPARQL через HTTP.

Смотрите также:

person castagna    schedule 11.03.2012

SPARQL — это язык запросов для запроса RDF, он позволяет писать запросы, подобные SQL. Большинство баз данных RDF реализуют интерфейсы SPARQL. Более того, Freebase позволяет вам экспортировать данные в RDF, чтобы вы потенциально могли использовать эти данные непосредственно в базе данных RDF и запрашивать их с помощью SPARQL.

Я бы посмотрел это руководство, чтобы лучше понять СПАРКЛ.

Если вы собираетесь работать с большим набором данных, таким как freebase, я бы использовал 4store вместе с любым из клиенты Python. 4store предоставляет SPARQL через HTTP, вы можете делать HTTP-запросы для подтверждения, удаления и запроса данных. Он также обрабатывает наборы результатов в формате JSON, и это очень удобно для Python. Я использовал эту инфраструктуру в нескольких проектах, не с CherryPy, а с Django, но я думаю, что эта разница не имеет большого значения.

person Manuel Salvadores    schedule 27.12.2011
comment
Freebase не предлагает массовую загрузку RDF, поэтому вам придется просканировать весь сайт (и даже это будет неполным). Использование freebase-quad-rdfize или его эквивалента, как упоминалось в ответе на вопрос, который дублирует этот, — это правильный путь, если вы имеете дело с хранилищем RDF и массовой загрузкой данных. Я удивлен, что вы получили приемлемые результаты в нескольких проектах с использованием сканирования Freebase RDF и 4store. - person Tom Morris; 27.12.2011
comment
Он прямо не сказал, что использует дамп данных Freebase. Лично я не заинтересован в дублировании дампа данных, я просто хочу сохранить некоторые вещи локально, которые я не хочу отправлять во Freebase. - person Bruce van der Kooij; 27.12.2011
comment
Брюс прав. Я не работал с полным дампом Freebase в RDF. Я использовал части Freebase, которые я получил от sameAs.org. В любом случае, суть в том, что моя рекомендация для больших наборов данных RDF — 4store. - person Manuel Salvadores; 28.12.2011

Хорошей новостью для пользователей дампов Freebase является то, что теперь Freebase предлагает дамп RDF: http://wiki.freebase.com/wiki/Data_dumps . Он в формате черепахи, поэтому очень удобно использовать любую графовую базу данных, разработанную для RDF.

Я также предлагаю 4store: http://4store.org/ . он прост и удобен в использовании. Вы можете использовать http-запрос для выполнения операции SPARQL.

Одна сложная вещь в моем проекте заключается в том, что "." используемый в дампе Freebase (для представления сокращенного URL-адреса), не распознается 4store. Поэтому я добавляю квадратную скобку "‹>" ко всем столбцам, содержащим "." и сам разбираюсь с укороченным URL.

person Chenyan Xiong    schedule 19.02.2013

Взгляните на https://cayley.io. Я считаю, что он написан тем же автором и использует те же принципы, что и graphd, серверная часть Freebase, до того, как Google убил его.

Что касается данных, вы, вероятно, захотите запустить что-то вроде это для очистки дампы БД Freebase или используйте датацентр.

person wires    schedule 24.03.2017

И это дополнительный код для моего другого ответа. Мясо находится в edb.py. Запустите из консоли Python и следуйте примерам. Или используйте контроллер web2py и запустите его в своем браузере.

Сохраните это как edb.py:

import MySQLdb
import sys

connection = MySQLdb.connect (host   = "localhost",
                              user   = "root",
                              passwd = "x",
                              db     = "y")
cursor = connection.cursor()

query_counter = 0
print_queries = False
limit         = 1000

def fetch_one( query ):
    global query_counter, print_queries
    query = query + ' LIMIT ' + str(limit)
    if print_queries:
        print query
    cursor = connection.cursor()
    cursor.execute( query )
    query_counter += 1
    result = cursor.fetchone() 
    if result:
        return result[0]
    else:
        return None

def fetch_all( query ):
    global query_counter, print_queries
    query = query + ' LIMIT ' + str(limit)
    if print_queries:
        print query
    cursor = connection.cursor()
    cursor.execute( query )
    query_counter += 1
    return cursor.fetchall()

def _flatten( list_of_lists ):
    import itertools
    return list(itertools.chain(*list_of_lists))

#Example: e._search_by_name('steve martin')
def _search_by_name( name, operator = '=' ):
    typed, ranked = {}, []
    if name:
        name = name.strip()
    if not name:
        return ( typed, ranked )

    filler = '' if operator == '=' else '%'
    ranks = {}

    #to filter meaningful stuff for every mid returned order by the number of types they have

    #search for value text if prop. is 
    #select * from ns where value = 'the king' and (property = '/m/01gr' or property = '/m/06b');

    name_mid  = _mid( '/type/object/name'   )
    alias_mid = _mid( '/common/topic/alias' )

    query = "select ns.source from ns where ns.value %s '%s%s' and ns.property in ('%s', '%s')" % ( operator, name, filler, name_mid, alias_mid )

    for i in fetch_all( query ):
        typed[ i[0] ] = _types( i[0] )

    import operator
    ranked = [ ( len( typed[i] ), i ) for i in typed ]
    ranked = [ e[1] for e in sorted( ranked, key=operator.itemgetter(0), reverse = True ) ]

    return (typed, ranked)


#Example: e._children('')               <---will get the top level domains
#         e._children('/film')          <---get all types from the domain
#         e._children('/film/film')     <---get all properties for the type
def _children( parent, expand = False, raw = False ):
    query = "select t.source, t.value from types t where t.destination = '%s'" % (parent)
    res = fetch_all( query )
    if raw:
        return [ row[0] for row in res ]
    if expand: prefix = parent
    else:      prefix = ''
    return [ prefix + '/' + row[1] for row in fetch_all(query) ]

#Example: e._parent('/film/film/songs')
def _parent( child ):                                       # '/people/marriage/to' -> '/people/marriage'
    #if not isinstance( child, str ): return None          # what kind of safety mechanisms do we need here?
    return '/'.join(child.split('/')[:-1])

#Example: e._domains()
def _domains():
    return _children('')

#Example: e._top_level_types()
def _top_level_types():
    return _children('/type')

#TODO get all primitive types

#Example: e._mid('/type/object')
#         e._mid('/authority/imdb/name/nm0000188')
def _mid( key ):
    if key == '':
        return None
    elif key == '/':
        key = '/boot/root_namespace'
    parts = key.split('/')
    if parts[1] == 'm':            #already a mid
        return key
    namespace = '/'.join(parts[:-1])
    key = parts[-1]
    return fetch_one( "select source from types t where t.destination = '%s' and t.value = '%s'" % (namespace, key) )

#Example: e._key('/type')
def _key( mid ):
    if isinstance( mid, str):
        res = _keys( mid )
        if not res:
            return None
        rt  = [ r for r in res if r.startswith( '/type' ) ]
        if rt:
            return rt[0]
        else:
            return res[0]
    elif isinstance( mid, list ) or isinstance( mid, tuple ):
        res = [ _key( e ) for e in mid ]
        return [ r for r in res if r is not None ]
    else:
        return None

def _keys( mid ):
    # check for '/type/object/key' as well?
    query = "select t.destination, t.value from types t where t.source = '%s'" % mid
    return [ row[0]+'/'+row[1] for row in fetch_all( query ) ]

#Example: e._types('/m/0p_47')
def _types( mid ):
    tm = _mid( '/type/object/type' )
    query = "select l.destination from links l where l.source = '%s' and l.property = '%s'" % (mid, tm)
    return [ row[0] for row in fetch_all( query ) ]

#Example: e._props_n('/m/0p_47')   <---Named immediate properties (like name, etc.)
def _props_n( mid ):  #the same property can be set more than once per topic!
    query = "select ns.property from ns where ns.source = '%s'" % (mid)
    return list( set( [ row[0] for row in fetch_all( query ) ] ) )

#Example: e._props_l('/m/0p_47')   <---All remote properties, some are named, some are anonymous
def _props_l( mid ):  #the same property can be set more than once per topic!
    tm = _mid( '/type/object/type' )    #exclude types, they have tons of instance links
    res = fetch_all( "select l.property, l.destination from links l where l.source = '%s' and property <> '%s'" % (mid, tm) )
    output = {}
    for r in res:
        dests = output.get( r[0], False )
        if dests:
            dests.append( r[1] )
        else:
            output[ r[0] ] = [ r[1] ]
    return output

#Example: e._props_ln('/m/0p_47')  <---All remote named properties
def _props_ln( mid ): #named properties
    result = []
    ps = _props_l( mid )
    common_topic = _mid( '/common/topic' )
    for p in ps:
        ts = _types( ps[p][0] )
        if common_topic in ts:               #it's a common topic
            result.append( p )
    return result

#Example: e._props_la('/m/0p_47')  <---All remote anonymous properties, these actually belong to the children!
#instead of has type /common/topic we used to check if it has name
def _props_la( mid, raw = True ): #anonymous properties (blank nodes in RDF?)
    result = []
    ps = _props_l( mid )
    common_topic = _mid( '/common/topic' )
    for p in ps:
        ts = _types( ps[p][0] )
        if common_topic not in ts:                                       #it is not a common topic
            t = _key( _types( ps[p][0] ) )
            if t and '/type/type' not in t:                              #FIXME: hack not to go into types, could be done better
                result.append( _children( t[0], expand=True, raw=raw ) ) #get the first, is this correct?
    return _flatten( result )                                            #it is a list of lists

#FIXME: try to get '/film/actor/film' -> '/type/property/expected_type' -> '/film/performance' -> properties/children 
#instead of trying is something has name

#Example: e._get_n('/m/0p_47', e._props_n('/m/0p_47')[0])['/lang/en'] <---These come with a namespace
def _get_n( mid, prop ):  #the same property can be set more than once per topic!
    p = _mid( prop )
    query = "select ns.value from ns where ns.source = '%s' and ns.property = '%s'" % (mid, p)
    return [ r[0] for r in fetch_all( query ) ]

#Example: e._get_l('/m/0p_47', e._props_l('/m/0p_47')[0])  <---returns a list of mids coresponding to that prop.
#         e._name(e._get_l('/m/0p_47', '/film/writer/film'))          
def _get_l( mid, prop ):  #the same property can be set more than once per topic!
    p = _mid( prop )
    query = "select l.destination from links l where l.source = '%s' and l.property = '%s'" % (mid, p)
    return  [ row[0] for row in fetch_all( query ) ]           

#Example: e._name(e._get_ln('/m/0p_47', e._props_ln('/m/0p_47')[0]))
def _get_ln( mid, p ):        #just alias for _get_l, keeping for consistency
    return _get_l( mid, p )                                 

#Example: e._name(e._get_la('/m/0p_47', '/film/performance/film'))
def _get_la( mid, prop ):
    result = []
    ps = _props_l( mid )
    for p in ps:
        es = _get_l( mid, p )                       #get the destinations
        if not es: continue
        ts = set( _types( es[0] ) )
        if _mid(_parent(_key(_mid(prop)))) in ts:   #should be able to do this more efficiently!!!
            for e in es:
                result.append( _get_l( e, prop ) )
            return _flatten( result )               #return after the first result 

#How do we determine properties with multiple values vs those with singular (i.e. place of birth)?
#is this in the ontology?
#Ans: yes, /type/property/unique

#Example: e._all_names_ln('/m/0p_47')  <---gets all of object's remote named properties
def _all_names_ln( mid ):
    result = {}
    for p in _props_ln( mid ):
        result[ _key(p) ] = _name( _get_ln( mid, p ) )
    return result 

#Example: e._all_names_la('/m/0p_47')  <---gets all of object's remote anonymous properties
def _all_names_la( mid ):       #TODO: prevent loops, run e.all_names_la('/m/0p_47')
    result = {}
    for p in _props_la( mid ):
        result[ _key( p ) ] = _name ( _get_la( mid, p ) )
    return result 

#FIXME: _all_names_la is going into destinations which are types and have a ton of instance links...


#Example: e._name('/m/0p_47')   <---the name of a topic
#
def _name( mid ):
    if isinstance( mid, str ):
        nm = _mid( '/type/object/name' )
        return _get_n( mid, nm )
    elif isinstance( mid, list ) or isinstance( mid, tuple ) or isinstance( mid, set ):
        return [ _name( e ) for e in mid ]
    else:
        return None

#for internal use only
def _get_linked( mid ):
    tm = _mid( '/type/object/type' )    #exclude types, they have tons of instance links
    query = "select destination from links where source = '%s' and property <> '%s' " % ( mid, tm )
    return set( [ r[0] for r in fetch_all( query ) ] )

#for internal use only
def _get_connections_internal( entity1, target, path, all_paths, depth, max_depth):
    import copy

    if depth > max_depth:
        return

    if True:
        print
        print str(entity1) + ', ' + str(target)
        print str( path )
        print str( all_paths )
        print depth

    path.append( entity1 )

    linked1 = _get_linked( entity1 )

    if target in linked1 or entity1 == target:
        path.append( target )
        all_paths.append( path )
        #print str( path )
        return

    for l1 in linked1:
        if l1 in path:
            continue
        _get_connections_internal( l1, 
                                   target, 
                                   copy.copy( path ),
                                   all_paths,
                                   depth+1, 
                                   max_depth )

#Example: e._name(e._get_connections('/m/0p_47', '/m/0cwtm'))  <---find path in the graph between the two entities    
def _get_connections( entity1, target ):
    result = []
    _get_connections_internal( entity1, target, [], result, 0, 2 )
    return result

#for internal use only
def _get_connections_internal2( entity1, entity2, path1, path2, all_paths, depth, max_depth, level ):
    import copy

    if depth > max_depth:
        return

    if level < 0: level = 0

    path1.append( entity1 )
    path2.append( entity2 )

    if entity1 == entity2 and level == 0:
        all_paths.append( ( path1, path2 ) ) #no need to append entity1 or entity2 to the paths
        return

    linked1 = _get_linked( entity1 )
    if entity2 in linked1 and entity2 not in path1 and level == 0:
        path1.append( entity2 )
        all_paths.append( ( path1, path2 ) )
        return

    linked2 = _get_linked( entity2 )
    if entity1 in linked2 and entity1 not in path2 and level == 0:
        path2.append( entity1 )
        all_paths.append( ( path1, path2 ) )
        return

    inters = linked1.intersection( linked2 )
    inters = inters.difference( set( path1 ) )
    inters = inters.difference( set( path2 ) )

    if inters and level == 0:
        for e in inters:               #these are many paths, have to clone
            p1 = copy.copy( path1 )
            p1.append( e )
            p2 = copy.copy( path2 )
            p2.append( e )
            all_paths.append( ( p1,p2 ) )
        return

    for l1 in linked1:
        if l1 in path1 or l1 in path2:
            continue
        for l2 in linked2:
            if l2 in path1 or l2 in path2:
                continue
            _get_connections_internal2( l1,                 l2, 
                                        copy.copy( path1 ), copy.copy( path2 ),
                                        all_paths,
                                        depth+1, 
                                        max_depth,
                                        level - 1 )

#Example: e._name(e._get_connections2('/m/0p_47', '/m/0cwtm'))          <---returns two meeting paths starting from both entities
#         e._name(e._get_connections('/m/0p_47', '/m/0cwtm', level=1))  <---search deeper
#         e._name(e._get_connections('/m/0p_47', '/m/0cwtm', level=2))  <---even deeper
def _get_connections2( entity1, entity2, level = 0 ):
    result = []
    _get_connections_internal2( entity1, entity2, [], [], result, 0, 15, level )
    return result

А вот пример контроллера web2py (просто скопируйте edb.py в каталог моделей web2py):

# -*- coding: utf-8 -*-

def mid_to_url( mid ):
    return mid.split('/')[2]

def index():

    form = FORM( TABLE( TR( INPUT(_name='term', _value=request.vars.term ) ),
                        TR(INPUT(_type='submit', _value='Search') ) ),
                 _method='get')

    typed, ranked = _search_by_name( request.vars.term )

    rows = []

    for r in ranked:
        keys = []
        for t in typed[r]:
            k = _key( t )
            if k:
                keys.append( k )
        rows.append( TR( TD( A(_name( r ), 
                               _href = URL('result', args = [mid_to_url(r)]))), 
                         TD( XML( '<br/>'.join( keys ) ) ) ) )

    result = TABLE( *rows )

    return { 
             'form': form, 
             'result' : result 
             }

def result():
    path, data = '', ''
    if not request.args:
        return { 'path':path, 'data':data}

    path_rows = []
    for ra in range(len(request.args)):
        if ra%2:
            arrow_url = URL( 'static', 'images/blue_arr.png' )
            display_name = _key('/m/'+request.args[ra])        #it's a property
        else:
            arrow_url = URL( 'static', 'images/red_arr.png' )
            display_name = _name('/m/'+request.args[ra])       #it's a topic
        path_rows.append( TD( A( display_name, _href=URL( args = request.args[0:ra+1] ) ) ) )
        path_rows.append( TD( IMG( _src = arrow_url ) ) )
    path = TABLE( *path_rows )

    elems = [ '/m/'+a  for a in request.args ]
    if _mid( '/type/property' ) in _types( elems[-1] ): #we are rendering a property
        objects = _get_ln( elems[-2], elems[-1] )
        if not objects: #there should be a better way to see if this is anonymous
            objects = _get_la( elems[-2], elems[-1] )

        data = TABLE( *[ TR( TD( A(_name(o), _href = URL( args = request.args+[mid_to_url(o)])))) for o in objects ] )

    else:                                               #we are rendering a topic
        direct_props = TABLE(*[TR(TD(_key(p)), TD(', '.join(_get_n( elems[-1], p)))) for p in _props_n( elems[-1] )])
        linked_named_props = TABLE(*[TR(TD(A(_key(p), 
                                             _href = URL(args = request.args+[mid_to_url(p)])))) for p in _props_ln( elems[-1] ) ] )
        linked_anon_props = TABLE(*[TR(TD(A(_key(p), 
                                            _href = URL(args = request.args+[mid_to_url(p)])))) for p in _props_la( elems[-1] ) ] )

        data = TABLE( TR( TH( 'Linked named data:'), TH( 'Linked anonymous data:' ), TH( 'Direct data:' ) ), 
                      TR( TD( linked_named_props ), TD( linked_anon_props ), TD( direct_props ) ) )

    return { 'path': path, 'data':data }
person ivaylo_iliev    schedule 14.09.2012