mirror of https://gitee.com/answerdev/answer.git
581 lines
16 KiB
Go
581 lines
16 KiB
Go
package search_common
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
tagcommon "github.com/answerdev/answer/internal/service/tag_common"
|
|
"github.com/answerdev/answer/plugin"
|
|
"strconv"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/answerdev/answer/pkg/htmltext"
|
|
|
|
"github.com/answerdev/answer/internal/base/data"
|
|
"github.com/answerdev/answer/internal/base/reason"
|
|
"github.com/answerdev/answer/internal/entity"
|
|
"github.com/answerdev/answer/internal/schema"
|
|
"github.com/answerdev/answer/internal/service/search_common"
|
|
"github.com/answerdev/answer/internal/service/unique"
|
|
usercommon "github.com/answerdev/answer/internal/service/user_common"
|
|
"github.com/answerdev/answer/pkg/converter"
|
|
"github.com/answerdev/answer/pkg/obj"
|
|
"github.com/segmentfault/pacman/errors"
|
|
"xorm.io/builder"
|
|
)
|
|
|
|
var (
|
|
qFields = []string{
|
|
"`question`.`id`",
|
|
"`question`.`id` as `question_id`",
|
|
"`title`",
|
|
"`parsed_text`",
|
|
"`question`.`created_at` as `created_at`",
|
|
"`user_id`",
|
|
"`vote_count`",
|
|
"`answer_count`",
|
|
"CASE WHEN `accepted_answer_id` > 0 THEN 2 ELSE 0 END as `accepted`",
|
|
"`question`.`status` as `status`",
|
|
"`post_update_time`",
|
|
}
|
|
aFields = []string{
|
|
"`answer`.`id` as `id`",
|
|
"`question_id`",
|
|
"`question`.`title` as `title`",
|
|
"`answer`.`parsed_text` as `parsed_text`",
|
|
"`answer`.`created_at` as `created_at`",
|
|
"`answer`.`user_id` as `user_id`",
|
|
"`answer`.`vote_count` as `vote_count`",
|
|
"0 as `answer_count`",
|
|
"`adopted` as `accepted`",
|
|
"`answer`.`status` as `status`",
|
|
"`answer`.`created_at` as `post_update_time`",
|
|
}
|
|
)
|
|
|
|
// searchRepo tag repository
|
|
type searchRepo struct {
|
|
data *data.Data
|
|
userCommon *usercommon.UserCommon
|
|
uniqueIDRepo unique.UniqueIDRepo
|
|
tagCommon *tagcommon.TagCommonService
|
|
}
|
|
|
|
// NewSearchRepo new repository
|
|
func NewSearchRepo(
|
|
data *data.Data,
|
|
uniqueIDRepo unique.UniqueIDRepo,
|
|
userCommon *usercommon.UserCommon,
|
|
tagCommon *tagcommon.TagCommonService,
|
|
) search_common.SearchRepo {
|
|
return &searchRepo{
|
|
data: data,
|
|
uniqueIDRepo: uniqueIDRepo,
|
|
userCommon: userCommon,
|
|
tagCommon: tagCommon,
|
|
}
|
|
}
|
|
|
|
// SearchContents search question and answer data
|
|
func (sr *searchRepo) SearchContents(ctx context.Context, words []string, tagIDs []string, userID string, votes int, page, size int, order string) (resp []*schema.SearchResult, total int64, err error) {
|
|
words = filterWords(words)
|
|
|
|
var (
|
|
b *builder.Builder
|
|
ub *builder.Builder
|
|
qfs = qFields
|
|
afs = aFields
|
|
argsQ = []interface{}{}
|
|
argsA = []interface{}{}
|
|
)
|
|
|
|
if order == "relevance" {
|
|
if len(words) > 0 {
|
|
qfs, argsQ = addRelevanceField([]string{"title", "original_text"}, words, qfs)
|
|
afs, argsA = addRelevanceField([]string{"`answer`.`original_text`"}, words, afs)
|
|
} else {
|
|
order = "newest"
|
|
}
|
|
}
|
|
|
|
b = builder.MySQL().Select(qfs...).From("`question`")
|
|
ub = builder.MySQL().Select(afs...).From("`answer`").
|
|
LeftJoin("`question`", "`question`.id = `answer`.question_id")
|
|
|
|
b.Where(builder.Lt{"`question`.`status`": entity.QuestionStatusDeleted}).
|
|
And(builder.Eq{"`question`.`show`": entity.QuestionShow})
|
|
ub.Where(builder.Lt{"`question`.`status`": entity.QuestionStatusDeleted}).
|
|
And(builder.Lt{"`answer`.`status`": entity.AnswerStatusDeleted}).
|
|
And(builder.Eq{"`question`.`show`": entity.QuestionShow})
|
|
|
|
argsQ = append(argsQ, entity.QuestionStatusDeleted, entity.QuestionShow)
|
|
argsA = append(argsA, entity.QuestionStatusDeleted, entity.AnswerStatusDeleted, entity.QuestionShow)
|
|
|
|
likeConQ := builder.NewCond()
|
|
likeConA := builder.NewCond()
|
|
for _, word := range words {
|
|
likeConQ = likeConQ.Or(builder.Like{"title", word}).
|
|
Or(builder.Like{"original_text", word})
|
|
argsQ = append(argsQ, "%"+word+"%")
|
|
argsQ = append(argsQ, "%"+word+"%")
|
|
|
|
likeConA = likeConA.Or(builder.Like{"`answer`.original_text", word})
|
|
argsA = append(argsA, "%"+word+"%")
|
|
}
|
|
|
|
b.Where(likeConQ)
|
|
ub.Where(likeConA)
|
|
|
|
// check tag
|
|
if len(tagIDs) > 0 {
|
|
for ti, tagID := range tagIDs {
|
|
ast := "tag_rel" + strconv.Itoa(ti)
|
|
b.Join("INNER", "tag_rel as "+ast, "question.id = "+ast+".object_id").
|
|
And(builder.Eq{
|
|
ast + ".tag_id": tagID,
|
|
ast + ".status": entity.TagRelStatusAvailable,
|
|
})
|
|
ub.Join("INNER", "tag_rel as "+ast, "question_id = "+ast+".object_id").
|
|
And(builder.Eq{
|
|
ast + ".tag_id": tagID,
|
|
ast + ".status": entity.TagRelStatusAvailable,
|
|
})
|
|
argsQ = append(argsQ, entity.TagRelStatusAvailable, tagID)
|
|
argsA = append(argsA, entity.TagRelStatusAvailable, tagID)
|
|
}
|
|
}
|
|
|
|
// check user
|
|
if userID != "" {
|
|
b.Where(builder.Eq{"question.user_id": userID})
|
|
ub.Where(builder.Eq{"answer.user_id": userID})
|
|
argsQ = append(argsQ, userID)
|
|
argsA = append(argsA, userID)
|
|
}
|
|
|
|
// check vote
|
|
if votes == 0 {
|
|
b.Where(builder.Eq{"question.vote_count": votes})
|
|
ub.Where(builder.Eq{"answer.vote_count": votes})
|
|
argsQ = append(argsQ, votes)
|
|
argsA = append(argsA, votes)
|
|
} else if votes > 0 {
|
|
b.Where(builder.Gte{"question.vote_count": votes})
|
|
ub.Where(builder.Gte{"answer.vote_count": votes})
|
|
argsQ = append(argsQ, votes)
|
|
argsA = append(argsA, votes)
|
|
}
|
|
|
|
//b = b.Union("all", ub)
|
|
ubSQL, _, err := ub.ToSQL()
|
|
if err != nil {
|
|
return
|
|
}
|
|
bSQL, _, err := b.ToSQL()
|
|
if err != nil {
|
|
return
|
|
}
|
|
sql := fmt.Sprintf("(%s UNION ALL %s)", bSQL, ubSQL)
|
|
|
|
countSQL, _, err := builder.MySQL().Select("count(*) total").From(sql, "c").ToSQL()
|
|
if err != nil {
|
|
return
|
|
}
|
|
|
|
querySQL, _, err := builder.MySQL().Select("*").From(sql, "t").OrderBy(sr.parseOrder(ctx, order)).Limit(size, page-1).ToSQL()
|
|
if err != nil {
|
|
return
|
|
}
|
|
|
|
queryArgs := []interface{}{}
|
|
countArgs := []interface{}{}
|
|
|
|
queryArgs = append(queryArgs, querySQL)
|
|
queryArgs = append(queryArgs, argsQ...)
|
|
queryArgs = append(queryArgs, argsA...)
|
|
|
|
countArgs = append(countArgs, countSQL)
|
|
countArgs = append(countArgs, argsQ...)
|
|
countArgs = append(countArgs, argsA...)
|
|
|
|
res, err := sr.data.DB.Context(ctx).Query(queryArgs...)
|
|
if err != nil {
|
|
return
|
|
}
|
|
|
|
tr, err := sr.data.DB.Context(ctx).Query(countArgs...)
|
|
if len(tr) != 0 {
|
|
total = converter.StringToInt64(string(tr[0]["total"]))
|
|
}
|
|
if err != nil {
|
|
err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()
|
|
return
|
|
} else {
|
|
resp, err = sr.parseResult(ctx, res)
|
|
return
|
|
}
|
|
}
|
|
|
|
// SearchQuestions search question data
|
|
func (sr *searchRepo) SearchQuestions(ctx context.Context, words []string, tagIDs []string, notAccepted bool, views, answers int, page, size int, order string) (resp []*schema.SearchResult, total int64, err error) {
|
|
words = filterWords(words)
|
|
var (
|
|
qfs = qFields
|
|
args = []interface{}{}
|
|
)
|
|
if order == "relevance" {
|
|
if len(words) > 0 {
|
|
qfs, args = addRelevanceField([]string{"title", "original_text"}, words, qfs)
|
|
} else {
|
|
order = "newest"
|
|
}
|
|
}
|
|
|
|
b := builder.MySQL().Select(qfs...).From("question")
|
|
|
|
b.Where(builder.Lt{"`question`.`status`": entity.QuestionStatusDeleted}).And(builder.Eq{"`question`.`show`": entity.QuestionShow})
|
|
args = append(args, entity.QuestionStatusDeleted, entity.QuestionShow)
|
|
|
|
likeConQ := builder.NewCond()
|
|
for _, word := range words {
|
|
likeConQ = likeConQ.Or(builder.Like{"title", word}).
|
|
Or(builder.Like{"original_text", word})
|
|
args = append(args, "%"+word+"%")
|
|
args = append(args, "%"+word+"%")
|
|
}
|
|
b.Where(likeConQ)
|
|
|
|
// check tag
|
|
if len(tagIDs) > 0 {
|
|
for ti, tagID := range tagIDs {
|
|
ast := "tag_rel" + strconv.Itoa(ti)
|
|
b.Join("INNER", "tag_rel as "+ast, "question.id = "+ast+".object_id").
|
|
And(builder.Eq{
|
|
ast + ".tag_id": tagID,
|
|
ast + ".status": entity.TagRelStatusAvailable,
|
|
})
|
|
args = append(args, entity.TagRelStatusAvailable, tagID)
|
|
}
|
|
}
|
|
|
|
// check need filter has not accepted
|
|
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)
|
|
} else if answers > 0 {
|
|
b.And(builder.Gte{"answer_count": answers})
|
|
args = append(args, answers)
|
|
}
|
|
|
|
queryArgs := []interface{}{}
|
|
countArgs := []interface{}{}
|
|
|
|
countSQL, _, err := builder.MySQL().Select("count(*) total").From(b, "c").ToSQL()
|
|
if err != nil {
|
|
return
|
|
}
|
|
|
|
querySQL, _, err := b.OrderBy(sr.parseOrder(ctx, order)).Limit(size, page-1).ToSQL()
|
|
if err != nil {
|
|
return
|
|
}
|
|
queryArgs = append(queryArgs, querySQL)
|
|
queryArgs = append(queryArgs, args...)
|
|
|
|
countArgs = append(countArgs, countSQL)
|
|
countArgs = append(countArgs, args...)
|
|
|
|
res, err := sr.data.DB.Context(ctx).Query(queryArgs...)
|
|
if err != nil {
|
|
return
|
|
}
|
|
|
|
tr, err := sr.data.DB.Context(ctx).Query(countArgs...)
|
|
if err != nil {
|
|
return
|
|
}
|
|
|
|
if len(tr) != 0 {
|
|
total = converter.StringToInt64(string(tr[0]["total"]))
|
|
}
|
|
resp, err = sr.parseResult(ctx, res)
|
|
if err != nil {
|
|
err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()
|
|
}
|
|
return
|
|
}
|
|
|
|
// SearchAnswers search answer data
|
|
func (sr *searchRepo) SearchAnswers(ctx context.Context, words []string, tagIDs []string, accepted bool, questionID string, page, size int, order string) (resp []*schema.SearchResult, total int64, err error) {
|
|
words = filterWords(words)
|
|
|
|
var (
|
|
afs = aFields
|
|
args = []interface{}{}
|
|
)
|
|
if order == "relevance" {
|
|
if len(words) > 0 {
|
|
afs, args = addRelevanceField([]string{"`answer`.`original_text`"}, words, afs)
|
|
} else {
|
|
order = "newest"
|
|
}
|
|
}
|
|
|
|
b := builder.MySQL().Select(afs...).From("`answer`").
|
|
LeftJoin("`question`", "`question`.id = `answer`.question_id")
|
|
|
|
b.Where(builder.Lt{"`question`.`status`": entity.QuestionStatusDeleted}).
|
|
And(builder.Lt{"`answer`.`status`": entity.AnswerStatusDeleted}).And(builder.Eq{"`question`.`show`": entity.QuestionShow})
|
|
args = append(args, entity.QuestionStatusDeleted, entity.AnswerStatusDeleted, entity.QuestionShow)
|
|
|
|
likeConA := builder.NewCond()
|
|
for _, word := range words {
|
|
likeConA = likeConA.Or(builder.Like{"`answer`.original_text", word})
|
|
args = append(args, "%"+word+"%")
|
|
}
|
|
|
|
b.Where(likeConA)
|
|
|
|
// check tag
|
|
if len(tagIDs) > 0 {
|
|
for ti, tagID := range tagIDs {
|
|
ast := "tag_rel" + strconv.Itoa(ti)
|
|
b.Join("INNER", "tag_rel as "+ast, "question_id = "+ast+".object_id").
|
|
And(builder.Eq{
|
|
ast + ".tag_id": tagID,
|
|
ast + ".status": entity.TagRelStatusAvailable,
|
|
})
|
|
args = append(args, entity.TagRelStatusAvailable, tagID)
|
|
}
|
|
}
|
|
|
|
// check limit accepted
|
|
if accepted {
|
|
b.Where(builder.Eq{"adopted": schema.AnswerAcceptedEnable})
|
|
args = append(args, schema.AnswerAcceptedEnable)
|
|
}
|
|
|
|
// check question id
|
|
if questionID != "" {
|
|
b.Where(builder.Eq{"question_id": questionID})
|
|
args = append(args, questionID)
|
|
}
|
|
|
|
queryArgs := []interface{}{}
|
|
countArgs := []interface{}{}
|
|
|
|
countSQL, _, err := builder.MySQL().Select("count(*) total").From(b, "c").ToSQL()
|
|
if err != nil {
|
|
return
|
|
}
|
|
|
|
querySQL, _, err := b.OrderBy(sr.parseOrder(ctx, order)).Limit(size, page-1).ToSQL()
|
|
if err != nil {
|
|
return
|
|
}
|
|
|
|
queryArgs = append(queryArgs, querySQL)
|
|
queryArgs = append(queryArgs, args...)
|
|
|
|
countArgs = append(countArgs, countSQL)
|
|
countArgs = append(countArgs, args...)
|
|
|
|
res, err := sr.data.DB.Context(ctx).Query(queryArgs...)
|
|
if err != nil {
|
|
return
|
|
}
|
|
|
|
tr, err := sr.data.DB.Context(ctx).Query(countArgs...)
|
|
if err != nil {
|
|
return
|
|
}
|
|
|
|
total = converter.StringToInt64(string(tr[0]["total"]))
|
|
resp, err = sr.parseResult(ctx, res)
|
|
if err != nil {
|
|
err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()
|
|
}
|
|
return
|
|
}
|
|
|
|
func (sr *searchRepo) parseOrder(ctx context.Context, order string) (res string) {
|
|
switch order {
|
|
case "newest":
|
|
res = "created_at desc"
|
|
case "active":
|
|
res = "post_update_time desc"
|
|
case "score":
|
|
res = "vote_count desc"
|
|
case "relevance":
|
|
res = "relevance desc"
|
|
default:
|
|
res = "created_at desc"
|
|
}
|
|
return
|
|
}
|
|
|
|
// ParseSearchPluginResult parse search plugin result
|
|
func (sr *searchRepo) ParseSearchPluginResult(ctx context.Context, sres []plugin.SearchResult) (resp []*schema.SearchResult, 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.Context(ctx).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.SearchResult, err error) {
|
|
questionIDs := make([]string, 0)
|
|
userIDs := make([]string, 0)
|
|
resultList := make([]*schema.SearchResult, 0)
|
|
for _, r := range res {
|
|
questionIDs = append(questionIDs, string(r["question_id"]))
|
|
userIDs = append(userIDs, string(r["user_id"]))
|
|
tp, _ := time.ParseInLocation("2006-01-02 15:04:05", string(r["created_at"]), time.Local)
|
|
object := &schema.SearchObject{
|
|
ID: string(r["id"]),
|
|
QuestionID: string(r["question_id"]),
|
|
Title: string(r["title"]),
|
|
Excerpt: htmltext.FetchExcerpt(string(r["parsed_text"]), "...", 240),
|
|
CreatedAtParsed: tp.Unix(),
|
|
UserInfo: &schema.SearchObjectUser{
|
|
ID: string(r["user_id"]),
|
|
},
|
|
Tags: make([]*schema.TagResp, 0),
|
|
VoteCount: converter.StringToInt(string(r["vote_count"])),
|
|
Accepted: string(r["accepted"]) == "2",
|
|
AnswerCount: converter.StringToInt(string(r["answer_count"])),
|
|
}
|
|
|
|
objectKey, err := obj.GetObjectTypeStrByObjectID(string(r["id"]))
|
|
if err != nil {
|
|
continue
|
|
}
|
|
|
|
switch objectKey {
|
|
case "question":
|
|
for k, v := range entity.AdminQuestionSearchStatus {
|
|
if v == converter.StringToInt(string(r["status"])) {
|
|
object.StatusStr = k
|
|
break
|
|
}
|
|
}
|
|
case "answer":
|
|
for k, v := range entity.AdminAnswerSearchStatus {
|
|
if v == converter.StringToInt(string(r["status"])) {
|
|
object.StatusStr = k
|
|
break
|
|
}
|
|
}
|
|
}
|
|
|
|
resultList = append(resultList, &schema.SearchResult{
|
|
ObjectType: objectKey,
|
|
Object: object,
|
|
})
|
|
}
|
|
|
|
tagsMap, err := sr.tagCommon.BatchGetObjectTag(ctx, questionIDs)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
userInfoMap, err := sr.userCommon.BatchUserBasicInfoByID(ctx, userIDs)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
for _, item := range resultList {
|
|
tags, ok := tagsMap[item.Object.QuestionID]
|
|
if ok {
|
|
item.Object.Tags = tags
|
|
}
|
|
if userInfo := userInfoMap[item.Object.UserInfo.ID]; userInfo != nil {
|
|
item.Object.UserInfo.Username = userInfo.Username
|
|
item.Object.UserInfo.DisplayName = userInfo.DisplayName
|
|
item.Object.UserInfo.Rank = userInfo.Rank
|
|
item.Object.UserInfo.Status = userInfo.Status
|
|
}
|
|
}
|
|
return resultList, nil
|
|
}
|
|
|
|
func addRelevanceField(searchFields, words, fields []string) (res []string, args []interface{}) {
|
|
relevanceRes := []string{}
|
|
args = []interface{}{}
|
|
|
|
for _, searchField := range searchFields {
|
|
var (
|
|
relevance = "(LENGTH(" + searchField + ") - LENGTH(%s))"
|
|
replacement = "REPLACE(%s, ?, '')"
|
|
replaceField = searchField
|
|
replaced string
|
|
argsField = []interface{}{}
|
|
)
|
|
|
|
res = fields
|
|
for i, word := range words {
|
|
if i == 0 {
|
|
argsField = append(argsField, word)
|
|
replaced = fmt.Sprintf(replacement, replaceField)
|
|
} else {
|
|
argsField = append(argsField, word)
|
|
replaced = fmt.Sprintf(replacement, replaced)
|
|
}
|
|
}
|
|
args = append(args, argsField...)
|
|
|
|
relevance = fmt.Sprintf(relevance, replaced)
|
|
relevanceRes = append(relevanceRes, relevance)
|
|
}
|
|
|
|
res = append(res, "("+strings.Join(relevanceRes, " + ")+") as relevance")
|
|
return
|
|
}
|
|
|
|
func filterWords(words []string) (res []string) {
|
|
for _, word := range words {
|
|
if strings.TrimSpace(word) != "" {
|
|
res = append(res, word)
|
|
}
|
|
}
|
|
return
|
|
}
|