Распределенное развертывание#

Platform V Vector DB (далее - Vector DB) поддерживает режим распределенного развертывания. В этом режиме несколько служб обмениваются данными друг с другом для распределения данных между узлами с целью расширения возможностей хранения и повышения стабильности.

Предусловия#

При настройке кластера необходимо определить оптимальное соотношение оперативной памяти и дискового пространства. Оптимальная конфигурация зависит от нескольких факторов:

  • количество векторов и их размерность;

  • объем данных полезной нагрузки и индексов;

  • какие данные будут храниться в памяти, а какие на диске;

  • настройки репликации кластера;

  • факт использования квантования и его настройки.

Пререквизиты:

  • Доступ к серверу: SSH или физический доступ.

  • Установленный продукт: сервер должен быть запущен (кроме операций с файлами данных).

  • Исходный код: скрипты находятся в /src/bin/ репозитория продукта.

  • Rust: установленный компилятор для сборки утилит.

Привилегии:

  • Файловая система: права на чтение/запись в storage_path.

  • API-ключ: обязателен ключ с правами admin для большинства операций.

  • Сетевой доступ: доступность API-порта (порт по умолчанию 6333).

Ключевые требования:

  • API-ключ admin на всех узлах. Сетевой доступ между всеми узлами кластера.

Последовательность выполнения#

Определение оптимального количества узлов#

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

  • Приоритизация экономии средств: если стоимость является наиболее важным фактором, запустите один узел. Это не рекомендуется для производственных сред. Недостатки:

    • Устойчивость: пользователи будут испытывать простои во время перезапуска узла, а восстановление невозможно, если нет резервных копий или снимков состояния.

    • Производительность: ограничена ресурсами одного сервера.

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

    • Стоимость: большие кластеры обходятся дороже маленьких, что является единственным недостатком данной конфигурации.

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

    • Устойчивость (время бесперебойной работы): кластер не может выполнять операции над коллекциями, когда один узел недоступен. Эти операции требуют наличия >50% работающих узлов, поэтому это возможно только в кластере с 3+ узлами. Поскольку создание, редактирование и удаление коллекций обычно являются редкими операциями, многие пользователи считают этот недостаток незначительным.

    • Устойчивость (целостность данных): если данные на одном из двух узлов безвозвратно потеряны или повреждены, их нельзя восстановить иначе как из снимков или резервных копий. Только кластеры с 3+ узлами могут восстановиться после полной потери одного узла, поскольку операции восстановления требуют >50% здоровых узлов кластера.

    • Стоимость: репликация шардов требует хранения двух копий данных.

    • Производительность: максимальная производительность кластера увеличивается по мере добавления новых узлов.

Одноузловые кластеры лучше всего подходят для непроизводственных нагрузок, реплицированные кластеры с 3+ узлами являются золотым стандартом, а реплицированные кластеры с 2 узлами обеспечивают хороший компромисс.

Включение режима распределенного развертывания в самостоятельно размещенном продукте#

Чтобы включить режим распределенного развертывания, включите режим кластера в конфигурации или используя переменную окружения QDRANT__CLUSTER__ENABLED=true.

cluster:
  # Use `enabled: true` to run Qdrant in distributed deployment mode
  enabled: true
  # Configuration of the inter-cluster communication
  p2p:
    # Port for internal communication between peers
    port: 6335

  # Configuration related to distributed consensus algorithm
  consensus:
    # How frequently peers should ping each other.
    # Setting this parameter to lower value will allow consensus
    # to detect disconnected node earlier, but too frequent
    # tick period may create significant network and CPU overhead.
    # We encourage you NOT to change this parameter unless you know what you are doing.
    tick_period_ms: 100

По умолчанию Vector DB будет использовать порт 6335 для внутренней коммуникации. Все узлы должны быть доступны по этому порту внутри кластера, однако убедитесь, что доступ извне к этому порту изолирован, так как он может использоваться для выполнения операций записи.

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

./qdrant --uri 'http://qdrant_node_1:6335'

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

Для этого им необходимо указать адрес начальной загрузки:

./qdrant --bootstrap 'http://qdrant_node_1:6335'

URL-адреса самих новых узлов будут автоматически вычисляться исходя из IP-адресов их запросов. Однако их также можно указывать индивидуально с помощью аргумента --uri.

USAGE:
    qdrant [OPTIONS]

OPTIONS:
        --bootstrap <URI>
            Uri of the peer to bootstrap from in case of multi-peer deployment. If not specified -
            this peer will be considered as a first in a new deployment

        --uri <URI>
            Uri of this peer. Other peers should be able to reach it by this uri.

            This value has to be supplied if this is the first peer in a new deployment.

            In case this is not the first peer and it bootstraps the value is optional. If not
            supplied then qdrant will take internal grpc port from config and derive the IP address
            of this peer on bootstrap peer (receiving side)

После успешной синхронизации состояние кластера можно наблюдать через REST API:

GET /cluster

Пример результата:

{
  "result": {
    "status": "enabled",
    "peer_id": {peer_id},
    "peers": {
      "{peer_id}": {
        "uri": "http://{ip_address}:6335/"
      },
      "{peer_id}": {
        "uri": "http://qdrant_node_1:6335/"
      }
    },
    "raft_info": {
      "term": 1,
      "commit": 4,
      "pending_operations": 1,
      "leader": {leader_id},
      "role": "Leader"
    }
  },
  "status": "ok",
  "time": 5.731e-06
}

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

Использование нового распределенного кластера Vector DB#

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

  • Создайте новую реплицированную коллекцию, установив параметр replication_factor равным двум или больше и задав параметр количество шардов кратным количеству узлов.

  • Если существующая коллекция не содержит достаточно шардов для каждого узла, нужно создать новую коллекцию, как описано в предыдущем пункте.

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

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

Использование технологии Рафт#

Описание#

Vector DB использует протокол согласования Raft для поддержания согласованности относительно топологии кластера и структуры коллекций.

Операции с точками, напротив, не проходят через инфраструктуру согласования. Vector DB не предназначен для обеспечения строгих гарантий транзакций, что позволяет ему выполнять операции с точками с низкой накладной стоимостью. На практике это означает, что Vector DB не гарантирует атомарное выполнение распределенных обновлений, но позволяет дождаться окончания операции, прежде чем увидеть результаты записей.

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

На практике это означает, что если кластер находится в переходном состоянии — либо выбирая нового лидера после сбоя, либо начиная работу, операции обновления коллекции будут отклоняться.

