diff --git a/internal/repo/answer/answer_repo.go b/internal/repo/answer/answer_repo.go index 2cf18c9d..48a3166d 100644 --- a/internal/repo/answer/answer_repo.go +++ b/internal/repo/answer/answer_repo.go @@ -2,6 +2,8 @@ package answer import ( "context" + "github.com/answerdev/answer/plugin" + "strings" "time" "github.com/answerdev/answer/internal/base/constant" @@ -59,6 +61,7 @@ func (ar *answerRepo) AddAnswer(ctx context.Context, answer *entity.Answer) (err answer.ID = uid.EnShortID(answer.ID) answer.QuestionID = uid.EnShortID(answer.QuestionID) } + _ = ar.updateSearch(ctx, answer.ID) return nil } @@ -73,6 +76,7 @@ func (ar *answerRepo) RemoveAnswer(ctx context.Context, id string) (err error) { if err != nil { return errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() } + _ = ar.updateSearch(ctx, answer.ID) return nil } @@ -84,6 +88,7 @@ func (ar *answerRepo) UpdateAnswer(ctx context.Context, answer *entity.Answer, C if err != nil { err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() } + _ = ar.updateSearch(ctx, answer.ID) return err } @@ -95,6 +100,7 @@ func (ar *answerRepo) UpdateAnswerStatus(ctx context.Context, answer *entity.Ans if err != nil { return errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() } + _ = ar.updateSearch(ctx, answer.ID) return } @@ -184,6 +190,7 @@ func (ar *answerRepo) UpdateAccepted(ctx context.Context, id string, questionID return errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() } } + _ = ar.updateSearch(ctx, id) return nil } @@ -319,3 +326,72 @@ func (ar *answerRepo) AdminSearchList(ctx context.Context, req *schema.AdminAnsw } return resp, total, nil } + +// updateSearch update search, if search plugin not enable, do nothing +func (ar *answerRepo) updateSearch(ctx context.Context, answerID string) (err error) { + answerID = uid.DeShortID(answerID) + // check search plugin + var ( + s plugin.Search + ) + _ = plugin.CallSearch(func(search plugin.Search) error { + s = search + return nil + }) + if s == nil { + return + } + answer, exist, err := ar.GetAnswer(ctx, answerID) + if !exist { + return + } + if err != nil { + return err + } + + // get question + var ( + question *entity.Question + ) + exist, err = ar.data.DB.Where("id = ?", answer.QuestionID).Get(&question) + if err != nil { + err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() + } + if !exist { + return + } + + // get tags + var ( + tagListList = make([]*entity.TagRel, 0) + tags = make([]string, 0) + ) + st := ar.data.DB.Where("object_id = ?", uid.DeShortID(question.ID)) + st.Where("status = ?", entity.TagRelStatusAvailable) + err = st.Find(&tagListList) + if err != nil { + err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() + } + for _, tag := range tagListList { + tags = append(tags, tag.TagID) + } + + content := &plugin.SearchContent{ + ObjectID: answerID, + Title: question.Title, + Type: "question", + Content: answer.ParsedText, + Answers: 0, + Status: int64(answer.Status), + Tags: tags, + QuesionID: answerID, + UserID: answer.UserID, + Views: int64(question.ViewCount), + Created: answer.CreatedAt.Unix(), + Active: answer.UpdatedAt.Unix(), + Score: int64(answer.VoteCount), + HasAccepted: answer.Accepted == schema.AnswerAcceptedEnable, + } + err = s.UpdateContent(ctx, answerID, content) + return +} diff --git a/internal/repo/question/question_repo.go b/internal/repo/question/question_repo.go index 7f1c8b28..aa0dd3a2 100644 --- a/internal/repo/question/question_repo.go +++ b/internal/repo/question/question_repo.go @@ -5,6 +5,7 @@ import ( "encoding/json" "fmt" "github.com/segmentfault/pacman/log" + "github.com/answerdev/answer/plugin" "strings" "time" "unicode" @@ -56,6 +57,7 @@ func (qr *questionRepo) AddQuestion(ctx context.Context, question *entity.Questi if handler.GetEnableShortID(ctx) { question.ID = uid.EnShortID(question.ID) } + _ = qr.updateSearch(ctx, question.ID) return } @@ -79,6 +81,7 @@ func (qr *questionRepo) UpdateQuestion(ctx context.Context, question *entity.Que if handler.GetEnableShortID(ctx) { question.ID = uid.EnShortID(question.ID) } + _ = qr.updateSearch(ctx, question.ID) return } @@ -89,6 +92,7 @@ func (qr *questionRepo) UpdatePvCount(ctx context.Context, questionID string) (e if err != nil { return errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() } + _ = qr.updateSearch(ctx, question.ID) return nil } @@ -100,6 +104,7 @@ func (qr *questionRepo) UpdateAnswerCount(ctx context.Context, questionID string if err != nil { return errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() } + _ = qr.updateSearch(ctx, question.ID) return nil } @@ -121,6 +126,7 @@ func (qr *questionRepo) UpdateQuestionStatus(ctx context.Context, question *enti if err != nil { return errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() } + _ = qr.updateSearch(ctx, question.ID) return nil } @@ -130,6 +136,7 @@ func (qr *questionRepo) UpdateQuestionStatusWithOutUpdateTime(ctx context.Contex if err != nil { return errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() } + _ = qr.updateSearch(ctx, question.ID) return nil } @@ -148,6 +155,7 @@ func (qr *questionRepo) UpdateAccepted(ctx context.Context, question *entity.Que if err != nil { return errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() } + _ = qr.updateSearch(ctx, question.ID) return nil } @@ -157,6 +165,7 @@ func (qr *questionRepo) UpdateLastAnswer(ctx context.Context, question *entity.Q if err != nil { return errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() } + _ = qr.updateSearch(ctx, question.ID) return nil } @@ -407,3 +416,59 @@ func (qr *questionRepo) AdminQuestionPage(ctx context.Context, search *schema.Ad } return rows, count, nil } + +// updateSearch update search, if search plugin not enable, do nothing +func (qr *questionRepo) updateSearch(ctx context.Context, questionID string) (err error) { + questionID = uid.DeShortID(questionID) + // check search plugin + var ( + s plugin.Search + ) + _ = plugin.CallSearch(func(search plugin.Search) error { + s = search + return nil + }) + if s == nil { + return + } + question, exist, err := qr.GetQuestion(ctx, questionID) + if !exist { + return + } + if err != nil { + return err + } + + // get tags + var ( + tagListList = make([]*entity.TagRel, 0) + tags = make([]string, 0) + ) + session := qr.data.DB.Where("object_id = ?", questionID) + session.Where("status = ?", entity.TagRelStatusAvailable) + err = session.Find(&tagListList) + if err != nil { + err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() + } + for _, tag := range tagListList { + tags = append(tags, tag.TagID) + } + content := &plugin.SearchContent{ + ObjectID: questionID, + Title: question.Title, + Type: "question", + Content: question.ParsedText, + Answers: int64(question.AnswerCount), + Status: int64(question.Status), + Tags: tags, + QuesionID: questionID, + UserID: question.UserID, + Views: int64(question.ViewCount), + Created: question.CreatedAt.Unix(), + Active: question.UpdatedAt.Unix(), + Score: int64(question.VoteCount), + HasAccepted: question.AcceptedAnswerID != "" && question.AcceptedAnswerID != "0", + } + err = s.UpdateContent(ctx, questionID, content) + return +} diff --git a/internal/repo/search_common/search_repo.go b/internal/repo/search_common/search_repo.go index 1f46bdf4..1674698e 100644 --- a/internal/repo/search_common/search_repo.go +++ b/internal/repo/search_common/search_repo.go @@ -3,6 +3,7 @@ package search_common import ( "context" "fmt" + "github.com/answerdev/answer/plugin" "strconv" "strings" "time" @@ -426,6 +427,34 @@ func (sr *searchRepo) parseOrder(ctx context.Context, order string) (res string) return } +// ParseSearchPluginResult parse search plugin result +func (sr *searchRepo) ParseSearchPluginResult(ctx context.Context, sres []plugin.SearchResult) (resp []schema.SearchResp, err error) { + var ( + qres []map[string][]byte + res = make([]map[string][]byte, 0) + b *builder.Builder + ) + for _, r := range sres { + switch r.Type { + case "question": + b = builder.MySQL().Select(qFields...).From("question").Where(builder.Eq{"id": r.ID}). + And(builder.Lt{"`status`": entity.QuestionStatusDeleted}) + case "answer": + b = builder.MySQL().Select(aFields...).From("answer").LeftJoin("`question`", "`question`.`id` = `answer`.`question_id`"). + Where(builder.Eq{"`answer`.`id`": r.ID}). + And(builder.Lt{"`question`.`status`": entity.QuestionStatusDeleted}). + And(builder.Lt{"`answer`.`status`": entity.AnswerStatusDeleted}).And(builder.Eq{"`question`.`show`": entity.QuestionShow}) + } + qres, err = sr.data.DB.Query(b) + if err != nil || len(qres) == 0 { + continue + } + res = append(res, qres[0]) + } + return sr.parseResult(ctx, res) +} + +// parseResult parse search result, return the data structure func (sr *searchRepo) parseResult(ctx context.Context, res []map[string][]byte) (resp []schema.SearchResp, err error) { for _, r := range res { var ( diff --git a/internal/service/search_common/search.go b/internal/service/search_common/search.go index b9fc3041..ef7fe305 100644 --- a/internal/service/search_common/search.go +++ b/internal/service/search_common/search.go @@ -3,10 +3,12 @@ package search_common import ( "context" "github.com/answerdev/answer/internal/schema" + "github.com/answerdev/answer/plugin" ) type SearchRepo interface { SearchContents(ctx context.Context, words []string, tagIDs []string, userID string, votes, page, size int, order string) (resp []schema.SearchResp, total int64, err error) SearchQuestions(ctx context.Context, words []string, tagIDs []string, notAccepted bool, views, answers int, page, size int, order string) (resp []schema.SearchResp, total int64, err error) SearchAnswers(ctx context.Context, words []string, tagIDs []string, accepted bool, questionID string, page, size int, order string) (resp []schema.SearchResp, total int64, err error) + ParseSearchPluginResult(ctx context.Context, sres []plugin.SearchResult) (resp []schema.SearchResp, err error) } diff --git a/internal/service/search_service.go b/internal/service/search_service.go index 66d762ab..87e6b1eb 100644 --- a/internal/service/search_service.go +++ b/internal/service/search_service.go @@ -5,6 +5,7 @@ import ( "github.com/answerdev/answer/internal/schema" "github.com/answerdev/answer/internal/service/search_common" "github.com/answerdev/answer/internal/service/search_parser" + "github.com/answerdev/answer/plugin" ) type SearchService struct { @@ -47,16 +48,49 @@ func (ss *SearchService) Search(ctx context.Context, dto *schema.SearchDTO) (res tags, words := ss.searchParser.ParseStructure(dto) + // check search plugin + var ( + s plugin.Search + sres []plugin.SearchResult + ) + _ = plugin.CallSearch(func(search plugin.Search) error { + s = search + return nil + }) + + // search plugin is not found, call system search + if s == nil { + switch searchType { + case "all": + resp, total, err = ss.searchRepo.SearchContents(ctx, words, tags, userID, votes, dto.Page, dto.Size, dto.Order) + if err != nil { + return nil, 0, nil, err + } + case "question": + resp, total, err = ss.searchRepo.SearchQuestions(ctx, words, tags, notAccepted, views, answers, dto.Page, dto.Size, dto.Order) + case "answer": + resp, total, err = ss.searchRepo.SearchAnswers(ctx, words, tags, accepted, questionID, dto.Page, dto.Size, dto.Order) + } + return + } + + // call search plugin switch searchType { case "all": - resp, total, err = ss.searchRepo.SearchContents(ctx, words, tags, userID, votes, dto.Page, dto.Size, dto.Order) - if err != nil { - return nil, 0, nil, err - } + sres, total, err = s.SearchContents(ctx, words, tags, userID, votes, dto.Page, dto.Size, dto.Order) case "question": - resp, total, err = ss.searchRepo.SearchQuestions(ctx, words, tags, notAccepted, views, answers, dto.Page, dto.Size, dto.Order) + sres, total, err = s.SearchQuestions(ctx, words, tags, notAccepted, views, answers, dto.Page, dto.Size, dto.Order) case "answer": - resp, total, err = ss.searchRepo.SearchAnswers(ctx, words, tags, accepted, questionID, dto.Page, dto.Size, dto.Order) + sres, total, err = s.SearchAnswers(ctx, words, tags, accepted, questionID, dto.Page, dto.Size, dto.Order) + } + if err != nil || len(sres) == 0 { + return nil, 0, nil, err + } + + // parse search plugin result + resp, err = ss.searchRepo.ParseSearchPluginResult(ctx, sres) + if err != nil { + return nil, 0, nil, err } return } diff --git a/plugin/plugin.go b/plugin/plugin.go index 7f6e4a41..0b1c9de0 100644 --- a/plugin/plugin.go +++ b/plugin/plugin.go @@ -56,6 +56,10 @@ func Register(p Base) { if _, ok := p.(Agent); ok { registerAgent(p.(Agent)) } + + if _, ok := p.(Search); ok { + registerSearch(p.(Search)) + } } type Stack[T Base] struct { diff --git a/plugin/search.go b/plugin/search.go new file mode 100644 index 00000000..ccea025b --- /dev/null +++ b/plugin/search.go @@ -0,0 +1,44 @@ +package plugin + +import ( + "context" +) + +type SearchResult struct { + // ID content ID + ID string + // Type content type, example: "answer", "question" + Type string +} + +type SearchContent struct { + ObjectID string `json:"objectID"` + Title string `json:"title"` + Type string `json:"type"` + Content string `json:"content"` + Answers int64 `json:"answers"` + Status int64 `json:"status"` + Tags []string `json:"tags"` + QuesionID string `json:"questionID"` + UserID string `json:"userID"` + Views int64 `json:"views"` + Created int64 `json:"created"` + Active int64 `json:"active"` + Score int64 `json:"score"` + HasAccepted bool `json:"hasAccepted"` +} + +type Search interface { + Base + SearchContents(ctx context.Context, words []string, tagIDs []string, userID string, votes int, page, size int, order string) (res []SearchResult, total int64, err error) + SearchQuestions(ctx context.Context, words []string, tagIDs []string, notAccepted bool, views, answers int, page, size int, order string) (res []SearchResult, total int64, err error) + SearchAnswers(ctx context.Context, words []string, tagIDs []string, accepted bool, questionID string, page, size int, order string) (res []SearchResult, total int64, err error) + UpdateContent(ctx context.Context, contentID string, content *SearchContent) error + DeleteContent(ctx context.Context, contentID string) error +} + +var ( + // CallUserCenter is a function that calls all registered parsers + CallSearch, + registerSearch = MakePlugin[Search](false) +)