|
116 | 116 |
|
117 | 117 | ## Вставка векторов {#insert-vectors} |
118 | 118 |
|
119 | | -Для вставки векторов необходимо подготовить правильный YQL-запрос. Для унификации вставки разных данных он параметризован. |
120 | | -В готовом YQL-запросе важно корректно преобразовать вектор в тип `String`. Для этого используется [функция преобразования](../../yql/reference/udf/list/knn.md#functions-convert) вектора в бинарное представление. |
| 119 | +Для вставки векторов необходимо подготовить и выполнить правильный YQL-запрос. Для унификации вставки разных данных он параметризован. |
121 | 120 |
|
122 | | -Запрос оперирует контейнерным типом данных `List<Struct<...>>` (список структур), что позволяет передавать произвольное количество объектов за один раз. |
| 121 | +Запрос оперирует контейнерным типом данных `List<Struct<...>>` (список структур), что позволяет передавать через параметры произвольное количество объектов за один раз. |
| 122 | + |
| 123 | +В {{ ydb-short-name }} таблицах же вектора хранятся в виде сериализованной последовательности байт. Конвертацию в такое представление **рекомендуется выполнять на клиенте**. Альтернативный способ — делегировать конвертацию на сервер с помощью функции преобразования [Knn UDF](../../yql/reference/udf/list/knn.md#functions-convert). Ниже будут приведены примеры, демонстрирующие оба подхода. |
123 | 124 |
|
124 | 125 | {% list tabs %} |
125 | 126 |
|
126 | 127 | - Python |
127 | 128 |
|
| 129 | + Метод принимает массив словарей `items`, где каждый словарь содержит поля `id` - идентификатор, `document` - текст, `embedding` - векторное представление текста, заранее сериализованное в последовательность байт. |
| 130 | + |
| 131 | + Для использования структуры в примере ниже создается `items_struct_type = ydb.StructType()`, в котором задаются типы всех полей. Для передачи списка таких структур его необходимо обернуть в `ydb.ListType`: `ydb.ListType(items_struct_type)`. |
| 132 | + |
| 133 | + ```python |
| 134 | + import struct |
| 135 | + |
| 136 | + def convert_vector_to_bytes(vector: list[float]) -> bytes: |
| 137 | + b = struct.pack("<f" * len(vector), *vector) |
| 138 | + return b + b"\x01" |
| 139 | + |
| 140 | + def insert_items_vector_as_bytes( |
| 141 | + pool: ydb.QuerySessionPool, |
| 142 | + table_name: str, |
| 143 | + items: list[dict], |
| 144 | + ) -> None: |
| 145 | + query = f""" |
| 146 | + DECLARE $items AS List<Struct< |
| 147 | + id: Utf8, |
| 148 | + document: Utf8, |
| 149 | + embedding: String |
| 150 | + >>; |
| 151 | +
|
| 152 | + UPSERT INTO `{table_name}` |
| 153 | + ( |
| 154 | + id, |
| 155 | + document, |
| 156 | + embedding |
| 157 | + ) |
| 158 | + SELECT |
| 159 | + id, |
| 160 | + document, |
| 161 | + embedding, |
| 162 | + FROM AS_TABLE($items); |
| 163 | + """ |
| 164 | + |
| 165 | + items_struct_type = ydb.StructType() |
| 166 | + items_struct_type.add_member("id", ydb.PrimitiveType.Utf8) |
| 167 | + items_struct_type.add_member("document", ydb.PrimitiveType.Utf8) |
| 168 | + items_struct_type.add_member("embedding", ydb.PrimitiveType.String) |
| 169 | + |
| 170 | + for item in items: |
| 171 | + item["embedding"] = convert_vector_to_bytes(item["embedding"]) |
| 172 | + |
| 173 | + pool.execute_with_retries( |
| 174 | + query, {"$items": (items, ydb.ListType(items_struct_type))} |
| 175 | + ) |
| 176 | + |
| 177 | + print(f"{len(items)} items inserted") |
| 178 | + ``` |
| 179 | + |
| 180 | +- C++ |
| 181 | + |
| 182 | + ```cpp |
| 183 | + std::string ConvertVectorToBytes(const std::vector<float>& vector) |
| 184 | + { |
| 185 | + std::string result; |
| 186 | + for (const auto& value : vector) { |
| 187 | + const char* bytes = reinterpret_cast<const char*>(&value); |
| 188 | + result += std::string(bytes, sizeof(float)); |
| 189 | + } |
| 190 | + return result + "\x01"; |
| 191 | + } |
| 192 | + |
| 193 | + void InsertItemsAsBytes( |
| 194 | + NYdb::NQuery::TQueryClient& client, |
| 195 | + const std::string& tableName, |
| 196 | + const std::vector<TItem>& items) |
| 197 | + { |
| 198 | + std::string query = std::format(R"( |
| 199 | + DECLARE $items AS List<Struct< |
| 200 | + id: Utf8, |
| 201 | + document: Utf8, |
| 202 | + embedding: String |
| 203 | + >>; |
| 204 | + UPSERT INTO `{0}` |
| 205 | + ( |
| 206 | + id, |
| 207 | + document, |
| 208 | + embedding |
| 209 | + ) |
| 210 | + SELECT |
| 211 | + id, |
| 212 | + document, |
| 213 | + embedding, |
| 214 | + FROM AS_TABLE($items); |
| 215 | + )", tableName); |
| 216 | + |
| 217 | + NYdb::TParamsBuilder paramsBuilder; |
| 218 | + auto& valueBuilder = paramsBuilder.AddParam("$items"); |
| 219 | + valueBuilder.BeginList(); |
| 220 | + for (const auto& item : items) { |
| 221 | + valueBuilder.AddListItem(); |
| 222 | + valueBuilder.BeginStruct(); |
| 223 | + valueBuilder.AddMember("id").Utf8(item.Id); |
| 224 | + valueBuilder.AddMember("document").Utf8(item.Document); |
| 225 | + valueBuilder.AddMember("embedding").String(ConvertVectorToBytes(item.Embedding)); |
| 226 | + valueBuilder.EndStruct(); |
| 227 | + } |
| 228 | + valueBuilder.EndList(); |
| 229 | + valueBuilder.Build(); |
| 230 | + |
| 231 | + NYdb::NStatusHelpers::ThrowOnError(client.RetryQuerySync([params = paramsBuilder.Build(), &query](NYdb::NQuery::TSession session) { |
| 232 | + return session.ExecuteQuery(query, NYdb::NQuery::TTxControl::BeginTx(NYdb::NQuery::TTxSettings::SerializableRW()).CommitTx(), params).ExtractValueSync(); |
| 233 | + })); |
| 234 | + |
| 235 | + std::cout << items.size() << " items inserted" << std::endl; |
| 236 | + } |
| 237 | + ``` |
| 238 | + |
| 239 | + {% note info %} |
| 240 | + |
| 241 | + В функции `ConvertVectorToBytes` подразумевается, что на клиенте используется процессор с [little-endian порядком байт](https://ru.wikipedia.org/wiki/Порядок_байтов), например x86\_64. Если используется другой порядок байт, функцию `ConvertVectorToBytes` необходимо адаптировать. |
| 242 | + |
| 243 | + {% endnote %} |
| 244 | + |
| 245 | +- Python (альтернативный) |
| 246 | + |
128 | 247 | Метод принимает массив словарей `items`, где каждый словарь содержит поля `id` - идентификатор, `document` - текст, `embedding` - векторное представление текста. |
129 | 248 |
|
130 | 249 | Для использования структуры в примере ниже создается `items_struct_type = ydb.StructType()`, в котором задаются типы всех полей. Для передачи списка таких структур его необходимо обернуть в `ydb.ListType`: `ydb.ListType(items_struct_type)`. |
131 | 250 |
|
132 | 251 | ```python |
133 | | - def insert_items( |
| 252 | + def insert_items_vector_as_float_list( |
134 | 253 | pool: ydb.QuerySessionPool, |
135 | 254 | table_name: str, |
136 | 255 | items: list[dict], |
|
167 | 286 | print(f"{len(items)} items inserted") |
168 | 287 | ``` |
169 | 288 |
|
170 | | -- C++ |
| 289 | +- C++ (альтернативный) |
171 | 290 |
|
172 | 291 | ```cpp |
173 | | - void InsertItems( |
| 292 | + void InsertItemsAsFloatList( |
174 | 293 | NYdb::NQuery::TQueryClient& client, |
175 | 294 | const std::string& tableName, |
176 | 295 | const std::vector<TItem>& items) |
|
364 | 483 | - Python |
365 | 484 |
|
366 | 485 | ```python |
367 | | - def search_items( |
| 486 | + def search_items_vector_as_bytes( |
| 487 | + pool: ydb.QuerySessionPool, |
| 488 | + table_name: str, |
| 489 | + embedding: list[float], |
| 490 | + strategy: str = "CosineSimilarity", |
| 491 | + limit: int = 1, |
| 492 | + index_name: str | None = None, |
| 493 | + ) -> list[dict]: |
| 494 | + view_index = f"VIEW {index_name}" if index_name else "" |
| 495 | + |
| 496 | + sort_order = "DESC" if strategy.endswith("Similarity") else "ASC" |
| 497 | + |
| 498 | + query = f""" |
| 499 | + DECLARE $embedding as String; |
| 500 | +
|
| 501 | + SELECT |
| 502 | + id, |
| 503 | + document, |
| 504 | + Knn::{strategy}(embedding, $embedding) as score |
| 505 | + FROM {table_name} {view_index} |
| 506 | + ORDER BY score {sort_order} |
| 507 | + LIMIT {limit}; |
| 508 | + """ |
| 509 | + |
| 510 | + result = pool.execute_with_retries( |
| 511 | + query, |
| 512 | + { |
| 513 | + "$embedding": ( |
| 514 | + convert_vector_to_bytes(embedding), |
| 515 | + ydb.PrimitiveType.String, |
| 516 | + ), |
| 517 | + }, |
| 518 | + ) |
| 519 | + |
| 520 | + items = [] |
| 521 | + |
| 522 | + for result_set in result: |
| 523 | + for row in result_set.rows: |
| 524 | + items.append( |
| 525 | + { |
| 526 | + "id": row["id"], |
| 527 | + "document": row["document"], |
| 528 | + "score": row["score"], |
| 529 | + } |
| 530 | + ) |
| 531 | + |
| 532 | + return items |
| 533 | + ``` |
| 534 | + |
| 535 | +- C++ |
| 536 | + |
| 537 | + ```cpp |
| 538 | + std::vector<TResultItem> SearchItemsAsBytes( |
| 539 | + NYdb::NQuery::TQueryClient& client, |
| 540 | + const std::string& tableName, |
| 541 | + const std::vector<float>& embedding, |
| 542 | + const std::string& strategy, |
| 543 | + std::uint64_t limit, |
| 544 | + const std::optional<std::string>& indexName) |
| 545 | + { |
| 546 | + std::string viewIndex = indexName ? "VIEW " + *indexName : ""; |
| 547 | + std::string sortOrder = strategy.ends_with("Similarity") ? "DESC" : "ASC"; |
| 548 | + |
| 549 | + std::string query = std::format(R"( |
| 550 | + DECLARE $embedding as String; |
| 551 | + SELECT |
| 552 | + id, |
| 553 | + document, |
| 554 | + Knn::{2}(embedding, $embedding) as score |
| 555 | + FROM {0} {1} |
| 556 | + ORDER BY score {3} |
| 557 | + LIMIT {4}; |
| 558 | + )", tableName, viewIndex, strategy, sortOrder, limit); |
| 559 | + |
| 560 | + auto params = NYdb::TParamsBuilder() |
| 561 | + .AddParam("$embedding") |
| 562 | + .String(ConvertVectorToBytes(embedding)) |
| 563 | + .Build() |
| 564 | + .Build(); |
| 565 | + |
| 566 | + std::vector<TResultItem> result; |
| 567 | + |
| 568 | + NYdb::NStatusHelpers::ThrowOnError(client.RetryQuerySync([params, &query, &result](NYdb::NQuery::TSession session) { |
| 569 | + auto execResult = session.ExecuteQuery(query, NYdb::NQuery::TTxControl::BeginTx(NYdb::NQuery::TTxSettings::SerializableRW()).CommitTx(), params).ExtractValueSync(); |
| 570 | + if (execResult.IsSuccess()) { |
| 571 | + auto parser = execResult.GetResultSetParser(0); |
| 572 | + while (parser.TryNextRow()) { |
| 573 | + result.push_back({ |
| 574 | + .Id = *parser.ColumnParser(0).GetOptionalUtf8(), |
| 575 | + .Document = *parser.ColumnParser(1).GetOptionalUtf8(), |
| 576 | + .Score = *parser.ColumnParser(2).GetOptionalFloat() |
| 577 | + }); |
| 578 | + } |
| 579 | + } |
| 580 | + return execResult; |
| 581 | + })); |
| 582 | + |
| 583 | + return result; |
| 584 | + } |
| 585 | + ``` |
| 586 | + |
| 587 | +- Python (alternative) |
| 588 | + |
| 589 | + ```python |
| 590 | + def search_items_vector_as_float_list( |
368 | 591 | pool: ydb.QuerySessionPool, |
369 | 592 | table_name: str, |
370 | 593 | embedding: list[float], |
|
413 | 636 | return items |
414 | 637 | ``` |
415 | 638 |
|
416 | | -- C++ |
| 639 | +- C++ (alternative) |
417 | 640 |
|
418 | 641 | ```cpp |
419 | | - std::vector<TResultItem> SearchItems( |
| 642 | + std::vector<TResultItem> SearchItemsAsFloatList( |
420 | 643 | NYdb::NQuery::TQueryClient& client, |
421 | 644 | const std::string& tableName, |
422 | 645 | const std::vector<float>& embedding, |
|
535 | 758 | {"id": "9", "document": "vector 9", "embedding": [0.0, 1.0, 0.05]}, |
536 | 759 | ] |
537 | 760 |
|
538 | | - insert_items(pool, table_name, items) |
| 761 | + insert_items_vector_as_bytes(pool, table_name, items) |
539 | 762 |
|
540 | | - items = search_items( |
| 763 | + items = search_items_vector_as_bytes( |
541 | 764 | pool, |
542 | 765 | table_name, |
543 | 766 | embedding=[1, 0, 0], |
|
552 | 775 | table_name, |
553 | 776 | index_name=index_name, |
554 | 777 | strategy="similarity=cosine", |
555 | | - dim=3, |
| 778 | + dimension=3, |
556 | 779 | levels=1, |
557 | 780 | clusters=3, |
558 | 781 | ) |
559 | 782 |
|
560 | | - items = search_items( |
| 783 | + items = search_items_vector_as_bytes( |
561 | 784 | pool, |
562 | 785 | table_name, |
563 | 786 | embedding=[1, 0, 0], |
|
639 | 862 | {.Id = "8", .Document = "document 8", .Embedding = {0.02, 0.98, 0.1}}, |
640 | 863 | {.Id = "9", .Document = "document 9", .Embedding = {0.0, 1.0, 0.05}}, |
641 | 864 | }; |
642 | | - InsertItems(client, tableName, items); |
643 | | - PrintResults(SearchItems(client, tableName, {1.0, 0.0, 0.0}, "CosineSimilarity", 3)); |
| 865 | + InsertItemsAsBytes(client, tableName, items); |
| 866 | + PrintResults(SearchItemsAsBytes(client, tableName, {1.0, 0.0, 0.0}, "CosineSimilarity", 3)); |
644 | 867 | AddIndex(driver, client, database, tableName, indexName, "similarity=cosine", 3, 1, 3); |
645 | | - PrintResults(SearchItems(client, tableName, {1.0, 0.0, 0.0}, "CosineSimilarity", 3, indexName)); |
| 868 | + PrintResults(SearchItemsAsBytes(client, tableName, {1.0, 0.0, 0.0}, "CosineSimilarity", 3, indexName)); |
646 | 869 | } catch (const std::exception& e) { |
647 | 870 | std::cerr << "Execution failed: " << e.what() << std::endl; |
648 | 871 | } |
|
0 commit comments