Можно проверить состояние соглашения через интерфейс REST API кластера.

Технология шардирования#

Описание технологии#

Коллекция в Vector DB состоит из одного или нескольких шардов. Шард представляет собой независимое хранилище точек, способное выполнять все операции, предоставляемые коллекциями. Существует два метода распределения точек по шардам:

  • Автоматический шардинг: точки распределяются среди шардов с использованием алгоритма согласованного хеширования, так что каждый шард управляет непересекающимися подмножествами точек. Это поведение используется по умолчанию.

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

Каждый узел знает, где хранятся все части коллекции благодаря протоколу согласования Raft. Таким образом, отправляя запрос поиска одному узлу Vector DB, он автоматически запрашивает остальные узлы для получения полного результата поиска.

Выбор правильного количества шардов#

При создании коллекции Vector DB разбивает ее на shard_number шарда. Если значение не задано явно, shard_number устанавливается равным числу узлов в кластере на момент создания коллекции. Параметр shard_number изменить невозможно без пересоздания коллекции.

PUT /collections/{collection_name}
{
    "vectors": {
      "size": 300,
      "distance": "Cosine"
    },
    "shard_number": 6
}
from qdrant_client import QdrantClient, models

client = QdrantClient(url="http://localhost:6333")

client.create_collection(
    collection_name="{collection_name}",
    vectors_config=models.VectorParams(size=300, distance=models.Distance.COSINE),
    shard_number=6,
)
import { QdrantClient } from "@qdrant/js-client-rest";

const client = new QdrantClient({ host: "localhost", port: 6333 });

client.createCollection("{collection_name}", {
    vectors: {
        size: 300,
        distance: "Cosine",
    },
    shard_number: 6,
});
use qdrant_client::qdrant::{CreateCollectionBuilder, Distance, VectorParamsBuilder};
use qdrant_client::Qdrant;

let client = Qdrant::from_url("http://localhost:6334").build()?;

client
    .create_collection(
        CreateCollectionBuilder::new("{collection_name}")
            .vectors_config(VectorParamsBuilder::new(300, Distance::Cosine))
            .shard_number(6),
    )
    .await?;
import io.qdrant.client.QdrantClient;
import io.qdrant.client.QdrantGrpcClient;
import io.qdrant.client.grpc.Collections.CreateCollection;
import io.qdrant.client.grpc.Collections.Distance;
import io.qdrant.client.grpc.Collections.VectorParams;
import io.qdrant.client.grpc.Collections.VectorsConfig;

QdrantClient client =
    new QdrantClient(QdrantGrpcClient.newBuilder("localhost", 6334, false).build());

client
    .createCollectionAsync(
        CreateCollection.newBuilder()
            .setCollectionName("{collection_name}")
            .setVectorsConfig(
                VectorsConfig.newBuilder()
                    .setParams(
                        VectorParams.newBuilder()
                            .setSize(300)
                            .setDistance(Distance.Cosine)
                            .build())
                    .build())
            .setShardNumber(6)
            .build())
    .get();
using Qdrant.Client;
using Qdrant.Client.Grpc;

var client = new QdrantClient("localhost", 6334);

await client.CreateCollectionAsync(
  collectionName: "{collection_name}",
  vectorsConfig: new VectorParams { Size = 300, Distance = Distance.Cosine },
  shardNumber: 6
);
import (
  "context"

  "github.com/qdrant/go-client/qdrant"
)

client, err := qdrant.NewClient(&qdrant.Config{
  Host: "localhost",
  Port: 6334,
})

client.CreateCollection(context.Background(), &qdrant.CreateCollection{
  CollectionName: "{collection_name}",
  VectorsConfig: qdrant.NewVectorsConfig(&qdrant.VectorParams{
      Size:     300,
      Distance: qdrant.Distance_Cosine,
  }),
  ShardNumber: qdrant.PtrOf(uint32(6)),
})

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

Примечание

Продвинутые сценарии использования, такие как мультитенантность, могут потребовать неравномерного распределения шардов. См. раздел Мультитенантность.

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

Если ожидаете значительный рост, рекомендуется выбрать 12 шардов, так как тогда можно будет расширять кластер от 1 узла до 2, 3, 6 и 12 узлов без необходимости переделывать разбиение. Использование более 12 шардов в небольшом кластере может оказаться неоправданным с точки зрения издержек производительности.

Шарды равномерно распределяются по всем имеющимся узлам при первом создании коллекции, но Vector DB не выполняет автоматическое перераспределение шардов, если изменяется размер кластера или коэффициент репликации (так как эта операция дорогостояща на больших кластерах). Следующий раздел описывает, как перемещать шарды после операций масштабирования.

Перемещение шардов#

Vector DB позволяет перемещать шарды между узлами в кластере и удалять узлы из кластера. Эта функциональность открывает возможность динамически менять размер кластера без простоя. Она также позволяет обновлять или мигрировать узлы без простоя.

Информация о текущем распределении шардов в кластере доступна через API сведений о кластере коллекции.

Используйте API настройки кластера коллекции для инициирования переноса шарда:

POST /collections/{collection_name}/cluster
{
    "move_shard": {
        "shard_id": 0,
        "from_peer_id": 381894127,
        "to_peer_id": 467122995
    }
}

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

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

Если необходимо уменьшить размер кластера, можно перенести все шарды с пира и затем удалить пир с помощью API удаления пира.

DELETE /cluster/peer/{peer_id}

После этого Vector DB исключит узел из протокола согласования, и экземпляр будет готов к выключению.

Пользовательский шардинг#

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

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

Чтобы активировать пользовательский шардинг, установите sharding_method равным custom при создании коллекции:

PUT /collections/{collection_name}
{
    "shard_number": 1,
    "sharding_method": "custom"
    // ... other collection parameters
}
from qdrant_client import QdrantClient, models

client = QdrantClient(url="http://localhost:6333")

client.create_collection(
    collection_name="{collection_name}",
    shard_number=1,
    sharding_method=models.ShardingMethod.CUSTOM,
    # ... other collection parameters
)
client.create_shard_key("{collection_name}", "{shard_key}")
import { QdrantClient } from "@qdrant/js-client-rest";

const client = new QdrantClient({ host: "localhost", port: 6333 });

client.createCollection("{collection_name}", {
    shard_number: 1,
    sharding_method: "custom",
    // ... other collection parameters
});

