Skip to content

Commit 00e29cd

Browse files
authored
feat: support partial array upsert ops (#553)
Signed-off-by: ryjiang <jiangruiyi@gmail.com>
1 parent bac7691 commit 00e29cd

9 files changed

Lines changed: 465 additions & 17 deletions

File tree

3.0_change.md

Lines changed: 18 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -164,17 +164,28 @@ message UpsertRequest {
164164
}
165165
```
166166

167-
Node SDK impact:
167+
Node SDK support:
168168

169-
```text
170-
milvus/types/Data.ts or upsert request types
171-
- Add field_ops support.
172-
- Expose op values REPLACE, ARRAY_APPEND, ARRAY_REMOVE.
169+
```typescript
170+
import { FieldPartialUpdateOpType } from '@zilliz/milvus2-sdk-node';
173171

174-
milvus/grpc/Data.ts and request preparation logic
175-
- Pass field_ops through to UpsertRequest.
172+
await client.upsert({
173+
collection_name: 'my_collection',
174+
data: [{ id: 1, tags: ['new'], scores: [3] }],
175+
field_ops: [
176+
{ field_name: 'tags', op: FieldPartialUpdateOpType.ARRAY_APPEND },
177+
{ field_name: 'scores', op: 'ARRAY_REMOVE' },
178+
],
179+
});
176180
```
177181

182+
Implementation notes:
183+
184+
- `UpsertReq.field_ops` is passed through to `UpsertRequest.field_ops`.
185+
- `FieldPartialUpdateOpType` exposes `REPLACE`, `ARRAY_APPEND`, and `ARRAY_REMOVE`.
186+
- Numeric enum values are converted to proto enum names before sending the gRPC request.
187+
- Providing `field_ops` automatically sends `partial_update: true`.
188+
178189
## Element-Level Query/Search
179190

180191
`common.proto` adds element-level placeholder support:

README.md

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -184,6 +184,25 @@ await client.upsert({
184184
collection_name: string;
185185
data: Record<string, any>[];
186186
partition_name?: string;
187+
partial_update?: boolean;
188+
field_ops?: Array<{
189+
field_name: string;
190+
op: 'REPLACE' | 'ARRAY_APPEND' | 'ARRAY_REMOVE';
191+
}>;
192+
});
193+
```
194+
195+
Partial array upsert can append to or remove from existing Array fields:
196+
197+
```typescript
198+
import { FieldPartialUpdateOpType } from '@zilliz/milvus2-sdk-node';
199+
200+
await client.upsert({
201+
collection_name: 'articles',
202+
data: [{ id: 1, tags: ['featured'] }],
203+
field_ops: [
204+
{ field_name: 'tags', op: FieldPartialUpdateOpType.ARRAY_APPEND },
205+
],
187206
});
188207
```
189208

docs/content/operations/data-operations-insert.mdx

Lines changed: 53 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -69,7 +69,9 @@ Insert large amounts of data efficiently:
6969

7070
```javascript
7171
const batchSize = 1000;
72-
const allData = [/* large array of data */];
72+
const allData = [
73+
/* large array of data */
74+
];
7375

7476
for (let i = 0; i < allData.length; i += batchSize) {
7577
const batch = allData.slice(i, i + batchSize);
@@ -196,6 +198,49 @@ await client.upsert({
196198
});
197199
```
198200

201+
### Partial Array Upsert
202+
203+
For Array fields, use `field_ops` to append values to or remove values from the existing array instead of replacing the whole field.
204+
205+
```javascript
206+
import { FieldPartialUpdateOpType } from '@zilliz/milvus2-sdk-node';
207+
208+
await client.upsert({
209+
collection_name: 'my_collection',
210+
data: [
211+
{
212+
id: 1,
213+
tags: ['new-tag'], // Appended to the existing tags array
214+
scores: [3], // Removed from the existing scores array
215+
},
216+
],
217+
field_ops: [
218+
{ field_name: 'tags', op: FieldPartialUpdateOpType.ARRAY_APPEND },
219+
{ field_name: 'scores', op: FieldPartialUpdateOpType.ARRAY_REMOVE },
220+
],
221+
});
222+
```
223+
224+
You can also pass operation names as strings:
225+
226+
```javascript
227+
await client.upsert({
228+
collection_name: 'my_collection',
229+
data: [{ id: 1, tags: ['urgent'] }],
230+
field_ops: [{ field_name: 'tags', op: 'ARRAY_APPEND' }],
231+
});
232+
```
233+
234+
Notes:
235+
236+
- `field_ops` is supported only by `upsert()`.
237+
- Each `field_ops[].field_name` must refer to an Array field.
238+
- The primary key must be included in each upsert row.
239+
- Fields not included in the upsert row remain unchanged.
240+
- `ARRAY_APPEND` must not make the array exceed its `max_capacity`.
241+
- `ARRAY_REMOVE` removes matching values from the existing array.
242+
- When `field_ops` is provided, the SDK automatically sends `partial_update: true`.
243+
199244
## Insert Transformers
200245

201246
For Float16 and BFloat16 vectors, use transformers:
@@ -211,7 +256,7 @@ await client.insert({
211256
},
212257
],
213258
transformers: {
214-
vector: (data) => f32ArrayToF16Bytes(data), // Convert to f16 bytes
259+
vector: data => f32ArrayToF16Bytes(data), // Convert to f16 bytes
215260
},
216261
});
217262
```
@@ -223,7 +268,9 @@ The insert operation returns mutation results:
223268
```javascript
224269
const result = await client.insert({
225270
collection_name: 'my_collection',
226-
data: [/* ... */],
271+
data: [
272+
/* ... */
273+
],
227274
});
228275

229276
console.log('IDs:', result.IDs); // Array of inserted IDs
@@ -251,7 +298,9 @@ Handle insertion errors:
251298
try {
252299
await client.insert({
253300
collection_name: 'my_collection',
254-
data: [/* ... */],
301+
data: [
302+
/* ... */
303+
],
255304
});
256305
} catch (error) {
257306
if (error.message.includes('field does not exist')) {
@@ -335,4 +384,3 @@ await client.flush({
335384
- Learn about [Query Operations](./data-operations-query)
336385
- Explore [Delete Operations](./data-operations-delete)
337386
- Check out [Data Management](./data-management)
338-

docs/content/reference/api-reference.mdx

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -302,6 +302,42 @@ Upsert data (insert or update).
302302
upsert(data: UpsertReq): Promise<MutationResult>
303303
```
304304

305+
**Parameters**:
306+
307+
- `collection_name`: Collection name
308+
- `data` or `fields_data`: Array of row objects
309+
- `partition_name?`: Partition name
310+
- `partial_update?`: Whether to update only provided fields
311+
- `field_ops?`: Per-field partial update operations for Array fields. Providing `field_ops` automatically enables `partial_update`.
312+
313+
```typescript
314+
type FieldPartialUpdateOp = {
315+
field_name: string;
316+
op:
317+
| FieldPartialUpdateOpType.REPLACE
318+
| FieldPartialUpdateOpType.ARRAY_APPEND
319+
| FieldPartialUpdateOpType.ARRAY_REMOVE
320+
| 'REPLACE'
321+
| 'ARRAY_APPEND'
322+
| 'ARRAY_REMOVE';
323+
};
324+
```
325+
326+
Example:
327+
328+
```typescript
329+
import { FieldPartialUpdateOpType } from '@zilliz/milvus2-sdk-node';
330+
331+
await client.upsert({
332+
collection_name: 'my_collection',
333+
data: [{ id: 1, tags: ['new'], scores: [3] }],
334+
field_ops: [
335+
{ field_name: 'tags', op: FieldPartialUpdateOpType.ARRAY_APPEND },
336+
{ field_name: 'scores', op: 'ARRAY_REMOVE' },
337+
],
338+
});
339+
```
340+
305341
### query
306342

307343
Query entities by filter.

milvus/const/milvus.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -304,6 +304,13 @@ export enum DataType {
304304
Struct = 201,
305305
}
306306

307+
// partial update operation enum for array fields in upsert
308+
export enum FieldPartialUpdateOpType {
309+
REPLACE = 0,
310+
ARRAY_APPEND = 1,
311+
ARRAY_REMOVE = 2,
312+
}
313+
307314
export enum PlaceholderType {
308315
None = 0,
309316
BinaryVector = 100,

milvus/grpc/Data.ts

Lines changed: 41 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,8 @@ import {
7979
GetQuerySegmentInfoResponse,
8080
SearchData,
8181
FloatVector,
82+
FieldPartialUpdateOpType,
83+
FieldPartialUpdateOp,
8284
} from '../';
8385
import { Collection } from './Collection';
8486

@@ -144,6 +146,9 @@ export class Data extends Collection {
144146
throw new Error(ERROR_REASONS.INSERT_CHECK_FIELD_DATA_IS_REQUIRED);
145147
}
146148
const { collection_name } = data;
149+
const fieldOps = upsert
150+
? this.normalizeFieldOps((data as UpsertReq).field_ops)
151+
: [];
147152

148153
const describeReq = { collection_name, cache: enable_cache };
149154
if (data.db_name) {
@@ -234,7 +239,8 @@ export class Data extends Collection {
234239

235240
// Track which fields are present in the original row data for partial updates
236241
const isPartialUpdate =
237-
'partial_update' in data && data.partial_update === true;
242+
('partial_update' in data && data.partial_update === true) ||
243+
fieldOps.length > 0;
238244
const originalRowKeys = new Set<string>();
239245

240246
// Loop through each row and set the corresponding field values in the Map.
@@ -297,10 +303,11 @@ export class Data extends Collection {
297303
schema_timestamp:
298304
collectionInfo.update_timestamp_str ||
299305
(collectionInfo.update_timestamp as string | number | undefined),
300-
// Ensure partial_update is passed for upsert operations
301-
...(upsert && (data as UpsertReq).partial_update
302-
? { partial_update: true }
303-
: {}),
306+
// Ensure partial_update is passed for explicit partial updates and
307+
// non-REPLACE field_ops. Milvus server also auto-promotes non-REPLACE
308+
// ops, but setting the flag keeps the request self-descriptive.
309+
...(upsert && isPartialUpdate ? { partial_update: true } : {}),
310+
...(upsert && fieldOps.length > 0 ? { field_ops: fieldOps } : {}),
304311
};
305312
/* istanbul ignore next if */
306313
if (data.skip_check_schema) {
@@ -529,6 +536,35 @@ export class Data extends Collection {
529536
return promise;
530537
}
531538

539+
private normalizeFieldOps(
540+
fieldOps?: FieldPartialUpdateOp[]
541+
): { field_name: string; op: keyof typeof FieldPartialUpdateOpType }[] {
542+
if (!fieldOps || fieldOps.length === 0) {
543+
return [];
544+
}
545+
546+
return fieldOps.map(({ field_name, op }) => {
547+
if (!field_name) {
548+
throw new Error('field_ops field_name is required');
549+
}
550+
551+
if (typeof op === 'number') {
552+
const opName = FieldPartialUpdateOpType[op] as
553+
| keyof typeof FieldPartialUpdateOpType
554+
| undefined;
555+
if (typeof opName === 'undefined') {
556+
throw new Error(`unsupported field partial update op: ${op}`);
557+
}
558+
return { field_name, op: opName };
559+
}
560+
561+
if (typeof FieldPartialUpdateOpType[op] === 'undefined') {
562+
throw new Error(`unsupported field partial update op: ${op}`);
563+
}
564+
return { field_name, op };
565+
});
566+
}
567+
532568
/**
533569
* Delete entities in a Milvus collection.
534570
*

milvus/types/Insert.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ import {
1919
VectorTypes,
2020
BFloat16Vector,
2121
Float16Vector,
22+
FieldPartialUpdateOpType,
2223
} from '../';
2324

2425
// Represents the possible data types for a field(cell)
@@ -72,8 +73,19 @@ interface BaseInsertReq extends collectionNameReq {
7273

7374
// Union type to enforce mutual exclusivity
7475
export type InsertReq = DataInsertReq | FieldsDataInsertReq;
76+
export type FieldPartialUpdateOpName = keyof typeof FieldPartialUpdateOpType;
77+
export type FieldPartialUpdateOpValue =
78+
| FieldPartialUpdateOpType
79+
| FieldPartialUpdateOpName;
80+
81+
export interface FieldPartialUpdateOp {
82+
field_name: string;
83+
op: FieldPartialUpdateOpValue;
84+
}
85+
7586
export type UpsertReq = (DataInsertReq | FieldsDataInsertReq) & {
7687
partial_update?: boolean;
88+
field_ops?: FieldPartialUpdateOp[];
7789
};
7890

7991
// Variant with data property

0 commit comments

Comments
 (0)