Данные OSM в MongoDB

Возможно лучше сделать статью в вики и проект на github.com, но пока оставлю топик тут чтобы не пропало)

Возился давно с импортом в PostgreSQL, но как-то не подружился я с ней… со стороны девелоперов нету тестового сервочка, доки в вике были старые и т.д.
Надоело, равно как и сами sql базы уже не привлекают после того как попробовал Mongo)

Не могу сказать шустрая база или нет, но очень удобная! Тесты разные… в инете идут холивары) Но масштабируется, насколько я понял, она довольно просто.
Пытаюсь сделать распределение векторных данных по уровням для векторного рендера карты с любым масштабом в реалтайме… но это лучше вынести в отдельный топик.
На данный момент есть простенький импорт/экспорт данных в MongoDB скриптами на python

Импорт 164 Гигового planet-100908.osm занял 2 суток 14 часов 4 минуты и 38 секунд
база MongoDB занимает 158.5 Гб… хотя помимо osm данных там ее вспомогательная база статистика импорта, а также парочка тестовых баз…
Иморт на Core 2 Duo под Ubuntu Server 10.04 64 бит (3-4 тысячи нодов в секунду) в почти 4 раза медленнее чем на ноуте под Windows 7 32 бит на i3 (10-13 тысяч нодов)… не знаю в чем тонкость.
Возможно питон собран криво, возможно повлиял тот факт что база располагалась на медленном 1.5 Тб винче WD. Ну и разница в производительности i3 и Core 2 Duo…
В любом случае написал заготовку импорта на С++. Буду тестить на базах поменьше… скачал 4 гиговый rus.osm
XML парсер можно не использовать т.к. файлы осма довольно просто и надеюсь быстрее “руками” разобрать.

Какое приблизительное время импорта planet.osm в postgreSQL? И на каком железе.
В базе нужно заводить всякие там индексы, настройки и т.д… я пока делал импорт тупо без всего этого…
Остальное буду добавлять по необходимости.
Пока есть некоторые функции на Java-Script, которые исполняются самим MongoDB… например, вычисление площади или длины вея.
Также есть какие-то удобные фишки MongoDB, которые я не использовал… вроде того что элемент одного объекта коллекции может ссылаться на другой объект. Деревья…
У меня самый просто случай.

pyosm.py


import xml.sax
import unicodedata
    
class Member(object):
    def __init__(self, type, ref, role=''):

        self.type = type
        self.ref = ref
        self.role = role

    def list(self):
        return {'type':self.type, 'ref':self.ref, 'role':self.role}
    
    def __repr__(self):
        return "Member(%r, %r, %r)" % (self.type, self.ref, self.role)

class Node(object):
    def __init__(self, id, lon, lat, visible, tags=None):
        self.id = id
        self.lon = lon
        self.lat = lat
        self.visible = visible

        if tags:
            self.tags = tags
        else:
            self.tags = {}

    def __repr__(self):
        return "Node(id=%r, lon=%r, lat=%r, visible=%r, tags=%r)" % (self.id, self.lon, self.lat, self.visible, self.tags)

class Way(object):
    def __init__(self, id, visible, nodes=None, tags=None):
        self.id = id
        self.visible = visible
        
        if nodes:
            self.nodes = nodes
        else:
            self.nodes = []
            
        if tags:
            self.tags = tags
        else:
            self.tags = {}
            
    def __repr__(self):
        return "Way(id=%r, visible=%r, nodes=%r, tags=%r)" % (self.id, self.visible, self.nodes, self.tags)

class Relation(object):
    def __init__(self, id, members=None, tags=None):
        self.id = id
        
        if members:
            self.members = members
        else:
            self.members = []

        if tags:
            self.tags = tags
        else:
            self.tags = {}
            
    def __repr__(self):
        return "Relation(id=%r, members=%r, tags=%r)" % (self.id, self.members, self.tags)

Файлик import.py


import time
import xml.sax
from pymongo import *
from pyosm import *