client.createShardKey("{collection_name}", {
    shard_key: "{shard_key}"
});
use qdrant_client::qdrant::{
    CreateCollectionBuilder, CreateShardKeyBuilder, CreateShardKeyRequestBuilder, Distance,
    ShardingMethod, VectorParamsBuilder,
};
use qdrant_client::Qdrant;

let client = Qdrant::from_url("http://localhost:6334").build()?;

client
    .create_collection(
        CreateCollectionBuilder::new("{collection_name}")
            .vectors_config(VectorParamsBuilder::new(300, Distance::Cosine))
            .shard_number(1)
            .sharding_method(ShardingMethod::Custom.into()),
    )
    .await?;

client
    .create_shard_key(
        CreateShardKeyRequestBuilder::new("{collection_name}")
            .request(CreateShardKeyBuilder::default().shard_key("{shard_key}".to_string())),
    )
    .await?;
import static io.qdrant.client.ShardKeyFactory.shardKey;

import io.qdrant.client.QdrantClient;
import io.qdrant.client.QdrantGrpcClient;
import io.qdrant.client.grpc.Collections.CreateCollection;
import io.qdrant.client.grpc.Collections.ShardingMethod;
import io.qdrant.client.grpc.Collections.CreateShardKey;
import io.qdrant.client.grpc.Collections.CreateShardKeyRequest;

QdrantClient client =
    new QdrantClient(QdrantGrpcClient.newBuilder("localhost", 6334, false).build());

client
    .createCollectionAsync(
        CreateCollection.newBuilder()
            .setCollectionName("{collection_name}")
            // ... other collection parameters
            .setShardNumber(1)
            .setShardingMethod(ShardingMethod.Custom)
            .build())
    .get();

client.createShardKeyAsync(CreateShardKeyRequest.newBuilder()
                .setCollectionName("{collection_name}")
                .setRequest(CreateShardKey.newBuilder()
                                .setShardKey(shardKey("{shard_key}"))
                                .build())
                .build()).get();
using Qdrant.Client;
using Qdrant.Client.Grpc;

var client = new QdrantClient("localhost", 6334);

await client.CreateCollectionAsync(
  collectionName: "{collection_name}",
  // ... other collection parameters
  shardNumber: 1,
  shardingMethod: ShardingMethod.Custom
);

await client.CreateShardKeyAsync(
    "{collection_name}",
    new CreateShardKey { ShardKey = new ShardKey { Keyword = "{shard_key}", } }
    );
import (
  "context"

  "github.com/qdrant/go-client/qdrant"
)

client, err := qdrant.NewClient(&qdrant.Config{
  Host: "localhost",
  Port: 6334,
})

client.CreateCollection(context.Background(), &qdrant.CreateCollection{
  CollectionName: "{collection_name}",
  // ... other collection parameters
  ShardNumber:    qdrant.PtrOf(uint32(1)),
  ShardingMethod: qdrant.ShardingMethod_Custom.Enum(),
})

client.CreateShardKey(context.Background(), "{collection_name}", &qdrant.CreateShardKey{
  ShardKey: qdrant.NewShardKey("{shard_key}"),
})

В данном режиме shard_number обозначает количество шардов на ключ шарда, при котором точки будут распределяться равномерно. Например, если имеется 10 ключей шардов и конфигурация коллекции с такими настройками:

{
    "shard_number": 1,
    "sharding_method": "custom",
    "replication_factor": 2
}

Тогда будет 1 * 10 * 2 = 20 физических шардов в коллекции.

Физические шарды потребляют большое количество ресурсов, поэтому убедитесь, что собственное ключевое поле имеет низкую кардинальность.

Для ключевых полей с высокой кардинальностью рекомендуется вместо этого использовать функцию разделение по полезной нагрузке.

Чтобы задать шард для каждой точки, необходимо указать поле shard_key в запросе вставки:

PUT /collections/{collection_name}/points
{
    "points": [
        {
            "id": 1111,
            "vector": [0.1, 0.2, 0.3]
        },
    ]
    "shard_key": "user_1"
}
from qdrant_client import QdrantClient, models

client = QdrantClient(url="http://localhost:6333")

client.upsert(
    collection_name="{collection_name}",
    points=[
        models.PointStruct(
            id=1111,
            vector=[0.1, 0.2, 0.3],
        ),
    ],
    shard_key_selector="user_1",
)
client.upsert("{collection_name}", {
    points: [
        {
            id: 1111,
            vector: [0.1, 0.2, 0.3],
        },
    ],
    shard_key: "user_1",
});
use qdrant_client::qdrant::{PointStruct, UpsertPointsBuilder};
use qdrant_client::Payload;

client
    .upsert_points(
        UpsertPointsBuilder::new(
            "{collection_name}",
            vec![PointStruct::new(
                111,
                vec![0.1, 0.2, 0.3],
                Payload::default(),
            )],
        )
        .shard_key_selector("user_1".to_string()),
    )
    .await?;
import java.util.List;

import static io.qdrant.client.PointIdFactory.id;
import static io.qdrant.client.ShardKeySelectorFactory.shardKeySelector;
import static io.qdrant.client.VectorsFactory.vectors;

import io.qdrant.client.QdrantClient;
import io.qdrant.client.QdrantGrpcClient;
import io.qdrant.client.grpc.Points.PointStruct;
import io.qdrant.client.grpc.Points.UpsertPoints;

QdrantClient client =
    new QdrantClient(QdrantGrpcClient.newBuilder("localhost", 6334, false).build());

client
    .upsertAsync(
        UpsertPoints.newBuilder()
            .setCollectionName("{collection_name}")
            .addAllPoints(
                List.of(
                    PointStruct.newBuilder()
                        .setId(id(111))
                        .setVectors(vectors(0.1f, 0.2f, 0.3f))
                        .build()))
            .setShardKeySelector(shardKeySelector("user_1"))
            .build())
    .get();
using Qdrant.Client;
using Qdrant.Client.Grpc;

var client = new QdrantClient("localhost", 6334);

await client.UpsertAsync(
  collectionName: "{collection_name}",
  points: new List<PointStruct>
  {
      new() { Id = 111, Vectors = new[] { 0.1f, 0.2f, 0.3f } }
  },
  shardKeySelector: new ShardKeySelector { ShardKeys = { new List<ShardKey> { "user_1" } } }
);
import (
  "context"

  "github.com/qdrant/go-client/qdrant"
)

