Последние пару месяцев я работаю над 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