Skip to content

Commit b761598

Browse files
vgvolegblinkov
authored andcommitted
Update vector search recipe to vector as bytes (#22703)
Co-authored-by: Ivan Blinkov <ivan@blinkov.ru> Co-authored-by: Ivan Blinkov <ivan@ydb.tech> (cherry picked from commit 4fa3b7a)
1 parent 0c54d4f commit b761598

File tree

1 file changed

+239
-16
lines changed

1 file changed

+239
-16
lines changed

ydb/docs/ru/core/recipes/ydb-sdk/vector-search.md

Lines changed: 239 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -116,21 +116,140 @@
116116

117117
## Вставка векторов {#insert-vectors}
118118

119-
Для вставки векторов необходимо подготовить правильный YQL-запрос. Для унификации вставки разных данных он параметризован.
120-
В готовом YQL-запросе важно корректно преобразовать вектор в тип `String`. Для этого используется [функция преобразования](../../yql/reference/udf/list/knn.md#functions-convert) вектора в бинарное представление.
119+
Для вставки векторов необходимо подготовить и выполнить правильный YQL-запрос. Для унификации вставки разных данных он параметризован.
121120

122-
Запрос оперирует контейнерным типом данных `List<Struct<...>>` (список структур), что позволяет передавать произвольное количество объектов за один раз.
121+
Запрос оперирует контейнерным типом данных `List<Struct<...>>` (список структур), что позволяет передавать через параметры произвольное количество объектов за один раз.
122+
123+
В {{ ydb-short-name }} таблицах же вектора хранятся в виде сериализованной последовательности байт. Конвертацию в такое представление **рекомендуется выполнять на клиенте**. Альтернативный способ — делегировать конвертацию на сервер с помощью функции преобразования [Knn UDF](../../yql/reference/udf/list/knn.md#functions-convert). Ниже будут приведены примеры, демонстрирующие оба подхода.
123124

124125
{% list tabs %}
125126

126127
- Python
127128

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+
128247
Метод принимает массив словарей `items`, где каждый словарь содержит поля `id` - идентификатор, `document` - текст, `embedding` - векторное представление текста.
129248

130249
Для использования структуры в примере ниже создается `items_struct_type = ydb.StructType()`, в котором задаются типы всех полей. Для передачи списка таких структур его необходимо обернуть в `ydb.ListType`: `ydb.ListType(items_struct_type)`.
131250

132251
```python
133-
def insert_items(
252+
def insert_items_vector_as_float_list(
134253
pool: ydb.QuerySessionPool,
135254
table_name: str,
136255
items: list[dict],
@@ -167,10 +286,10 @@
167286
print(f"{len(items)} items inserted")
168287
```
169288

170-
- C++
289+
- C++ (альтернативный)
171290

172291
```cpp
173-
void InsertItems(
292+
void InsertItemsAsFloatList(
174293
NYdb::NQuery::TQueryClient& client,
175294
const std::string& tableName,
176295
const std::vector<TItem>& items)
@@ -364,7 +483,111 @@
364483
- Python
365484

366485
```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(
368591
pool: ydb.QuerySessionPool,
369592
table_name: str,
370593
embedding: list[float],
@@ -413,10 +636,10 @@
413636
return items
414637
```
415638

416-
- C++
639+
- C++ (alternative)
417640

418641
```cpp
419-
std::vector<TResultItem> SearchItems(
642+
std::vector<TResultItem> SearchItemsAsFloatList(
420643
NYdb::NQuery::TQueryClient& client,
421644
const std::string& tableName,
422645
const std::vector<float>& embedding,
@@ -535,9 +758,9 @@
535758
{"id": "9", "document": "vector 9", "embedding": [0.0, 1.0, 0.05]},
536759
]
537760

538-
insert_items(pool, table_name, items)
761+
insert_items_vector_as_bytes(pool, table_name, items)
539762

540-
items = search_items(
763+
items = search_items_vector_as_bytes(
541764
pool,
542765
table_name,
543766
embedding=[1, 0, 0],
@@ -552,12 +775,12 @@
552775
table_name,
553776
index_name=index_name,
554777
strategy="similarity=cosine",
555-
dim=3,
778+
dimension=3,
556779
levels=1,
557780
clusters=3,
558781
)
559782

560-
items = search_items(
783+
items = search_items_vector_as_bytes(
561784
pool,
562785
table_name,
563786
embedding=[1, 0, 0],
@@ -639,10 +862,10 @@
639862
{.Id = "8", .Document = "document 8", .Embedding = {0.02, 0.98, 0.1}},
640863
{.Id = "9", .Document = "document 9", .Embedding = {0.0, 1.0, 0.05}},
641864
};
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));
644867
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));
646869
} catch (const std::exception& e) {
647870
std::cerr << "Execution failed: " << e.what() << std::endl;
648871
}

0 commit comments

Comments
 (0)