client, err := qdrant.NewClient(&qdrant.Config{
  Host: "localhost",
  Port: 6334,
})

client.Upsert(context.Background(), &qdrant.UpsertPoints{
  CollectionName: "{collection_name}",
  Points: []*qdrant.PointStruct{
      {
          Id:      qdrant.NewIDNum(111),
          Vectors: qdrant.NewVectors(0.1, 0.2, 0.3),
      },
  },
  ShardKeySelector: &qdrant.ShardKeySelector{
      ShardKeys: []*qdrant.ShardKey{
          qdrant.NewShardKey("user_1"),
      },
  },
})

Важно

Использование одинакового идентификатора точки в разных ключах шардов не поддерживается и должно быть исключено.

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

Теперь можно нацеливаться на операции с конкретными шардами, указывая shard_key при выполнении любой операции. Операции, в которых не указан ключ шарда, будут выполнены на всех шардах.

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

Разбиение по дням

Метод передачи шарда#

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

Имеющиеся методы передачи шардов включают:

  • stream_records: (по умолчанию) передача путем потоковой отправки записей целевому узлу партиями.

  • snapshot: передача включая индекс и квантованные данные посредством автоматического создания снимка.

  • wal_delta: (используется по умолчанию для автоматического восстановления) передача путем разрешения разницы журнала упущенных изменений WAL пропущенные операции.

У каждого метода есть плюсы, минусы и специфические требования, некоторые из которых следующие:

Метод

Потоковая передача записей

Снимок

Разница журнала WAL

Цель

Новый/существующий шард

Новый/существующий шард

Существующий шард

Подключение

Внутренний API gRP (6335)

REST API (6333)
Внутренний API gRPC (6335)

Внутренний API gRPC (6335)

Индекс HNSW

Не передается, переиндексация на целевой стороне

Передается, сразу доступен на цели

Не передается, возможен процесс индексации на цели

Квантование

Не передается, переквантизация на целевой стороне

Передается, сразу доступен на цели

Не передается, возможен процесс квантизации на цели

Порядок

Неупорядоченные обновления на цели

Упорядоченные обновления на цели

Упорядоченные обновления на цели

Дисковое пространство

Дополнительная память не требуется

Требуется дополнительное пространство для снимка на обеих сторонах

Дополнительная память не требуется

Чтобы выбрать метод передачи шарда, укажите method следующим образом:

POST /collections/{collection_name}/cluster
{
    "move_shard": {
        "shard_id": 0,
        "from_peer_id": 381894127,
        "to_peer_id": 467122995,
        "method": "snapshot"
    }
}

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

Недостатки:

  1. Не переносит данные индексации или квантизации. Шард необходимо снова оптимизировать на новом узле, что может оказаться очень дорогостоящим.

  2. Гарантии упорядоченности составляют weak, что делает его неподходящим для некоторых приложений. Метод прост и чрезвычайно надежен, делая его надежным выбором, если указанные выше недостатки приемлемы в сценарии использования. Если кластер нестабилен и испытывает нехватку ресурсов, лучше всего использовать метод передачи данных stream_records, так как маловероятно, что он потерпит неудачу.

Метод передачи данных snapshot использует снимки состояния для переноса шарда. Снимок создается автоматически. Затем он переносится и восстанавливается на целевом узле. После завершения снимок удаляется с обоих узлов. В процессе создания снимка/сброса/восстановления исходный узел ставит все новые операции в очередь. Все обновления очереди отправляются по порядку на целевой шард, чтобы привести его в одно состояние с источником.

Важные особенности:

  1. Не требуется повторно оптимизировать шард на целевом узле, так как данные индекса и квантизации переносятся, что позволяет сразу получить доступ к данным. Таким образом, Vector DB обеспечивает отсутствие снижения производительности после завершения процесса переноса. Это может дать значительное повышение производительности на больших шардах .

  2. Гарантии упорядоченности могут составлять strong, необходимые для некоторых приложений.

Метод передачи данных wal_delta передает только разницу между двумя шардами. Более конкретно, он передает все операции, пропущенные целевым шардом. Для разрешения этой ситуации используется журнал упреждающей записи (WAL) обоих шардов.

Преимущества:

  1. Происходит очень быстро, поскольку передается только разница, а не все данные.

  2. Гарантии упорядоченности могут составлять strong, требуемые некоторыми приложениями.

Недостатки:

  1. Метод можно использовать только для передачи на шард, который уже существует на другом узле.

  2. Применимость ограничена тем фактом, что журналы обычно содержат не более 64 МБ недавних операций. Но этого должно хватить для узла, который быстро перезагружается, например, при обновлении. Если невозможно разрешить дельту, данный метод автоматически возвращается к варианту stream_records, равному передаче полного шарда.

В настоящее время метод stream_records используется по умолчанию. Для автоматического реплицирования шардов с целью восстановления мертвых шардов используется wal_delta.

Использование технологии репликации#

Описание репликации#

Vector DB позволяет реплицировать шарды между узлами кластера.

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

Коэффициент репликации#

При создании коллекции можно контролировать, сколько реплик шардов планируется хранить, изменяя значение параметра replication_factor. По умолчанию параметр replication_factor установлен на 1, что означает, что автоматическое поддержание дополнительной копии отсутствует. Значение по умолчанию можно изменить в файле конфигурации. Можно изменить это значение, установив replication_factor при создании коллекции.

Параметр replication_factor можно обновить для существующей коллекции, но эффект от этого зависит от того, как именно запускается Vector DB. При самостоятельном размещение версии с открытым исходным кодом Qdrant, изменение коэффициента репликации после создания коллекции ничего не даст. Чтобы достичь желаемого коэффициента репликации, необходимо вручную создать или удалить реплики шардов.

PUT /collections/{collection_name}
{
    "vectors": {
        "size": 300,
        "distance": "Cosine"
    },
    "shard_number": 6,
    "replication_factor": 2
}
from qdrant_client import QdrantClient, models

client = QdrantClient(url="http://localhost:6333")

client.create_collection(
    collection_name="{collection_name}",
    vectors_config=models.VectorParams(size=300, distance=models.Distance.COSINE),
    shard_number=6,
    replication_factor=2,
)
import { QdrantClient } from "@qdrant/js-client-rest";

const client = new QdrantClient({ host: "localhost", port: 6333 });

