• XSS.stack #1 – первый литературный журнал от юзеров форума

Статья Разворачиваем API для прямого и обратного геокодирования (Elasticsearch, OpenStreetMap)

0mb

HDD-drive
Пользователь
Регистрация
28.01.2021
Сообщения
48
Реакции
58
Статья будет полезна тем, кто желает немного обогатить имеющиеся геоданные:
  • добавить поле с координатой при наличии города-района-улицы-дома-корпуса
  • или, наоборот, по координате получить строку с адресом.
1612157921906.png
1612158017844.png
1612158096350.png


Однажды мне понадобился гео-API для нахождения координаты по строке с адресом, и сделать свой для меня стало целесообразно из-за того, что попадавшиеся мне готовые решения по тем или иным причинам не подходили - регистрация, оплата, ограничения на количество запросов в минуту/час/сутки и т.д. + обработка данных с внешними сервисами займет значительно больше времени при большом количестве записей, т.к. зачастую время выполнения запроса с внешними сервисами больше, чем с локальными.

Оговорюсь, что исходные адресные данные у меня были разделены по полям "район", "улица", "дом", и в процессе своей работы меня интересовали данные только в рамках одного города - СПб, и для примера здесь я буду использовать данные по СПб.
Вероятно, для Москвы подобное делать будет немного подольше ввиду более сложного административного деления. По остальным населенным пунктам не могу сказать.
Также, стоит заметить, что сообщество OSM не очень активно (РУ особенно, по ощущениям), и поэтому информация в OSM не всегда достаточно актуальная, иногда старые дома могут годами оставаться неразмеченными на карте, не говоря уже новых.

Как вы уже догадались, нам понадобится Linux-сервер с установленным Elasticsearch. Также надо установить Kibana для визуализаций и для Dev Tools с запросником. Нужно установить пакет jq.
Дальше мы возьмем информацию из OSM и обработаем ее, чтобы пользоваться ей локально.


1) Получим информацию о домах из OSM
Мы сможем это сделать с помощью специального Overpass API, который позволяет за раз выгружать много данных, удовлетворяющих запросу.
Почитать про Overpass API: https://wiki.openstreetmap.org/wiki/RU:Overpass_API
Попробовать сделать запрос: https://overpass-turbo.eu/
Bash:
#!/bin/bash

mkdir ./json 2>/dev/null
curl 'https://overpass-api.de/api/interpreter' -H 'Content-Type: application/x-www-form-urlencoded; charset=UTF-8' --data-raw 'data=[out:json][bbox:59.4,29.1,60.4,31.6];(node[building];relation[building];way[building];);out+center;' >./json/osm_spb_buildings.json
[out:json] -вывод в формате json
[bbox:59.4,29.1,60.4,31.6]; - рамка из которой будут извлекаться объекты (последовательность координат: минимальная широта, минимальная долгота, максимальная широта, максимальная долгота).
(node[building];relation[building];way[building]; ) - указывает, что будут возвращены все типы базовых элементов (точки, линии, отношения), обозначающие здания.
out+center; - кроме стандартной информации вернется координата центра каждого здания.
Немного подождав мы получим файл osm_spb_buildings.json примерно такого содержания:
1612145681057.png

2) Приводим файл в удобоваримое для Elasticsearch состояние, попутно делая пару улучшений.
Чуть позже с помощью Bulk API мы импортируем сразу все здания одним запросом. Иначе, делая >100к запросов время импорта будет значительно больше.
Сделаем из полученного в предыдущем пункте файла новый файл, который будет содержать в себе множество запросов на добавление зданий.
Почитать про Bulk API: https://www.elastic.co/guide/en/elasticsearch/reference/current/docs-bulk.html
Bash:
#!/bin/bash

IFS=$'\n'

