// 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 ( "flag" // 命令行参数解析 "fmt" // 格式化(搜索服务端口) "log" // 日志输出 "os" // 操作系统信号 "os/signal" // 信号捕获 "path/filepath" // 路径处理 "syscall" // 系统调用(SIGTERM) "sese-engine/analyzer" // 文本分析和关键词提取 "sese-engine/backlink" // 反向链接(繁荣值)计算 "sese-engine/config" // 全局配置 "sese-engine/crawler" // BFS 爬虫 "sese-engine/info" // info 服务(繁荣表、调整表、屏蔽词) "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: "loli_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://zh.wikipedia.org/" # 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") // --entry:BFS 爬取的起始 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. 存储层:打开 bbolt 数据库 ---- db, err := storage.Open(*storageDir) if err != nil { log.Fatalf("failed to open storage: %v", err) } defer db.Close() db.StartWriteFlusher() // 启动异步写缓冲后台刷盘 // ---- 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(db, infoSvc, anal) go func() { addr := fmt.Sprintf(":%d", config.SearchServerPort()) if err := searchSrv.ListenAndServe(addr); err != nil { log.Fatalf("[search] fatal: %v", err) } }() // ---- 6. 反向链接计算器:每 48 小时运行一次 ---- bl := backlink.New(db, *storageDir) searchSrv.SetBacklinkRunner(bl) go bl.Run() // ---- 7. 爬虫:从入口 URL 开始 BFS 爬取 ---- // 从 info 服务获取繁荣表快照,用于调度优先级决策 prosperMap := infoSvc.ProsperMap() crawl := crawler.New(db, anal, prosperMap) searchSrv.SetCrawler(crawl) // 注入爬虫用于 Priority URL 立即触发 go crawl.Run(*entryURL, config.MaxEpoch()) log.Println("all modules started — press Ctrl-C to stop") // ---- 优雅退出 ---- // 阻塞等待 SIGINT(Ctrl-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") // 最后刷盘,确保数据不丢失 log.Println("flushing index...") searchSrv.Flush() log.Println("shutdown complete") }