client.createCollection("{collection_name}", {
  vectors: {
    size: 300,
    distance: "Cosine",
  },
  shard_number: 6,
  replication_factor: 2,
});
use qdrant_client::qdrant::{CreateCollectionBuilder, Distance, VectorParamsBuilder};
use qdrant_client::Qdrant;

let client = Qdrant::from_url("http://localhost:6334").build()?;

client
    .create_collection(
        CreateCollectionBuilder::new("{collection_name}")
            .vectors_config(VectorParamsBuilder::new(300, Distance::Cosine))
            .shard_number(6)
            .replication_factor(2),
    )
    .await?;
import io.qdrant.client.QdrantClient;
import io.qdrant.client.QdrantGrpcClient;
import io.qdrant.client.grpc.Collections.CreateCollection;
import io.qdrant.client.grpc.Collections.Distance;
import io.qdrant.client.grpc.Collections.VectorParams;
import io.qdrant.client.grpc.Collections.VectorsConfig;

QdrantClient client =
    new QdrantClient(QdrantGrpcClient.newBuilder("localhost", 6334, false).build());

client
    .createCollectionAsync(
        CreateCollection.newBuilder()
            .setCollectionName("{collection_name}")
            .setVectorsConfig(
                VectorsConfig.newBuilder()
                    .setParams(
                        VectorParams.newBuilder()
                            .setSize(300)
                            .setDistance(Distance.Cosine)
                            .build())
                    .build())
            .setShardNumber(6)
            .setReplicationFactor(2)
            .build())
    .get();
using Qdrant.Client;
using Qdrant.Client.Grpc;

var client = new QdrantClient("localhost", 6334);

await client.CreateCollectionAsync(
  collectionName: "{collection_name}",
  vectorsConfig: new VectorParams { Size = 300, Distance = Distance.Cosine },
  shardNumber: 6,
  replicationFactor: 2
);
import (
  "context"

  "github.com/qdrant/go-client/qdrant"
)

client, err := qdrant.NewClient(&qdrant.Config{
  Host: "localhost",
  Port: 6334,
})

client.CreateCollection(context.Background(), &qdrant.CreateCollection{
  CollectionName: "{collection_name}",
  VectorsConfig: qdrant.NewVectorsConfig(&qdrant.VectorParams{
      Size:     300,
      Distance: qdrant.Distance_Cosine,
  }),
  ShardNumber:       qdrant.PtrOf(uint32(6)),
  ReplicationFactor: qdrant.PtrOf(uint32(2)),
})

Этот фрагмент кода создает коллекцию с общим числом логических шардов, равным 6, обеспечиваемых общим числом физических шардов, равным 12.

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

Создание новых реплик шардов#

Можно вручную создавать или удалять реплики на существующей коллекции с помощью API обновления настройки кластера коллекции. Обычно это нужно делать только в том случае, если используется открытый исходный код Vector DB.

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

POST /collections/{collection_name}/cluster
{
    "replicate_shard": {
        "shard_id": 0,
        "from_peer_id": 381894127,
        "to_peer_id": 467122995
    }
}

Вероятно, может появиться необходимость выбрать определенный метод передачи шарда, чтобы добиться желаемой производительности и гарантий.

А реплику можно удалить на конкретном пире.

POST /collections/{collection_name}/cluster
{
    "drop_replica": {
        "shard_id": 0,
        "peer_id": 381894127
    }
}

Помните, что коллекция должна содержать хотя бы одну активную реплику каждого шарда.

Обработка ошибок#

Реплики могут находиться в разных состояниях:

  • активная: здоровая и готовая обслуживать трафик;

  • мертвая: нездоровая и не готова обслуживать трафик;

  • частичная: в настоящий момент находится под ресинхронизацией перед активацией.

Если реплика не отвечает на внутренние проверки состояния (healthchecks) или не способна обслуживать трафик, она помечается как мертвая.

На мертвую реплику не поступает трафик от других пиров, и ей может потребоваться ручное вмешательство, если она не восстановилась автоматически.

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

Восстановление после выхода узла из строя#

Иногда аппаратные сбои могут сделать некоторые узлы кластера невосстанавливаемыми. Ни одна система не застрахована от этого.

Но несколько сценариев восстановления позволяют оставаться доступным для запросов и даже избегать деградации производительности.

Восстановление с использованием реплицированной коллекции#

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

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

Если отказавший узел никогда не перезапускался, можно восстановить потерянные шарды, если кластер из трех или более узлов. Потерянные шарды нельзя восстановить в меньших кластерах, потому что операции восстановления проходят через raft, требующий наличия >50% здоровых узлов.

Пересоздание узла с реплицированными коллекциями#

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

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

Когда создаете новый узел, убедитесь, что подключили его к существующему кластеру, указав параметр командной строки --bootstrap с адресом любого работающего узла кластера.

После того, как новый узел станет готовым и синхронизированным с кластером, возможно, потребуется убедиться, что реплики шардов коллекции достаточно реплицированы. Помните, что Vector DB не будет автоматически балансировать шарды, поскольку эта операция требует значительных затрат. Используйте операцию репликации шарда, чтобы создать еще одну копию шарда на вновь подключенном узле.

Стоит отметить, что Vector DB предоставляет лишь необходимые строительные блоки для создания автоматизированного восстановления после сбоя. Построение полностью автоматического процесса масштабирования коллекций потребовало бы контроля над машинами самого кластера.

Восстановление из снимка#

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

Выполните те же шаги, чтобы отсоединить отказавший узел и добавить новый в кластер:

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

  • Создайте новый узел, убедившись, что подключили его к существующему кластеру, указав параметр командной строки --bootstrap с адресом любого работающего узла кластера.

Восстановление из снимков, используемое в развертывании с одним узлом, отличается от восстановления в кластере. Консенсус управляет всей метаданными о коллекциях и не требует снимков для их восстановления. Однако можно использовать снимки для восстановления отсутствующих шардов коллекций.

Используйте API восстановления коллекции из снимка для выполнения данной задачи. Сервис загрузит указанный снимок коллекции и восстановит ее шарды с данными из него.

Как только все шарды коллекции будут восстановлены, сама коллекция снова начнет работать.

Временный выход узла из строя#

Если конфигурация выполнена правильно, работа Vector DB в распределенном режиме может сделать кластер устойчивым к простоям, когда один узел временно выходит из строя.

