Поиск по содержимому JSON-документов

Поиск по JSON — это способ находить строки таблицы по содержимому JSON-документа, хранящегося в колонке типа Json или JsonDocument: по существованию пути в документе и по значению, расположенному в указанном пути. Путь задаётся выражением JsonPath, а проверки выполняются уже существующими функциями JSON_EXISTS и JSON_VALUE. Типичные сценарии:

  • поиск документов с определённым вложенным полем;
  • поиск документов, у которых поле по заданному пути равно нужному значению;
  • фильтрация полуструктурированных данных без заранее зафиксированной схемы.

В YDB поиск по JSON можно выполнять двумя основными способами:

  • без индекса — сканированием таблицы и применением функций JSON_EXISTS / JSON_VALUE к каждой строке. Подход прост, но плохо масштабируется: объём работы по сканированию растёт вместе с размером таблицы.
  • с JSON-индексом — созданием JSON-индекса по JSON-колонке. Этот подход предназначен для масштабируемого поиска.

Поиск по JSON с JSON-индексом

Ключевая идея JSON-индекса — построение инвертированного индекса по путям и парам «путь + значение». Каждый путь JSON-документа (а для проверок равенства — и скалярное значение по этому пути) кодируется в токен. Для каждого токена в индексе хранится список значений первичного ключа соответствующих строк таблицы. Запросы к индексу сводятся к инвертированному поиску по этим токенам — по той же схеме, что и у полнотекстового индекса, но с собственным токенизатором JSON.

JSON-индекс не хранит копию JSON-документа целиком, а раскладывает его на отдельные токены:

  • для каждого пути в JSON-дереве создаётся токен «путь существует»;
  • если путь ведёт к скалярному значению (строке, числу, логическому значению или null), дополнительно создаётся токен «путь + значение».

Массивы при этом прозрачны: элементы массива индексируются под тем же путём, что и сам массив, поэтому индекс одинаково отвечает на запросы к $.items, $.items[0] и $.items[*].

Например, для документа:

{
    "id": 42042,
    "name": "Michael",
    "email": null,
    "items": [null, "str"],
    "parts": {
        "key": "k1",
        "value": false
    }
}

концептуально индексируются следующие токены (без учёта специфики внутреннего представления):

Токен Что позволяет найти
$ документ не равен NULL
$.id / $.id == 42042 путь id существует / равен 42042
$.name / $.name == "Michael" путь name существует / равен "Michael"
$.email / $.email == null путь email существует / равен null
$.items / $.items == null / $.items == "str" путь items существует / содержит элемент null / содержит элемент "str"
$.parts путь parts существует
$.parts.key / $.parts.key == "k1" вложенный путь существует / равен "k1"
$.parts.value / $.parts.value == false вложенный путь существует / равен false

JSON-индекс позволяет:

  • находить строки по существованию пути через JSON_EXISTS;
  • находить строки по значению в пути через JSON_VALUE или фильтр-предикат внутри JSON_EXISTS.

При выполнении запроса JSON-индекс может быть автоматически задействован оптимизатором. Достаточно написать обычное условие WHERE с выражениями JSON_EXISTS / JSON_VALUE по индексированной JSON-колонке — оптимизатор распознает такой предикат и применит чтение по JSON-индексу вместо перебора записей исходной таблицы. Кроме того, JSON-индекс, как и любой другой вторичный индекс, можно принудительно задействовать, указав его имя в секции VIEW IndexName.

Если предикат не удаётся превратить в обращение к индексу, поведение YDB зависит от того, был ли в запросе явно указан требуемый JSON-индекс:

  • при автоматическом выборе индекса оптимизатором запрос просто выполняется без ускорения индексом (результат остаётся корректным);
  • при явном указании индекса через выражение VIEW возвращается ошибка.

Подробнее см. VIEW (JSON-индекс).

Дополнительная информация: