Merge branch 'fix/search' into 'test'

Fix/search

See merge request opensource/answer!233
This commit is contained in:
杨光富 2022-11-15 03:41:54 +00:00
commit 96f2ad92d1
18 changed files with 440 additions and 836 deletions

View File

@ -58,6 +58,7 @@ import (
"github.com/answerdev/answer/internal/service/report_backyard"
"github.com/answerdev/answer/internal/service/report_handle_backyard"
"github.com/answerdev/answer/internal/service/revision_common"
"github.com/answerdev/answer/internal/service/search_parser"
"github.com/answerdev/answer/internal/service/service_config"
"github.com/answerdev/answer/internal/service/siteinfo"
"github.com/answerdev/answer/internal/service/siteinfo_common"
@ -154,8 +155,9 @@ func initApplication(debug bool, serverConf *conf.Server, dbConf *data.Database,
answerService := service.NewAnswerService(answerRepo, questionRepo, questionCommon, userCommon, collectionCommon, userRepo, revisionService, answerActivityService, answerCommon, voteRepo)
dashboardService := dashboard.NewDashboardService(questionRepo, answerRepo, commentCommonRepo, voteRepo, userRepo, reportRepo, configRepo, siteInfoCommonService, serviceConf, dataData)
answerController := controller.NewAnswerController(answerService, rankService, dashboardService)
searchParser := search_parser.NewSearchParser(tagRepo, userCommon)
searchRepo := search_common.NewSearchRepo(dataData, uniqueIDRepo, userCommon)
searchService := service.NewSearchService(searchRepo, tagRepo, userCommon, followRepo)
searchService := service.NewSearchService(searchParser, searchRepo)
searchController := controller.NewSearchController(searchService)
serviceRevisionService := service.NewRevisionService(revisionRepo, userCommon, questionCommon, answerService)
revisionController := controller.NewRevisionController(serviceRevisionService)

View File

@ -67,7 +67,7 @@ func NewSearchRepo(data *data.Data, uniqueIDRepo unique.UniqueIDRepo, userCommon
}
// SearchContents search question and answer data
func (sr *searchRepo) SearchContents(ctx context.Context, words []string, tagID, userID string, votes int, page, size int, order string) (resp []schema.SearchResp, total int64, err error) {
func (sr *searchRepo) SearchContents(ctx context.Context, words []string, tagIDs []string, userID string, votes int, page, size int, order string) (resp []schema.SearchResp, total int64, err error) {
if words = filterWords(words); len(words) == 0 {
return
}
@ -116,10 +116,12 @@ func (sr *searchRepo) SearchContents(ctx context.Context, words []string, tagID,
}
// check tag
if tagID != "" {
if len(tagIDs) > 0 {
b.Join("INNER", "tag_rel", "question.id = tag_rel.object_id").
Where(builder.Eq{"tag_rel.tag_id": tagID})
argsQ = append(argsQ, tagID)
Where(builder.In("tag_rel.tag_id", tagIDs))
for _, tagID := range tagIDs {
argsQ = append(argsQ, tagID)
}
}
// check user
@ -193,7 +195,7 @@ func (sr *searchRepo) SearchContents(ctx context.Context, words []string, tagID,
}
// SearchQuestions search question data
func (sr *searchRepo) SearchQuestions(ctx context.Context, words []string, limitNoAccepted bool, answers, page, size int, order string) (resp []schema.SearchResp, total int64, err error) {
func (sr *searchRepo) SearchQuestions(ctx context.Context, words []string, notAccepted bool, views, answers int, page, size int, order string) (resp []schema.SearchResp, total int64, err error) {
if words = filterWords(words); len(words) == 0 {
return
}
@ -223,11 +225,26 @@ func (sr *searchRepo) SearchQuestions(ctx context.Context, words []string, limit
}
// check need filter has not accepted
if limitNoAccepted {
if notAccepted {
b.And(builder.Eq{"accepted_answer_id": 0})
args = append(args, 0)
}
// check views
if views > -1 {
b.And(builder.Gte{"view_count": views})
args = append(args, views)
}
// check answers
if answers == 0 {
b.And(builder.Eq{"answer_count": answers})
args = append(args, answers)
} else if answers > 0 {
b.And(builder.Gte{"answer_count": answers})
args = append(args, answers)
}
if answers == 0 {
b.And(builder.Eq{"answer_count": 0})
args = append(args, 0)
@ -274,7 +291,7 @@ func (sr *searchRepo) SearchQuestions(ctx context.Context, words []string, limit
}
// SearchAnswers search answer data
func (sr *searchRepo) SearchAnswers(ctx context.Context, words []string, limitAccepted bool, questionID string, page, size int, order string) (resp []schema.SearchResp, total int64, err error) {
func (sr *searchRepo) SearchAnswers(ctx context.Context, words []string, tagIDs []string, accepted bool, questionID string, page, size int, order string) (resp []schema.SearchResp, total int64, err error) {
if words = filterWords(words); len(words) == 0 {
return
}
@ -303,11 +320,23 @@ func (sr *searchRepo) SearchAnswers(ctx context.Context, words []string, limitAc
}
}
if limitAccepted {
// check tags
// check tag
if len(tagIDs) > 0 {
b.Join("INNER", "tag_rel", "question.id = tag_rel.object_id").
Where(builder.In("tag_rel.tag_id", tagIDs))
for _, tagID := range tagIDs {
args = append(args, tagID)
}
}
// check limit accepted
if accepted {
b.Where(builder.Eq{"adopted": schema.AnswerAdoptedEnable})
args = append(args, schema.AnswerAdoptedEnable)
}
// check question id
if questionID != "" {
b.Where(builder.Eq{"question_id": questionID})
args = append(args, questionID)

View File

@ -24,6 +24,7 @@ import (
"github.com/answerdev/answer/internal/service/revision_common"
"github.com/answerdev/answer/internal/service/siteinfo"
"github.com/answerdev/answer/internal/service/siteinfo_common"
"github.com/answerdev/answer/internal/service/search_parser"
"github.com/answerdev/answer/internal/service/tag"
tagcommon "github.com/answerdev/answer/internal/service/tag_common"
"github.com/answerdev/answer/internal/service/uploader"
@ -57,6 +58,7 @@ var ProviderSetService = wire.NewSet(
revision_common.NewRevisionService,
NewRevisionService,
rank.NewRankService,
search_parser.NewSearchParser,
NewSearchService,
meta.NewMetaService,
object_info.NewObjService,

View File

@ -1,55 +0,0 @@
package search
import (
"context"
"strings"
"github.com/answerdev/answer/internal/schema"
"github.com/answerdev/answer/internal/service/search_common"
)
type AcceptedAnswerSearch struct {
repo search_common.SearchRepo
w string
page int
size int
order string
}
func NewAcceptedAnswerSearch(repo search_common.SearchRepo) *AcceptedAnswerSearch {
return &AcceptedAnswerSearch{
repo: repo,
}
}
func (s *AcceptedAnswerSearch) Parse(dto *schema.SearchDTO) (ok bool) {
var (
q,
w,
p string
)
q = dto.Query
w = dto.Query
p = `isaccepted:yes`
if strings.Index(q, p) == 0 {
ok = true
w = strings.TrimPrefix(q, p)
}
s.w = strings.TrimSpace(w)
s.page = dto.Page
s.size = dto.Size
s.order = dto.Order
return
}
func (s *AcceptedAnswerSearch) Search(ctx context.Context) (resp []schema.SearchResp, total int64, err error) {
words := strings.Split(s.w, " ")
if len(words) > 3 {
words = words[:4]
}
return s.repo.SearchAnswers(ctx, words, true, "", s.page, s.size, s.order)
}

View File

@ -1,54 +0,0 @@
package search
import (
"context"
"github.com/answerdev/answer/internal/schema"
"github.com/answerdev/answer/internal/service/search_common"
"strings"
)
type AnswerSearch struct {
repo search_common.SearchRepo
w string
page int
size int
order string
}
func NewAnswerSearch(repo search_common.SearchRepo) *AnswerSearch {
return &AnswerSearch{
repo: repo,
}
}
func (s *AnswerSearch) Parse(dto *schema.SearchDTO) (ok bool) {
var (
q,
w,
p string
)
q = dto.Query
w = dto.Query
p = `is:answer`
if strings.Index(q, p) == 0 {
ok = true
w = strings.TrimPrefix(q, p)
}
s.w = strings.TrimSpace(w)
s.page = dto.Page
s.size = dto.Size
s.order = dto.Order
return
}
func (s *AnswerSearch) Search(ctx context.Context) (resp []schema.SearchResp, total int64, err error) {
words := strings.Split(s.w, " ")
if len(words) > 3 {
words = words[:4]
}
return s.repo.SearchAnswers(ctx, words, false, "", s.page, s.size, s.order)
}

View File

@ -1,65 +0,0 @@
package search
import (
"context"
"regexp"
"strings"
"github.com/answerdev/answer/internal/schema"
"github.com/answerdev/answer/internal/service/search_common"
"github.com/answerdev/answer/pkg/converter"
)
type AnswersSearch struct {
repo search_common.SearchRepo
exp int
w string
page int
size int
order string
}
func NewAnswersSearch(repo search_common.SearchRepo) *AnswersSearch {
return &AnswersSearch{
repo: repo,
}
}
func (s *AnswersSearch) Parse(dto *schema.SearchDTO) (ok bool) {
var (
q,
w,
p,
exp string
)
q = dto.Query
w = dto.Query
p = `(?m)^answers:([0-9]+)`
re := regexp.MustCompile(p)
res := re.FindStringSubmatch(q)
if len(res) == 2 {
exp = res[1]
trimLen := len(res[0])
w = q[trimLen:]
ok = true
}
s.exp = converter.StringToInt(exp)
s.w = strings.TrimSpace(w)
s.page = dto.Page
s.size = dto.Size
s.order = dto.Order
return
}
func (s *AnswersSearch) Search(ctx context.Context) (resp []schema.SearchResp, total int64, err error) {
words := strings.Split(s.w, " ")
if len(words) > 3 {
words = words[:4]
}
return s.repo.SearchQuestions(ctx, words, false, s.exp, s.page, s.size, s.order)
}

View File

@ -1,90 +0,0 @@
package search
import (
"context"
"regexp"
"strings"
"github.com/answerdev/answer/internal/schema"
"github.com/answerdev/answer/internal/service/search_common"
usercommon "github.com/answerdev/answer/internal/service/user_common"
)
type AuthorSearch struct {
repo search_common.SearchRepo
userCommon *usercommon.UserCommon
exp string
w string
page int
size int
order string
}
func NewAuthorSearch(repo search_common.SearchRepo, userCommon *usercommon.UserCommon) *AuthorSearch {
return &AuthorSearch{
repo: repo,
userCommon: userCommon,
}
}
// Parse
// example: "user:12345" -> {exp="" w="12345"}
func (s *AuthorSearch) Parse(dto *schema.SearchDTO) (ok bool) {
var (
exp,
q,
w,
p,
me,
name string
)
exp = ""
q = dto.Query
w = q
p = `(?m)^user:([a-z0-9._-]+)`
me = "user:me"
re := regexp.MustCompile(p)
res := re.FindStringSubmatch(q)
if len(res) == 2 {
name = res[1]
user, has, err := s.userCommon.GetUserBasicInfoByUserName(nil, name)
if err == nil && has {
exp = user.ID
trimLen := len(res[0])
w = q[trimLen:]
ok = true
}
} else if strings.Index(q, me) == 0 {
exp = dto.UserID
w = strings.TrimPrefix(q, me)
ok = true
}
w = strings.TrimSpace(w)
s.exp = exp
s.w = w
s.page = dto.Page
s.size = dto.Size
s.order = dto.Order
return
}
func (s *AuthorSearch) Search(ctx context.Context) (resp []schema.SearchResp, total int64, err error) {
var (
words []string
)
if len(s.exp) == 0 {
return
}
words = strings.Split(s.w, " ")
if len(words) > 3 {
words = words[:4]
}
resp, total, err = s.repo.SearchContents(ctx, words, "", s.exp, -1, s.page, s.size, s.order)
return
}

View File

@ -1,65 +0,0 @@
package search
import (
"context"
"github.com/answerdev/answer/internal/schema"
"github.com/answerdev/answer/internal/service/search_common"
"regexp"
"strings"
)
type InQuestionSearch struct {
repo search_common.SearchRepo
w string
exp string
page int
size int
order string
}
func NewInQuestionSearch(repo search_common.SearchRepo) *InQuestionSearch {
return &InQuestionSearch{
repo: repo,
}
}
func (s *InQuestionSearch) Parse(dto *schema.SearchDTO) (ok bool) {
var (
w,
q,
p,
exp string
)
q = dto.Query
w = dto.Query
p = `(?m)^inquestion:([0-9]+)`
re := regexp.MustCompile(p)
res := re.FindStringSubmatch(q)
if len(res) == 2 {
exp = res[1]
trimLen := len(res[0])
w = q[trimLen:]
ok = true
}
s.exp = exp
s.w = strings.TrimSpace(w)
s.page = dto.Page
s.size = dto.Size
s.order = dto.Order
return
}
func (s *InQuestionSearch) Search(ctx context.Context) (resp []schema.SearchResp, total int64, err error) {
var (
words []string
)
words = strings.Split(s.w, " ")
if len(words) > 3 {
words = words[:4]
}
return s.repo.SearchAnswers(ctx, words, false, s.exp, s.page, s.size, s.order)
}

View File

@ -1,58 +0,0 @@
package search
import (
"context"
"strings"
"github.com/answerdev/answer/internal/schema"
"github.com/answerdev/answer/internal/service/search_common"
)
type NotAcceptedQuestion struct {
repo search_common.SearchRepo
w string
page int
size int
order string
}
func NewNotAcceptedQuestion(repo search_common.SearchRepo) *NotAcceptedQuestion {
return &NotAcceptedQuestion{
repo: repo,
}
}
func (s *NotAcceptedQuestion) Parse(dto *schema.SearchDTO) (ok bool) {
var (
q,
w,
p string
)
q = dto.Query
w = dto.Query
p = `hasaccepted:no`
if strings.Index(q, p) == 0 {
ok = true
w = strings.TrimPrefix(q, p)
}
s.w = strings.TrimSpace(w)
s.page = dto.Page
s.size = dto.Size
s.order = dto.Order
return
}
func (s *NotAcceptedQuestion) Search(ctx context.Context) (resp []schema.SearchResp, total int64, err error) {
var (
words []string
)
words = strings.Split(s.w, " ")
if len(words) > 3 {
words = words[:4]
}
return s.repo.SearchQuestions(ctx, words, true, -1, s.page, s.size, s.order)
}

View File

@ -1,47 +0,0 @@
package search
import (
"context"
"strings"
"github.com/answerdev/answer/internal/schema"
"github.com/answerdev/answer/internal/service/search_common"
)
type ObjectSearch struct {
repo search_common.SearchRepo
w string
page int
size int
order string
}
func NewObjectSearch(repo search_common.SearchRepo) *ObjectSearch {
return &ObjectSearch{
repo: repo,
}
}
func (s *ObjectSearch) Parse(dto *schema.SearchDTO) (ok bool) {
var (
w string
)
w = strings.TrimSpace(dto.Query)
if len(w) > 0 {
ok = true
}
s.w = w
s.page = dto.Page
s.size = dto.Size
s.order = dto.Order
return
}
func (s *ObjectSearch) Search(ctx context.Context) (resp []schema.SearchResp, total int64, err error) {
words := strings.Split(s.w, " ")
if len(words) > 3 {
words = words[:4]
}
return s.repo.SearchContents(ctx, words, "", "", -1, s.page, s.size, s.order)
}

View File

@ -1,55 +0,0 @@
package search
import (
"context"
"github.com/answerdev/answer/internal/schema"
"github.com/answerdev/answer/internal/service/search_common"
"strings"
)
type QuestionSearch struct {
repo search_common.SearchRepo
w string
page int
size int
order string
}
func NewQuestionSearch(repo search_common.SearchRepo) *QuestionSearch {
return &QuestionSearch{
repo: repo,
}
}
func (s *QuestionSearch) Parse(dto *schema.SearchDTO) (ok bool) {
var (
q,
w,
p string
)
q = dto.Query
w = dto.Query
p = `is:question`
if strings.Index(q, p) == 0 {
ok = true
w = strings.TrimPrefix(q, p)
}
s.w = strings.TrimSpace(w)
s.page = dto.Page
s.size = dto.Size
s.order = dto.Order
return
}
func (s *QuestionSearch) Search(ctx context.Context) (resp []schema.SearchResp, total int64, err error) {
words := strings.Split(s.w, " ")
if len(words) > 3 {
words = words[:4]
}
return s.repo.SearchQuestions(ctx, words, false, -1, s.page, s.size, s.order)
}

View File

@ -1,63 +0,0 @@
package search
import (
"context"
"regexp"
"strings"
"github.com/answerdev/answer/internal/schema"
"github.com/answerdev/answer/internal/service/search_common"
"github.com/answerdev/answer/pkg/converter"
)
type ScoreSearch struct {
repo search_common.SearchRepo
exp int
w string
page int
size int
order string
}
func NewScoreSearch(repo search_common.SearchRepo) *ScoreSearch {
return &ScoreSearch{
repo: repo,
}
}
func (s *ScoreSearch) Parse(dto *schema.SearchDTO) (ok bool) {
exp := ""
q := dto.Query
w := q
p := `(?m)^score:([0-9]+)`
re := regexp.MustCompile(p)
res := re.FindStringSubmatch(w)
if len(res) == 2 {
exp = res[1]
trimLen := len(res[0])
w = q[trimLen:]
ok = true
}
w = strings.TrimSpace(w)
s.exp = converter.StringToInt(exp)
s.w = w
s.page = dto.Page
s.size = dto.Size
s.order = dto.Order
return
}
func (s *ScoreSearch) Search(ctx context.Context) (resp []schema.SearchResp, total int64, err error) {
var (
words []string
)
words = strings.Split(s.w, " ")
if len(words) > 3 {
words = words[:4]
}
resp, total, err = s.repo.SearchContents(ctx, words, "", "", s.exp, s.page, s.size, s.order)
return
}

View File

@ -1,99 +0,0 @@
package search
import (
"context"
"regexp"
"strings"
"github.com/answerdev/answer/internal/entity"
"github.com/answerdev/answer/internal/schema"
"github.com/answerdev/answer/internal/service/activity_common"
"github.com/answerdev/answer/internal/service/search_common"
tagcommon "github.com/answerdev/answer/internal/service/tag_common"
)
type TagSearch struct {
repo search_common.SearchRepo
tagRepo tagcommon.TagRepo
followCommon activity_common.FollowRepo
page int
size int
exp string
w string
userID string
Extra schema.GetTagPageResp
order string
}
func NewTagSearch(repo search_common.SearchRepo, tagRepo tagcommon.TagRepo, followCommon activity_common.FollowRepo) *TagSearch {
return &TagSearch{
repo: repo,
tagRepo: tagRepo,
followCommon: followCommon,
}
}
// Parse
// example: "[tag]hello" -> {exp="tag" w="hello"}
func (ts *TagSearch) Parse(dto *schema.SearchDTO) (ok bool) {
exp := ""
w := dto.Query
q := w
p := `(?m)^\[([a-zA-Z0-9-\+\.#]+)\]`
re := regexp.MustCompile(p)
res := re.FindStringSubmatch(q)
if len(res) == 2 {
exp = res[1]
trimLen := len(res[0])
w = q[trimLen:]
ok = true
}
w = strings.TrimSpace(w)
ts.exp = exp
ts.w = w
ts.page = dto.Page
ts.size = dto.Size
ts.userID = dto.UserID
ts.order = dto.Order
return ok
}
func (ts *TagSearch) Search(ctx context.Context) (resp []schema.SearchResp, total int64, err error) {
var (
words []string
tag *entity.Tag
exists, followed bool
)
tag, exists, err = ts.tagRepo.GetTagBySlugName(ctx, ts.exp)
if err != nil {
return
}
if ts.userID != "" {
followed, err = ts.followCommon.IsFollowed(ts.userID, tag.ID)
}
ts.Extra = schema.GetTagPageResp{
TagID: tag.ID,
SlugName: tag.SlugName,
DisplayName: tag.DisplayName,
OriginalText: tag.OriginalText,
ParsedText: tag.ParsedText,
QuestionCount: tag.QuestionCount,
IsFollower: followed,
}
ts.Extra.GetExcerpt()
if !exists {
return
}
words = strings.Split(ts.w, " ")
if len(words) > 3 {
words = words[:4]
}
resp, total, err = ts.repo.SearchContents(ctx, words, tag.ID, "", -1, ts.page, ts.size, ts.order)
return
}

View File

@ -1,47 +0,0 @@
package search
import (
"context"
"github.com/answerdev/answer/internal/schema"
"github.com/answerdev/answer/internal/service/search_common"
"regexp"
"strings"
)
type ViewsSearch struct {
repo search_common.SearchRepo
exp string
q string
order string
}
func NewViewsSearch(repo search_common.SearchRepo) *ViewsSearch {
return &ViewsSearch{
repo: repo,
}
}
func (s *ViewsSearch) Parse(dto *schema.SearchDTO) (ok bool) {
exp := ""
w := dto.Query
q := w
p := `(?m)^views:([0-9]+)`
re := regexp.MustCompile(p)
res := re.FindStringSubmatch(q)
if len(res) == 2 {
exp = res[1]
trimLen := len(res[0])
q = w[trimLen:]
ok = true
}
q = strings.TrimSpace(q)
s.exp = exp
s.q = q
s.order = dto.Order
return
}
func (s *ViewsSearch) Search(ctx context.Context) (resp []schema.SearchResp, total int64, err error) {
return
}

View File

@ -1,59 +0,0 @@
package search
import (
"context"
"github.com/answerdev/answer/internal/schema"
"github.com/answerdev/answer/internal/service/search_common"
)
type WithinSearch struct {
repo search_common.SearchRepo
w string
page int
size int
order string
}
func NewWithinSearch(repo search_common.SearchRepo) *WithinSearch {
return &WithinSearch{
repo: repo,
}
}
func (s *WithinSearch) Parse(dto *schema.SearchDTO) (ok bool) {
var (
q string
w []rune
hasEnd bool
)
q = dto.Query
if q[0:1] == `"` {
for _, v := range []rune(q) {
if len(w) == 0 && string(v) == `"` {
continue
} else if string(v) == `"` {
hasEnd = true
break
} else {
w = append(w, v)
}
}
}
if hasEnd {
ok = true
}
s.w = string(w)
s.page = dto.Page
s.size = dto.Size
s.order = dto.Order
return
}
func (s *WithinSearch) Search(ctx context.Context) (resp []schema.SearchResp, total int64, err error) {
return s.repo.SearchContents(ctx, []string{s.w}, "", "", -1, s.page, s.size, s.order)
}

View File

@ -6,7 +6,7 @@ import (
)
type SearchRepo interface {
SearchContents(ctx context.Context, words []string, tagID, userID string, votes, page, size int, order string) (resp []schema.SearchResp, total int64, err error)
SearchQuestions(ctx context.Context, words []string, limitNoAccepted bool, answers, page, size int, order string) (resp []schema.SearchResp, total int64, err error)
SearchAnswers(ctx context.Context, words []string, limitAccepted bool, questionID string, page, size int, order string) (resp []schema.SearchResp, total int64, err error)
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, 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)
}

View File

@ -0,0 +1,359 @@
package search_parser
import (
"context"
"github.com/answerdev/answer/internal/schema"
tagcommon "github.com/answerdev/answer/internal/service/tag_common"
usercommon "github.com/answerdev/answer/internal/service/user_common"
"github.com/answerdev/answer/pkg/converter"
"regexp"
"strings"
)
type SearchParser struct {
tagRepo tagcommon.TagRepo
userCommon *usercommon.UserCommon
}
func NewSearchParser(tagRepo tagcommon.TagRepo, userCommon *usercommon.UserCommon) *SearchParser {
return &SearchParser{
tagRepo: tagRepo,
userCommon: userCommon,
}
}
// ParseStructure parse search structure, maybe match one of type all/questions/answers,
// but if match two type, it will return false
func (sp *SearchParser) ParseStructure(dto *schema.SearchDTO) (
searchType string,
// search all
userID string,
votes int,
// search questions
notAccepted bool,
isQuestion bool,
views,
answers int,
// search answers
accepted bool,
questionID string,
isAnswer bool,
// common fields
tags,
words []string,
) {
var (
query = dto.Query
currentUserID = dto.UserID
all = 0
q = 0
a = 0
withWords = []string{}
limitWords = 5
)
// match tags
tags = sp.parseTags(&query)
// match all
userID = sp.parseUserID(&query, currentUserID)
if userID != "" {
searchType = "all"
all = 1
}
votes = sp.parseVotes(&query)
if votes != -1 {
searchType = "all"
all = 1
}
withWords = sp.parseWithin(&query)
if len(withWords) > 0 {
searchType = "all"
all = 1
}
// match questions
notAccepted = sp.parseNotAccepted(&query)
if notAccepted {
searchType = "question"
q = 1
}
isQuestion = sp.parseIsQuestion(&query)
if isQuestion {
searchType = "question"
q = 1
}
views = sp.parseViews(&query)
if views != -1 {
searchType = "question"
q = 1
}
answers = sp.parseAnswers(&query)
if answers != -1 {
searchType = "question"
q = 1
}
// match answers
accepted = sp.parseAccepted(&query)
if accepted {
searchType = "answer"
a = 1
}
questionID = sp.parseQuestionID(&query)
if questionID != "" {
searchType = "answer"
a = 1
}
isAnswer = sp.parseIsAnswer(&query)
if isAnswer {
searchType = "answer"
a = 1
}
words = strings.Split(query, " ")
if len(withWords) > 0 {
words = append(withWords, words...)
}
// check limit words
if len(words) > limitWords {
words = words[:limitWords]
}
// check tags' search is all or question
if len(tags) > 0 {
if len(words) > 0 {
searchType = "all"
all = 1
} else {
searchType = "question"
q = 1
}
}
// check match types greater than 1
if all+q+a > 1 {
searchType = ""
}
// check not match
if all+q+a == 0 && len(words) > 0 {
searchType = "all"
}
return
}
// parseTags parse search tags, return tag ids array
func (sp *SearchParser) parseTags(query *string) (tags []string) {
var (
// expire tag pattern
exprTag = `(?m)\[([a-zA-Z0-9-\+\.#]+)\]{1}?`
q = *query
limit = 5
)
re := regexp.MustCompile(exprTag)
res := re.FindAllStringSubmatch(q, -1)
if len(res) == 0 {
return
}
tags = make([]string, len(res))
for i, item := range res {
tag, exists, err := sp.tagRepo.GetTagBySlugName(context.TODO(), item[1])
if err != nil || !exists {
continue
}
tags[i] = tag.ID
}
// limit maximum 5 tags
if len(tags) > limit {
tags = tags[:limit]
}
q = strings.TrimSpace(re.ReplaceAllString(q, ""))
*query = q
return
}
// parseUserID return user id or current login user id
func (sp *SearchParser) parseUserID(query *string, currentUserID string) (userID string) {
var (
exprUserID = `(?m)^user:([a-z0-9._-]+)`
exprMe = "user:me"
q = *query
)
re := regexp.MustCompile(exprUserID)
res := re.FindStringSubmatch(q)
if len(res) == 2 {
name := res[1]
user, has, err := sp.userCommon.GetUserBasicInfoByUserName(nil, name)
if err == nil && has {
userID = user.ID
q = re.ReplaceAllString(q, "")
}
} else if strings.Index(q, exprMe) != -1 {
userID = currentUserID
q = strings.ReplaceAll(q, exprMe, "")
}
*query = strings.TrimSpace(q)
return
}
// parseVotes return the votes of search query
func (sp *SearchParser) parseVotes(query *string) (votes int) {
var (
expr = `(?m)^score:([0-9]+)`
q = *query
)
votes = -1
re := regexp.MustCompile(expr)
res := re.FindStringSubmatch(q)
if len(res) == 2 {
votes = converter.StringToInt(res[1])
q = re.ReplaceAllString(q, "")
}
*query = strings.TrimSpace(q)
return
}
// parseWithin parse quotes within words like: "hello world"
func (sp *SearchParser) parseWithin(query *string) (words []string) {
var (
q = *query
expr = `(?U)(".+")`
)
re := regexp.MustCompile(expr)
matches := re.FindAllStringSubmatch(q, -1)
words = []string{}
for _, match := range matches {
words = append(words, match[1])
}
q = re.ReplaceAllString(q, "")
*query = strings.TrimSpace(q)
return
}
// parseNotAccepted return the question has not accepted the answer
func (sp *SearchParser) parseNotAccepted(query *string) (notAccepted bool) {
var (
q = *query
expr = `hasaccepted:no`
)
if strings.Index(q, expr) != -1 {
q = strings.ReplaceAll(q, expr, "")
notAccepted = true
}
*query = strings.TrimSpace(q)
return
}
// parseIsQuestion check the result if only limit question or not
func (sp *SearchParser) parseIsQuestion(query *string) (isQuestion bool) {
var (
q = *query
expr = `is:question`
)
if strings.Index(q, expr) == 0 {
q = strings.ReplaceAll(q, expr, "")
isQuestion = true
}
*query = strings.TrimSpace(q)
return
}
// parseViews check search has views or not
func (sp *SearchParser) parseViews(query *string) (views int) {
var (
q = *query
expr = `(?m)^views:([0-9]+)`
)
views = -1
re := regexp.MustCompile(expr)
res := re.FindStringSubmatch(q)
if len(res) == 2 {
views = converter.StringToInt(res[1])
q = re.ReplaceAllString(q, "")
}
*query = strings.TrimSpace(q)
return
}
// parseAnswers check whether specified answer count for question
func (sp *SearchParser) parseAnswers(query *string) (answers int) {
var (
q = *query
expr = `(?m)^answers:([0-9]+)`
)
answers = -1
re := regexp.MustCompile(expr)
res := re.FindStringSubmatch(q)
if len(res) == 2 {
answers = converter.StringToInt(res[1])
q = re.ReplaceAllString(q, "")
}
*query = strings.TrimSpace(q)
return
}
// parseAccepted check the search is limit accepted answer or not
func (sp *SearchParser) parseAccepted(query *string) (accepted bool) {
var (
q = *query
expr = `isaccepted:yes`
)
if strings.Index(q, expr) != -1 {
accepted = true
strings.ReplaceAll(q, expr, "")
}
*query = strings.TrimSpace(q)
return
}
// parseQuestionID check whether specified question's id
func (sp *SearchParser) parseQuestionID(query *string) (questionID string) {
var (
q = *query
expr = `(?m)^inquestion:([0-9]+)`
)
re := regexp.MustCompile(expr)
res := re.FindStringSubmatch(q)
if len(res) == 2 {
questionID = res[1]
q = re.ReplaceAllString(q, "")
}
*query = strings.TrimSpace(q)
return
}
// parseIsAnswer check the result if only limit answer or not
func (sp *SearchParser) parseIsAnswer(query *string) (isAnswer bool) {
var (
q = *query
expr = `is:answer`
)
if strings.Index(q, expr) != -1 {
isAnswer = true
q = strings.ReplaceAll(q, expr, "")
}
*query = strings.TrimSpace(q)
return
}

View File

@ -2,92 +2,61 @@ package service
import (
"context"
"github.com/answerdev/answer/internal/schema"
"github.com/answerdev/answer/internal/service/activity_common"
"github.com/answerdev/answer/internal/service/search"
"github.com/answerdev/answer/internal/service/search_common"
tagcommon "github.com/answerdev/answer/internal/service/tag_common"
usercommon "github.com/answerdev/answer/internal/service/user_common"
"github.com/answerdev/answer/internal/service/search_parser"
)
type Search interface {
Parse(dto *schema.SearchDTO) (ok bool)
Search(ctx context.Context) (resp []schema.SearchResp, total int64, err error)
}
type SearchService struct {
searchRepo search_common.SearchRepo
tagSearch *search.TagSearch
withinSearch *search.WithinSearch
authorSearch *search.AuthorSearch
scoreSearch *search.ScoreSearch
answersSearch *search.AnswersSearch
notAcceptedQuestion *search.NotAcceptedQuestion
acceptedAnswerSearch *search.AcceptedAnswerSearch
inQuestionSearch *search.InQuestionSearch
questionSearch *search.QuestionSearch
answerSearch *search.AnswerSearch
viewsSearch *search.ViewsSearch
objectSearch *search.ObjectSearch
searchParser *search_parser.SearchParser
searchRepo search_common.SearchRepo
}
func NewSearchService(
searchParser *search_parser.SearchParser,
searchRepo search_common.SearchRepo,
tagRepo tagcommon.TagRepo,
userCommon *usercommon.UserCommon,
followCommon activity_common.FollowRepo,
) *SearchService {
return &SearchService{
searchRepo: searchRepo,
tagSearch: search.NewTagSearch(searchRepo, tagRepo, followCommon),
withinSearch: search.NewWithinSearch(searchRepo),
authorSearch: search.NewAuthorSearch(searchRepo, userCommon),
scoreSearch: search.NewScoreSearch(searchRepo),
answersSearch: search.NewAnswersSearch(searchRepo),
acceptedAnswerSearch: search.NewAcceptedAnswerSearch(searchRepo),
notAcceptedQuestion: search.NewNotAcceptedQuestion(searchRepo),
inQuestionSearch: search.NewInQuestionSearch(searchRepo),
questionSearch: search.NewQuestionSearch(searchRepo),
answerSearch: search.NewAnswerSearch(searchRepo),
viewsSearch: search.NewViewsSearch(searchRepo),
objectSearch: search.NewObjectSearch(searchRepo),
searchParser: searchParser,
searchRepo: searchRepo,
}
}
// Search search contents
func (ss *SearchService) Search(ctx context.Context, dto *schema.SearchDTO) (resp []schema.SearchResp, total int64, extra interface{}, err error) {
extra = nil
if dto.Page < 1 {
dto.Page = 1
}
switch {
case ss.tagSearch.Parse(dto):
resp, total, err = ss.tagSearch.Search(ctx)
extra = ss.tagSearch.Extra
case ss.withinSearch.Parse(dto):
resp, total, err = ss.withinSearch.Search(ctx)
case ss.authorSearch.Parse(dto):
resp, total, err = ss.authorSearch.Search(ctx)
case ss.scoreSearch.Parse(dto):
resp, total, err = ss.scoreSearch.Search(ctx)
case ss.answersSearch.Parse(dto):
resp, total, err = ss.answersSearch.Search(ctx)
case ss.acceptedAnswerSearch.Parse(dto):
resp, total, err = ss.acceptedAnswerSearch.Search(ctx)
case ss.notAcceptedQuestion.Parse(dto):
resp, total, err = ss.notAcceptedQuestion.Search(ctx)
case ss.inQuestionSearch.Parse(dto):
resp, total, err = ss.inQuestionSearch.Search(ctx)
case ss.questionSearch.Parse(dto):
resp, total, err = ss.questionSearch.Search(ctx)
case ss.answerSearch.Parse(dto):
resp, total, err = ss.answerSearch.Search(ctx)
case ss.viewsSearch.Parse(dto):
resp, total, err = ss.viewsSearch.Search(ctx)
default:
ss.objectSearch.Parse(dto)
resp, total, err = ss.objectSearch.Search(ctx)
// search type
searchType,
// search all
userID,
votes,
// search questions
notAccepted,
_,
views,
answers,
// search answers
accepted,
questionID,
_,
// common fields
tags,
words := ss.searchParser.ParseStructure(dto)
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, 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 resp, total, extra, err
return
}