Частичное обновление документов в Solr

Обзор

Solr предоставляет три основных метода для частичного обновления данных в индексе. Данная статья описывает использование каждого метода с примерами, а именно:

  • Атомарные обновления (atomic updates). Позволяют изменять одно или несколько полей документа путем переиндексирования всего документа.

  • Обновления типа in-place (in-place updates). Подвид атомарных обновлений, позволяющие модифицировать числовые поля без переиндексирования всего документа.

  • Метод оптимистичного параллелизма (optimistic concurrency). Позволяет выполнять обновления по условию, используя версию документа.

В этой статье примеры операций обновления предназначены для изменения следующего тестового документа:

{
  "id": 1,
  "post_name": "Sample blog post...",
  "categories": ["leisure", "hobby"],
  "post_rank": 75.0,
  "post_date": "2024-01-02",
  "post_text": "Lorem ipsum dolor sit amet ...",
  "description": "A sample post for testing purposes"
}

Данный документ хранится в индексе Solr в коллекции test_collection. Информация о добавлении документа в индекс доступна в разделе Обзор работы с индексами в Solr.

Атомарные обновления

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

Solr поддерживает несколько модификаторов, которые используются для обновления отдельных полей документа. Эти модификаторы представлены ниже:

  • set — устанавливает новое или перезаписывает существующее значение поля. Удаляет значение, если указан null или пустой список. Принимает одно значение или список.

  • add — добавляет указанное значение(я) в поле типа multiValued. Принимает одно значение или список.

  • add-distinct — добавляет указанное значение(я) в поле типа multiValued, если указанное значение не присутствует в индексе. Принимает одно значение или список.

  • remove — удаляет все совпадения указанного значения из поля типа multiValued. Принимает одно значение или список.

  • removeregex — удаляет все совпадения, соответствующие регулярному выражению, из поля типа multiValued. Принимает одно значение или список.

  • inc — увеличивает числовое поле на заданное значение. Принимает одно числовое значение.

Чтобы обновить документ, укажите соответствующий модификатор в качестве значения обновляемого поля и загрузите документ в Solr. Следующий пример обновляет сразу несколько полей тестового документа:

{
  "id": 1,
  "post_name": {"set": "Updated post name ..."},
  "categories": {"add": ["science"]},
  "post_rank": {"inc": 5},
  "post_date": "2024-01-02",
  "post_text": "Lorem ipsum dolor sit amet ...",
  "description": {"removeregex": ".*testing.*"}
}
Команда загрузки с помощью curl
$ curl -X POST 'http://ka-adh-1.ru-central1.internal:8983/solr/test_collection/update?commit=true' -H 'Content-Type: application/json' --data-binary '[{
  "id": 1,
  "post_name": {"set": "Updated post name ..."},
  "categories": {"add": ["science"]},
  "post_rank": {"inc": 5},
  "post_date": "2024-01-02",
  "post_text": "Lorem ipsum dolor sit amet ...",
  "description": {"removeregex": ".*testing.*"}
}]'

Когда Solr получает такой документ, он распознает выражения-модификаторы и обновляет соответствующие поля документа. После обновления документ имеет следующий вид:

{
  "responseHeader":{
    "zkConnected":true,
    "status":0,
    "QTime":0,
    "params":{
      "q":"*:*",
      "indent":"true",
      "q.op":"OR",
      "_":"1726172218281"}},
  "response":{"numFound":1,"start":0,"numFoundExact":true,"docs":[
      {
        "id":"1",
        "post_name":["Updated post name ..."],
        "categories":["leisure",
          "hobby",
          "science"],
        "post_rank":[80.0],
        "post_date":["2024-01-02T00:00:00Z"],
        "post_text":["Lorem ipsum dolor sit amet ..."],
        "_version_":1810022770405277696}]
  }}

Обновление вложенных документов

Solr позволяет изменять, добавлять и удалять вложенные документы с помощью операций атомарного обновления. Процесс очень похож на обновление обычных (невложенных) документов, а основные особенности, которые следует учитывать, заключаются в следующем:

  • Каждая операция по обновлению вложенного объекта должна быть направлена в нужный шард Solr — тот, в котором хранится родительский документ. Поскольку Solr определяет целевой шард на основе ID документа, важно использовать ID корневого (root) документа, а не ID обновляемого вложенного документа. Для выполнения этого требования можно либо явно указать Solr-роутер (через параметр _route_), либо использовать compositeId router (используется по умолчанию) для направления операций обновления в нужный шард. Больше информации о правилах маршрутизации при обновлении вложенных документов доступно в документации Solr.

  • При обновлении вложенных объектов необходимо проинформировать Solr, какой документ является родительским для обновляемого объекта. Это можно сделать, указав поле _root_ в запросе на обновление.

  • По умолчанию Solr пытается применить обновления ко всему дереву вложенных документов вместо того, чтобы изменять отдельные объекты. Это может приводить к дополнительной нагрузке.

