Files
2026-04-20 18:26:54 +08:00

241 lines
9.5 KiB
Go
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// sese-engine — Go rewrite
// Go 版 sese-engine:个人搜索引擎的主入口文件。
//
// 所有模块(爬虫、收获服务器、搜索服务器、反向链接计算)均作为 goroutine 在同一进程中启动。
// 主线程阻塞等待系统信号(Ctrl-C / SIGTERM),收到后优雅退出。
//
// 运行方式:
//
// cd golang && go run . [--storage ./savedata] [--entry https://zh.wikipedia.org/]
package main
import (
"context"
"flag" // 命令行参数解析
"fmt" // 格式化(搜索服务端口)
"log" // 日志输出
"os" // 操作系统信号
"os/signal" // 信号捕获
"path/filepath" // 路径处理
"syscall" // 系统调用(SIGTERM
"time" // 时间(刷盘间隔)
"sese-engine/analyzer" // 文本分析和关键词提取
"sese-engine/backlink" // 反向链接(繁荣值)计算
"sese-engine/config" // 全局配置
"sese-engine/crawler" // BFS 爬虫
"sese-engine/info" // info 服务(繁荣表、调整表、屏蔽词)
"sese-engine/mysql" // MySQL 数据库连接
"sese-engine/redis" // Redis 连接
"sese-engine/search" // 搜索服务器(内嵌收获服务)
"sese-engine/storage" // 持久化存储
)
// initConfig 检查并初始化配置文件
// 如果 savedata/config.yml 不存在,则从模板生成
func initConfig() error {
configDir := "savedata"
configPath := filepath.Join(configDir, "config.yml")
// 检查配置文件是否已存在
if _, err := os.Stat(configPath); err == nil {
// 配置文件已存在,直接返回
return nil
}
// 确保 data 目录存在
if err := os.MkdirAll(configDir, 0755); err != nil {
return fmt.Errorf("failed to create config directory: %v", err)
}
// 从模板生成配置文件
defaultConfig := `# SESE Engine 配置文件
# 程序实际加载的配置文件
# 索引 / 存储相关限制
index:
max_urls_per_key: 11000 # 每个索引词最多保存的 URL 数量上限
max_same_domain_per_key: 20 # 同一域名在每个索引词下最多出现的次数
big_clean_threshold: 2000000 # 内存中累计多少条索引后触发一次刷盘清理
max_new_urls_per_key: 10000 # 每次刷盘时,每个索引词最多写入的新 URL 数量上限
min_urls_for_new_key: 3 # 新索引词如果 URL 数少于该值则丢弃,不写入磁盘
# 爬虫行为相关配置
crawler:
spider_name: "Haibara_AI_spider" # HTTP 请求的 User-Agent 标识
cooldown: 3 # 同一主机相邻两次请求的最小间隔(秒),用于遵守 robots.txt 和避免被封
workers: 22 # 爬虫并发 goroutine 数量
crawl_focus: 0.7 # 域名集中度因子,越大越倾向在少量域名内深挖,越小越分散
max_keywords_per_page: 250 # 单个页面最多提取的关键词数量
max_epoch: 100 # BFS 爬取的最大轮次上限
expected_prosper_ratio: 0.6 # 队列中预期"繁荣"域名(高反向链接)的占比,用于调度决策
entry_url: "https://haibara.ai/" # BFS 爬取的起始入口 URL
max_page_size: 5242880 # 单个页面最大抓取字节数(0=不限,默认 5MB)
recrawl_max_age: 2592000 # URL 过期时间(秒),超过此时间的 URL 允许被重爬,默认 30 天
recrawl_check_interval: 3600 # 运行期间检查过期 URL 的间隔(秒),默认 1 小时
recrawl_batch_size: 500 # 每次检查最多释放多少个过期 URL
# 搜索结果排序权重配置
search:
use_online_snippet: true # 是否在线抓取摘要(搜索时实时抓取页面补充摘要)
online_snippet_timeout: 3 # 在线抓取摘要的超时时间(秒)
weight_daily_decay: 0.996 # 页面年龄的时间衰减因子(每天乘以此系数)
language_weight: 0.5 # 语种匹配权重:与查询语种一致时加分
consecutive_key_weight: 1.3 # 连续关键词命中权重:多词连续出现时加分
backlink_weight: 1.0 # 反向链接权重:指向该 URL 的链接越多得分越高
server_port: 50082 # 搜索服务和收获服务的统一 HTTP 监听端口
flush_interval_seconds: 300 # 定期刷盘间隔(秒):将内存索引批量写入磁盘
# 反向链接(PageRank 类)计算相关配置
backlink:
baseline: 200000 # 反向链接得分归一化的除数(用于将原始链接数映射到 [0,1] 区间)
# 存储根目录路径,相对于进程启动时的工作目录
storage:
path: "./savedata"
# 各模块 Prometheus 监控指标的 HTTP 端口
prometheus:
crawler_port: 14950 # 爬虫模块的 metrics 端口
backlink_port: 14952 # 反向链接计算模块的 metrics 端口
search_port: 14953 # 搜索服务(含收获功能)模块的 metrics 端口
`
if err := os.WriteFile(configPath, []byte(defaultConfig), 0644); err != nil {
return fmt.Errorf("failed to write config file: %v", err)
}
log.Printf("Generated default config file: %s", configPath)
return nil
}
// loadConfig 从 savedata/config.yml 加载配置
func loadConfig() error {
if err := config.LoadFromSavedata(); err != nil {
return fmt.Errorf("failed to load config: %v", err)
}
log.Printf("Config loaded successfully from savedata/config.yml")
return nil
}
func main() {
// ---- 0. 初始化配置文件 ----
if err := initConfig(); err != nil {
log.Fatalf("failed to init config: %v", err)
}
// ---- 0.5 加载配置文件 ----
if err := loadConfig(); err != nil {
log.Fatalf("failed to load config: %v", err)
}
// ---- 命令行参数 ----
// --storage:存储根目录路径,默认使用 config.StoragePath
storageDir := flag.String("storage", config.StoragePath, "path to savedata directory")
// --entryBFS 爬取的起始 URL,默认使用 config.EntryURL()(维基百科中文首页)
entryURL := flag.String("entry", config.EntryURL(), "BFS crawl entry URL")
// --stopwords:屏蔽词 JSON 文件路径
stopWords := flag.String("stopwords", "/savedata/标点符号.json", "path to stop-words JSON")
flag.Parse()
// 设置日志格式:时间戳 + 短文件名
log.SetFlags(log.LstdFlags | log.Lshortfile)
log.Printf("sese-engine starting storage=%s entry=%s", *storageDir, *entryURL)
// ---- 1. Redis 连接(高性能内存存储)----
if err := redis.Open(); err != nil {
log.Fatalf("failed to open redis: %v", err)
}
defer redis.Close()
// ---- 1.1 MySQL 连接(持久化存储,默认关闭)----
var flusher *mysql.Flusher
if config.MySQLEnabled() {
if err := mysql.Open(); err != nil {
log.Fatalf("failed to open mysql: %v", err)
}
defer mysql.Close()
// 从 MySQL 恢复数据到 Redis(如 Redis 数据丢失)
// 仅当 Redis 为空时才执行恢复,避免覆盖已有数据
ctx := context.Background()
size, _ := redis.Client.DBSize(ctx).Result()
if size > 0 {
log.Printf("[mysql-restore] Redis has %d keys, skipping restore", size)
} else if err := mysql.RestoreFromMySQLToRedis(redis.Client); err != nil {
log.Printf("[mysql-restore] warning: %v", err)
}
// MySQL 刷盘器:定期将 Redis 数据刷到 MySQL
flusher = mysql.NewFlusher(redis.Client, 5*time.Minute, 1000)
flusher.Start()
defer flusher.Stop()
} else {
log.Println("[mysql] disabled in config, skipping MySQL init")
}
// ---- 1.3 存储层:初始化 Redis 存储 ----
store := storage.NewRedisStoreV2()
if err := store.Init(); err != nil {
log.Fatalf("failed to init redis store: %v", err)
}
defer store.Close()
// ---- 2. Info 服务:加载繁荣表、调整表和屏蔽词 ----
infoSvc := info.New(*storageDir)
// ---- 3. Analyzer:初始化分词器和语言检测器 ----
// modelPath 参数已废弃(lingua-go 使用内置模型,无需外部文件)
anal, err := analyzer.New("", *stopWords)
if err != nil {
log.Fatalf("failed to init analyzer: %v", err)
}
defer anal.Close()
// ---- 4. 搜索服务器(默认 :80):对外提供搜索 API,同时内嵌收获服务(统一端口)
searchSrv := search.New(store, infoSvc, anal)
go func() {
addr := fmt.Sprintf(":%d", config.SearchServerPort())
if err := searchSrv.ListenAndServe(addr, config.UnixSocket()); err != nil {
log.Fatalf("[search] fatal: %v", err)
}
}()
// ---- 6. 反向链接计算器:每 48 小时运行一次 ----
bl := backlink.New(store, *storageDir)
searchSrv.SetBacklinkRunner(bl)
go bl.Run()
// ---- 7. 爬虫:从入口 URL 开始 BFS 爬取 ----
// 从 info 服务获取繁荣表快照,用于调度优先级决策
prosperMap := infoSvc.ProsperMap()
crawl := crawler.New(store, anal, prosperMap)
searchSrv.SetCrawler(crawl) // 注入爬虫用于 Priority URL 立即触发
go crawl.Run(*entryURL, config.MaxEpoch())
log.Println("all modules started — press Ctrl-C to stop")
// ---- 优雅退出 ----
// 阻塞等待 SIGINTCtrl-C)或 SIGTERM 信号
quit := make(chan os.Signal, 1)
signal.Notify(quit, os.Interrupt, syscall.SIGTERM)
<-quit
log.Println("shutdown signal received, initiating graceful shutdown...")
// 通知爬虫停止(不阻塞,等待爬虫内部协调)
crawl.Stop()
// 等待爬虫完全停止(包括 priority worker
crawl.WaitUntilStopped()
log.Println("crawler stopped")
// 最后刷盘(Redis → Disk,条件刷 MySQL),确保数据不丢失
log.Println("flushing index to disk...")
searchSrv.Flush()
if flusher != nil {
log.Println("flushing data to mysql...")
flusher.RunAll() // 同步执行一次 MySQL 刷盘
}
log.Println("shutdown complete")
}