mirror of https://gitee.com/answerdev/answer.git
377 lines
7.5 KiB
Go
377 lines
7.5 KiB
Go
package search_parser
|
|
|
|
import (
|
|
"context"
|
|
"regexp"
|
|
"strings"
|
|
|
|
"github.com/answerdev/answer/internal/schema"
|
|
"github.com/answerdev/answer/internal/service/tag_common"
|
|
usercommon "github.com/answerdev/answer/internal/service/user_common"
|
|
"github.com/answerdev/answer/pkg/converter"
|
|
)
|
|
|
|
type SearchParser struct {
|
|
tagCommonService *tag_common.TagCommonService
|
|
userCommon *usercommon.UserCommon
|
|
}
|
|
|
|
func NewSearchParser(tagCommonService *tag_common.TagCommonService, userCommon *usercommon.UserCommon) *SearchParser {
|
|
return &SearchParser{
|
|
tagCommonService: tagCommonService,
|
|
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
|
|
}
|
|
|
|
if len(strings.TrimSpace(query)) > 0 {
|
|
words = strings.Split(strings.TrimSpace(query), " ")
|
|
} else {
|
|
words = []string{}
|
|
}
|
|
|
|
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 if isAnswer {
|
|
searchType = "answer"
|
|
a = 1
|
|
all = 0
|
|
q = 0
|
|
} else {
|
|
searchType = "question"
|
|
q = 1
|
|
all = 0
|
|
a = 0
|
|
}
|
|
}
|
|
|
|
// 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 = []string{}
|
|
for _, item := range res {
|
|
tag, exists, err := sp.tagCommonService.GetTagBySlugName(context.TODO(), item[1])
|
|
if err != nil || !exists {
|
|
continue
|
|
}
|
|
tags = append(tags, 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 strings.Contains(q, exprMe) {
|
|
userID = currentUserID
|
|
q = strings.ReplaceAll(q, exprMe, "")
|
|
} else if len(res) == 2 {
|
|
name := res[1]
|
|
user, has, err := sp.userCommon.GetUserBasicInfoByUserName(context.TODO(), name)
|
|
if err == nil && has {
|
|
userID = user.ID
|
|
q = re.ReplaceAllString(q, "")
|
|
}
|
|
}
|
|
*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 {
|
|
if len(match[1]) == 0 {
|
|
continue
|
|
}
|
|
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.Contains(q, expr) {
|
|
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.Contains(q, expr) {
|
|
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.Contains(q, expr) {
|
|
accepted = true
|
|
q = 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.Contains(q, expr) {
|
|
isAnswer = true
|
|
q = strings.ReplaceAll(q, expr, "")
|
|
}
|
|
|
|
*query = strings.TrimSpace(q)
|
|
return
|
|
}
|