Вот как реагируют по-разному сконфигурированные кластеры Vector DB:

  • Кластеры с одним узлом: все операции завершатся тайм-аутом или ошибкой максимум на несколько минут. Это зависит от времени, необходимого для перезапуска и загрузки данных с диска.

  • Кластеры с двумя узлами, где шарды не разделены: все операции завершатся таймаутом или ошибкой максимум на несколько минут. Это зависит от времени, необходимого для перезапуска и загрузки данных с диска.

  • Кластеры с двумя узлами, где все шарды разделены на оба узла: все запросы, кроме операций с коллекциями, продолжают работать во время простоя.

  • Кластеры с тремя и более узлами, где все шарды реплицированы минимум на двух узлах: все запросы продолжают работать во время простоя.

Гарантии согласованности#

По умолчанию Vector DB ориентирован на доступность и максимальную пропускную способность поисковых операций. Для большинства вариантов использования это предпочтительный компромисс.

Во время нормального функционирования системы возможен поиск и модификация данных с любого пиара в кластере.

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

  • чтения используют стратегию частичного распределения нагрузки для оптимизации задержки и доступности.

  • записи выполняются параллельно на всех активных репликах шардов.

Вложения

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

Vector DB предлагает несколько опций для управления гарантиями согласованности:

  • write_consistency_factor- определяет количество реплик, которые должны подтвердить успешность операции записи прежде чем ответить клиенту. Увеличение данного значения сделает операции записи устойчивыми к разделению сети внутри кластера, однако потребует большего числа активных реплик для выполнения операций записи.

  • Читать параметр consistency, который можно использовать с операциями поиска и получения данных, чтобы гарантировать, что результаты, полученные со всех реплик, совпадают. Если данная опция включена, Vector DB выполнит операцию чтения на множестве реплик и разрешит результат в соответствии с выбранной стратегией. Эта опция полезна для предотвращения несогласованности данных в случае одновременных обновлений одного документа. Данная опция предпочтительна, если операции обновления происходят часто и количество реплик мало.

  • Записывать параметр ordering, который можно использовать с операциями обновления и удаления, чтобы гарантировать, что операции выполняются в одном порядке на всех репликах. Если эта опция использована, Vector DB направит операцию на лидерную реплику шарда и дождется ответа перед ответом клиенту. Этот вариант полезен для предотвращения несогласованности данных в случае одновременных обновлений одного документа. Данный вариант предпочтителен, если операции чтения встречаются чаще, чем обновления, и производительность поиска критична.

Фактор согласованности записей#

write_consistency_factor представляет собой количество реплик, которое должно подтвердить запись перед ответом клиенту. По умолчанию установлено значение 1. Оно может быть настроено при создании коллекции или при изменении параметров коллекции.

Это значение может варьироваться от 1 до количества реплик, имеющихся для каждого шарда.

PUT /collections/{collection_name}
{
    "vectors": {
        "size": 300,
        "distance": "Cosine"
    },
    "shard_number": 6,
    "replication_factor": 2,
    "write_consistency_factor": 2
}
from qdrant_client import QdrantClient, models

client = QdrantClient(url="http://localhost:6333")

client.create_collection(
    collection_name="{collection_name}",
    vectors_config=models.VectorParams(size=300, distance=models.Distance.COSINE),
    shard_number=6,
    replication_factor=2,
    write_consistency_factor=2,
)
import { QdrantClient } from "@qdrant/js-client-rest";

const client = new QdrantClient({ host: "localhost", port: 6333 });

client.createCollection("{collection_name}", {
  vectors: {
    size: 300,
    distance: "Cosine",
  },
  shard_number: 6,
  replication_factor: 2,
  write_consistency_factor: 2,
});
use qdrant_client::qdrant::{CreateCollectionBuilder, Distance, VectorParamsBuilder};
use qdrant_client::Qdrant;

let client = Qdrant::from_url("http://localhost:6334").build()?;

client
    .create_collection(
        CreateCollectionBuilder::new("{collection_name}")
            .vectors_config(VectorParamsBuilder::new(300, Distance::Cosine))
            .shard_number(6)
            .replication_factor(2)
            .write_consistency_factor(2),
    )
    .await?;
import io.qdrant.client.QdrantClient;
import io.qdrant.client.QdrantGrpcClient;
import io.qdrant.client.grpc.Collections.CreateCollection;
import io.qdrant.client.grpc.Collections.Distance;
import io.qdrant.client.grpc.Collections.VectorParams;
import io.qdrant.client.grpc.Collections.VectorsConfig;

QdrantClient client =
    new QdrantClient(QdrantGrpcClient.newBuilder("localhost", 6334, false).build());

client
    .createCollectionAsync(
        CreateCollection.newBuilder()
            .setCollectionName("{collection_name}")
            .setVectorsConfig(
                VectorsConfig.newBuilder()
                    .setParams(
                        VectorParams.newBuilder()
                            .setSize(300)
                            .setDistance(Distance.Cosine)
                            .build())
                    .build())
            .setShardNumber(6)
            .setReplicationFactor(2)
            .setWriteConsistencyFactor(2)
            .build())
    .get();
using Qdrant.Client;
using Qdrant.Client.Grpc;

var client = new QdrantClient("localhost", 6334);

await client.CreateCollectionAsync(
  collectionName: "{collection_name}",
  vectorsConfig: new VectorParams { Size = 300, Distance = Distance.Cosine },
  shardNumber: 6,
  replicationFactor: 2,
  writeConsistencyFactor: 2
);
import (
  "context"

  "github.com/qdrant/go-client/qdrant"
)

client, err := qdrant.NewClient(&qdrant.Config{
  Host: "localhost",
  Port: 6334,
})

client.CreateCollection(context.Background(), &qdrant.CreateCollection{
  CollectionName: "{collection_name}",
  VectorsConfig: qdrant.NewVectorsConfig(&qdrant.VectorParams{
      Size:     300,
      Distance: qdrant.Distance_Cosine,
  }),
  ShardNumber:            qdrant.PtrOf(uint32(6)),
  ReplicationFactor:      qdrant.PtrOf(uint32(2)),
  WriteConsistencyFactor: qdrant.PtrOf(uint32(2)),
})

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

Установка значения write_consistency_factor на низкое значение позволит принимать записи даже при наличии недоступных узлов. Недоступные узлы отмечаются как мертвые и автоматически восстанавливаются при появлении, обеспечивая согласованность данных.

Настройка write_consistency_factor важна для корректировки поведения кластера при выходе некоторых узлов офлайн вследствие перезагрузок, обновлений или отказов.