Предположим, что имеется следующий документ с вложенными объектами-комментариями, который необходимо обновить:

{
    "id": "post2",
    "name_s": "Another demo post...",
    "text_t": "Foo Bar Buzz ....",
    "content_type": "post",
    "comments":
    [
        {
            "id": "post2!comment3",
            "author_s": "Luke",
            "text_t": "I like this post!",
            "rated_i": 4,
            "content_type": "comment"
        }
    ]
}

Например, чтобы увеличить числовое поле rated_i, используя атомарные обновления, необходимо загрузить следующий документ в Solr:

{
  "id": "post2!comment3", (1)
  "_root_": "post2", (2)
  "rated_i": { "inc": 1 }
}
1 ID вложенного документа, который необходимо обновить. Обратите внимание, что ID является составным (состоит из частей, разделенных символом !). Использование составного ID позволяет стандартному compositeId router направить операцию обновления в соответствующий шард Solr.
2 Поле _root_ является обязательным. Оно информирует Solr о том, какой документ является родительским по отношению к обновляемому объекту. Если поле не указано, Solr отклонит операцию обновления.

Результат обновления:

{
  "responseHeader":{
    "zkConnected":true,
    "status":0,
    "QTime":0,
    "params":{
      "q":"*:*",
      "indent":"true",
      "q.op":"OR",
      "_":"1726172218281"}},
  "response":{"numFound":2,"start":0,"numFoundExact":true,"docs":[
      {
        "id":"post2!comment3",
        "author_s":"Luke",
        "text_t":"I like this post!",
        "rated_i":5,
        "content_type":["comment"],
        "_version_":1810022942789074944},
      {
        "id":"post2",
        "name_s":"Another demo post...",
        "text_t":"Foo Bar Buzz ....",
        "content_type":["post"],
        "_version_":1810022942789074944}]
  }}
Пример: добавление вложенных документов

 
Загрузка следующего документа в Solr добавляет еще один вложенный объект к родительскому документу ("id": "post2"):

{
  "id": "post2",
  "comments": { "add": { "id": "post2!comment4",
                        "author_s": "Rick Sanchez",
                        "text_t": "Another added comment here..",
                        "rated_i": 5,
                        "content_type": "comment"
                      } }
}

Результат:

{
  "responseHeader":{
    "zkConnected":true,
    "status":0,
    "QTime":0,
    "params":{
      "q":"*:*",
      "indent":"true",
      "q.op":"OR",
      "_":"1726172218281"}},
  "response":{"numFound":3,"start":0,"numFoundExact":true,"docs":[
      {
        "id":"post2!comment3",
        "author_s":"Luke",
        "text_t":"I like this post!",
        "rated_i":5,
        "content_type":["comment"],
        "_version_":1810023009958756352},
      {
        "id":"post2!comment4",
        "author_s":"Rick Sanchez",
        "text_t":"Another added comment here..",
        "rated_i":5,
        "content_type":["comment"],
        "_version_":1810023009958756352},
      {
        "id":"post2",
        "name_s":"Another demo post...",
        "text_t":"Foo Bar Buzz ....",
        "content_type":["post"],
        "_version_":1810023009958756352}]
  }}
Пример: удаление вложенных документов

 
Отправка следующего документа в Solr удаляет вложенный документ по ID:

{
    "id": "post2",
    "comments": {
        "remove": {
            "id": "post2!comment4"
        }
    }
}

Результат:

{
  "responseHeader":{
    "zkConnected":true,
    "status":0,
    "QTime":0,
    "params":{
      "q":"*:*",
      "indent":"true",
      "q.op":"OR",
      "_":"1726172218281"}},
  "response":{"numFound":2,"start":0,"numFoundExact":true,"docs":[
      {
        "id":"post2!comment3",
        "author_s":"Luke",
        "text_t":"I like this post!",
        "rated_i":5,
        "content_type":["comment"],
        "_version_":1810023111907606528},
      {
        "id":"post2",
        "name_s":"Another demo post...",
        "text_t":"Foo Bar Buzz ....",
        "content_type":["post"],
        "_version_":1810023111907606528}]
  }}

Обновления типа in-place

Обновления типа in-place (in-place updates) являются подмножеством атомарных обновлений. Однако при обычном атомарном обновлении под капотом происходит переиндексация всего документа. При обновлениях типа in-place изменяются только необходимые поля, а остальная часть документа остается нетронутой. Таким образом, эффективность in-place обновлений не зависит от количества полей в обновляемом документе. Кроме внутренних особенностей имплементации, влияющих на производительность, между атомарными и in-place обновлениями нет особых функциональных различий.