def db_clear():
    dbnodes.remove()
    dbways.remove()
    dbrelations.remove()

class run_import(object):
    def __init__(self, filename):
        self.filename = filename
        self.start = time.time()
        self.__parse()
        

    def __parse(self):
        """Parse the given XML file"""
        parser = xml.sax.make_parser()
        parser.setContentHandler(OSMXMLFileParser(self))
        parser.parse(self.filename)
        print 'Import time: ' + repr('%.2f'%(time.time() - self.start)) + ' seconds'

class OSMXMLFileParser(xml.sax.ContentHandler):
    def __init__(self, containing_obj):
        self.containing_obj = containing_obj
        self.curr_node = None
        self.curr_way = None
        self.curr_relation = None
        self.nodesCount = 0
        self.waysCount = 0
        self.relationsCount = 0

    def startElement(self, name, attrs):
        #print "Start of node " + name
        if name == 'node':
            self.curr_node = Node(id=long(attrs['id']), lon=float(attrs['lon']), lat=float(attrs['lat']), visible='true')
            
        elif name == 'way':
            #self.containing_obj.ways.append(Way())
            self.curr_way = Way(id=long(attrs['id']), visible='true')

        elif name == 'relation':
            self.curr_relation = Relation(id=long(attrs['id']))
            
        elif name == 'tag':
            #assert not self.curr_node and not self.curr_way, "curr_node (%r) and curr_way (%r) are both non-None" % (self.curr_node, self.curr_way)
            if self.curr_node:
                self.curr_node.tags[attrs['k']] = attrs['v']
            elif self.curr_way:
                self.curr_way.tags[attrs['k']] = attrs['v']
            elif self.curr_relation:
                self.curr_relation.tags[attrs['k']] = attrs['v']
                
        elif name == 'nd':
            #assert self.curr_node is None, "curr_node (%r) is non-none" % (self.curr_node)
            #assert self.curr_way is not None, "curr_way is None"
            self.curr_way.nodes.append(long(attrs['ref']))
                
        elif name == 'member':
            #assert self.curr_node is None, "curr_node (%r) is non-none" % (self.curr_node)
            #assert self.curr_way is not None, "curr_way is None"
            member = Member(type=attrs['type'], ref=long(attrs['ref']))
            if attrs.has_key('role') == True:
                member.role = attrs['role']
            self.curr_relation.members.append(member)

    def endElement(self, name):
        #print "End of node " + name
        #assert not self.curr_node and not self.curr_way, "curr_node (%r) and curr_way (%r) are both non-None" % (self.curr_node, self.curr_way)
        if name == 'node':
            dbnode = {'_id': self.curr_node.id,
                      'lon': self.curr_node.lon,
                      'lat': self.curr_node.lat,
                      'visible': self.curr_node.visible,
                      'in_ways': [],
                      'in_relations': []}
            
            if len(self.curr_node.tags) > 0:
                dbnode['tags'] = self.curr_node.tags
            dbnodes.save(dbnode)
            self.curr_node = None

        elif name == 'way':
            dbway = {'_id': self.curr_way.id,
                     'visible': self.curr_way.visible,
                      'in_relations': []}
            dbway['nodes'] = self.curr_way.nodes
            dbway['tags'] = self.curr_way.tags
            dbways.save(dbway)
            self.curr_way = None
            
        elif name == 'relation':
            dbrelation = {'_id': self.curr_relation.id,
                          'in_relations': []}
            dbrelation['members'] = []
            for member in self.curr_relation.members:
                dbrelation['members'].append(member.list())
            dbrelation['tags'] = self.curr_relation.tags
            dbrelations.save(dbrelation)
            self.curr_relation = None


if __name__ == '__main__':

    try:
        connection = Connection('localhost', 27017)
    except pymongo.errors.AutoReconnect:
        raise
    
    db = connection.planet_100908

    dbnodes = db.nodes
    dbways = db.ways
    dbrelations = db.relations

    db_clear()
    
    run_import('./../../planet-100908.osm')

    print 'In database ' + repr(dbnodes.find().count()) + ' nodes, ' + \
            repr(dbways.find().count()) + ' ways, ' + \
            repr(dbrelations.find().count()) + ' relations'

