- Added support for Typesense as a search engine alongside Meilisearch and PostgreSQL. - Updated configuration structure to include Typesense parameters in `Config` and `.env.example`. - Enhanced search handler and service to accommodate external search clients and filters. - Implemented new tests for external search clients and search service functionalities. - Updated project checklist to reflect completion of multi-index search features and contextual snippets.
171 lines
4.2 KiB
Go
171 lines
4.2 KiB
Go
package search
|
|
|
|
import (
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/ultisuite/ulti-backend/internal/api/query"
|
|
)
|
|
|
|
func TestNewExternalSearchClient(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
engine string
|
|
opts ServiceOptions
|
|
wantOK bool
|
|
}{
|
|
{
|
|
name: "meilisearch with url",
|
|
engine: "meilisearch",
|
|
opts: ServiceOptions{MeilisearchURL: "http://localhost:7700"},
|
|
wantOK: true,
|
|
},
|
|
{
|
|
name: "meilisearch empty url",
|
|
engine: "meilisearch",
|
|
opts: ServiceOptions{MeilisearchURL: " "},
|
|
wantOK: false,
|
|
},
|
|
{
|
|
name: "typesense with url",
|
|
engine: "typesense",
|
|
opts: ServiceOptions{TypesenseURL: "http://localhost:8108"},
|
|
wantOK: true,
|
|
},
|
|
{
|
|
name: "typesense empty url",
|
|
engine: "typesense",
|
|
opts: ServiceOptions{TypesenseURL: ""},
|
|
wantOK: false,
|
|
},
|
|
{
|
|
name: "postgres engine",
|
|
engine: "postgres",
|
|
opts: ServiceOptions{MeilisearchURL: "http://localhost:7700"},
|
|
wantOK: false,
|
|
},
|
|
{
|
|
name: "unknown engine",
|
|
engine: "elasticsearch",
|
|
opts: ServiceOptions{TypesenseURL: "http://localhost:8108"},
|
|
wantOK: false,
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
got := newExternalSearchClient(tt.engine, tt.opts)
|
|
if tt.wantOK && got == nil {
|
|
t.Fatal("expected non-nil client")
|
|
}
|
|
if !tt.wantOK && got != nil {
|
|
t.Fatal("expected nil client")
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestMapExternalHits(t *testing.T) {
|
|
hits := []map[string]any{
|
|
{
|
|
"id": "mail-42",
|
|
"type": "mail",
|
|
"title": "Invoice from Acme",
|
|
"snippet": "Your Acme invoice is ready",
|
|
},
|
|
}
|
|
|
|
results := mapExternalHits(hits, "acme")
|
|
if len(results) != 1 {
|
|
t.Fatalf("len(results) = %d, want 1", len(results))
|
|
}
|
|
|
|
r := results[0]
|
|
if r.Type != "mail" {
|
|
t.Fatalf("Type = %q, want mail", r.Type)
|
|
}
|
|
if r.ID != "mail-42" {
|
|
t.Fatalf("ID = %q, want mail-42", r.ID)
|
|
}
|
|
if r.Title == "" {
|
|
t.Fatal("Title is empty")
|
|
}
|
|
if r.Snippet == "" {
|
|
t.Fatal("Snippet is empty")
|
|
}
|
|
if r.Score <= 0 {
|
|
t.Fatalf("Score = %v, want positive fallback score", r.Score)
|
|
}
|
|
if !strings.Contains(r.Title, "<mark>") {
|
|
t.Fatalf("Title missing highlight: %q", r.Title)
|
|
}
|
|
if !strings.Contains(r.Snippet, "<mark>") {
|
|
t.Fatalf("Snippet missing highlight: %q", r.Snippet)
|
|
}
|
|
}
|
|
|
|
func TestMapExternalHitsMinimalDefaults(t *testing.T) {
|
|
hits := []map[string]any{
|
|
{"title": "Budget report"},
|
|
}
|
|
|
|
results := mapExternalHits(hits, "budget")
|
|
if len(results) != 1 {
|
|
t.Fatalf("len(results) = %d, want 1", len(results))
|
|
}
|
|
|
|
r := results[0]
|
|
if r.Type != "mail" {
|
|
t.Fatalf("Type = %q, want default mail", r.Type)
|
|
}
|
|
if r.ID != "mail:Budget report" {
|
|
t.Fatalf("ID = %q, want mail:Budget report", r.ID)
|
|
}
|
|
if r.Snippet == "" {
|
|
t.Fatal("Snippet is empty")
|
|
}
|
|
}
|
|
|
|
func TestFinalizeExternalResultsDateFiltering(t *testing.T) {
|
|
mid := time.Date(2024, 6, 15, 12, 0, 0, 0, time.UTC)
|
|
from := time.Date(2024, 6, 1, 0, 0, 0, 0, time.UTC)
|
|
to := time.Date(2024, 6, 30, 23, 59, 59, 0, time.UTC)
|
|
|
|
results := []Result{
|
|
{ID: "before", Title: "Old mail", Snippet: "Old mail", Date: time.Date(2024, 5, 15, 12, 0, 0, 0, time.UTC), Score: 5},
|
|
{ID: "in-range", Title: "Current mail", Snippet: "Current mail", Date: mid, Score: 5},
|
|
{ID: "after", Title: "Future mail", Snippet: "Future mail", Date: time.Date(2024, 7, 15, 12, 0, 0, 0, time.UTC), Score: 5},
|
|
}
|
|
|
|
params := query.ListParams{From: &from, To: &to}
|
|
finalizeExternalResults(results, "mail", params)
|
|
|
|
if results[0].Score >= 0 {
|
|
t.Fatalf("before-range Score = %v, want negative", results[0].Score)
|
|
}
|
|
if results[1].Score <= 0 {
|
|
t.Fatalf("in-range Score = %v, want positive with recency boost", results[1].Score)
|
|
}
|
|
if results[2].Score >= 0 {
|
|
t.Fatalf("after-range Score = %v, want negative", results[2].Score)
|
|
}
|
|
}
|
|
|
|
func TestFinalizeExternalResultsFallbackScore(t *testing.T) {
|
|
results := []Result{
|
|
{ID: "1", Title: "Invoice details", Snippet: "Invoice details", Score: 0},
|
|
}
|
|
|
|
finalizeExternalResults(results, "invoice", query.ListParams{})
|
|
if results[0].Score <= 0 {
|
|
t.Fatalf("Score = %v, want positive fallback score", results[0].Score)
|
|
}
|
|
}
|
|
|
|
func TestAsStringNil(t *testing.T) {
|
|
if got := asString(nil); got != "" {
|
|
t.Fatalf("asString(nil) = %q, want empty string", got)
|
|
}
|
|
}
|