更新搜索引擎
This commit is contained in:
9
controllers/search/close.go
Normal file
9
controllers/search/close.go
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
package search
|
||||||
|
|
||||||
|
// 程序退出时关闭索引(可选)
|
||||||
|
func CloseIndex() error {
|
||||||
|
if globalIndex != nil {
|
||||||
|
return globalIndex.Close()
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
39
controllers/search/init.go
Normal file
39
controllers/search/init.go
Normal file
@@ -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
|
||||||
|
}
|
||||||
98
controllers/search/insert.go
Normal file
98
controllers/search/insert.go
Normal file
@@ -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)
|
||||||
|
//}
|
||||||
85
controllers/search/search.go
Normal file
85
controllers/search/search.go
Normal file
@@ -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,
|
||||||
|
})
|
||||||
|
}
|
||||||
24
go.mod
24
go.mod
@@ -4,6 +4,26 @@ go 1.23.1
|
|||||||
|
|
||||||
require (
|
require (
|
||||||
filippo.io/edwards25519 v1.1.0 // indirect
|
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 v1.14.0 // indirect
|
||||||
github.com/bytedance/sonic/loader v0.3.0 // indirect
|
github.com/bytedance/sonic/loader v0.3.0 // indirect
|
||||||
github.com/cespare/xxhash/v2 v2.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/go-sql-driver/mysql v1.9.3 // indirect
|
||||||
github.com/goccy/go-json v0.10.5 // indirect
|
github.com/goccy/go-json v0.10.5 // indirect
|
||||||
github.com/golang-jwt/jwt/v5 v5.3.0 // 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/google/uuid v1.6.0 // indirect
|
||||||
github.com/gorilla/websocket v1.5.3 // indirect
|
github.com/gorilla/websocket v1.5.3 // indirect
|
||||||
github.com/json-iterator/go v1.1.12 // 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/minio/minio-go/v7 v7.0.95 // indirect
|
||||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
|
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
|
||||||
github.com/modern-go/reflect2 v1.0.2 // 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/pelletier/go-toml/v2 v2.2.4 // indirect
|
||||||
github.com/philhofer/fwd v1.2.0 // indirect
|
github.com/philhofer/fwd v1.2.0 // indirect
|
||||||
github.com/rs/xid v1.6.0 // indirect
|
github.com/rs/xid v1.6.0 // indirect
|
||||||
github.com/tinylib/msgp v1.3.0 // indirect
|
github.com/tinylib/msgp v1.3.0 // indirect
|
||||||
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
|
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
|
||||||
github.com/ugorji/go/codec v1.3.0 // 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/arch v0.20.0 // indirect
|
||||||
golang.org/x/crypto v0.41.0 // indirect
|
golang.org/x/crypto v0.41.0 // indirect
|
||||||
golang.org/x/net v0.43.0 // indirect
|
golang.org/x/net v0.43.0 // indirect
|
||||||
|
|||||||
3
main.go
3
main.go
@@ -3,12 +3,14 @@ package main
|
|||||||
import (
|
import (
|
||||||
"strconv"
|
"strconv"
|
||||||
"toutoukan/config"
|
"toutoukan/config"
|
||||||
|
"toutoukan/controllers/search"
|
||||||
"toutoukan/init/databaseInit"
|
"toutoukan/init/databaseInit"
|
||||||
"toutoukan/init/redisInit"
|
"toutoukan/init/redisInit"
|
||||||
"toutoukan/router"
|
"toutoukan/router"
|
||||||
)
|
)
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
|
search.InitIndex()
|
||||||
databaseInit.DbInit()
|
databaseInit.DbInit()
|
||||||
redisInit.RedisInit()
|
redisInit.RedisInit()
|
||||||
if err := config.Goinit(); err != nil {
|
if err := config.Goinit(); err != nil {
|
||||||
@@ -21,4 +23,5 @@ func main() {
|
|||||||
}
|
}
|
||||||
defer databaseInit.UserDB.Close()
|
defer databaseInit.UserDB.Close()
|
||||||
defer redisInit.RedisClient.Close()
|
defer redisInit.RedisClient.Close()
|
||||||
|
defer search.CloseIndex()
|
||||||
}
|
}
|
||||||
|
|||||||
6
model/article/article.go
Normal file
6
model/article/article.go
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
package article
|
||||||
|
|
||||||
|
type Document struct {
|
||||||
|
Title string `json:"title"`
|
||||||
|
Body string `json:"body"`
|
||||||
|
}
|
||||||
28
note.md
28
note.md
@@ -4,3 +4,31 @@ minio.exe server ./data --console-address "127.0.0.1:9000" --address "127.0.0.1:
|
|||||||
[redis启动]
|
[redis启动]
|
||||||
redis-server.exe redis.windows.conf
|
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 '投票文章表';
|
||||||
|
```
|
||||||
|
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ package router
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
|
"toutoukan/controllers/search"
|
||||||
"toutoukan/controllers/system"
|
"toutoukan/controllers/system"
|
||||||
"toutoukan/controllers/test"
|
"toutoukan/controllers/test"
|
||||||
"toutoukan/controllers/user"
|
"toutoukan/controllers/user"
|
||||||
@@ -11,7 +12,6 @@ import (
|
|||||||
|
|
||||||
func SetupRouter() *gin.Engine {
|
func SetupRouter() *gin.Engine {
|
||||||
r := gin.Default()
|
r := gin.Default()
|
||||||
|
|
||||||
apiGroup := r.Group("/user")
|
apiGroup := r.Group("/user")
|
||||||
{
|
{
|
||||||
apiGroup.POST("/login", user.UserLogin)
|
apiGroup.POST("/login", user.UserLogin)
|
||||||
@@ -24,6 +24,11 @@ func SetupRouter() *gin.Engine {
|
|||||||
{
|
{
|
||||||
systemGroup.POST("/sendMsg", system.SendMsg)
|
systemGroup.POST("/sendMsg", system.SendMsg)
|
||||||
}
|
}
|
||||||
|
searchGroup := r.Group("/search")
|
||||||
|
{
|
||||||
|
searchGroup.POST("/usersearch", search.DataSearch)
|
||||||
|
searchGroup.POST("/insert", search.InsertDocument)
|
||||||
|
}
|
||||||
|
|
||||||
return r
|
return r
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user