@@ -1006,4 +1006,210 @@ describe('Knowledge Search API Route', () => {
10061006 expect ( mockGenerateSearchEmbedding ) . not . toHaveBeenCalled ( ) // No embedding for tag-only
10071007 } )
10081008 } )
1009+
1010+ describe ( 'Deleted document filtering' , ( ) => {
1011+ it ( 'should exclude results from deleted documents in vector search' , async ( ) => {
1012+ mockGetUserId . mockResolvedValue ( 'user-123' )
1013+
1014+ mockCheckKnowledgeBaseAccess . mockResolvedValue ( {
1015+ hasAccess : true ,
1016+ knowledgeBase : {
1017+ id : 'kb-123' ,
1018+ userId : 'user-123' ,
1019+ name : 'Test KB' ,
1020+ deletedAt : null ,
1021+ } ,
1022+ } )
1023+
1024+ mockHandleVectorOnlySearch . mockResolvedValue ( [
1025+ {
1026+ id : 'chunk-1' ,
1027+ content : 'Content from active document' ,
1028+ documentId : 'doc-active' ,
1029+ chunkIndex : 0 ,
1030+ tag1 : null ,
1031+ tag2 : null ,
1032+ tag3 : null ,
1033+ tag4 : null ,
1034+ tag5 : null ,
1035+ tag6 : null ,
1036+ tag7 : null ,
1037+ distance : 0.2 ,
1038+ knowledgeBaseId : 'kb-123' ,
1039+ } ,
1040+ ] )
1041+
1042+ mockGetQueryStrategy . mockReturnValue ( {
1043+ useParallel : false ,
1044+ distanceThreshold : 1.0 ,
1045+ parallelLimit : 15 ,
1046+ singleQueryOptimized : true ,
1047+ } )
1048+
1049+ mockGenerateSearchEmbedding . mockResolvedValue ( [ 0.1 , 0.2 , 0.3 ] )
1050+ mockGetDocumentNamesByIds . mockResolvedValue ( {
1051+ 'doc-active' : 'Active Document.pdf' ,
1052+ } )
1053+
1054+ const mockTagDefs = {
1055+ select : vi . fn ( ) . mockReturnThis ( ) ,
1056+ from : vi . fn ( ) . mockReturnThis ( ) ,
1057+ where : vi . fn ( ) . mockResolvedValue ( [ ] ) ,
1058+ }
1059+ mockDbChain . select . mockReturnValueOnce ( mockTagDefs )
1060+
1061+ const req = createMockRequest ( 'POST' , {
1062+ knowledgeBaseIds : [ 'kb-123' ] ,
1063+ query : 'test query' ,
1064+ topK : 10 ,
1065+ } )
1066+
1067+ const { POST } = await import ( '@/app/api/knowledge/search/route' )
1068+ const response = await POST ( req )
1069+ const data = await response . json ( )
1070+
1071+ expect ( response . status ) . toBe ( 200 )
1072+ expect ( data . success ) . toBe ( true )
1073+ expect ( data . data . results ) . toHaveLength ( 1 )
1074+ expect ( data . data . results [ 0 ] . documentId ) . toBe ( 'doc-active' )
1075+ expect ( data . data . results [ 0 ] . documentName ) . toBe ( 'Active Document.pdf' )
1076+ } )
1077+
1078+ it ( 'should exclude results from deleted documents in tag search' , async ( ) => {
1079+ mockGetUserId . mockResolvedValue ( 'user-123' )
1080+
1081+ mockCheckKnowledgeBaseAccess . mockResolvedValue ( {
1082+ hasAccess : true ,
1083+ knowledgeBase : {
1084+ id : 'kb-123' ,
1085+ userId : 'user-123' ,
1086+ name : 'Test KB' ,
1087+ deletedAt : null ,
1088+ } ,
1089+ } )
1090+
1091+ mockHandleTagOnlySearch . mockResolvedValue ( [
1092+ {
1093+ id : 'chunk-2' ,
1094+ content : 'Content from active document with tag' ,
1095+ documentId : 'doc-active-tagged' ,
1096+ chunkIndex : 0 ,
1097+ tag1 : 'api' ,
1098+ tag2 : null ,
1099+ tag3 : null ,
1100+ tag4 : null ,
1101+ tag5 : null ,
1102+ tag6 : null ,
1103+ tag7 : null ,
1104+ distance : 0 ,
1105+ knowledgeBaseId : 'kb-123' ,
1106+ } ,
1107+ ] )
1108+
1109+ mockGetQueryStrategy . mockReturnValue ( {
1110+ useParallel : false ,
1111+ distanceThreshold : 1.0 ,
1112+ parallelLimit : 15 ,
1113+ singleQueryOptimized : true ,
1114+ } )
1115+
1116+ mockGetDocumentNamesByIds . mockResolvedValue ( {
1117+ 'doc-active-tagged' : 'Active Tagged Document.pdf' ,
1118+ } )
1119+
1120+ const mockTagDefs = {
1121+ select : vi . fn ( ) . mockReturnThis ( ) ,
1122+ from : vi . fn ( ) . mockReturnThis ( ) ,
1123+ where : vi . fn ( ) . mockResolvedValue ( [ ] ) ,
1124+ }
1125+ mockDbChain . select . mockReturnValueOnce ( mockTagDefs )
1126+
1127+ const req = createMockRequest ( 'POST' , {
1128+ knowledgeBaseIds : [ 'kb-123' ] ,
1129+ filters : { tag1 : 'api' } ,
1130+ topK : 10 ,
1131+ } )
1132+
1133+ const { POST } = await import ( '@/app/api/knowledge/search/route' )
1134+ const response = await POST ( req )
1135+ const data = await response . json ( )
1136+
1137+ expect ( response . status ) . toBe ( 200 )
1138+ expect ( data . success ) . toBe ( true )
1139+ expect ( data . data . results ) . toHaveLength ( 1 )
1140+ expect ( data . data . results [ 0 ] . documentId ) . toBe ( 'doc-active-tagged' )
1141+ expect ( data . data . results [ 0 ] . documentName ) . toBe ( 'Active Tagged Document.pdf' )
1142+ expect ( data . data . results [ 0 ] . metadata ) . toEqual ( { tag1 : 'api' } )
1143+ } )
1144+
1145+ it ( 'should exclude results from deleted documents in combined tag+vector search' , async ( ) => {
1146+ mockGetUserId . mockResolvedValue ( 'user-123' )
1147+
1148+ mockCheckKnowledgeBaseAccess . mockResolvedValue ( {
1149+ hasAccess : true ,
1150+ knowledgeBase : {
1151+ id : 'kb-123' ,
1152+ userId : 'user-123' ,
1153+ name : 'Test KB' ,
1154+ deletedAt : null ,
1155+ } ,
1156+ } )
1157+
1158+ mockHandleTagAndVectorSearch . mockResolvedValue ( [
1159+ {
1160+ id : 'chunk-3' ,
1161+ content : 'Relevant content from active document' ,
1162+ documentId : 'doc-active-combined' ,
1163+ chunkIndex : 0 ,
1164+ tag1 : 'guide' ,
1165+ tag2 : null ,
1166+ tag3 : null ,
1167+ tag4 : null ,
1168+ tag5 : null ,
1169+ tag6 : null ,
1170+ tag7 : null ,
1171+ distance : 0.15 ,
1172+ knowledgeBaseId : 'kb-123' ,
1173+ } ,
1174+ ] )
1175+
1176+ mockGetQueryStrategy . mockReturnValue ( {
1177+ useParallel : false ,
1178+ distanceThreshold : 1.0 ,
1179+ parallelLimit : 15 ,
1180+ singleQueryOptimized : true ,
1181+ } )
1182+
1183+ mockGenerateSearchEmbedding . mockResolvedValue ( [ 0.1 , 0.2 , 0.3 ] )
1184+ mockGetDocumentNamesByIds . mockResolvedValue ( {
1185+ 'doc-active-combined' : 'Active Combined Search.pdf' ,
1186+ } )
1187+
1188+ const mockTagDefs = {
1189+ select : vi . fn ( ) . mockReturnThis ( ) ,
1190+ from : vi . fn ( ) . mockReturnThis ( ) ,
1191+ where : vi . fn ( ) . mockResolvedValue ( [ ] ) ,
1192+ }
1193+ mockDbChain . select . mockReturnValueOnce ( mockTagDefs )
1194+
1195+ const req = createMockRequest ( 'POST' , {
1196+ knowledgeBaseIds : [ 'kb-123' ] ,
1197+ query : 'relevant content' ,
1198+ filters : { tag1 : 'guide' } ,
1199+ topK : 10 ,
1200+ } )
1201+
1202+ const { POST } = await import ( '@/app/api/knowledge/search/route' )
1203+ const response = await POST ( req )
1204+ const data = await response . json ( )
1205+
1206+ expect ( response . status ) . toBe ( 200 )
1207+ expect ( data . success ) . toBe ( true )
1208+ expect ( data . data . results ) . toHaveLength ( 1 )
1209+ expect ( data . data . results [ 0 ] . documentId ) . toBe ( 'doc-active-combined' )
1210+ expect ( data . data . results [ 0 ] . documentName ) . toBe ( 'Active Combined Search.pdf' )
1211+ expect ( data . data . results [ 0 ] . metadata ) . toEqual ( { tag1 : 'guide' } )
1212+ expect ( data . data . results [ 0 ] . similarity ) . toBe ( 0.85 ) // 1 - 0.15 distance
1213+ } )
1214+ } )
10091215} )
0 commit comments