Памяти скриптец есть мало, но зато работает долго…
Также есть скрипт экспорта в *.osm… довольно кривой)) Но Josm понимает и рисует правильно
Если кто работал или захочет поиграться с MongoDB, то пишите предложения, результаты тестов… может скрипты какие

ИМпорт планеты на 2.6ггц амдешном 4-ядернике с 8 гб памяти и софтрейдовой двухдисковой подсистемой занимает меньше суток в simple схему (плюс сутки на индексы) и порядка двух (+двое суток на индексы) в полноценное зеркало осм-апи.

Набросал С++ код импортирующий *.osm
Пока только ноды правда…
Но скорость!!! Ппц… 26’954.8 нодов в секунду… на дохлом-дохлом ноуте 2005 года выпуска)))
Это почти в 8-10 раз быстрее чем питоновский код на нормальном железе!
Есть что оптимизировать… хотя даже и так оочень шустро
Даж не верится, но в базе все объекты вроде на месте… с тегами, правильными параметрами и т.д…
Я счастилв!!! :smiley:

У меня тоже очень быстрый самописный парсер OSM есть. Проблема в том, что надо еще распарсивать XML entity.

x10kHz: попробуй запустить на PyPy, он раза в 2-3 быстрее, чем стандартный Питон.

            if self.curr_node:
                self.curr_node.tags[attrs['k']] = attrs['v']
            elif self.curr_way:
                self.curr_way.tags[attrs['k']] = attrs['v']
            elif self.curr_relation:
                self.curr_relation.tags[attrs['k']] = attrs['v']
              

Это сжимается до

if self.curr_elt:
    self.curr_elt.tags[attrs['k']] = attrs['v']

если в других частях кода curr_node/way/etc заменить на curr_elt.

Насколько я помню, уровней вложенности в файле osm всего 1. Если больше, надо просто сделать стек из элементов, вроде такого:

#!/usr/bin/python

stack = []

def put(item):
    stack.append(item)

def pop():
    return stack.pop()

def set_property(data):
    for k, v in data.items():
        stack[-1].tags[k] = v
    
def get_id():
    return stack[-1].id

Напомню, что если питоновский скрипт оформить фнкцией (да хоть main()), а в начале (до функции) дописать вызов psyco, то все числодробильные штуки начнут исполняться со скоростью, близкой к асмовой. Жаль, что только на 32-битных системах.
import psyco
psyco.full()
Да и плодить объекты не надо лишний раз, если критично именно время исполнения.

Чистый C/C++ будет быстрее работать, но его сложнее писать :slight_smile:

Ещё можно SteelBank Common Lisp, он работает так же быстро, как C. Только к нему доки и такие либы под XML фиг найдёшь :slight_smile:

А поглядеть на этот код можно? Уж очень фантастически цифры выглядят. На Python с учетом всех оптимизаций больше 3500 объектов в секунду выжать не удалось.

Kaylee, прошу прощения за столь большую задержку!

Исходники выложил “как есть”… пока там сплошной мусор, ненужные закомменченые дебажные куски кода и прочее, но основную идею понять можно т.к. объем кода довольно маленький и даже должно работать!)) Но я сейчас не проверял… надеюсь в будущем дойдут руки привести все в порядок!

http://github.com/chertov/OpenStreetMap-MongoDB

Простите за этот позор, надеюсь хоть кому-нибудь пригодится :smiley:

если кому интересно то в базе

mongo -uosm -posm wildman.bn.by/osm

лежит дамп беларуси. в ближайшее время сделаю ежедневное обновление. пока что за 7-е число.

