@@ -446,22 +446,23 @@ impl<const N: usize, E: Embedder, S: NodeStorage<N>> TextIndex<N, E, S> {
446446 /// `Chunker` into one or more chunks; each chunk is embedded and stored
447447 /// under its own chunk-id.
448448 ///
449- /// **Re-insert semantics:** existing chunks for this `id` are removed
450- /// first, so that switching to a chunker that produces fewer chunks
451- /// doesn't leak stale ones.
449+ /// **Re-insert semantics:** replacement chunks are embedded first, then
450+ /// existing chunks for this `id` are removed, so a failed embed leaves the
451+ /// previous document searchable. A successful empty chunk set removes the
452+ /// document from the index.
452453 pub fn insert ( & mut self , id : & [ u8 ] , text : & str ) -> Result < ( ) , TextIndexError > {
453- // Drop any previous chunks for this doc_id so the new chunk count
454- // overwrites cleanly (handles "re-chunk after switching chunkers"
455- // as well as plain upserts).
456- self . delete_chunks_for_doc ( id) ;
457-
458454 let chunks = self . chunker . split ( text) ;
459455 if chunks. is_empty ( ) {
460456 // The chunker explicitly opted out (e.g. an empty document under
461457 // LineChunker). Treat as "don't index" rather than as an error —
462458 // matches the cascade transformer's `None` semantics.
459+ self . delete_chunks_for_doc ( id) ;
463460 return Ok ( ( ) ) ;
464461 }
462+ if self . embedder . dim ( ) == 0 {
463+ return Err ( TextIndexError :: Proximity ( ProximityError :: ZeroDim ) ) ;
464+ }
465+ let mut replacements = Vec :: with_capacity ( chunks. len ( ) ) ;
465466 for ( idx, chunk_text) in chunks. iter ( ) . enumerate ( ) {
466467 let vec = self . embedder . embed ( chunk_text) ?;
467468 if vec. len ( ) != usize:: from ( self . embedder . dim ( ) ) {
@@ -471,6 +472,11 @@ impl<const N: usize, E: Embedder, S: NodeStorage<N>> TextIndex<N, E, S> {
471472 } ) ) ;
472473 }
473474 let chunk_id = make_chunk_id ( id, idx as u32 ) ;
475+ replacements. push ( ( chunk_id, vec) ) ;
476+ }
477+
478+ self . delete_chunks_for_doc ( id) ;
479+ for ( chunk_id, vec) in replacements {
474480 self . inner . insert ( chunk_id, vec) ?;
475481 }
476482 Ok ( ( ) )
@@ -604,6 +610,42 @@ mod tests {
604610 TextIndexConfig :: new ( HashEmbedder :: new ( dim, 0 ) )
605611 }
606612
613+ #[ derive( Debug , Clone ) ]
614+ struct FailsOnNeedleEmbedder {
615+ inner : HashEmbedder ,
616+ needle : & ' static str ,
617+ }
618+
619+ impl FailsOnNeedleEmbedder {
620+ fn new ( dim : u16 , needle : & ' static str ) -> Self {
621+ Self {
622+ inner : HashEmbedder :: new ( dim, 0 ) ,
623+ needle,
624+ }
625+ }
626+ }
627+
628+ impl Embedder for FailsOnNeedleEmbedder {
629+ fn id ( & self ) -> & str {
630+ self . inner . id ( )
631+ }
632+
633+ fn version ( & self ) -> & str {
634+ self . inner . version ( )
635+ }
636+
637+ fn dim ( & self ) -> u16 {
638+ self . inner . dim ( )
639+ }
640+
641+ fn embed ( & self , text : & str ) -> Result < Vec < f32 > , EmbedError > {
642+ if text. contains ( self . needle ) {
643+ return Err ( EmbedError :: Failure ( "forced insert failure" . to_string ( ) ) ) ;
644+ }
645+ self . inner . embed ( text)
646+ }
647+ }
648+
607649 #[ test]
608650 fn insert_and_search_finds_exact_match ( ) {
609651 let storage = InMemoryNodeStorage :: < 32 > :: new ( ) ;
@@ -623,6 +665,23 @@ mod tests {
623665 ) ;
624666 }
625667
668+ #[ test]
669+ fn failed_reinsert_preserves_existing_document ( ) {
670+ let storage = InMemoryNodeStorage :: < 32 > :: new ( ) ;
671+ let mut idx = TextIndex :: new (
672+ storage,
673+ TextIndexConfig :: new ( FailsOnNeedleEmbedder :: new ( 8 , "fail-insert" ) ) ,
674+ ) ;
675+ idx. insert ( b"doc:1" , "stable text" ) . unwrap ( ) ;
676+
677+ let err = idx. insert ( b"doc:1" , "please fail-insert" ) . unwrap_err ( ) ;
678+ assert ! ( err. to_string( ) . contains( "forced insert failure" ) ) ;
679+
680+ let hits = idx. search ( "stable text" , 1 ) . unwrap ( ) ;
681+ assert_eq ! ( hits[ 0 ] . id, b"doc:1" . to_vec( ) ) ;
682+ assert ! ( hits[ 0 ] . score < 1e-4 ) ;
683+ }
684+
626685 #[ test]
627686 fn delete_removes_document_from_search ( ) {
628687 let storage = InMemoryNodeStorage :: < 32 > :: new ( ) ;
0 commit comments