По умолчанию кластер продолжает принимать обновления до тех пор, пока хотя бы одна реплика каждого шарда остается онлайн. Тем не менее, данное поведение означает, что после возвращения офлайновой реплики ее придется дополнительно синхронизировать с остальной частью кластера. Иногда такая синхронизация может потребовать много ресурсов и нежелательна.

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

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

Эта стратегия может быть предпочтительной для асинхронных обновлений и конвейеров инъекций, способных обрабатывать ошибки и повторы.

Согласованность чтения#

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

  • all запрашивает все узлы и возвращает точки, присутствующие на всех них.

  • majority запрашивает все узлы и возвращает точки, присутствующие на большинстве из них.

  • quorum запрашивает случайно выбранное большинство узлов и возвращает точки, присутствующие на всех из них.

  • 1/2/3/etc - запрашивают указанное количество случайных узлов и возвращают точки, присутствующие на всех из них.

  • значение consistency по умолчанию - 1.

POST /collections/{collection_name}/points/query?consistency=majority
{
    "query": [0.2, 0.1, 0.9, 0.7],
    "filter": {
        "must": [
            {
                "key": "city",
                "match": {
                    "value": "London"
                }
            }
        ]
    },
    "params": {
        "hnsw_ef": 128,
        "exact": false
    },
    "limit": 3
}
client.query_points(
    collection_name="{collection_name}",
    query=[0.2, 0.1, 0.9, 0.7],
    query_filter=models.Filter(
        must=[
            models.FieldCondition(
                key="city",
                match=models.MatchValue(
                    value="London",
                ),
            )
        ]
    ),
    search_params=models.SearchParams(hnsw_ef=128, exact=False),
    limit=3,
    consistency="majority",
)
client.query("{collection_name}", {
    query: [0.2, 0.1, 0.9, 0.7],
    filter: {
        must: [{ key: "city", match: { value: "London" } }],
    },
    params: {
        hnsw_ef: 128,
        exact: false,
    },
    limit: 3,
    consistency: "majority",
});
use qdrant_client::qdrant::{
    read_consistency::Value, Condition, Filter, QueryPointsBuilder, ReadConsistencyType,
    SearchParamsBuilder,
};
use qdrant_client::{Qdrant, QdrantError};

let client = Qdrant::from_url("http://localhost:6334").build()?;

client
    .query(
        QueryPointsBuilder::new("{collection_name}")
            .query(vec![0.2, 0.1, 0.9, 0.7])
            .limit(3)
            .filter(Filter::must([Condition::matches(
                "city",
                "London".to_string(),
            )]))
            .params(SearchParamsBuilder::default().hnsw_ef(128).exact(false))
            .read_consistency(Value::Type(ReadConsistencyType::Majority.into())),
    )
    .await?;
import io.qdrant.client.QdrantClient;
import io.qdrant.client.QdrantGrpcClient;
import io.qdrant.client.grpc.Points.Filter;
import io.qdrant.client.grpc.Points.QueryPoints;
import io.qdrant.client.grpc.Points.ReadConsistency;
import io.qdrant.client.grpc.Points.ReadConsistencyType;
import io.qdrant.client.grpc.Points.SearchParams;

import static io.qdrant.client.QueryFactory.nearest;
import static io.qdrant.client.ConditionFactory.matchKeyword;

QdrantClient client =
    new QdrantClient(QdrantGrpcClient.newBuilder("localhost", 6334, false).build());

client.queryAsync(
        QueryPoints.newBuilder()
                .setCollectionName("{collection_name}")
                .setFilter(Filter.newBuilder().addMust(matchKeyword("city", "London")).build())
                .setQuery(nearest(.2f, 0.1f, 0.9f, 0.7f))
                .setParams(SearchParams.newBuilder().setHnswEf(128).setExact(false).build())
                .setLimit(3)
                .setReadConsistency(
                        ReadConsistency.newBuilder().setType(ReadConsistencyType.Majority).build())
                .build())
        .get();
using Qdrant.Client;
using Qdrant.Client.Grpc;
using static Qdrant.Client.Grpc.Conditions;

var client = new QdrantClient("localhost", 6334);

await client.QueryAsync(
  collectionName: "{collection_name}",
  query: new float[] { 0.2f, 0.1f, 0.9f, 0.7f },
  filter: MatchKeyword("city", "London"),
  searchParams: new SearchParams { HnswEf = 128, Exact = false },
  limit: 3,
  readConsistency: new ReadConsistency { Type = ReadConsistencyType.Majority }
);
import (
  "context"

  "github.com/qdrant/go-client/qdrant"
)

client, err := qdrant.NewClient(&qdrant.Config{
  Host: "localhost",
  Port: 6334,
})

client.Query(context.Background(), &qdrant.QueryPoints{
  CollectionName: "{collection_name}",
  Query:          qdrant.NewQuery(0.2, 0.1, 0.9, 0.7),
  Filter: &qdrant.Filter{
      Must: []*qdrant.Condition{
          qdrant.NewMatch("city", "London"),
      },
  },
  Params: &qdrant.SearchParams{
      HnswEf: qdrant.PtrOf(uint64(128)),
  },
  Limit:           qdrant.PtrOf(uint64(3)),
  ReadConsistency: qdrant.NewReadConsistencyType(qdrant.ReadConsistencyType_Majority),
})

Последовательность записи#

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

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

  • medium порядок сериализует все операции записи через динамически избранного лидера, что может вызвать небольшие несоответствия в случае смены лидера.

  • strong порядок сериализует все операции записи через постоянного лидера, предоставляя сильную согласованность, но операции записи могут стать недоступны, если лидер выйдет из строя.

Некоторые методы передачи шардов могут повлиять на гарантии порядка.

PUT /collections/{collection_name}/points?ordering=strong
{
    "batch": {
        "ids": [1, 2, 3],
        "payloads": [
            {"color": "red"},
            {"color": "green"},
            {"color": "blue"}
        ],
        "vectors": [
            [0.9, 0.1, 0.1],
            [0.1, 0.9, 0.1],
            [0.1, 0.1, 0.9]
        ]
    }
}
client.upsert(
    collection_name="{collection_name}",
    points=models.Batch(
        ids=[1, 2, 3],
        payloads=[
            {"color": "red"},
            {"color": "green"},
            {"color": "blue"},
        ],
        vectors=[
            [0.9, 0.1, 0.1],
            [0.1, 0.9, 0.1],
            [0.1, 0.1, 0.9],
        ],
    ),
    ordering=models.WriteOrdering.STRONG,
)
client.upsert("{collection_name}", {
  batch: {
    ids: [1, 2, 3],
    payloads: [{ color: "red" }, { color: "green" }, { color: "blue" }],
    vectors: [
      [0.9, 0.1, 0.1],
      [0.1, 0.9, 0.1],
      [0.1, 0.1, 0.9],
    ],
  },
  ordering: "strong",
});
use qdrant_client::qdrant::{
    PointStruct, UpsertPointsBuilder, WriteOrdering, WriteOrderingType
};
use qdrant_client::Qdrant;

