Geospatial

20/10/2013

Последние пару месяцев я работаю над HTTP API одного продукта, за это время мне пришлось познакомиться с софтом для работы с геопространственными индексами. В этой статье хочу поделиться опытом, болью и решениями, полученными за этот короткий срок.

В блогозаписи я рассмотрю:

  • PostgreSQL + PostGIS
  • CouchDB + GeoCouch
  • Elasticsearch

В список не попал Sphinx - потому что разработчики как-то не особо продвигают работу с пространственными индексами в своем продукте, о его возможностях мне сказали когда я уже рефакторил код для Elasticsearch. Эту часть адвенчуры можете оставить на себя, а еще лучше – отпишитесь в комментариях, как оно?

О других я не знаю, если что-то есть - кидайте, если это не mongodb.

Все это дело должно хорошо интегрироваться в Ruby и Rails, иметь хороший API и одну жизненно важную фичу, о которой я расскажу в самом конце.

PostgreSQL + PostGIS

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

Для интеграции в Ruby есть RGeo, умеет парсить wkb/wkt, знает о точках, полигонах и прочих примитивах. С документаций все очень плохо, в исходники лучше не смотреть, лично у меня глаза кровью наливаются от такого кода.

С Rails все несколько сложнее, мы возвращаемся к plain sql миграциям или с продаем душу дьяволу и используем activerecord-postgis-adapter от того же автора, на улучшение качества кода даже не надейтесь. Серьезно, не смотрите в сорцы, поберегите нервы.

SQL. Не, SQL это круто, гибко, но в наш use case никак не подходит, об этом позже.

CouchDB + GeoCouch

Я до сих пор не понимаю почему CouchDB обделен вниманием, очень хороший продукт, написан на Erlang, имеет простой HTTP API. Легко поставить, настроить и использовать.

Из примитивов есть только bbox - поиск координат в квадрате. Для работы достаточно взять REST клиент и написать немного кода для вашей бизнес логики.

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

Elasticsearch

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

Предоставляет HTTP API для поиска и работы с данными, для интеграции в Rails есть retire и обертка searchkick (кто боится разбираться в retire). Прекрасно интегрируется в Rails, но есть проблема с тестированием - после создания записей он начинает добавлять данные в эластик, и, как я полагаю долго создает индексы, в любом случае конец создания записи не означает что по этой записи можно искать. Решил проблему как лох, в самом начале тестирования поиска создаю все записи и делаю sleep 1, грустно? Вот и мне грустно. Решения найти не смог, а может неправильно искал, буду рад помощи.

Из гео поиска есть поиск по квадрату и окружности, а больше нам и не надо.

А теперь задачка

Есть карта, на ней отображаются измерения. Чтобы измерения не складывались в одну кучу, а показывались распределенно по карте, мы разделяем видимую территорию на квадраты и для каждого получаем точки. В этом случае мы генерируем кучу запросов к базе данных. За один запрос за точками раньше требовалось ждать 1-2 секунды, сейчас не более 200мс. Все это благодаря Multi Search API, таким образом мы генерируем эту кучу запросов и пачкой посылаем поисковику, тем самым убивая оверхед http.

Правда для нашей задачи пришлось таки немного согрешить (код демонстрации ради):

def build_query
  Tire.search 'index_name' do
    # your filters...
  end
end

def msearch(queries)
  search = Tire.multi_search('index_name')
  search.instance_variable_set(:@searches, queries) # holy shiet
  search.results
end

queries = (0..10).map do
   build_query
end

msearch(queries)

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

UPD.

Нашел таки прекрасное решение, в котором удалось отказаться от Elasticsearch и использовать только PostgreSQL + PostGIS.

Эта моя задача называется geo clustering и решается одним запросом в базу:

SELECT
  array_agg(id) AS ids
FROM
  "table"
GROUP BY
  ST_SnapToGrid(location, 0.01, 0.01)

Функция ST_SnapToGrid делит карту на квадраты и группирует точки в каждом из них, в результате мы получаем список квадратов в каждом из которых лежит список точек в этом квадрате, дальше все просто. У меня все это выглядит примерно так:

def clusterize(take = 2, x1, y1, x2, y2)
  select('array_agg(id) AS ids')
    .where("location && ST_MakeEnvelope(#{x1}, #{y1}, #{x2}, #{y2}, #{SRID})")
    .group("ST_SnapToGrid(location, 0.01, 0.01)")
    .map(&:ids).map { |points| points.sample(take) }
    .flatten
end