if [ "$#" -eq  "1" ]
then

    mkdir ./bulk-json 2>/dev/null

    json_path=$1
    bulk_json_path=./bulk-json/$(echo $json_path | rev | cut -d'/' -f 1 | rev).bulk.json

    echo ""
    echo "input: $json_path"
    echo "output: $bulk_json_path"
    echo ""

    for row in $(cat $json_path | jq -c '.elements[] | select(.tags."addr:housenumber" != null) | select(.tags."addr:street" != null) | select(.center != null) | {id} + {center} + (.tags|{"addr:street"}) + (.tags|{"addr:housenumber"}) + (.tags|{"addr:letter"}) + (.tags|{"addr:city"}) + (.tags|{"addr:suburb"}) + (.tags|{"addr:region"}) + (.tags|{"addr:place"}) + (.tags|{"addr:district"}) + (.tags|{"addr:subdistrict"}) + (.tags|{"addr:province"}) | with_entries( select( .value != null ) )')
    do

        echo '{ "index":{} }' >>$bulk_json_path
        echo $row >>$bulk_json_path

    done

    sed -i -e 's/"addr:/"/g' $bulk_json_path

    sed -i -e 's/Ё/Е/g' $bulk_json_path
    sed -i -e 's/ё/е/g' $bulk_json_path

else
    echo "1 argument expected"
fi
Скрипт в аргументе принимает путь до файла, полученного в предыдущем пункте, после отработки в каталоге ./bulk-json появляется файл, пригодный для импорта в Elasticsearch.
Каждый объект выравнивается в строку, далее остаются только объекты с заполненными полями street+housenumber+center, выводятся эти и еще некоторые поля, удаляются пустые поля.
Перед каждым объектом добавляется "{ "index":{} }"
Из названий полей удаляется бесполезное для нас "addr:"
Все "ё" меняются на "е" в целях улучшения поиска, т.к. часто в исходных данных могут встречаться разные написания улиц, прим.: "Берёзовая/Березовая" улица. (в исходных данных было проделано то же: ё -> е)
После ./json-to-bulk-json.sh ./json/osm_spb_buildings.json на выходе у нас будет файл ./bulk-json/osm_spb_buildings.json.bulk.json:
1612148417022.png

3) Создаем маппинг в Elasticsearch.
Перед добавлением данных лучше всегда задавать маппинг, т.к. анализатор Elasticsearch почти всегда не угадывает тип поля и проставляет по-умолчанию тип "text".
В нашем случае, самое главное - заранее назначить полю "center" тип "geo_point".
Bash:
#!/bin/bash

elasticsearch_host=0.0.0.0

curl -X PUT "$elasticsearch_host:9200/osm_spb_buildings" | jq '.'
curl -X PUT "$elasticsearch_host:9200/osm_spb_buildings/_mapping/" -H 'Content-Type: application/json' -d'{
    "properties": {
        "center": {
            "type": "geo_point"
        }
    }
}' | jq '.'

4) Импортируем здания.
(./bulk-json-to-elasticsearch.sh ./bulk-json/osm_spb_buildings.json.bulk.json)
Bash:
#!/bin/bash
#set -x

IFS=$'\n'

elasticsearch_host=0.0.0.0

bulk_json_path=$1

if [ "$#" -eq  "1" ]
then

    echo ""
    echo "file: $(echo $bulk_json_path | rev | cut -d'/' -f 1 | rev)"
    echo ""

    printf "\n\n$(date --rfc-3339=seconds)\n$bulk_json_path\n" >> import.log
    curl -s -XPOST $elasticsearch_host:9200/osm_spb_buildings/_bulk -H 'Content-Type: application/json' --data-binary @$bulk_json_path | jq '.' >> import.log

else
    echo "1 argument expected"
fi
Теперь надо подождать, когда закончится отправка запроса, и потом дождаться завершения индексации.
В Kibana, в разделе "Stack Management - Index Management" можно увидеть количество записей в столбеце "Docs count" у индекса "osm_spb_buildings".

5) Заполняем у домов поле с названием района.
Порядок действий: создаем маппинг индекса с границами районов -> заполняем этот индекс -> для всех домов, входящих в границы каждого района заполняем поле "district" (соответствующее название района) и "region" (Санкт-Петербург).
id районов взял из OSM Вики: https://wiki.openstreetmap.org/wiki/RU:Санкт-Петербург/Районы
И доставал границы такими запросами, подставив id: https://nominatim.openstreetmap.org...R&osmid=1114193&polygon_geojson=1&format=json

