fix 分词bug,添加重爬机制
This commit is contained in:
+80
-10
@@ -265,12 +265,18 @@ func (s *Server) handleAdminStats(w http.ResponseWriter, r *http.Request) {
|
||||
langCount := make(map[string]int)
|
||||
totalWords := 0
|
||||
total := 0
|
||||
recrawlEligible := 0
|
||||
now := time.Now().Unix()
|
||||
maxAge := int64(config.RecrawlMaxAge())
|
||||
|
||||
s.db.ForEachSnippet(func(url string, snippet *storage.SnippetEntry) error {
|
||||
total++
|
||||
domain := netloc(url)
|
||||
domainCount[domain]++
|
||||
totalWords += len(snippet.Text)
|
||||
if now-snippet.Timestamp >= maxAge {
|
||||
recrawlEligible++
|
||||
}
|
||||
|
||||
siteInfo, _ := s.db.GetSiteInfo(domain)
|
||||
if siteInfo != nil {
|
||||
@@ -315,12 +321,13 @@ func (s *Server) handleAdminStats(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
|
||||
resp := map[string]any{
|
||||
"total_urls": total,
|
||||
"total_words": totalWords,
|
||||
"total_domains": len(domainCount), // 真实的域名总数(非Top 20)
|
||||
"domains": domainsMap,
|
||||
"languages": langsMap,
|
||||
"pending": atomic.LoadInt64(&s.rowCount), // 内存中未刷盘的索引条目数
|
||||
"total_urls": total,
|
||||
"total_words": totalWords,
|
||||
"total_domains": len(domainCount), // 真实的域名总数(非Top 20)
|
||||
"domains": domainsMap,
|
||||
"languages": langsMap,
|
||||
"pending": atomic.LoadInt64(&s.rowCount), // 内存中未刷盘的索引条目数
|
||||
"recrawl_eligible": recrawlEligible, // 已过期、可被重爬的 URL 数量
|
||||
}
|
||||
|
||||
json.NewEncoder(w).Encode(resp)
|
||||
@@ -575,7 +582,8 @@ func (s *Server) handleSearch(w http.ResponseWriter, r *http.Request) {
|
||||
if m := siteRe.FindStringSubmatch(part); len(m) > 1 {
|
||||
siteFilter = m[1] // site:example.com 提取目标主机名
|
||||
} else {
|
||||
segs := s.analyzer.Segment(part, false)
|
||||
// 搜索模式分词(CutForSearch):更细粒度,"气象局" → ["气象", "局", "气象局"]
|
||||
segs := s.analyzer.Segment(part, true)
|
||||
for _, t := range segs {
|
||||
if !s.infoSvc.IsBlocked(t) { // 过滤屏蔽词
|
||||
tokens = append(tokens, t)
|
||||
@@ -584,6 +592,11 @@ func (s *Server) handleSearch(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
}
|
||||
|
||||
// 智能去重:当一个词是另一个词的子串时,保留两者但标记子词关系。
|
||||
// 这样评分时可以用长词(精确匹配)加分,同时不因缺少短词而过度惩罚。
|
||||
// 算法:对于每一对词 (a, b),如果 a 是 b 的子串且 a != b,则 a 是 b 的子词。
|
||||
tokens = deduplicateSubstrings(tokens)
|
||||
|
||||
// 最多保留 20 个词(避免查询过于宽泛)
|
||||
if len(tokens) > 20 {
|
||||
tokens = tokens[:20]
|
||||
@@ -691,10 +704,15 @@ func (s *Server) query(tokens []string, from, to int, siteFilter string) ([]sear
|
||||
}
|
||||
|
||||
// 计算每个 URL 的相关性和初始分数
|
||||
// 评分策略:部分匹配加权和 + 缺词软惩罚(替代原来的全词乘积)
|
||||
// 全词乘积问题:一个 URL 只要缺少任何一个查询词,rel 就接近 0,
|
||||
// 导致"气象局"拆成 ["气象局","局"] 后,只有"气象"没有"气象局"的页面被淹没。
|
||||
missPenalty := config.MissPenalty()
|
||||
candidates := make([]candidate, 0, len(urlWeights))
|
||||
for u, vs := range urlWeights {
|
||||
// 词权重相乘(贝叶斯概率近似),缺省权重填充缺失词
|
||||
rel := 1.0
|
||||
// 统计实际匹配的词数和权重总和
|
||||
matchedCount := 0
|
||||
sumWeight := 0.0
|
||||
for _, ti := range tokenIndexes {
|
||||
vp := vs[ti.token]
|
||||
if vp == 0 {
|
||||
@@ -703,8 +721,23 @@ func (s *Server) query(tokens []string, from, to int, siteFilter string) ([]sear
|
||||
if vp > 0.06 {
|
||||
vp = math.Log((vp-0.06)*40+1)/40 + 0.06
|
||||
}
|
||||
rel *= vp
|
||||
sumWeight += vp
|
||||
// 只有权重超过默认值才算真正匹配(排除了 defVal 填充的假匹配)
|
||||
if vs[ti.token] > 0 {
|
||||
matchedCount++
|
||||
}
|
||||
}
|
||||
totalTokens := len(tokenIndexes)
|
||||
// 部分匹配相关性 = 加权平均 × 匹配覆盖率加成
|
||||
// matchRatio:匹配词占比,全部匹配=1,全部缺失=0
|
||||
matchRatio := float64(matchedCount) / float64(totalTokens)
|
||||
// avgWeight:匹配词的平均权重
|
||||
avgWeight := sumWeight / float64(totalTokens)
|
||||
// rel = 平均权重 × (匹配率 + 未匹配部分的软惩罚)
|
||||
// missPenalty 控制未匹配词的惩罚力度:
|
||||
// 0 = 完全不惩罚(纯加权和)
|
||||
// 1 = 缺词的权重取 0(等同于全词乘积的极端情况)
|
||||
rel := avgWeight * (matchRatio + (1-matchRatio)*missPenalty)
|
||||
// 反向链接繁荣加分
|
||||
prosper := 1 + s.infoSvc.Prosper(u)*config.BacklinkWeight()
|
||||
bad := badURL(u)
|
||||
@@ -969,6 +1002,43 @@ func badURL(u string) float64 {
|
||||
return math.Min(s, 0.9)
|
||||
}
|
||||
|
||||
// deduplicateSubstrings 对分词结果进行智能去重。
|
||||
// 当词 A 是词 B 的子串时(A ≠ B),移除较短的 A。
|
||||
// 例如 ["气象", "局", "气象局"] → ["气象局", "局"]
|
||||
// 保留最长词以确保精确匹配优先,同时短词作为兜底召回。
|
||||
func deduplicateSubstrings(tokens []string) []string {
|
||||
if len(tokens) <= 1 {
|
||||
return tokens
|
||||
}
|
||||
// 按长度降序排列,等长按字典序
|
||||
sort.Slice(tokens, func(i, j int) bool {
|
||||
if len(tokens[i]) != len(tokens[j]) {
|
||||
return len(tokens[i]) > len(tokens[j])
|
||||
}
|
||||
return tokens[i] < tokens[j]
|
||||
})
|
||||
seen := make(map[string]bool)
|
||||
var result []string
|
||||
for _, t := range tokens {
|
||||
if seen[t] {
|
||||
continue // 完全重复的词跳过
|
||||
}
|
||||
seen[t] = true
|
||||
// 检查是否已被更长的词包含(t 是某个已保留词的子串)
|
||||
isSubstr := false
|
||||
for _, kept := range result {
|
||||
if strings.Contains(kept, t) && kept != t {
|
||||
isSubstr = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !isSubstr {
|
||||
result = append(result, t)
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
// netloc 从 URL 提取主机名。
|
||||
func netloc(rawURL string) string {
|
||||
parts := strings.SplitN(rawURL, "/", 4)
|
||||
|
||||
Reference in New Issue
Block a user