Merge branch 'fix/search' into 'main'

Fix/search

See merge request opensource/answer!68
This commit is contained in:
杨光富 2022-10-17 03:22:47 +00:00
commit 2509d84379
5 changed files with 222 additions and 67 deletions

View File

@ -2594,7 +2594,8 @@ const docTemplate = `{
"enum": [
"newest",
"active",
"score"
"score",
"relevance"
],
"type": "string",
"description": "order",

View File

@ -2582,7 +2582,8 @@
"enum": [
"newest",
"active",
"score"
"score",
"relevance"
],
"type": "string",
"description": "order",

View File

@ -2934,6 +2934,7 @@ paths:
- newest
- active
- score
- relevance
in: query
name: order
required: true

View File

@ -28,7 +28,7 @@ func NewSearchController(searchService *service.SearchService) *SearchController
// @Produce json
// @Security ApiKeyAuth
// @Param q query string true "query string"
// @Param order query string true "order" Enums(newest,active,score)
// @Param order query string true "order" Enums(newest,active,score,relevance)
// @Success 200 {object} handler.RespBody{data=schema.SearchListResp}
// @Router /answer/api/v1/search [get]
func (sc *SearchController) Search(ctx *gin.Context) {
@ -54,7 +54,7 @@ func (sc *SearchController) Search(ctx *gin.Context) {
size = "30"
}
order, ok = ctx.GetQuery("order")
if !ok || (order != "newest" && order != "active" && order != "score") {
if !ok || (order != "newest" && order != "active" && order != "score" && order != "relevance") {
order = "newest"
}

View File

@ -2,6 +2,7 @@ package repo
import (
"context"
"fmt"
"strings"
"time"
@ -19,6 +20,35 @@ import (
"xorm.io/builder"
)
var (
q_fields = []string{
"`question`.`id`",
"`question`.`id` as `question_id`",
"`title`",
"`original_text`",
"`question`.`created_at`",
"`user_id`",
"`vote_count`",
"`answer_count`",
"0 as `accepted`",
"`question`.`status` as `status`",
"`post_update_time`",
}
a_fields = []string{
"`answer`.`id` as `id`",
"`question_id`",
"`question`.`title` as `title`",
"`answer`.`original_text` as `original_text`",
"`answer`.`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
@ -35,47 +65,49 @@ 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) {
var (
b *builder.Builder
ub *builder.Builder
b *builder.Builder
ub *builder.Builder
qfs = q_fields
afs = a_fields
argsQ = []interface{}{}
argsA = []interface{}{}
)
b = builder.MySQL().Select(
"`question`.`id`",
"`question`.`id` as `question_id`",
"`title`",
"`original_text`",
"`question`.`created_at`",
"`user_id`",
"`vote_count`",
"`answer_count`",
"0 as `accepted`",
"`question`.`status` as `status`",
"`post_update_time`",
).From("`question`")
ub = builder.MySQL().Select(
"`answer`.`id` as `id`",
"`question_id`",
"`question`.`title` as `title`",
"`answer`.`original_text` as `original_text`",
"`answer`.`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`",
).From("`answer`").
if order == "relevance" {
qfs, argsQ = addRelevanceField([]string{"title", "original_text"}, words, qfs)
afs, argsA = addRelevanceField([]string{"`answer`.`original_text`"}, words, afs)
}
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})
ub.Where(builder.Lt{"`question`.`status`": entity.QuestionStatusDeleted}).
And(builder.Lt{"`answer`.`status`": entity.AnswerStatusDeleted})
argsQ = append(argsQ, entity.QuestionStatusDeleted)
argsA = append(argsA, entity.QuestionStatusDeleted, entity.AnswerStatusDeleted)
for i, word := range words {
if i == 0 {
b.Where(builder.Like{"title", word}).
Or(builder.Like{"original_text", word})
argsQ = append(argsQ, "%"+word+"%")
argsQ = append(argsQ, "%"+word+"%")
ub.Where(builder.Like{"`answer`.original_text", word})
argsA = append(argsA, "%"+word+"%")
} else {
b.Or(builder.Like{"original_text", word})
b.Or(builder.Like{"title", word}).
Or(builder.Like{"original_text", word})
argsQ = append(argsQ, "%"+word+"%")
argsQ = append(argsQ, "%"+word+"%")
ub.Or(builder.Like{"`answer`.original_text", word})
argsA = append(argsA, "%"+word+"%")
}
}
@ -83,32 +115,58 @@ func (sr *searchRepo) SearchContents(ctx context.Context, words []string, tagID,
if tagID != "" {
b.Join("INNER", "tag_rel", "question.id = tag_rel.object_id").
Where(builder.Eq{"tag_rel.tag_id": tagID})
argsQ = append(argsQ, 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)
_, _, err = b.ToSQL()
querySql, _, err := builder.MySQL().Select("*").From(b, "t").OrderBy(sr.parseOrder(ctx, order)).Limit(size, page-1).ToSQL()
if err != nil {
return
}
countSql, _, err := builder.MySQL().Select("count(*) total").From(b, "c").ToSQL()
if err != nil {
return
}
res, err := sr.data.DB.Query(builder.MySQL().Select("*").From(b, "t").OrderBy(sr.parseOrder(ctx, order)).Limit(size, page-1))
queryArgs := []interface{}{}
countArgs := []interface{}{}
tr, err := sr.data.DB.Query(builder.MySQL().Select("count(*) total").From(b, "c"))
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.Query(queryArgs...)
if err != nil {
return
}
tr, err := sr.data.DB.Query(countArgs...)
if len(tr) != 0 {
total = converter.StringToInt64(string(tr[0]["total"]))
}
@ -121,43 +179,73 @@ 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) {
b := builder.MySQL().Select(
"`id`",
"`id` as `question_id`",
"`title`",
"`original_text`",
"`created_at`",
"`user_id`",
"`vote_count`",
"`answer_count`",
"0 as `accepted`",
"`status`",
"`post_update_time`",
).From("question")
var (
qfs = q_fields
args = []interface{}{}
)
if order == "relevance" {
qfs, args = addRelevanceField([]string{"title", "original_text"}, words, qfs)
}
b := builder.MySQL().Select(qfs...).From("question")
b.Where(builder.Lt{"`question`.`status`": entity.QuestionStatusDeleted})
args = append(args, entity.QuestionStatusDeleted)
for i, word := range words {
if i == 0 {
b.Where(builder.Like{"title", word}).
Or(builder.Like{"original_text", word})
args = append(args, "%"+word+"%")
args = append(args, "%"+word+"%")
} else {
b.Or(builder.Like{"original_text", word})
args = append(args, "%"+word+"%")
}
}
// check need filter has not accepted
if limitNoAccepted {
b.And(builder.Eq{"accepted_answer_id": 0})
args = append(args, 0)
}
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)
}
res, err := sr.data.DB.Query(b.OrderBy(sr.parseOrder(ctx, order)).Limit(size, page-1))
tr, err := sr.data.DB.Query(builder.MySQL().Select("count(*) total").From(b, "c"))
queryArgs := []interface{}{}
countArgs := []interface{}{}
querySql, _, err := b.OrderBy(sr.parseOrder(ctx, order)).Limit(size, page-1).ToSQL()
if err != nil {
return
}
countSql, _, err := builder.MySQL().Select("count(*) total").From(b, "c").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.Query(queryArgs...)
if err != nil {
return
}
tr, err := sr.data.DB.Query(countArgs...)
if err != nil {
return
}
if len(tr) != 0 {
total = converter.StringToInt64(string(tr[0]["total"]))
@ -173,41 +261,70 @@ func (sr *searchRepo) SearchQuestions(ctx context.Context, words []string, limit
return
}
// 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) {
b := builder.MySQL().Select(
"`answer`.`id` as `id`",
"`question_id`",
"`question`.`title` as `title`",
"`answer`.`original_text` as `original_text`",
"`answer`.`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`",
).From("`answer`").
var (
afs = a_fields
args = []interface{}{}
)
if order == "relevance" {
afs, args = addRelevanceField([]string{"`answer`.`original_text`"}, words, afs)
}
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})
args = append(args, entity.QuestionStatusDeleted, entity.AnswerStatusDeleted)
for i, word := range words {
if i == 0 {
b.Where(builder.Like{"`answer`.original_text", word})
args = append(args, "%"+word+"%")
} else {
b.Or(builder.Like{"`answer`.original_text", word})
args = append(args, "%"+word+"%")
}
}
if limitAccepted {
b.Where(builder.Eq{"adopted": 2})
b.Where(builder.Eq{"adopted": schema.Answer_Adopted_Enable})
args = append(args, schema.Answer_Adopted_Enable)
}
if questionID != "" {
b.Where(builder.Eq{"question_id": questionID})
args = append(args, questionID)
}
res, err := sr.data.DB.Query(b.OrderBy(sr.parseOrder(ctx, order)).Limit(size, page-1))
queryArgs := []interface{}{}
countArgs := []interface{}{}
querySql, _, err := b.OrderBy(sr.parseOrder(ctx, order)).Limit(size, page-1).ToSQL()
if err != nil {
return
}
countSql, _, err := builder.MySQL().Select("count(*) total").From(b, "c").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.Query(queryArgs...)
if err != nil {
return
}
tr, err := sr.data.DB.Query(countArgs...)
if err != nil {
return
}
tr, err := sr.data.DB.Query(builder.MySQL().Select("count(*) total").From(b, "c"))
total = converter.StringToInt64(string(tr[0]["total"]))
if err != nil {
err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()
@ -228,6 +345,8 @@ func (sr *searchRepo) parseOrder(ctx context.Context, order string) (res string)
res = "post_update_time desc"
case "score":
res = "vote_count desc"
case "relevance":
res = "relevance desc"
default:
res = "created_at desc"
}
@ -331,3 +450,36 @@ func cutOutParsedText(parsedText string) string {
}
return parsedText
}
func addRelevanceField(search_fields, words, fields []string) (res []string, args []interface{}) {
var relevanceRes = []string{}
args = []interface{}{}
for _, search_field := range search_fields {
var (
relevance = "(LENGTH(" + search_field + ") - LENGTH(%s))"
replacement = "REPLACE(%s, ?, '')"
replace_field = search_field
replaced string
argsField = []interface{}{}
)
res = fields
for i, word := range words {
if i == 0 {
argsField = append(argsField, word)
replaced = fmt.Sprintf(replacement, replace_field)
} 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
}