Merge remote-tracking branch 'github/feat/plugin/search' into feat/1.1.1/state
# Conflicts: # internal/repo/answer/answer_repo.go # internal/repo/question/question_repo.go
This commit is contained in:
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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 (
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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)
|
||||
)
|
||||
Reference in New Issue
Block a user