Bash:
#!/bin/bash

elasticsearch_host=0.0.0.0

curl -X PUT "$elasticsearch_host:9200/osm_spb_districts" | jq '.'

curl -X PUT "$elasticsearch_host:9200/osm_spb_districts/_mapping/" -H 'Content-Type: application/json' -d'{
    "properties": {
        "geometry": {
            "type": "geo_shape"
        }
    }
}' | jq '.'

echo Адмиралтейский
curl -s -XPOST $elasticsearch_host:9200/osm_spb_districts/_doc -H 'Content-Type: application/json' -d'
{ "name": "Адмиралтейский район","geometry": { "type": "Polygon","coordinates":[[[30.2502295,59.9012907],[30.2511227,59.9010298],[30.2529959,59.9004065],[30.2536453,59.9002229],[30.254016,59.9001698],[30.2545032,59.9002996],[30.2550207,59.9004425],[30.2556283,59.9005558],[30.2561513,59.9005716],[30.2570052,59.900495],[30.2575727,59.9003963],[30.2611487,59.8998644],[30.2621002,59.8998911],[30.262968,59.900082],[30.2646122,59.9005706],[30.2650538,59.90096],[30.2657085,59.901819],[30.26615,59.9019564],[30.2666828,59.901964],[30.2676695,59.9020115],[30.2681268,59.902092],[30.2686119,59.9022005],[30.2689758,59.9023113],[30.2690959,59.9024091],[30.269288,59.9023757],[30.2694215,59.9023548],[30.269548,59.9023345],[30.269637,59.9023163],[30.2703424,59.9021777],[30.2710687,59.9020349],[30.2713176,59.9019862],[30.271593,59.9019331],[30.2719329,59.9018665],[30.2719736,59.9018586],[30.2725453,59.9017448],[30.2727358,59.9017071],[30.2728677,59.9016808],[30.2729147,59.9017113],[30.272967,59.9017361],[30.2730218,59.9017564],[30.2730901,59.9017773],[30.2731614,59.9017919],[30.2732405,59.9018017],[30.2733141,59.9018049],[30.2733895,59.9018034],[30.2734591,59.9017971],[30.2735274,59.9017871],[30.2735963,59.9017715],[30.273656,59.9017526],[30.2737201,59.9017268],[30.273768,59.901696],[30.2738068,59.9016667],[30.2738418,59.9016322],[30.273872,59.9015835],[30.2738823,59.9015438],[30.2738776,59.9015061],[30.2739961,59.9015009],[30.2741066,59.9014956],[30.2742053,59.9014945],[30.2742969,59.9014986],[30.2743874,59.9015116],[30.2744755,59.9015275],[30.2745826,59.9015633],[30.2758289,59.901114],[30.2763414,59.9013598],[30.276802,59.9016308],[30.2767748,59.9016446],[30.2769779,59.9017701],[30.2771023,59.9018461],[30.27724,59.9019409],[30.2774615,59.9020959],[30.2776363,59.9022725],[30.2796349,59.9033556],[30.2800151,59.9035588],[30.2801824,59.9035934],[30.2803545,59.9036369],[30.2807081,59.9037477],[30.2809136,59.9037968],[30.2811581,59.9038415],[30.2814008,59.9038761],[30.2819152,59.9039237],[30.2821248,59.903935],[30.283617,59.9038043],[30.2845809,59.9037106],[30.2866383,59.9035471],[30.2890871,59.9033022],[30.2899085,59.9032431],[30.2899133,59.903178],[30.2905912,59.9031405],[30.2907441,59.9031252],[30.2908979,59.9031098],[30.2911294,59.9031296],[30.2920476,59.903078],[30.2927639,59.9029629],[30.2930904,59.9028945],[30.2931241,59.9029252],[30.2934545,59.9028647],[30.293935,59.902771],[30.2942298,59.9027354],[30.2942624,59.9027989],[30.2946082,59.9027544],[30.2946438,59.9027466],[30.2946088,59.9026776],[30.2947606,59.9025213],[30.2949666,59.9025689],[30.2950397,59.9025016],[30.295407,59.9026035],[30.2956998,59.9023201],[30.2956591,59.9023079],[30.2956415,59.902244],[30.2955046,59.9022039],[30.2955699,59.9021427],[30.2954743,59.902116],[30.2957213,59.9018932],[30.2957365,59.9018974],[30.2957903,59.9018642],[30.2958444,59.9017993],[30.2961957,59.9018731],[30.2962863,59.9018921],[30.2965218,59.9019645],[30.2966036,59.901988],[30.2966693,59.9019901],[30.2967364,59.9019578],[30.2968544,59.9019443],[30.2968799,59.9019373],[30.2968993,59.9019077],[30.2969395,59.9017012],[30.2970058,59.9008783],[30.2970343,59.9008787],[30.2970541,59.9005573],[30.2970287,59.9003672],[30.2970086,59.9002865],[30.2969563,59.900265],[30.2967994,59.8999717],[30.2967815,59.8999404],[30.2966638,59.8996998],[30.2966117,59.8995862],[30.2965069,59.8992927],[30.2965098,59.8991609],[30.2964566,59.8991603],[30.2963841,59.8991595],[30.2963556,59.8989089],[30.2961814,59.8986841],[30.2957445,59.8980067],[30.2952848,59.8972204],[30.2953505,59.8972104],[30.2951106,59.8967722],[30.2950022,59.8965579],[30.2951139,59.8965448],[30.2949784,59.8962945],[30.2947812,59.8961229],[30.2978907,59.8960308],[30.3001883,59.8961298],[30.3004041,59.8961338],[30.3007921,59.8961409],[30.3017053,59.8961576],[30.3026068,59.8961863],[30.3036287,59.8962009],[30.3038429,59.896204],[30.3039698,59.8962058],[30.3053317,59.8962252],[30.3053982,59.8962262],[30.3060814,59.8962359],[30.3067253,59.8962451],[30.3071072,59.8962506],[30.3074927,59.8962561],[30.3078111,59.8962606],[30.308691,59.8962732],[30.3093163,59.8962821],[30.3095353,59.8962853],[30.3098413,59.8962896],[30.30996,59.8962889],[30.3102009,59.8962901],[30.3103169,59.8962879],[30.3104101,59.896286],[30.3105069,59.8962809],[30.3106097,59.896267],[30.3129922,59.8962891],[30.3139128,59.8962977],[30.3140264,59.8963208],[30.3141313,59.896331],[30.31447,59.8963426],[30.3146793,59.8963525],[30.3149543,59.8963655],[30.3155086,59.8963854],[30.3166085,59.8963995],[30.3174361,59.8964056],[30.3184733,59.896413],[30.318643,59.8964135],[30.3186865,59.8964157],[30.3190301,59.896423],[30.3191434,59.8964256],[30.3191757,59.8964261],[30.3188175,59.9023867],[30.3186584,59.9027619],[30.3184023,59.9081949],[30.3183963,59.9085089],[30.3184052,59.9086376],[30.3195008,59.908642],[30.3201317,59.9086962],[30.3207753,59.9087756],[30.3219512,59.9091147],[30.3251527,59.9103327],[30.3325755,59.9130514],[30.3335212,59.9133749],[30.3394365,59.9155014],[30.3400733,59.9156876],[30.3401333,59.9161085],[30.3405231,59.9162781],[30.3407121,59.9163595],[30.340973,59.9164669],[30.3412284,59.9165592],[30.3421574,59.9168975],[30.3425304,59.9170392],[30.342732,59.9171158],[30.3425712,59.9172392],[30.3421995,59.9175534],[30.3420227,59.9177016],[30.3418488,59.9178448],[30.341555,59.9180866],[30.3413606,59.9182466],[30.3408572,59.918661],[30.340578,59.9188907],[30.3397238,59.9195937],[30.3396136,59.9196886],[30.3394833,59.9198042],[30.3393791,59.9198848],[30.338235,59.9208295],[30.3381155,59.9209282],[30.3379449,59.9210815],[30.337345,59.9215649],[30.3366892,59.922106],[30.3365825,59.922193],[30.3364684,59.9222873],[30.3362309,59.9224866],[30.3352309,59.9233162],[30.3357858,59.9235368],[30.3304534,59.9260735],[30.3300187,59.9262994],[30.3270334,59.925151],[30.3267011,59.9253692],[30.326572,59.925454],[30.318983,59.93053],[30.3178175,59.9313099],[30.3153107,59.9329953],[30.3118605,59.9353013],[30.3117762,59.9353581],[30.311539,59.9355176],[30.3115121,59.9355356],[30.3114609,59.9355701],[30.3113239,59.9356623],[30.3112298,59.9357255],[30.3109312,59.9359263],[30.3108416,59.9359866],[30.3105593,59.9361765],[30.3103119,59.9363429],[30.3102313,59.9363971],[30.3101295,59.9364655],[30.3103024,59.9366458],[30.3123914,59.9373876],[30.3124162,59.9374581],[30.3126331,59.9379477],[30.3126379,59.9380088],[30.3126288,59.9380613],[30.3126137,59.9381018],[30.3125887,59.9381449],[30.3125619,59.9381783],[30.3125203,59.9382183],[30.3124762,59.9382558],[30.3124315,59.9382896],[30.309989,59.9398884],[30.3090786,59.9405117],[30.308274,59.9410565],[30.2983909,59.9374942],[30.285014,59.9338202],[30.2817969,59.9330639],[30.2773869,59.9320271],[30.2724587,59.9287347],[30.2707173,59.9256064],[30.2690368,59.9225872],[30.2677802,59.9213203],[30.2653115,59.918831],[30.2624446,59.9173952],[30.2621175,59.9173546],[30.2604927,59.9172603],[30.261523,59.9169571],[30.2620661,59.9164361],[30.2621519,59.9158408],[30.2622007,59.9136743],[30.2619127,59.912359],[30.2609047,59.9107547],[30.2599408,59.9093491],[30.2576418,59.9068884],[30.2575405,59.9067851],[30.2560605,59.9049665],[30.2559255,59.9048007],[30.2553848,59.9041362],[30.2535127,59.9026841],[30.2502783,59.9013188],[30.2502295,59.9012907 ] ] ] } }
' | jq '.'