let client = Qdrant::from_url("http://localhost:6334").build()?;

client
    .upsert_points(
        UpsertPointsBuilder::new(
            "{collection_name}",
            vec![
                PointStruct::new(1, vec![0.9, 0.1, 0.1], [("color", "red".into())]),
                PointStruct::new(2, vec![0.1, 0.9, 0.1], [("color", "green".into())]),
                PointStruct::new(3, vec![0.1, 0.1, 0.9], [("color", "blue".into())]),
            ],
        )
        .ordering(WriteOrdering {
            r#type: WriteOrderingType::Strong.into(),
        }),
    )
    .await?;
import java.util.List;
import java.util.Map;

import static io.qdrant.client.PointIdFactory.id;
import static io.qdrant.client.ValueFactory.value;
import static io.qdrant.client.VectorsFactory.vectors;

import io.qdrant.client.grpc.Points.PointStruct;
import io.qdrant.client.grpc.Points.UpsertPoints;
import io.qdrant.client.grpc.Points.WriteOrdering;
import io.qdrant.client.grpc.Points.WriteOrderingType;

client
    .upsertAsync(
        UpsertPoints.newBuilder()
            .setCollectionName("{collection_name}")
            .addAllPoints(
                List.of(
                    PointStruct.newBuilder()
                        .setId(id(1))
                        .setVectors(vectors(0.9f, 0.1f, 0.1f))
                        .putAllPayload(Map.of("color", value("red")))
                        .build(),
                    PointStruct.newBuilder()
                        .setId(id(2))
                        .setVectors(vectors(0.1f, 0.9f, 0.1f))
                        .putAllPayload(Map.of("color", value("green")))
                        .build(),
                    PointStruct.newBuilder()
                        .setId(id(3))
                        .setVectors(vectors(0.1f, 0.1f, 0.94f))
                        .putAllPayload(Map.of("color", value("blue")))
                        .build()))
            .setOrdering(WriteOrdering.newBuilder().setType(WriteOrderingType.Strong).build())
            .build())
    .get();
using Qdrant.Client;
using Qdrant.Client.Grpc;

var client = new QdrantClient("localhost", 6334);

await client.UpsertAsync(
  collectionName: "{collection_name}",
  points: new List<PointStruct>
  {
      new()
      {
          Id = 1,
          Vectors = new[] { 0.9f, 0.1f, 0.1f },
          Payload = { ["color"] = "red" }
      },
      new()
      {
          Id = 2,
          Vectors = new[] { 0.1f, 0.9f, 0.1f },
          Payload = { ["color"] = "green" }
      },
      new()
      {
          Id = 3,
          Vectors = new[] { 0.1f, 0.1f, 0.9f },
          Payload = { ["color"] = "blue" }
      }
  },
  ordering: WriteOrderingType.Strong
);
import (
  "context"

  "github.com/qdrant/go-client/qdrant"
)

client, err := qdrant.NewClient(&qdrant.Config{
  Host: "localhost",
  Port: 6334,
})

client.Upsert(context.Background(), &qdrant.UpsertPoints{
  CollectionName: "{collection_name}",
  Points: []*qdrant.PointStruct{
      {
          Id:      qdrant.NewIDNum(1),
          Vectors: qdrant.NewVectors(0.9, 0.1, 0.1),
          Payload: qdrant.NewValueMap(map[string]any{"color": "red"}),
      },
      {
          Id:      qdrant.NewIDNum(2),
          Vectors: qdrant.NewVectors(0.1, 0.9, 0.1),
          Payload: qdrant.NewValueMap(map[string]any{"color": "green"}),
      },
      {
          Id:      qdrant.NewIDNum(3),
          Vectors: qdrant.NewVectors(0.1, 0.1, 0.9),
          Payload: qdrant.NewValueMap(map[string]any{"color": "blue"}),
      },
  },
  Ordering: &qdrant.WriteOrdering{
      Type: qdrant.WriteOrderingType_Strong,
  },
})

Режим прослушивания#

Важно

Это экспериментальная функция, в дальнейшем ее поведение может измениться.

В некоторых случаях полезно иметь узел Vector DB, который накапливает данные, но не участвует в операциях поиска. Вот несколько ситуаций, где это может пригодиться:

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

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

Чтобы включить режим слушателя, установите node_type в Listener в файле конфигурации:

storage:
  node_type: "Listener"

Узел в режиме слушателя не будет участвовать в операциях поиска, но продолжит принимать операции записи и сохранять данные в локальное хранилище.

Все шарды, хранящиеся на узле слушателя, перейдут в состояние Listener.

Кроме того, все запросы на запись, отправленные на узел слушателя, будут обрабатываться с опцией wait=false, что означает, что операции записи будут считаться успешными, как только они будут записаны в журнал упреждающей записи (WAL). Такой подход должен позволить минимизировать задержку вставки в случае параллельного снятия снимков.

Контрольные точки консенсуса#

Контрольные точки консенсуса — техника, применяемая в Raft для повышения производительности и упрощения управления журналом путем периодического создания согласованной контрольной точки текущего состояния системы. Такая контрольная точка представляет собой точку во времени, в которой все узлы кластера достигли соглашения относительно состояния, и ее можно использовать для усечения журнала, уменьшая объем хранимых и передаваемых между узлами данных.

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

Чтобы предотвратить это, можно воспользоваться специальным механизмом контрольных точек, который усекает журнал и создает снимок текущего состояния.

Чтобы воспользоваться этим функционалом, просто вызовите API /cluster/recover на нужном узле:

POST /cluster/recover

API можно запустить на любом нелидерском узле, он пошлет запрос текущему лидеру консенсуса на создание снимка. Лидер, в свою очередь, отправит снимок обратно запрашивающему узлу для применения.

В некоторых ситуациях API можно использовать для восстановления из некорректного состояния кластера, принудительно создав снимок.