2026-07-18 21:58:15 +08:00
package search
import (
"strings"
2026-07-21 17:16:45 +08:00
"gorm.io/gorm"
2026-07-18 21:58:15 +08:00
"senlinai-agent/backend/internal/models"
)
type Service struct {
2026-07-21 17:16:45 +08:00
db * gorm . DB
2026-07-18 21:58:15 +08:00
}
2026-07-21 17:16:45 +08:00
const maxSearchResults = 50
// SearchResultDTO 是搜索接口的稳定结果,所有关联均使用公开 identity。
type SearchResultDTO struct {
2026-07-18 21:58:15 +08:00
Type string ` json:"type" `
2026-07-21 17:16:45 +08:00
ID string ` json:"id" `
ProjectID string ` json:"projectId" `
2026-07-18 21:58:15 +08:00
Title string ` json:"title" `
Snippet string ` json:"snippet" `
}
2026-07-21 17:16:45 +08:00
func NewService ( databases ... * gorm . DB ) * Service {
var database * gorm . DB
if len ( databases ) > 0 {
database = databases [ 0 ]
}
return & Service { db : database }
2026-07-18 21:58:15 +08:00
}
2026-07-21 17:16:45 +08:00
func ( s * Service ) database ( ) * gorm . DB {
if s . db != nil {
return s . db
}
return models . DBService
}
func ( s * Service ) Search ( userID uint , query string ) ( [ ] SearchResultDTO , error ) {
2026-07-18 21:58:15 +08:00
query = strings . TrimSpace ( query )
if query == "" {
2026-07-21 17:16:45 +08:00
return [ ] SearchResultDTO { } , nil
2026-07-18 21:58:15 +08:00
}
2026-07-21 17:16:45 +08:00
if s . database ( ) . Dialector . Name ( ) == "postgres" {
2026-07-18 21:58:15 +08:00
return s . searchPostgres ( userID , query )
}
2026-07-21 17:16:45 +08:00
like := containsPattern ( query )
results := [ ] SearchResultDTO { }
2026-07-18 21:58:15 +08:00
var projects [ ] models . SenlinAgentProject
2026-07-21 17:16:45 +08:00
if err := s . database ( ) . Where ( "owner_id = ? AND (name LIKE ? ESCAPE '!' OR description LIKE ? ESCAPE '!')" , userID , like , like ) . Limit ( maxSearchResults ) . Find ( & projects ) . Error ; err != nil {
2026-07-18 21:58:15 +08:00
return nil , err
}
for _ , project := range projects {
2026-07-21 17:16:45 +08:00
results = append ( results , SearchResultDTO { Type : "project" , ID : project . Identity , ProjectID : project . Identity , Title : project . Name , Snippet : project . Description } )
2026-07-18 21:58:15 +08:00
}
var tasks [ ] models . SenlinAgentTask
2026-07-21 17:16:45 +08:00
if err := s . database ( ) . Select ( "senlin_agent_tasks.*" ) . Joins ( "JOIN senlin_agent_projects ON senlin_agent_projects.id = senlin_agent_tasks.project_id" ) .
Where ( "(senlin_agent_projects.owner_id = ? OR senlin_agent_tasks.assignee_id = ?) AND (senlin_agent_tasks.title LIKE ? ESCAPE '!' OR senlin_agent_tasks.description LIKE ? ESCAPE '!')" , userID , userID , like , like ) .
Limit ( maxSearchResults ) .
2026-07-18 21:58:15 +08:00
Find ( & tasks ) . Error ; err != nil {
return nil , err
}
for _ , task := range tasks {
2026-07-21 17:16:45 +08:00
results = append ( results , SearchResultDTO { Type : "task" , ID : task . Identity , ProjectID : task . ProjectIdentity , Title : task . Title , Snippet : task . Description } )
2026-07-18 21:58:15 +08:00
}
var notes [ ] models . SenlinAgentNote
2026-07-21 17:16:45 +08:00
if err := s . database ( ) . Select ( "senlin_agent_notes.*" ) . Joins ( "JOIN senlin_agent_projects ON senlin_agent_projects.id = senlin_agent_notes.project_id" ) .
Where ( ` ( senlin_agent_projects . owner_id = ? OR EXISTS (
SELECT 1 FROM senlin_agent_task_shares
JOIN senlin_agent_tasks ON senlin_agent_tasks . id = senlin_agent_task_shares . task_id
WHERE senlin_agent_task_shares . object_type = ' note '
AND senlin_agent_task_shares . object_id = senlin_agent_notes . id
AND senlin_agent_tasks . project_id = senlin_agent_notes . project_id
AND senlin_agent_tasks . assignee_id = ?
) ) AND ( senlin_agent_notes . title LIKE ? ESCAPE '!' OR senlin_agent_notes . markdown LIKE ? ESCAPE '!' ) ` , userID , userID , like , like ) .
Limit ( maxSearchResults ) .
2026-07-18 21:58:15 +08:00
Find ( & notes ) . Error ; err != nil {
return nil , err
}
for _ , note := range notes {
2026-07-21 17:16:45 +08:00
results = append ( results , SearchResultDTO { Type : "note" , ID : note . Identity , ProjectID : note . ProjectIdentity , Title : note . Title , Snippet : note . Markdown } )
2026-07-18 21:58:15 +08:00
}
2026-07-21 17:16:45 +08:00
if len ( results ) > maxSearchResults {
results = results [ : maxSearchResults ]
2026-07-18 21:58:15 +08:00
}
return results , nil
}
2026-07-21 17:16:45 +08:00
func ( s * Service ) searchPostgres ( userID uint , query string ) ( [ ] SearchResultDTO , error ) {
var results [ ] SearchResultDTO
like := containsPattern ( query )
err := s . database ( ) . Raw ( `
SELECT ' project ' AS type , p . identity AS id , p . identity AS project_id , p . name AS title , p . description AS snippet
2026-07-18 21:58:15 +08:00
FROM senlin_agent_projects p
WHERE p . owner_id = ?
2026-07-21 17:16:45 +08:00
AND ( coalesce ( p . name , ' ' ) ILIKE ? ESCAPE '!' OR coalesce ( p . description , ' ' ) ILIKE ? ESCAPE '!' )
2026-07-18 21:58:15 +08:00
UNION ALL
2026-07-21 17:16:45 +08:00
SELECT ' task ' AS type , t . identity AS id , p . identity AS project_id , t . title , t . description AS snippet
2026-07-18 21:58:15 +08:00
FROM senlin_agent_tasks t
JOIN senlin_agent_projects p ON p . id = t . project_id
2026-07-21 17:16:45 +08:00
WHERE ( p . owner_id = ? OR t . assignee_id = ? )
AND ( coalesce ( t . title , ' ' ) ILIKE ? ESCAPE '!' OR coalesce ( t . description , ' ' ) ILIKE ? ESCAPE '!' )
2026-07-18 21:58:15 +08:00
UNION ALL
2026-07-21 17:16:45 +08:00
SELECT ' note ' AS type , n . identity AS id , p . identity AS project_id , n . title , n . markdown AS snippet
2026-07-18 21:58:15 +08:00
FROM senlin_agent_notes n
JOIN senlin_agent_projects p ON p . id = n . project_id
2026-07-21 17:16:45 +08:00
WHERE ( p . owner_id = ? OR EXISTS (
SELECT 1 FROM senlin_agent_task_shares ts
JOIN senlin_agent_tasks t ON t . id = ts . task_id
WHERE ts . object_type = ' note '
AND ts . object_id = n . id
AND t . project_id = n . project_id
AND t . assignee_id = ?
) )
AND ( coalesce ( n . title , ' ' ) ILIKE ? ESCAPE '!' OR coalesce ( n . markdown , ' ' ) ILIKE ? ESCAPE '!' )
LIMIT ?
` , userID , like , like , userID , userID , like , like , userID , userID , like , like , maxSearchResults ) . Scan ( & results ) . Error
2026-07-18 21:58:15 +08:00
return results , err
}
2026-07-21 17:16:45 +08:00
func containsPattern ( query string ) string {
escaped := strings . NewReplacer ( "!" , "!!" , "%" , "!%" , "_" , "!_" ) . Replace ( query )
return "%" + escaped + "%"
}