diff --git a/commands/command.go b/commands/command.go index 2852d990..b40d2b6b 100644 --- a/commands/command.go +++ b/commands/command.go @@ -237,6 +237,12 @@ func RegisterCommand() { } else if len(os.Args) >= 2 && os.Args[1] == "update" { Update() os.Exit(0) + } else if len(os.Args) >= 2 && os.Args[1] == "reindex" { + ResolveCommand(os.Args[2:]) + fmt.Println("开始全量重建倒排索引...") + models.RebuildAllIndexes() + fmt.Println("倒排索引重建完成") + os.Exit(0) } } diff --git a/controllers/SearchController.go b/controllers/SearchController.go index 8375dd3f..44483b56 100644 --- a/controllers/SearchController.go +++ b/controllers/SearchController.go @@ -1,9 +1,11 @@ package controllers import ( + "sort" "strconv" "strings" + "github.com/beego/beego/v2/client/orm" "github.com/beego/beego/v2/core/logs" "github.com/beego/i18n" "github.com/mindoc-org/mindoc/conf" @@ -62,16 +64,116 @@ func PerformSearchV2Raw(keyword string, pageIndex, pageSize int, memberId int) ( words = []string{keyword} } + // 将原始关键词(小写)加入搜索词列表,确保能匹配索引中存储的完整词条 + lowerKeyword := strings.ToLower(strings.TrimSpace(keyword)) + if lowerKeyword != "" { + found := false + for _, w := range words { + if w == lowerKeyword { + found = true + break + } + } + if !found { + words = append(words, lowerKeyword) + } + } + // 使用倒排索引模型进行搜索 index := models.NewContentReverseIndex() - results, totalCount, err := index.FindByWordsWithPagination(words, pageIndex, pageSize) + allResults, err := index.FindByWords(words) if err != nil { return nil, words, 0, err } + // 收集需要批量查询的ID + docIds := make([]int, 0) + blogIds := make([]int, 0) + for _, result := range allResults { + if result.ContentType == 1 { + docIds = append(docIds, result.ContentId) + } else if result.ContentType == 2 { + blogIds = append(blogIds, result.ContentId) + } + } + + // 批量加载 Document + docMap := make(map[int]*models.Document) + if len(docIds) > 0 { + var docs []*models.Document + o := orm.NewOrm() + _, err := o.QueryTable(models.NewDocument().TableNameWithPrefix()).Filter("document_id__in", docIds).All(&docs) + if err == nil { + for _, doc := range docs { + docMap[doc.DocumentId] = doc + } + } + } + + // 批量加载 Blog + blogMap := make(map[int]*models.Blog) + if len(blogIds) > 0 { + var blogs []*models.Blog + o := orm.NewOrm() + _, err := o.QueryTable(models.NewBlog().TableNameWithPrefix()).Filter("blog_id__in", blogIds).All(&blogs) + if err == nil { + for _, blog := range blogs { + blogMap[blog.BlogId] = blog + } + } + } + + // 收集需要加载的 BookId 和 MemberId + bookIds := make([]int, 0) + memberIds := make([]int, 0) + bookIdSet := make(map[int]bool) + memberIdSet := make(map[int]bool) + for _, doc := range docMap { + if doc.BookId > 0 && !bookIdSet[doc.BookId] { + bookIds = append(bookIds, doc.BookId) + bookIdSet[doc.BookId] = true + } + if doc.MemberId > 0 && !memberIdSet[doc.MemberId] { + memberIds = append(memberIds, doc.MemberId) + memberIdSet[doc.MemberId] = true + } + } + for _, blog := range blogMap { + if blog.MemberId > 0 && !memberIdSet[blog.MemberId] { + memberIds = append(memberIds, blog.MemberId) + memberIdSet[blog.MemberId] = true + } + } + + // 批量加载 Book + bookMap := make(map[int]*models.Book) + if len(bookIds) > 0 { + var books []*models.Book + o := orm.NewOrm() + _, err := o.QueryTable(models.NewBook().TableNameWithPrefix()).Filter("book_id__in", bookIds).All(&books) + if err == nil { + for _, book := range books { + bookMap[book.BookId] = book + } + } + } + + // 批量加载 Member + memberMap := make(map[int]*models.Member) + if len(memberIds) > 0 { + var members []*models.Member + o := orm.NewOrm() + _, err := o.QueryTable(models.NewMember().TableNameWithPrefix()).Filter("member_id__in", memberIds).All(&members, "member_id", "account", "real_name") + if err == nil { + for _, member := range members { + memberMap[member.MemberId] = member + } + } + } + // 构建返回结果 searchResults := make([]*SearchV2RawResult, 0) - for _, result := range results { + for _, result := range allResults { item := &SearchV2RawResult{ ContentType: result.ContentType, ContentId: result.ContentId, @@ -81,100 +183,150 @@ func PerformSearchV2Raw(keyword string, pageIndex, pageSize int, memberId int) ( // 根据内容类型获取详细信息 if result.ContentType == 1 { - // Document类型 - doc, err := models.NewDocument().Find(result.ContentId) - if err == nil { - // 检查文档权限 - book, bookErr := models.NewBook().Find(doc.BookId) - if bookErr != nil { - continue + doc, ok := docMap[result.ContentId] + if !ok { + continue + } + book, ok := bookMap[doc.BookId] + if !ok { + continue + } + + item.SearchType = "document" + item.DocumentId = doc.DocumentId + item.DocumentName = doc.DocumentName + item.BookId = doc.BookId + item.BookName = book.BookName + item.Identify = doc.Identify + item.BookIdentify = book.Identify + item.CreateTime = doc.CreateTime + item.ModifyTime = doc.ModifyTime + item.Content = doc.Release + + // 获取作者信息 + if member, ok := memberMap[doc.MemberId]; ok { + if member.RealName != "" { + item.Author = member.RealName + } else { + item.Author = member.Account } + } - item.SearchType = "document" - item.DocumentId = doc.DocumentId - item.DocumentName = doc.DocumentName - item.BookId = doc.BookId - item.BookName = book.BookName - item.Identify = doc.Identify - item.BookIdentify = book.Identify - item.CreateTime = doc.CreateTime - item.ModifyTime = doc.ModifyTime - item.Content = doc.Release - - // 获取作者信息 - if doc.MemberId > 0 { - member, _ := models.NewMember().Find(doc.MemberId, "real_name", "account") - if member != nil { - if member.RealName != "" { - item.Author = member.RealName - } else { - item.Author = member.Account - } - } + // 提取描述 + description := doc.Release + if description == "" { + description = doc.Markdown + } + description = utils.StripTags(description) + if len([]rune(description)) > 100 { + description = string([]rune(description)[:100]) + "..." + } + item.Description = description + + // 标题匹配加权:搜索词命中标题时提升分数 + titleLower := strings.ToLower(doc.DocumentName) + for _, w := range words { + if strings.Contains(titleLower, w) { + item.Score *= 1.5 } + } - // 提取描述 - description := doc.Release - if description == "" { - description = doc.Markdown + // 精确匹配加权:文档内容包含原始关键词时大幅提分 + if lowerKeyword != "" { + contentLower := strings.ToLower(utils.StripTags(doc.Release)) + if contentLower == "" { + contentLower = strings.ToLower(utils.StripTags(doc.Markdown)) } - // 去除HTML标签 - description = utils.StripTags(description) - if len([]rune(description)) > 100 { - description = string([]rune(description)[:100]) + "..." + if strings.Contains(contentLower, lowerKeyword) { + item.Score *= 5.0 } - item.Description = description - - searchResults = append(searchResults, item) } + + searchResults = append(searchResults, item) } else if result.ContentType == 2 { - // Blog类型 - blog, err := models.NewBlog().Find(result.ContentId) - if err == nil { - item.SearchType = "blog" - item.BlogId = blog.BlogId - item.BlogTitle = blog.BlogTitle - item.DocumentId = blog.BlogId - item.DocumentName = blog.BlogTitle - item.BlogIdentify = blog.BlogIdentify - item.Identify = blog.BlogIdentify - item.BlogExcerpt = blog.BlogExcerpt - item.CreateTime = blog.Created - item.ModifyTime = blog.Modified - item.Content = blog.BlogRelease - - // 获取作者信息 - if blog.MemberId > 0 { - member, _ := models.NewMember().Find(blog.MemberId, "real_name", "account") - if member != nil { - if member.RealName != "" { - item.Author = member.RealName - } else { - item.Author = member.Account - } - } + blog, ok := blogMap[result.ContentId] + if !ok { + continue + } + + item.SearchType = "blog" + item.BlogId = blog.BlogId + item.BlogTitle = blog.BlogTitle + item.DocumentId = blog.BlogId + item.DocumentName = blog.BlogTitle + item.BlogIdentify = blog.BlogIdentify + item.Identify = blog.BlogIdentify + item.BlogExcerpt = blog.BlogExcerpt + item.CreateTime = blog.Created + item.ModifyTime = blog.Modified + item.Content = blog.BlogRelease + + // 获取作者信息 + if member, ok := memberMap[blog.MemberId]; ok { + if member.RealName != "" { + item.Author = member.RealName + } else { + item.Author = member.Account } + } - // 提取描述 - description := blog.BlogExcerpt + // 提取描述 + description := blog.BlogExcerpt + if description == "" { + description = blog.BlogRelease if description == "" { - description = blog.BlogRelease - if description == "" { - description = blog.BlogContent - } + description = blog.BlogContent } - description = utils.StripTags(description) - if len([]rune(description)) > 100 { - description = string([]rune(description)[:100]) + "..." + } + description = utils.StripTags(description) + if len([]rune(description)) > 100 { + description = string([]rune(description)[:100]) + "..." + } + item.Description = description + + // 标题匹配加权:搜索词命中标题时提升分数 + titleLower := strings.ToLower(blog.BlogTitle) + for _, w := range words { + if strings.Contains(titleLower, w) { + item.Score *= 1.5 } - item.Description = description + } - searchResults = append(searchResults, item) + // 精确匹配加权:博客内容包含原始关键词时大幅提分 + if lowerKeyword != "" { + contentLower := strings.ToLower(utils.StripTags(blog.BlogRelease)) + if contentLower == "" { + contentLower = strings.ToLower(utils.StripTags(blog.BlogContent)) + } + if strings.Contains(contentLower, lowerKeyword) { + item.Score *= 5.0 + } } + + searchResults = append(searchResults, item) } } - return searchResults, words, totalCount, nil + // 按加权后的分数重新排序 + sort.Slice(searchResults, func(i, j int) bool { + return searchResults[i].Score > searchResults[j].Score + }) + + // 分页 + totalCount := len(searchResults) + offset := (pageIndex - 1) * pageSize + end := offset + pageSize + if offset > totalCount { + offset = totalCount + } + if end > totalCount { + end = totalCount + } + if offset >= end { + return nil, words, totalCount, nil + } + + return searchResults[offset:end], words, totalCount, nil } // performSearchV2 执行倒排索引搜索,返回 SearchV2Result 列表 diff --git a/models/Blog.go b/models/Blog.go index 142622cf..d6a45b1b 100644 --- a/models/Blog.go +++ b/models/Blog.go @@ -252,8 +252,9 @@ func (b *Blog) Save(cols ...string) error { go func(blogId int, blogTitle, blogRelease, blogContent string) { content := blogRelease if content == "" { - content = blogTitle + "\n" + blogContent + content = blogContent } + content = blogTitle + "\n" + content content = utils.StripTags(content) if err := BuildIndexForBlog(blogId, content); err != nil { logs.Error("构建Blog倒排索引失败 ->", blogId, err) diff --git a/models/ContentReverseIndex.go b/models/ContentReverseIndex.go index 0ce325a4..e77f8c56 100644 --- a/models/ContentReverseIndex.go +++ b/models/ContentReverseIndex.go @@ -6,6 +6,7 @@ import ( "errors" "fmt" "math" + "sort" "github.com/beego/beego/v2/client/orm" "github.com/beego/beego/v2/core/logs" @@ -102,30 +103,22 @@ type ContentReverseIndexResult struct { WordCounts []int `json:"word_counts"` // 各个词的词频 } -// FindByWordsWithPagination 根据多个分词词汇分页批量查询结果,按IDF值排序 +// FindByWords 根据多个分词词汇查询结果,按TF-IDF值排序,返回全部匹配结果(不分页) // words: 分词词汇列表 -// pageIndex: 页码,从1开始 -// pageSize: 每页数量 -func (c *ContentReverseIndex) FindByWordsWithPagination(words []string, pageIndex, pageSize int) ([]*ContentReverseIndexResult, int, error) { +func (c *ContentReverseIndex) FindByWords(words []string) ([]*ContentReverseIndexResult, error) { if len(words) == 0 { - return nil, 0, errors.New("分词词汇列表不能为空") - } - if pageIndex <= 0 { - pageIndex = 1 - } - if pageSize <= 0 { - pageSize = 10 + return nil, errors.New("分词词汇列表不能为空") } o := orm.NewOrm() tableName := c.TableNameWithPrefix() // 计算总文档数 - totalDocsSql := "SELECT COUNT(DISTINCT CONCAT(content_type, '-', content_id)) FROM " + tableName + totalDocsSql := "SELECT COUNT(*) FROM (SELECT DISTINCT content_type, content_id FROM " + tableName + ") AS t" var totalDocs int err := o.Raw(totalDocsSql).QueryRow(&totalDocs) if err != nil { - return nil, 0, err + return nil, err } // 构建IN条件 @@ -149,102 +142,80 @@ func (c *ContentReverseIndex) FindByWordsWithPagination(words []string, pageInde var records []indexRecord _, err = o.Raw(sql, wordArgs...).QueryRows(&records) if err != nil { - return nil, 0, err - } - - // 计算各文档的总词数 - sql = "SELECT content_type, content_id, count(word_count) total_word_count FROM " + tableName + - " GROUP BY content_type, content_id" - type docWordCountRecord struct { - ContentType int - ContentId int - TotalWordCount int - } - var docWordCountRecords []docWordCountRecord - _, err = o.Raw(sql).QueryRows(&docWordCountRecords) - if err != nil { - return nil, 0, err - } - - docTotalWordCountMap := make(map[string]int) - for _, record := range docWordCountRecords { - key := fmt.Sprintf("%d-%d", record.ContentType, record.ContentId) - docTotalWordCountMap[key] = record.TotalWordCount + return nil, err } - // 聚合每个(content_type, content_id)的词频和计算TF-IDF - contentMap := make(map[string]*ContentReverseIndexResult) + // 计算每个词的文档频率(DF):每个词出现在多少个文档中 + wordDocFreq := make(map[string]map[string]bool) for _, record := range records { key := fmt.Sprintf("%d-%d", record.ContentType, record.ContentId) - if result, exists := contentMap[key]; exists { - result.WordCounts = append(result.WordCounts, record.WordCount) - } else { - contentMap[key] = &ContentReverseIndexResult{ - ContentId: record.ContentId, - ContentType: record.ContentType, - WordCounts: []int{record.WordCount}, - } + if wordDocFreq[record.Word] == nil { + wordDocFreq[record.Word] = make(map[string]bool) } + wordDocFreq[record.Word][key] = true } - docMapWithWords := make(map[string]int) // 用于计算包含搜索词的文档数 - // 计算每个文档包含多少个查询词 - docWordCount := make(map[string]int) + // 聚合每个文档的匹配词信息 + type docWordInfo struct { + Word string + WordCount int + } + docWords := make(map[string][]docWordInfo) for _, record := range records { key := fmt.Sprintf("%d-%d", record.ContentType, record.ContentId) - docWordCount[key] += record.WordCount - docMapWithWords[key] += 1 - } - - // 计算IDF并生成结果 - results := make([]*ContentReverseIndexResult, 0, len(contentMap)) - for key := range contentMap { - result := contentMap[key] - // 计算TF:词频之和 - tf := float64(docWordCount[key]) / float64(docTotalWordCountMap[key]+1) - // 计算DF:包含该词的文档数(简化处理,使用该文档包含的查询词数量) - df := len(docMapWithWords) - // 计算IDF - idf := 0.0 - if df > 0 && totalDocs > 0 { - idf = math.Log(float64(totalDocs+1) / float64(df)) + docWords[key] = append(docWords[key], docWordInfo{ + Word: record.Word, + WordCount: record.WordCount, + }) + } + + // 计算每个文档的TF-IDF分数(使用正确的per-word IDF) + results := make([]*ContentReverseIndexResult, 0, len(docWords)) + for key, wordInfos := range docWords { + var contentType, contentId int + fmt.Sscanf(key, "%d-%d", &contentType, &contentId) + + score := 0.0 + wordCounts := make([]int, 0, len(wordInfos)) + + for _, wi := range wordInfos { + wordCounts = append(wordCounts, wi.WordCount) + // TF: 使用对数TF(sublinear TF),避免长文档被不合理惩罚 + tf := 1.0 + math.Log(float64(wi.WordCount)+1) + // IDF: 每个词独立计算,稀有词权重更高 + df := len(wordDocFreq[wi.Word]) + idf := 0.0 + if df > 0 && totalDocs > 0 { + idf = math.Log(float64(totalDocs+1) / float64(df+1)) + } + // 词长权重:长词(更具体的词)贡献更大 + wordLen := float64(len([]rune(wi.Word))) + lengthWeight := math.Log2(1.0 + wordLen) + score += tf * idf * lengthWeight } - // 用于根据文档总词数调整TF-IDF的权重,避免总词数过小的文档权重过高 - alpha := math.Log(1.0+float64(docTotalWordCountMap[key])*0.01) * 100 - // TF-IDF分数 - result.Score = float64(tf) * idf * float64(alpha) - results = append(results, result) + // 查询词覆盖率加成:匹配的查询词越多,分数越高 + coverage := float64(len(wordInfos)) / float64(len(words)) + score *= (1.0 + coverage) + + results = append(results, &ContentReverseIndexResult{ + ContentId: contentId, + ContentType: contentType, + Score: score, + WordCounts: wordCounts, + }) } // 按Score降序排序 sortResultsByScore(results) - totalCount := len(results) - // 分页 - offset := (pageIndex - 1) * pageSize - start := offset - end := offset + pageSize - if start > totalCount { - start = totalCount - } - if end > totalCount { - end = totalCount - } - if start >= end { - return nil, totalCount, nil - } - return results[start:end], totalCount, nil + return results, nil } func sortResultsByScore(results []*ContentReverseIndexResult) { - for i := 0; i < len(results)-1; i++ { - for j := i + 1; j < len(results); j++ { - if results[i].Score < results[j].Score { - results[i], results[j] = results[j], results[i] - } - } - } + sort.Slice(results, func(i, j int) bool { + return results[i].Score > results[j].Score + }) } func generateIndexId(contentType, contentId int, word string) string { @@ -446,9 +417,7 @@ func InitializeMissingDocumentIndexes() { if content == "" { content = doc.Markdown } - for i := 0; i < 10; i++ { // 标题内容"十分"重要 - content = doc.DocumentName + "\n" + content - } + content = doc.DocumentName + "\n" + content content = utils.StripTags(content) err := BuildIndexForDocument(doc.DocumentId, content) if err != nil { @@ -481,9 +450,7 @@ func InitializeMissingBlogIndexes() { if content == "" { content = blog.BlogContent } - for i := 0; i < 10; i++ { // 标题内容"十分"重要 - content = blog.BlogTitle + "\n" + content - } + content = blog.BlogTitle + "\n" + content content = utils.StripTags(content) err := BuildIndexForBlog(blog.BlogId, content) @@ -496,3 +463,105 @@ func InitializeMissingBlogIndexes() { } } } + +// RebuildAllIndexes 全量重建倒排索引(先清空再重建) +func RebuildAllIndexes() { + logs.Info("开始全量重建倒排索引...") + + // 清空倒排索引表 + o := orm.NewOrm() + tableName := NewContentReverseIndex().TableNameWithPrefix() + _, err := o.Raw("DELETE FROM " + tableName).Exec() + if err != nil { + logs.Error("清空倒排索引表失败 ->", err) + return + } + logs.Info("倒排索引表已清空") + + // 重建文档索引 + rebuildDocumentIndexes() + // 重建博客索引 + rebuildBlogIndexes() + + logs.Info("全量重建倒排索引完成") +} + +func rebuildDocumentIndexes() { + o := orm.NewOrm() + batchSize := 100 + offset := 0 + total := 0 + + for { + var documents []*Document + _, err := o.QueryTable(NewDocument().TableNameWithPrefix()). + OrderBy("document_id"). + Limit(batchSize, offset). + All(&documents) + if err != nil { + logs.Error("查询文档失败 ->", err) + break + } + if len(documents) == 0 { + break + } + + for _, doc := range documents { + content := doc.Release + if content == "" { + content = doc.Markdown + } + content = doc.DocumentName + "\n" + content + content = utils.StripTags(content) + if err := BuildIndexForDocument(doc.DocumentId, content); err != nil { + logs.Error("重建文档倒排索引失败 ->", doc.DocumentId, err) + } else { + total++ + } + } + + offset += batchSize + logs.Info("已重建文档索引:", total) + } + logs.Info("文档索引重建完成, 共:", total) +} + +func rebuildBlogIndexes() { + o := orm.NewOrm() + batchSize := 100 + offset := 0 + total := 0 + + for { + var blogs []*Blog + _, err := o.QueryTable(NewBlog().TableNameWithPrefix()). + OrderBy("blog_id"). + Limit(batchSize, offset). + All(&blogs) + if err != nil { + logs.Error("查询博客失败 ->", err) + break + } + if len(blogs) == 0 { + break + } + + for _, blog := range blogs { + content := blog.BlogRelease + if content == "" { + content = blog.BlogContent + } + content = blog.BlogTitle + "\n" + content + content = utils.StripTags(content) + if err := BuildIndexForBlog(blog.BlogId, content); err != nil { + logs.Error("重建Blog倒排索引失败 ->", blog.BlogId, err) + } else { + total++ + } + } + + offset += batchSize + logs.Info("已重建Blog索引:", total) + } + logs.Info("Blog索引重建完成, 共:", total) +} diff --git a/models/DocumentModel.go b/models/DocumentModel.go index 2533b744..8b543185 100644 --- a/models/DocumentModel.go +++ b/models/DocumentModel.go @@ -268,10 +268,11 @@ func (item *Document) ReleaseContent() error { // 刷新倒排索引 go func(docId int, docName, release, markdown string) { - content := docName + "\n" + release + content := release if content == "" { content = markdown } + content = docName + "\n" + content content = utils.StripTags(content) if err := BuildIndexForDocument(docId, content); err != nil { logs.Error("error: 构建文档倒排索引失败 ->", docId, err) diff --git a/models/DocumentSearchResult.go b/models/DocumentSearchResult.go index c93477b3..df070a85 100644 --- a/models/DocumentSearchResult.go +++ b/models/DocumentSearchResult.go @@ -84,7 +84,8 @@ FROM ( book.book_name, rel.member_id, mdmb.account AS author, - 'document' AS search_type + 'document' AS search_type, + CASE WHEN doc.document_name LIKE ? THEN 2 ELSE 1 END AS relevance FROM md_documents AS doc LEFT JOIN md_books AS book ON doc.book_id = book.book_id LEFT JOIN md_relationship AS rel ON book.book_id = rel.book_id AND rel.role_id = 0 @@ -102,7 +103,8 @@ SELECT book.book_name, rel.member_id, mdmb.account AS author, - 'book' AS search_type + 'book' AS search_type, + CASE WHEN book.book_name LIKE ? THEN 2 ELSE 1 END AS relevance FROM md_books AS book LEFT JOIN md_relationship AS rel ON book.book_id = rel.book_id AND rel.role_id = 0 LEFT JOIN md_members AS mdmb ON rel.member_id = mdmb.member_id @@ -120,12 +122,13 @@ WHERE book.privately_owned = 0 AND (book.book_name LIKE ? OR book.description LI blog.blog_title as book_name, blog.member_id, mdmb.account, - 'blog' AS search_type + 'blog' AS search_type, + CASE WHEN blog.blog_title LIKE ? THEN 2 ELSE 1 END AS relevance FROM md_blogs AS blog LEFT JOIN md_members AS mdmb ON blog.member_id = mdmb.member_id WHERE blog.blog_status = 'public' AND (blog.blog_release LIKE ? OR blog.blog_title LIKE ?) ) AS union_table -ORDER BY create_time DESC +ORDER BY relevance DESC, create_time DESC LIMIT ? OFFSET ?;` err = o.Raw(escape_sql(sql1), keyword, keyword).QueryRow(&totalCount) @@ -158,7 +161,7 @@ WHERE book.privately_owned = 0 AND (book.book_name LIKE ? OR book.description LI totalCount += c - _, err = o.Raw(escape_sql(sql2), keyword, keyword, keyword, keyword, keyword, keyword, pageSize, offset).QueryRows(&searchResult) + _, err = o.Raw(escape_sql(sql2), keyword, keyword, keyword, keyword, keyword, keyword, keyword, keyword, keyword, pageSize, offset).QueryRows(&searchResult) if err != nil { logs.Error("查询搜索结果失败 -> ", err) return @@ -187,7 +190,8 @@ FROM ( book.book_name, rel.member_id, mdmb.account AS author, - 'document' AS search_type + 'document' AS search_type, + CASE WHEN doc.document_name LIKE ? THEN 2 ELSE 1 END AS relevance FROM md_documents AS doc LEFT JOIN md_books AS book ON doc.book_id = book.book_id LEFT JOIN md_relationship AS rel ON book.book_id = rel.book_id AND rel.role_id = 0 @@ -218,7 +222,8 @@ FROM ( book.book_name, rel.member_id, mdmb.account AS author, - 'book' AS search_type + 'book' AS search_type, + CASE WHEN book.book_name LIKE ? THEN 2 ELSE 1 END AS relevance FROM md_books AS book LEFT JOIN md_relationship AS rel ON book.book_id = rel.book_id AND rel.role_id = 0 LEFT JOIN md_members AS mdmb ON rel.member_id = mdmb.member_id @@ -247,13 +252,14 @@ FROM ( blog.blog_title as book_name, blog.member_id, mdmb.account, - 'blog' AS search_type + 'blog' AS search_type, + CASE WHEN blog.blog_title LIKE ? THEN 2 ELSE 1 END AS relevance FROM md_blogs AS blog LEFT JOIN md_members AS mdmb ON blog.member_id = mdmb.member_id WHERE (blog.blog_status = 'public' OR blog.member_id = ?) AND blog.blog_type = 0 AND (blog.blog_release LIKE ? OR blog.blog_title LIKE ?) ) AS union_table -ORDER BY create_time DESC +ORDER BY relevance DESC, create_time DESC LIMIT ? OFFSET ?;` err = o.Raw(escape_sql(sql1), memberId, memberId, keyword, keyword).QueryRow(&totalCount) @@ -292,7 +298,7 @@ WHERE (book.privately_owned = 0 OR rel1.relationship_id > 0 or team.team_member_ totalCount += c - _, err = o.Raw(escape_sql(sql2), memberId, memberId, keyword, keyword, memberId, memberId, keyword, keyword, memberId, keyword, keyword, pageSize, offset).QueryRows(&searchResult) + _, err = o.Raw(escape_sql(sql2), keyword, memberId, memberId, keyword, keyword, keyword, memberId, memberId, keyword, keyword, keyword, memberId, keyword, keyword, pageSize, offset).QueryRows(&searchResult) if err != nil { return } diff --git a/utils/segmenter/segmenter.go b/utils/segmenter/segmenter.go index 86cac74a..5c17cf66 100644 --- a/utils/segmenter/segmenter.go +++ b/utils/segmenter/segmenter.go @@ -5,6 +5,7 @@ import ( "path/filepath" "strings" "sync" + "unicode" "github.com/beego/beego/v2/core/logs" "github.com/mindoc-org/mindoc/conf" @@ -75,6 +76,11 @@ func Segment(text string) []string { } // 转小写(英文) word = strings.ToLower(word) + // 过滤单字符标点符号/特殊字符,避免匹配大量无关文档 + runes := []rune(word) + if len(runes) == 1 && !unicode.IsLetter(runes[0]) && !unicode.IsDigit(runes[0]) { + continue + } result = append(result, word) }