@@ -685,6 +685,126 @@ func TestSearchService_RemoveNode(t *testing.T) {
685685 }
686686}
687687
688+ // TestSearchService_RemoveNode_DecrementsEmbeddingCount verifies that removing a node
689+ // decrements the embedding count in stats. This is critical for ensuring that when nodes
690+ // are deleted via Cypher, the embedding count is updated correctly without requiring
691+ // a manual "regenerate" operation.
692+ func TestSearchService_RemoveNode_DecrementsEmbeddingCount (t * testing.T ) {
693+ engine := storage .NewMemoryEngine ()
694+ defer engine .Close ()
695+
696+ svc := NewServiceWithDimensions (engine , 4 ) // 4-dimensional embeddings for test
697+
698+ // Initial state: no embeddings
699+ assert .Equal (t , 0 , svc .EmbeddingCount (), "Should start with 0 embeddings" )
700+
701+ // Create and index 3 nodes with embeddings
702+ // Note: Node.Embedding is the struct field used by IndexNode, not Properties["embedding"]
703+ nodes := []* storage.Node {
704+ {
705+ ID : "node1" ,
706+ Labels : []string {"Person" },
707+ Properties : map [string ]any {"name" : "Alice" },
708+ Embedding : []float32 {1 , 0 , 0 , 0 },
709+ },
710+ {
711+ ID : "node2" ,
712+ Labels : []string {"Person" },
713+ Properties : map [string ]any {"name" : "Bob" },
714+ Embedding : []float32 {0 , 1 , 0 , 0 },
715+ },
716+ {
717+ ID : "node3" ,
718+ Labels : []string {"Person" },
719+ Properties : map [string ]any {"name" : "Charlie" },
720+ Embedding : []float32 {0 , 0 , 1 , 0 },
721+ },
722+ }
723+
724+ for _ , node := range nodes {
725+ require .NoError (t , engine .CreateNode (node ))
726+ require .NoError (t , svc .IndexNode (node ))
727+ }
728+
729+ // Verify all 3 nodes are indexed
730+ assert .Equal (t , 3 , svc .EmbeddingCount (), "Should have 3 embeddings after indexing" )
731+
732+ // Remove node1
733+ require .NoError (t , svc .RemoveNode ("node1" ))
734+ assert .Equal (t , 2 , svc .EmbeddingCount (), "Should have 2 embeddings after removing node1" )
735+
736+ // Remove node2
737+ require .NoError (t , svc .RemoveNode ("node2" ))
738+ assert .Equal (t , 1 , svc .EmbeddingCount (), "Should have 1 embedding after removing node2" )
739+
740+ // Remove node3
741+ require .NoError (t , svc .RemoveNode ("node3" ))
742+ assert .Equal (t , 0 , svc .EmbeddingCount (), "Should have 0 embeddings after removing all nodes" )
743+
744+ // Removing non-existent node should not affect count
745+ require .NoError (t , svc .RemoveNode ("non-existent" ))
746+ assert .Equal (t , 0 , svc .EmbeddingCount (), "Count should remain 0 after removing non-existent node" )
747+ }
748+
749+ // TestSearchService_RemoveNode_OnlyRemovesTargetNode ensures RemoveNode is precise
750+ // and doesn't affect other nodes' embeddings.
751+ func TestSearchService_RemoveNode_OnlyRemovesTargetNode (t * testing.T ) {
752+ engine := storage .NewMemoryEngine ()
753+ defer engine .Close ()
754+
755+ svc := NewServiceWithDimensions (engine , 4 ) // 4-dimensional embeddings for test
756+
757+ // Create distinct embeddings for easy search verification
758+ // Note: Node.Embedding is the struct field used by IndexNode
759+ node1 := & storage.Node {
760+ ID : "target-to-remove" ,
761+ Labels : []string {"Document" },
762+ Properties : map [string ]any {"content" : "unique alpha content" },
763+ Embedding : []float32 {1 , 0 , 0 , 0 },
764+ }
765+ node2 := & storage.Node {
766+ ID : "should-remain-1" ,
767+ Labels : []string {"Document" },
768+ Properties : map [string ]any {"content" : "unique beta content" },
769+ Embedding : []float32 {0 , 1 , 0 , 0 },
770+ }
771+ node3 := & storage.Node {
772+ ID : "should-remain-2" ,
773+ Labels : []string {"Document" },
774+ Properties : map [string ]any {"content" : "unique gamma content" },
775+ Embedding : []float32 {0 , 0 , 1 , 0 },
776+ }
777+
778+ for _ , node := range []* storage.Node {node1 , node2 , node3 } {
779+ require .NoError (t , engine .CreateNode (node ))
780+ require .NoError (t , svc .IndexNode (node ))
781+ }
782+
783+ assert .Equal (t , 3 , svc .EmbeddingCount ())
784+
785+ // Remove only the target node
786+ require .NoError (t , svc .RemoveNode ("target-to-remove" ))
787+
788+ // Verify count decreased
789+ assert .Equal (t , 2 , svc .EmbeddingCount ())
790+
791+ // Verify remaining nodes are still searchable
792+ opts := DefaultSearchOptions ()
793+
794+ // Search for remaining nodes by their unique content
795+ response , err := svc .Search (context .Background (), "beta" , nil , opts )
796+ require .NoError (t , err )
797+ found := false
798+ for _ , r := range response .Results {
799+ if r .ID == "should-remain-1" {
800+ found = true
801+ }
802+ // The removed node should NOT appear
803+ assert .NotEqual (t , "target-to-remove" , r .ID , "Removed node should not appear in results" )
804+ }
805+ assert .True (t , found , "Remaining node 'should-remain-1' should be searchable" )
806+ }
807+
688808// TestSearchService_HybridSearch tests the hybrid RRF search.
689809func TestSearchService_HybridSearch (t * testing.T ) {
690810 engine := storage .NewMemoryEngine ()
0 commit comments