echo "Извините, остальные районы не влезли в текст топика =)"
sleep 123
Bash:
#!/bin/bash
#set -x


IFS=$'\n'


elasticsearch_host=0.0.0.0


districts="$(curl -s -XGET "http://$elasticsearch_host:9200/osm_spb_districts/_search" -H 'Content-Type: application/json' -d'{ "size": 1000,  "_source": "name",   "query": {    "match_all": {}  }}' | jq -c '.hits.hits[]')"


districts_size=$(echo "$districts" | wc -l)


echo ""
echo "districts: $districts_size"
echo ""
echo ""


for district in $districts
do


    district_id=$(echo $district | jq -r '._id')
    echo "id: $district_id"


    district_name=$(echo $district | jq -r '._source.name')
    echo "name: $district_name"


    curl -s -XPOST "http://$elasticsearch_host:9200/osm_spb_buildings/_update_by_query" -H 'Content-Type: application/json' -d'{
    "query": {
        "bool": {
            "filter": [
                {
                    "geo_shape": {
                        "center": {
                            "indexed_shape": {
                                "index": "osm_spb_districts",
                                "id": "'$district_id'",
                                "path": "geometry"
                            }
                        }
                    }
                }
            ]
        }
    },
    "script": {
        "inline": "ctx._source.district = \"'$district_name'\"; ctx._source.region = \"'Санкт-Петербург'\";",
        "lang": "painless"
    }
}' | jq '.'


    echo ""
    echo ""


done

Готово! =)

Теперь можно приступить к использованию.
Пример запроса по адресу:
1612156822637.png

Пример запроса по координате (distance - радиус поиска):
1612157474046.png
 


Напишите ответ...
  • Вставить:
Прикрепить файлы
Верх