From 61d74561efd18611951283fb8c6705e90da92b01 Mon Sep 17 00:00:00 2001 From: mayiming <1627832236@qq.com> Date: Mon, 11 Aug 2025 23:10:36 +0800 Subject: [PATCH] =?UTF-8?q?=E6=9B=B4=E6=96=B0=E6=90=9C=E7=B4=A2=E5=BC=95?= =?UTF-8?q?=E6=93=8E?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- controllers/search/close.go | 9 ++++ controllers/search/init.go | 39 ++++++++++++++ controllers/search/insert.go | 98 ++++++++++++++++++++++++++++++++++++ controllers/search/search.go | 85 +++++++++++++++++++++++++++++++ data/search.index | 0 go.mod | 24 +++++++++ main.go | 3 ++ model/article/article.go | 6 +++ note.md | 28 +++++++++++ router/setupRouter.go | 7 ++- 10 files changed, 298 insertions(+), 1 deletion(-) create mode 100644 controllers/search/close.go create mode 100644 controllers/search/init.go create mode 100644 controllers/search/insert.go create mode 100644 controllers/search/search.go delete mode 100644 data/search.index create mode 100644 model/article/article.go diff --git a/controllers/search/close.go b/controllers/search/close.go new file mode 100644 index 0000000..4ab8b9b --- /dev/null +++ b/controllers/search/close.go @@ -0,0 +1,9 @@ +package search + +// 程序退出时关闭索引(可选) +func CloseIndex() error { + if globalIndex != nil { + return globalIndex.Close() + } + return nil +} diff --git a/controllers/search/init.go b/controllers/search/init.go new file mode 100644 index 0000000..e4222a7 --- /dev/null +++ b/controllers/search/init.go @@ -0,0 +1,39 @@ +package search + +import ( + "fmt" + "github.com/blevesearch/bleve/v2" + "log" + "os" +) + +// 初始化索引(程序启动时调用一次) +func InitIndex() error { + // 检查索引目录是否存在(Bleve v2 方式) + _, err := os.Stat("data") + exists := !os.IsNotExist(err) + if err != nil && !os.IsNotExist(err) { + // 除了"不存在"之外的其他错误(如权限问题) + return fmt.Errorf("检查索引目录失败: %v", err) + } + + if exists { + // 打开已有索引 + globalIndex, err = bleve.Open("data") + if err != nil { + return fmt.Errorf("打开索引失败: %v", err) + } + log.Println("成功打开已有索引") + } else { + // 创建新索引 + mapping := bleve.NewIndexMapping() + globalIndex, err = bleve.New("data", mapping) + if err != nil { + return fmt.Errorf("创建索引失败: %v", err) + } + log.Println("成功创建新索引") + + // 初始化文档(略) + } + return nil +} diff --git a/controllers/search/insert.go b/controllers/search/insert.go new file mode 100644 index 0000000..1646df2 --- /dev/null +++ b/controllers/search/insert.go @@ -0,0 +1,98 @@ +package search + +import ( + "github.com/gin-gonic/gin" + "github.com/google/uuid" + "log" + "toutoukan/model/article" +) + +// InsertDocument 插入单条文档到索引 +func InsertDocument(c *gin.Context) { + // 检查索引是否初始化 + if globalIndex == nil { + c.JSON(500, gin.H{"error": "索引未初始化"}) + return + } + + // 绑定请求体中的文档数据 + var doc article.Document + if err := c.ShouldBindJSON(&doc); err != nil { + c.JSON(400, gin.H{"error": "请求格式错误: " + err.Error()}) + return + } + + // 验证文档必填字段 + if doc.Title == "" && doc.Body == "" { + c.JSON(400, gin.H{"error": "文档ID不能为空,且标题和内容不能同时为空"}) + return + } + + docID := uuid.New().String() + + // 将文档插入索引 + if err := globalIndex.Index(docID, doc); err != nil { + log.Printf("插入文档失败: %v", err) + c.JSON(500, gin.H{"error": "插入文档到索引失败: " + err.Error()}) + return + } + + // 插入成功响应 + c.JSON(200, gin.H{ + "message": "文档插入成功", + "doc_id": docID, + }) +} + +// BatchInsertDocuments 批量插入文档到索引 +//func BatchInsertDocuments(c *gin.Context) { +// // 检查索引是否初始化 +// if globalIndex == nil { +// c.JSON(500, gin.H{"error": "索引未初始化"}) +// return +// } +// +// // 绑定请求体中的文档数组 +// var docs []article.Document +// if err := c.ShouldBindJSON(&docs); err != nil { +// c.JSON(400, gin.H{"error": "请求格式错误: " + err.Error()}) +// return +// } +// +// // 验证批量文档 +// if len(docs) == 0 { +// c.JSON(400, gin.H{"error": "文档列表不能为空"}) +// return +// } +// +// // 批量插入文档 +// successCount := 0 +// failures := make(map[string]string) +// +// for _, doc := range docs { +// if doc.ID == "" || (doc.Title == "" && doc.Body == "") { +// failures[doc.ID] = "ID不能为空或内容为空" +// continue +// } +// +// if err := globalIndex.Index(doc.ID, doc); err != nil { +// failures[doc.ID] = err.Error() +// continue +// } +// successCount++ +// } +// +// // 批量插入结果响应 +// response := gin.H{ +// "total": len(docs), +// "success": successCount, +// "failed": len(failures), +// "message": fmt.Sprintf("批量插入完成,成功%d条,失败%d条", successCount, len(failures)), +// } +// +// if len(failures) > 0 { +// response["failures"] = failures +// } +// +// c.JSON(200, response) +//} diff --git a/controllers/search/search.go b/controllers/search/search.go new file mode 100644 index 0000000..1384ed3 --- /dev/null +++ b/controllers/search/search.go @@ -0,0 +1,85 @@ +package search + +import ( + "github.com/blevesearch/bleve/v2" + "github.com/gin-gonic/gin" + "log" + "toutoukan/model/article" +) + +// 全局索引变量,避免重复创建 +var globalIndex bleve.Index + +func DataSearch(c *gin.Context) { + if globalIndex == nil { + c.JSON(500, gin.H{"error": "索引未初始化,请先调用InitIndex()"}) + return + } + + keyword := c.Query("keyword") + if keyword == "" { + c.JSON(400, gin.H{"error": "请提供搜索关键词(?keyword=xxx)"}) + return + } + + query := bleve.NewQueryStringQuery(`title:"` + keyword + `" OR body:"` + keyword + `"`) + searchRequest := bleve.NewSearchRequest(query) + + // 只返回必要字段减少资源消耗 + searchRequest.Fields = []string{"title", "body"} + + searchResult, err := globalIndex.Search(searchRequest) + if err != nil { + c.JSON(500, gin.H{"error": "搜索失败: " + err.Error()}) + return + } + + type Result struct { + ID string `json:"id"` + Score float64 `json:"score"` + Doc *article.Document `json:"doc"` + } + var results []Result + + for _, hit := range searchResult.Hits { + doc := &article.Document{} + hasField := false + + // 宽松的字段处理逻辑 + if title, ok := hit.Fields["title"].(string); ok { + doc.Title = title + hasField = true + } else { + log.Printf("文档 %s 缺少title字段", hit.ID) + } + + if body, ok := hit.Fields["body"].(string); ok { + doc.Body = body + hasField = true + } else { + log.Printf("文档 %s 缺少body字段", hit.ID) + } + + // 至少有一个字段存在才包含结果 + if hasField { + results = append(results, Result{ + ID: hit.ID, + Score: hit.Score, + Doc: doc, + }) + } + } + + // 添加调试信息 + if len(results) == 0 { + log.Printf("关键词'%s'搜索返回%d个文档,但有效结果为0", keyword, searchResult.Total) + } else { + log.Printf("关键词'%s'搜索返回%d个文档", keyword, searchResult.Total) + } + + c.JSON(200, gin.H{ + "total": len(results), // 返回实际有效结果数 + "took": searchResult.Took, + "result": results, + }) +} diff --git a/data/search.index b/data/search.index deleted file mode 100644 index e69de29..0000000 diff --git a/go.mod b/go.mod index 163abed..c1bf20e 100644 --- a/go.mod +++ b/go.mod @@ -4,6 +4,26 @@ go 1.23.1 require ( filippo.io/edwards25519 v1.1.0 // indirect + github.com/RoaringBitmap/roaring/v2 v2.9.0 // indirect + github.com/bits-and-blooms/bitset v1.24.0 // indirect + github.com/blevesearch/bleve/v2 v2.5.3 // indirect + github.com/blevesearch/bleve_index_api v1.2.8 // indirect + github.com/blevesearch/geo v0.2.4 // indirect + github.com/blevesearch/go-faiss v1.0.25 // indirect + github.com/blevesearch/go-porterstemmer v1.0.3 // indirect + github.com/blevesearch/gtreap v0.1.1 // indirect + github.com/blevesearch/mmap-go v1.0.4 // indirect + github.com/blevesearch/scorch_segment_api/v2 v2.3.10 // indirect + github.com/blevesearch/segment v0.9.1 // indirect + github.com/blevesearch/snowballstem v0.9.0 // indirect + github.com/blevesearch/upsidedown_store_api v1.0.2 // indirect + github.com/blevesearch/vellum v1.1.0 // indirect + github.com/blevesearch/zapx/v11 v11.4.2 // indirect + github.com/blevesearch/zapx/v12 v12.4.2 // indirect + github.com/blevesearch/zapx/v13 v13.4.2 // indirect + github.com/blevesearch/zapx/v14 v14.4.2 // indirect + github.com/blevesearch/zapx/v15 v15.4.2 // indirect + github.com/blevesearch/zapx/v16 v16.2.4 // indirect github.com/bytedance/sonic v1.14.0 // indirect github.com/bytedance/sonic/loader v0.3.0 // indirect github.com/cespare/xxhash/v2 v2.3.0 // indirect @@ -22,6 +42,8 @@ require ( github.com/go-sql-driver/mysql v1.9.3 // indirect github.com/goccy/go-json v0.10.5 // indirect github.com/golang-jwt/jwt/v5 v5.3.0 // indirect + github.com/golang/protobuf v1.5.4 // indirect + github.com/golang/snappy v1.0.0 // indirect github.com/google/uuid v1.6.0 // indirect github.com/gorilla/websocket v1.5.3 // indirect github.com/json-iterator/go v1.1.12 // indirect @@ -34,12 +56,14 @@ require ( github.com/minio/minio-go/v7 v7.0.95 // indirect github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect github.com/modern-go/reflect2 v1.0.2 // indirect + github.com/mschoch/smat v0.2.0 // indirect github.com/pelletier/go-toml/v2 v2.2.4 // indirect github.com/philhofer/fwd v1.2.0 // indirect github.com/rs/xid v1.6.0 // indirect github.com/tinylib/msgp v1.3.0 // indirect github.com/twitchyliquid64/golang-asm v0.15.1 // indirect github.com/ugorji/go/codec v1.3.0 // indirect + go.etcd.io/bbolt v1.4.2 // indirect golang.org/x/arch v0.20.0 // indirect golang.org/x/crypto v0.41.0 // indirect golang.org/x/net v0.43.0 // indirect diff --git a/main.go b/main.go index 6fae451..b4e9fa1 100644 --- a/main.go +++ b/main.go @@ -3,12 +3,14 @@ package main import ( "strconv" "toutoukan/config" + "toutoukan/controllers/search" "toutoukan/init/databaseInit" "toutoukan/init/redisInit" "toutoukan/router" ) func main() { + search.InitIndex() databaseInit.DbInit() redisInit.RedisInit() if err := config.Goinit(); err != nil { @@ -21,4 +23,5 @@ func main() { } defer databaseInit.UserDB.Close() defer redisInit.RedisClient.Close() + defer search.CloseIndex() } diff --git a/model/article/article.go b/model/article/article.go new file mode 100644 index 0000000..1760df9 --- /dev/null +++ b/model/article/article.go @@ -0,0 +1,6 @@ +package article + +type Document struct { + Title string `json:"title"` + Body string `json:"body"` +} diff --git a/note.md b/note.md index e00d16f..e3c8756 100644 --- a/note.md +++ b/note.md @@ -4,3 +4,31 @@ minio.exe server ./data --console-address "127.0.0.1:9000" --address "127.0.0.1: [redis启动] redis-server.exe redis.windows.conf + +[表设计] +-投票文章表 +``` +create table vote_article +( +id bigint auto_increment comment '文章ID' +primary key, +title varchar(255) not null comment '文章标题', +content text null comment '文章内容', +cover_image varchar(512) null comment '封面图片URL', +author_id bigint not null comment '作者ID', +status tinyint default 0 not null comment '状态(0:草稿;1:待审核;2:已发布;3:已结束;4:已下架)', +vote_start_time datetime not null comment '投票开始时间', +vote_end_time datetime not null comment '投票结束时间', +total_votes int default 0 not null comment '总投票数', +total_voters int default 0 not null comment '参与人数', +is_anonymous tinyint(1) default 0 not null comment '是否匿名投票(0:否;1:是)', +is_multiple tinyint(1) default 0 not null comment '是否允许多选(0:单选;1:多选)', +max_choices int default 1 not null comment '最大可选数量', +view_count int default 0 not null comment '浏览次数', +created_at datetime default CURRENT_TIMESTAMP not null comment '创建时间', +updated_at datetime null on update CURRENT_TIMESTAMP comment '更新时间', +deleted_at datetime null comment '软删除时间' +) +comment '投票文章表'; +``` + diff --git a/router/setupRouter.go b/router/setupRouter.go index ec63a56..a070cd6 100644 --- a/router/setupRouter.go +++ b/router/setupRouter.go @@ -2,6 +2,7 @@ package router import ( "github.com/gin-gonic/gin" + "toutoukan/controllers/search" "toutoukan/controllers/system" "toutoukan/controllers/test" "toutoukan/controllers/user" @@ -11,7 +12,6 @@ import ( func SetupRouter() *gin.Engine { r := gin.Default() - apiGroup := r.Group("/user") { apiGroup.POST("/login", user.UserLogin) @@ -24,6 +24,11 @@ func SetupRouter() *gin.Engine { { systemGroup.POST("/sendMsg", system.SendMsg) } + searchGroup := r.Group("/search") + { + searchGroup.POST("/usersearch", search.DataSearch) + searchGroup.POST("/insert", search.InsertDocument) + } return r }