Совершенно обычная скорость.
Парсер моего конвертера обрабатывает planet.osm примерно 2 ч 40 м на обычном двухъядернике 2.6 ГГц 2006 г. без всяких RAID’ов.
Скорость обработки узлов составляет 139962.8 узлов/сек.
Собственно, такое указание скорости не совсем корректно. Лучше посчитать по-другому:
исходный файл - 204 Гб + результирующие файлы суммарным объемом около 49 Гбайт, то скорость обмена с диском составляет 26.3 Мб/сек.
Другими словами, скорость обработки определяется не столько производительностью процессора, сколько скоростью работы жесткого диска.

PS. Собственно, парсер работает хоть и на двухъядернике, но в 1 поток. Учитывая, что узким местом является производительность диска, я не стал заботиться о распараллеливании обработки на 2 ядра.
В том и прелесть компилируемых языков, что, если не делать откровенных глупостей, любой алгоритм обработки данных сложности O(n) упирается именно в производительность жесткого диска, а не процессора.

Кто заливал osm в mongo, какие объёмы базы получаются? У меня файл Новосибирской области (12 мегов bz2, или 120 распакованный) занял 1.1G. :open_mouth:

У меня получился скрипт, обрабатывающий 2.2К точек в секунду.

Что интересно, парсер .osm.bz2 > .json.bz2 на pypy работает медленнее, чем на стандартном питоне 2.6.

Поскольку для js есть xml-парсеры, попробую переписать питоновский скрипт на js, чтобы работал прямо в mongodb, получится компактнее и быстрее.

Как скомпилировать и запустить файл .cpp? Интересно попробовать, насколько будет быстро работать. Ещё вопрос, можно ли ему направить через stdin поток из распаковщика bz2?

Индексы пробовал, работают нормально, ускоряют очень хорошо. Можно сделать индекс по любому тегу:
(через pymongo)

db.way.create_index('tags.building:levels')

(если теги находятся в отдельном объекте: way = {_id: …, {tags: {building: ‘yes’, building:levels: 9}}})

Индекс работает ещё быстрее, если у каких-то объектов этого поля нет. То есть если хотите ставить разные флаги, лучше чтобы у большинства объектов флага не было, тогда по нему ищется быстрее. То же самое с сортировкой.

Запросы лучше делать пореже и доставать данные сразу одним. Например, если нужно по одиночке обработать точки из линии, лучше запросить их сразу все:
db.node.find({‘id’: {‘$in’: [id1, id2, …]}})
чем по одной.

Запрос update поддерживает только либо установку конкретного значения, либо увеличение или уменьшение на число, поэтому если нужно записать сложные вычисления, запросы придётся делать по одному.

Если lon и lat (именно в таком порядке) собрать в массив или в объект, можно сделать по ним пространственный индекс. Но единственная команда, которая доступна - поиск точек в определённых границах либо по расстоянию до них. Для подсчёта расстояний между парами точек и длин путей такая команда слишком медленная.

После замеров скорости работы скриптов на питоновском профайлере cProfile оказалось, что самая затратная операция - count(). Вызвать .count у запроса - в 4 раза дороже, чем .next().

Оказалось, что вызовы count занимают в моём скрипте больше половины времени, 160 секунд из 290.

Переделал скрипт. Было:

for node in db.nodes.find(...):
    bad_neighbors = db.nodes.find({'_id': {'$in': node['neighbors']}})
    while bad_neighbors.count() > 0:
        for neighbor in bad_neighbors:
            ...
        bad_neighbors.rewind()

Заменил на счётчик в виде переменной, который запускается в цикле.

for node in db.nodes.find(...):
    bad_neighbors = db.nodes.find({'_id': {'$in': node['neighbors']}})
    bad_neighbors_count = 0
    while True:
        for neighbor in bad_neighbors:
            bad_neighbors_count += 1
            ...
        if bad_neighbors_count == 0:
            break
        bad_neighbors.rewind()

Суммарное время сократилось до 134 секунд, из них 107 - на cursor.next, в которых один и тот же объект достаётся минимум дважды. Есть куда оптимизировать.

siberiano, стесняюсь спросить, с монгой никогда не работал - а while bad_neighbors: не работает, или не подходит по контексту?

