mirror of https://gitee.com/answerdev/answer.git
Merge branch 'fix/search' into 'main'
Fix/search See merge request opensource/answer!68
This commit is contained in:
commit
2509d84379
|
@ -2594,7 +2594,8 @@ const docTemplate = `{
|
|||
"enum": [
|
||||
"newest",
|
||||
"active",
|
||||
"score"
|
||||
"score",
|
||||
"relevance"
|
||||
],
|
||||
"type": "string",
|
||||
"description": "order",
|
||||
|
|
|
@ -2582,7 +2582,8 @@
|
|||
"enum": [
|
||||
"newest",
|
||||
"active",
|
||||
"score"
|
||||
"score",
|
||||
"relevance"
|
||||
],
|
||||
"type": "string",
|
||||
"description": "order",
|
||||
|
|
|
@ -2934,6 +2934,7 @@ paths:
|
|||
- newest
|
||||
- active
|
||||
- score
|
||||
- relevance
|
||||
in: query
|
||||
name: order
|
||||
required: true
|
||||
|
|
|
@ -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"
|
||||
}
|
||||
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue