// Integrationstest (SRC-03): echte Manticore-Instanz, TEST_MANTICORE_URL // (gleiche Konvention wie integration_test.go). package search import ( "context" "fmt" "os" "testing" "time" ) // TestSearch_RankingReflectsFieldWeightAndIsTraceable ist die geforderte // Funktionsprüfung zu Akzeptanzkriterium 1: Treffer sind nach Relevanz // sortiert, und das Ranking ist nachvollziehbar — ein Treffer im höher // gewichteten Betrefffeld liegt vor einem Treffer, der den Suchbegriff // nur im niedriger gewichteten Anhangstext enthält. func TestSearch_RankingReflectsFieldWeightAndIsTraceable(t *testing.T) { client := setupClient(t) ctx := context.Background() tenant := "mandant-src03-ranking" if err := client.Index(ctx, Document{ ID: DocumentID(tenant, "msg-ranking-subject"), TenantSlug: tenant, MessageID: "msg-ranking-subject", Subject: "Vertragsentwurf", Body: "siehe Anhang", AttachmentText: "", SentAtUnixEpoch: 1000, }); err != nil { t.Fatalf("index: %v", err) } if err := client.Index(ctx, Document{ ID: DocumentID(tenant, "msg-ranking-attachment"), TenantSlug: tenant, MessageID: "msg-ranking-attachment", Subject: "Wochenrückblick", Body: "allgemeine Notizen", AttachmentText: "Im Anhang findet sich ein Vertragsentwurf zur Prüfung.", SentAtUnixEpoch: 2000, }); err != nil { t.Fatalf("index: %v", err) } results, err := client.Search(ctx, tenant, "Vertragsentwurf") if err != nil { t.Fatalf("search: %v", err) } if len(results) != 2 { t.Fatalf("erwartete 2 treffer, habe %d: %+v", len(results), results) } if results[0].MessageID != "msg-ranking-subject" { t.Fatalf("erwartete höher gewichteten betreff-treffer zuerst, habe: %+v", results) } if results[0].Score <= results[1].Score { t.Fatalf("erwartete nachvollziehbar höheren score für betreff-treffer: %+v", results) } } // TestSearch_PhraseAndExclusionOperators ist die geforderte // Pflichtprüfung 2: Phrasensuche und Ausschlussoperator liefern erwartete // Teilmengen. func TestSearch_PhraseAndExclusionOperators(t *testing.T) { client := setupClient(t) ctx := context.Background() tenant := "mandant-src03-operatoren" docs := []Document{ {MessageID: "msg-op-umsatz-verlust", Subject: "Umsatz Verlust", Body: "Verlust im dritten Quartal, kein Umsatzwachstum"}, {MessageID: "msg-op-umsatz-nur", Subject: "Quartalsbericht Umsatz", Body: "hoher Umsatz im dritten Quartal"}, {MessageID: "msg-op-anderes-thema", Subject: "Betriebsausflug", Body: "Planung für den nächsten Betriebsausflug"}, } for _, d := range docs { d.TenantSlug = tenant d.ID = DocumentID(tenant, d.MessageID) if err := client.Index(ctx, d); err != nil { t.Fatalf("index %s: %v", d.MessageID, err) } } phraseResults, err := client.Search(ctx, tenant, `"dritten Quartal"`) if err != nil { t.Fatalf("phrasensuche: %v", err) } phraseIDs := messageIDSet(phraseResults) if !phraseIDs["msg-op-umsatz-verlust"] || !phraseIDs["msg-op-umsatz-nur"] { t.Fatalf("erwartete beide 'dritten Quartal'-treffer, habe: %+v", phraseResults) } if phraseIDs["msg-op-anderes-thema"] { t.Fatalf("unerwarteter treffer ohne die phrase: %+v", phraseResults) } exclusionResults, err := client.Search(ctx, tenant, "Umsatz -Verlust") if err != nil { t.Fatalf("ausschlusssuche: %v", err) } exclusionIDs := messageIDSet(exclusionResults) if !exclusionIDs["msg-op-umsatz-nur"] { t.Fatalf("erwarteter treffer ohne 'Verlust' fehlt: %+v", exclusionResults) } if exclusionIDs["msg-op-umsatz-verlust"] { t.Fatalf("mit -Verlust ausgeschlossener treffer erschien dennoch: %+v", exclusionResults) } } func messageIDSet(results []Result) map[string]bool { set := make(map[string]bool, len(results)) for _, r := range results { set[r.MessageID] = true } return set } // TestSearch_PerformanceWithLargeCorpus ist die geforderte Pflichtprüfung // 3: Performance-Test mit großem Testkorpus bleibt innerhalb Zielzeit. // Zielzeit 500ms für eine Suche über 1000 indexierte Dokumente — großzügig // für die "kleinste Lösung", deckt aber real ab, dass die Suche nicht // linear mit der Korpusgröße spürbar einbricht. func TestSearch_PerformanceWithLargeCorpus(t *testing.T) { if os.Getenv("TEST_MANTICORE_URL") == "" { t.Skip("TEST_MANTICORE_URL nicht gesetzt, Integrationstest übersprungen") } client := setupClient(t) ctx := context.Background() tenant := "mandant-src03-performance" const corpusSize = 1000 for i := 0; i < corpusSize; i++ { messageID := fmt.Sprintf("msg-perf-%d", i) subject := "Alltägliche Nachricht" if i == corpusSize/2 { subject = "Einzigartiges Suchziel Zylotharion" } if err := client.Index(ctx, Document{ ID: DocumentID(tenant, messageID), TenantSlug: tenant, MessageID: messageID, Subject: subject, Body: "Routinetext ohne besonderen Inhalt für Testzwecke.", SentAtUnixEpoch: int64(i), }); err != nil { t.Fatalf("index %s: %v", messageID, err) } } const targetLatency = 500 * time.Millisecond start := time.Now() results, err := client.Search(ctx, tenant, "Zylotharion") elapsed := time.Since(start) if err != nil { t.Fatalf("search: %v", err) } if elapsed > targetLatency { t.Fatalf("suche über %d dokumente dauerte %s, ziel war %s", corpusSize, elapsed, targetLatency) } found := false for _, r := range results { if r.MessageID == fmt.Sprintf("msg-perf-%d", corpusSize/2) { found = true } } if !found { t.Fatal("erwartetes einzigartiges dokument im großen korpus nicht gefunden") } t.Logf("Suche über %d Dokumente: %s (Ziel %s)", corpusSize, elapsed, targetLatency) }