@@ -550,19 +550,151 @@ pub const Collection = struct {
550550 key : []const u8 ,
551551 value : []const u8 ,
552552 };
553+
554+ const BatchPreparedDoc = struct {
555+ key : []const u8 ,
556+ value : []const u8 ,
557+ key_hash : u64 ,
558+ doc_id : u64 = 0 ,
559+ enc_off : usize ,
560+ enc_len : usize ,
561+ page_no : u32 = 0 ,
562+ page_off : u16 = 0 ,
563+ };
564+
553565 pub fn insertBatch (
554566 self : * Collection ,
555567 items : []const BatchItem ,
556568 /// Optional output. If non-null, must have length >= items.len.
557569 out_doc_ids : ? []u64 ,
558570 ) ! usize {
559- var inserted : usize = 0 ;
571+ if (items .len == 0 ) return 0 ;
572+ if (out_doc_ids ) | ids | {
573+ if (ids .len < items .len ) return error .OutputTooSmall ;
574+ }
575+
576+ var prepared : std .ArrayList (BatchPreparedDoc ) = .empty ;
577+ defer prepared .deinit (self .alloc );
578+ var encoded : std .ArrayList (u8 ) = .empty ;
579+ defer encoded .deinit (self .alloc );
580+ var btree_entries : std .ArrayList (BTreeEntry ) = .empty ;
581+ defer btree_entries .deinit (self .alloc );
582+
583+ try prepared .ensureTotalCapacity (self .alloc , items .len );
584+ try btree_entries .ensureTotalCapacity (self .alloc , items .len );
585+
586+ var estimated_bytes : usize = 0 ;
560587 for (items ) | item | {
561- const id = try self .insert (item .key , item .value );
562- if (out_doc_ids ) | ids | ids [inserted ] = id ;
563- inserted += 1 ;
588+ const key_value_size = try std .math .add (usize , item .key .len , item .value .len );
589+ const total_size = try std .math .add (usize , DocHeader .size , key_value_size );
590+ if (total_size > page_mod .PAGE_USABLE ) return error .PageFull ;
591+ estimated_bytes = try std .math .add (usize , estimated_bytes , total_size );
592+ prepared .appendAssumeCapacity (.{
593+ .key = item .key ,
594+ .value = item .value ,
595+ .key_hash = doc_mod .fnv1a (item .key ),
596+ .enc_off = 0 ,
597+ .enc_len = total_size ,
598+ });
599+ }
600+ try encoded .ensureTotalCapacity (self .alloc , estimated_bytes );
601+
602+ var locked_stripes = [_ ]bool {false } ** STRIPE_COUNT ;
603+ for (prepared .items ) | item | {
604+ locked_stripes [stripeIndex (item .key_hash )] = true ;
605+ }
606+ for (locked_stripes , 0.. ) | locked , stripe | {
607+ if (locked ) self .stripe_locks [stripe ].lockUncancelable (runtime .io );
608+ }
609+ defer {
610+ var stripe = locked_stripes .len ;
611+ while (stripe > 0 ) {
612+ stripe -= 1 ;
613+ if (locked_stripes [stripe ]) self .stripe_locks [stripe ].unlock (runtime .io );
614+ }
564615 }
565- return inserted ;
616+
617+ const first_doc_id = self .next_doc_id .fetchAdd (@intCast (prepared .items .len ), .monotonic );
618+ for (prepared .items , 0.. ) | * item , i | {
619+ const doc_id = first_doc_id + @as (u64 , @intCast (i ));
620+ item .doc_id = doc_id ;
621+
622+ const hdr = doc_mod .newHeader (doc_id , item .key , item .value );
623+ const d = Doc { .header = hdr , .key = item .key , .value = item .value };
624+ item .enc_off = encoded .items .len ;
625+ const enc = encoded .addManyAsSliceAssumeCapacity (item .enc_len );
626+ _ = try d .encodeBuf (enc );
627+
628+ const txn = self .wal_log .next_lsn .load (.monotonic );
629+ _ = try self .wal_log .write (txn , .doc_insert , 0 , 0 , enc );
630+
631+ const pno = try self .findOrAllocLeaf (enc .len );
632+ const page_off = self .pf .leafAppend (pno , enc ) orelse return error .PageFull ;
633+ item .page_no = pno ;
634+ item .page_off = page_off ;
635+
636+ btree_entries .appendAssumeCapacity (.{
637+ .key_hash = item .key_hash ,
638+ .doc_id = doc_id ,
639+ .page_no = pno ,
640+ .page_off = page_off ,
641+ });
642+ }
643+
644+ try self .idx .insertMany (btree_entries .items );
645+
646+ for (prepared .items , btree_entries .items , 0.. ) | item , entry , i | {
647+ self .hash_idx .put (item .key_hash , entry ) catch {};
648+
649+ const epoch = self .epochs .advance ();
650+ self .versions .appendVersion (self .alloc , item .doc_id , entry .page_no , entry .page_off , epoch ) catch {};
651+ self .key_doc_ids .put (item .key_hash , item .doc_id ) catch {};
652+ if (out_doc_ids ) | ids | ids [i ] = item .doc_id ;
653+
654+ if (self .shouldIndexText (item .value )) {
655+ if (self .index_thread == null ) {
656+ self .tri .indexFile (item .key , item .value ) catch {};
657+ self .words .indexFile (item .key , item .value ) catch {};
658+ } else {
659+ const q = & self .index_queue ;
660+ if (! q .push (item .key , item .value )) {
661+ self .tri .indexFile (item .key , item .value ) catch {};
662+ self .words .indexFile (item .key , item .value ) catch {};
663+ } else {
664+ _ = self .index_wake .fetchAdd (1 , .release );
665+ runtime .io .futexWake (u32 , & self .index_wake .raw , 1 );
666+ }
667+ }
668+ }
669+
670+ if (self .vectors ) | vc | {
671+ const field = self .vector_field [0.. self .vector_field_len ];
672+ const dims : usize = vc .dims ;
673+ var embed_stack : [4096 ]f32 = undefined ;
674+ const emb = if (dims <= 4096 ) embed_stack [0.. dims ] else blk : {
675+ break :blk self .alloc .alloc (f32 , dims ) catch null ;
676+ };
677+ if (emb ) | e | {
678+ defer if (dims > 4096 ) self .alloc .free (e );
679+ if (extractJsonFloatArray (item .value , field , e )) | count | {
680+ if (count == dims ) {
681+ vc .append (self .alloc , e ) catch {};
682+ self .vec_entries .append (self .alloc , entry ) catch {};
683+ }
684+ }
685+ }
686+ }
687+
688+ emitChange (self , .insert , item .key , item .value , item .doc_id );
689+ }
690+
691+ const inserted_count : u64 = @intCast (prepared .items .len );
692+ const old_gc_counter = self .gc_counter .fetchAdd (inserted_count , .monotonic );
693+ if (old_gc_counter / GC_INTERVAL != (old_gc_counter + inserted_count ) / GC_INTERVAL ) {
694+ _ = self .gcVersions ();
695+ }
696+
697+ return prepared .items .len ;
566698 }
567699 pub fn insertWithEmbedding (self : * Collection , key : []const u8 , value : []const u8 , embedding : []const f32 ) ! u64 {
568700 const doc_id = self .insert (key , value ) catch | e | return e ;
0 commit comments