Операция обновления может быть выполнена как обновление типа in-place, если обновляемые поля отвечают следующим требованиям:

  • Обновляемые поля являются числовыми и имеют следующие свойства: indexed="false", stored="false", multiValued="false",docValues="true".

  • В обновляемом документе присутствует поле _version_, которое имеет свойства: indexed="false", stored="false", multiValued="false", docValues="true".

  • CopyField destination обновляемого поля (если таковые используются) является числовым и имеет следующие свойства: indexed="false", stored="false", multiValued="false", docValues="true".

Для обновлений типа in-place Solr поддерживает следующие модификаторы:

  • set — устанавливает новое или перезаписывает существующее значение поля.

  • inc — увеличивает числовое поле на определенное значение.

Чтобы обновить числовое поле документа с помощью метода in-place, соответствующее поле должно быть определено в схеме Solr. Например:

<field name="post_rank" type="float" indexed="false" stored="false" docValues="true"/>

Чтобы обновить тестовый документ, используя подход in-place, загрузите в Solr следующий документ:

{
    "id": 1,
    "post_rank": {
        "set": 100
    }
}

В этом случае документ будет изменен с помощью стратегии обновления in-place. Результаты операции обновления:

{
  "responseHeader":{
    "zkConnected":true,
    "status":0,
    "QTime":0,
    "params":{
      "q":"*:*",
      "indent":"true",
      "q.op":"OR",
      "_":"1726172218281"}},
  "response":{"numFound":1,"start":0,"numFoundExact":true,"docs":[
      {
        "id":"1",
        "post_name":["Updated post name ..."],
        "categories":["leisure",
          "hobby",
          "science"],
        "post_rank":[100.0],
        "post_date":["2024-01-02T00:00:00Z"],
        "post_text":["Lorem ipsum dolor sit amet ..."],
        "_version_":1810023165979525120}]
  }}

Метод оптимистичного параллелизма

Обновление документов с помощью метода оптимистичного параллелизма (optimistic concurrency) — это функция Solr, которая гарантирует, что один и тот же документ не будет изменен несколькими клиентскими приложениями параллельно. Эта функция активно использует поле _version_ и ожидает, что данное поле присутствует в каждом документе в индексе Solr. По умолчанию схема Solr включает поле _version_, так что поле автоматически добавляется в каждый новый документ. При обновлении документа Solr сравнивает значение _version_ из индекса со значением, предоставленным в запросе, тем самым гарантируя, что документ не был изменен другими клиентами.

Чтобы обновить документ с помощью метода оптимистичного параллелизма, клиент должен передать актуальное значение _version_ вместе с документом. Поле _version_ — это обычное поле Solr, которое можно запрашивать так же, как и любое другое поле, например:

$ curl -X GET 'http://ka-adh-1.ru-central1.internal:8983/solr/test_collection/query?q=*:*&fl=id,post_name,_version_&omitHeader=true' -H 'Content-Type: application/json'

Ответ Solr-сервера:

{
  "response":{"numFound":1,"start":0,"numFoundExact":true,"docs":[
      {
        "id":"1",
        "post_name":["Sample blog post..."],
        "_version_":1809999244663193600}]
  }}

Вы можете передать версию документа в Solr двумя способами:

  • Используя параметр запроса _version_=<version> в URL. Например:

    $ curl -X POST 'http://ka-adh-1.ru-central1.internal:8983/solr/test_collection/update?_version_=123' -H 'Content-Type: application/json' --data-binary '[{"id": 1, "updated_field": "updated value"}]'
  • Указав значение _version_ в обновленном документе. Например:

    {
      "id": 1,
      "_version_": 123,
      "post_rank": {"set": 80}
    }

    Этот способ удобен, когда документы отправляются в Solr пакетом и для каждого документа нужно указать разные значения _version_.

Если Solr получает некорректную версию документа, он отклоняет запрос на обновление, возвращает HTTP-ответ 409 и следующую ошибку:

 "error":{
    "metadata":[
      "error-class","org.apache.solr.common.SolrException",
      "root-error-class","org.apache.solr.common.SolrException"],
    "msg":"version conflict for 1 expected=12345 actual=1809999244663193600",
    "code":409}

Если версия документа указана корректно, Solr обновляет необходимые поля и присваивает новую версию для обновленного документа. Следующий запрос обновляет тестовый документ, передавая в качестве параметра запроса актуальную версию документа.

$ curl -X POST 'http://ka-adh-1.ru-central1.internal:8983/solr/test_collection/update?_version_=1810003184734699520&versions=true&omitHeader=true' -H 'Content-Type: application/json' --data-binary '[{"id":1,"post_rank": {"set": 80}}]'

Ответ сервера:

{
  "adds":[
    "1",1810019385852559360]
}
РЕКОМЕНДАЦИЯ
Параметр запроса versions=true добавляет версию документа в каждый ответ Solr-сервера. Это может быть полезно, чтобы избежать лишних GET-запросов для получения информации о версии документа.
Нашли ошибку? Выделите текст и нажмите Ctrl+Enter чтобы сообщить о ней