ArangoDB гибридная (документная и графовая) база данных. К ее положительным сторонам относятся:


  • мощный и удобный язык запросов AQL
  • JOIN (даже более мощный чем в реляционных базах данных)
  • репликация и шардинг
  • ACID (в кластере работает только в платной версии)

Из менее существенных, но не менее удобных возможностей:


  • нечеткий поиск
  • встроенный в базу данных движок микросервисов Foxx
  • работа в режиме подписки на изменения в базе данных

Справедливости ради отмечу и недостатки:


  • отсутствие ODM
  • низкая популярность (в сравнении например с MongoDB)

После анализа возможностей ArangoDB и, в особенности, после преодоления в последних версиях недостатков (таких как резкое падение производительности при превышении размера коллекции доступной оперативной памяти) и появлении новых возможностей (таких как нечеткий поиск) — пришло время испытаний в реальном приложении.


Возможности AQL (ArangoDB Query Language)


Один из главных вопросов, который меня волновал, будет ли выразительность AQL достаточной для выполнения всего спектра запросов в реальном приложении. И будет ли работа без ORM/ODM достаточно комфортной.


В ArangoDB есть несколько способов сделать запрос к данным. Есть привычный для тех, кто работает с MongoDB, объектно-ориентированный API, но такой способ в ArangoDB считается устаревшим и основной упор делается на запросы AQL.


Простейший запрос к одной коллекции выглядит так:


db.query({
  query: `for doc in managers
    filter doc.role == @role
    sort doc.@field @order
    limit @page * @perPage, @perPage
    return doc`,
  bindVars: { role, page, perPage, field, order },
});

Такой вот интересный язык запросов, построенный на ключевом слове FOR, которое в данном случае не означает перебор всех документов коллекции, если, конечно, по полю role создан индекс.


В большинстве случаев, для работы приложения нужно выбрать связанные объекты из нескольких коллекций. В библиотеке mongoose (MongoDB) для этого используют метод populate(). В ArangoDB это можно сделать одним запросом AQL:


db.query({
   query: `
      for mall in malls
        for city in cities
          filter mall.cityId == city._key
      return merge(mall, { city })
  `,
  bindVars: { },
});

Это типичный INNER JOIN. Только немного удобнее, так как объект city будет присутствовать в виде вложенного объекта, а не сольётся в список полей, как это происходит в стандартном SQL.


Что касается LEFT JOIN — для его реализации нужно использовать подзапросы и ключевое слово LET:


db.query({
  query: `
    for city in cities
      let malls=(
        for mall in malls
          filter mall.cityId==city._key
          return mall
      )
    return merge(city, {malls})`,
  bindVars: { },
});

Результирующий объект будет содержать поле malls типа array или значение null. Как Вы можете заметить, есть отличие от LEFT JOIN в стандартном SQL — это то, что количество объектов в результирующей коллекции будет равно количеству объектов в коллекции city, и не будет повторяться для каждого значение mall. Вместо этого mall представлено массивом. Я бы сказал, что такой вариант даже более удобен для работы. Получить же "классический" результат, как в SQL, также можно, но запрос будет более сложный.


Я привел самые простые запросы из тех, что есть в реальном приложении. Но, как выяснилось, на базе вышеперечисленных средств можно строить самые сложные запросы, которые не менее, а может быть и более выразительны, чем запросы SQL. При этом умолчу о других документо-ориентированных NoSQL базах данных, где аналогичные запросы просто невозможны.


Графы


Для реализации граф-ориентированных возможностей в ArangoDB применяются коллекции ребер графа. Документы в этой коллекции отличаются от документов в простых коллекциях наличием двух служебных полей: _from и _to. Работать с коллекциями ребер графом можно теми же средствами, что и с коллекциями документов. В дополнение существует несколько специальных средств для обходя графов.


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


Нечеткий поиск


Есть такая часто встречающаяся задача: искать текст, если в исходной строке есть опечатки или ошибки. Как правило, для этого используется база данных Elacticsearch. У такого решения есть два недостатка. Во-первых, нужно согласовывать в режиме реального времени значения в основной базе данных и в Elasticsearch. Это непросто, часто эти значения расходятся, и тогда приходится принудительно переиндексировать базу данных. И, во-вторых, Elasticsearch требовательна по ресурсам, что также не всегда приемлемо из соображений финансового порядка.


В последних версиях ArangoDB можно создать SEARCH VIEW в котором можно искать значения с неполным совпадением:


  await db.createAnalyzer('fuzzy_brand_search_bigram', {
    type: 'ngram',
    properties: { min: 2, max: 2, preserveOriginal: true },
    features: ['position', 'frequency', 'norm'],
  });
  await db.createView('brandSearch', {
    links: {
      brands: {
        includeAllFields: true,
        analyzers: ['fuzzy_brand_search_bigram'],
      },
    },
  });

Сам запрос выглядит так:


db.query({
    query: `
       for brand in brandSearch
          search NGRAM_MATCH(
              brand.name, 
              @brandName, 
              0.4, 
              'fuzzy_brand_search_bigram'
          )
          filter brand.mallId == @mallId
        return brand `,
    bindVars: { mallId, brandName },
});

Без ODM?


В своей статье я показал, что по статистике, MongoDB в половине случаев используется без ODM. То есть, это достаточно распространенная практика.


Действительно, сделать запрос, как это было показано выше, гораздо проще средствами AQL, чем определять схему с разными видами связей. Во всяком случае, не было еще ни одного проекта на Sequelize (ORM для реляционных баз данных), где не пришлось бы сделать один-два RAW запроса.


Однако, я, тем не менее, сторонник использования ODM. В своей статье я описал, что я хотел бы от ODM для ArangoDB. ODM не обязательно должна заниматься генерацией запросов в базу данных. Я бы хотел, чтобы ODM обеспечивала сохранение в базу данных только нужных полей, и следила за наличием обязательных полей. А при получении объекта из базы данных типизировала его, добавляла вычислимые поля, фильтровала набор полей для разных групп запросов, и обеспечивала локализацию значений полей.


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


В своей статье я описал и реализовал свою библиотеку. Её и использовал в реальном проекте. Конечно, были моменты стресса, когда выходило, что возможностей этой библиотеки недостаточно. Но их в основном удалось разрешить. Так что по-прежнему приглашаю к сотрудничеству желающих продвигать технологию ArangoDB.


apapacy@gmail.com
15 марта 2021 года