Я написал плагин к Osmosis. После некоторого размышления я изменил структуру базы, убрав оттуда обратные ссылки (node → way, way → relation, relation → relation). Слишком большой оверхед при импорте получался.

Пока получается такая статистика:
Импорт Новосибирской области (с gis-lab.info) занимает 84 секунды из которых 10 уходит на создание нужных индексов. Машина – одноядерный Athlon 3200.


> db.stats()
{
    "db" : "osm",
    "collections" : 6,
    "objects" : 967076,
    "avgObjSize" : 62.81341693930984,
    "dataSize" : 60745348,
    "storageSize" : 109661952,
    "numExtents" : 21,
    "indexes" : 6,
    "indexSize" : 67698688,
    "fileSize" : 469762048,
    "ok" : 1
}

Места действительно жрет как слон веники. Как это можно оптимизировать, не представляю.

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

А my_cursor всегда переводится в True, даже если в нём пусто:
In [21]: x = db.routes.find({‘_id’: -1})
In [22]: 1 if x else 0
Out[22]: 1

Он оставляет место под расширения документов, чтобы если начнёшь писать в них новые свойства, не пришлось раскидывать кластерами по файлу.

Если ты что-то удалил из базы, чтобы сжать, нужно $ mongod --repair --dbpath=…

А какие индексы делаешь? Я для того чтобы сделать дорожный граф, создал единственный индекс - tags.highway.

Сейчас после небольшой работы с монго я пришёл к такой схеме:

  1. исходную базу делать с минимумом информации, и в объекты ничего лишнего не писать, а лучше даже удалять лишнее сразу при импорте.

  2. никаких рабочих вычислений на этой базе делать нельзя. Для предметной области нужно делать отдельную БД, и туда экспортировать объекты уже в том виде, в котором нужно.

  3. Лучше держать немного больших документов, чем много мелких. Будет оптимальнее использоваться диск (больше реальных данных, меньше доля “заделов”). То есть нужно всё денормализовывать, чтобы не делать "join"ов. Чем меньше раз запрашиваешь курсор, тем лучше. Напротив, атомарная запись или запись одного документа в mongo очень быстрая, ею можно пользоваться часто.

  4. потоковая конвертация с mongo - тормозная. Если есть возможность, лучше всё сразу считать в память, потом формировать объекты, какие нужно, и записывать. У меня размер коллекции в базе - 87 МБ, в памяти Питона те же объекты занимают 9 МБ.

Примерно к тем же выводам я и пришел. У меня две базы. Одна под минутную репликацию, вторая под геометрию. В основной базе три коллекции примерно такой структуры:


Nodes:
{ 
    "_id" : NumberLong(32521222), 
    "lat" : 54.7724042, 
    "lon" : 83.1009863, 
    "tags": {"amenity": "parking"} 
}

Ways:
{ 
    "_id" : NumberLong(17237995), 
    "nodes" : [NumberLong(178727045), NumberLong(269537100)], 
    "tags" : { "highway" : "primary"}, 
}

Relations:
{ 
    "_id" : NumberLong(129298), 
    "members" : [{
        "ref" : NumberLong(23363997),
        "type" : "way",
        "role" : "to"
    }, {
        "ref" : NumberLong(35133693),
        "type" : "way",
        "role" : "from"
    }, {
        "ref" : NumberLong(248503088),
        "type" : "node",
        "role" : "via"
    }], 
    "tags" : { "restriction" : "no_left_turn", "type" : "restriction" } 
}

Индексы на полях ways.nodes и (relations.members.type, relations.members.ref). Без индексов не получится отслеживать изменения геометрии при изменении объекта. Что отсюда можно выкинуть, не представляю.

Основная проблема у меня в том, что для того чтобы построить геометрию, скажем, для вея, нужны координаты его точек, а это довольно тяжелая выборка. Выгрузить все ноды в память тоже не вариант. Для РФ они занимают 1Гб только в бинарном виде.