Files
ragflow/internal/ingestion/component/knowledge_compiler/golden/metrics.go
Zhichang Yu 29287ef74b refactor(knowledge_compiler): derive variant from template kind via single template id (#17630)
## Summary

Refactor the Go `KnowledgeCompilerComponent` so its parameter is a
**single string template id** instead of a DSL-level `variant` (or
plural group id list). The `variant` is no longer in the DSL — it is now
**derived at runtime from the resolved compilation template's `kind`
field**.

This aligns the Go ingestion port with the frontend Compiler operator,
which emits a singular `compilation_template_group_id` and does not
write `variant` into the generated `compiler.json`.
2026-07-31 18:01:55 +08:00

153 lines
4.7 KiB
Go

package golden
import (
"encoding/json"
"strings"
"ragflow/internal/ingestion/component/schema"
)
// TreeMetrics summarizes the structural shape of a tree product tree.
type TreeMetrics struct {
ProductCount int
RootCount int
LeafClusters int // number of level-0 summary nodes (bottom clusters)
MaxDepth int // root(0) .. deepest summary level
AllParented bool
VectorOK bool // every product carries a non-empty vector
SchemaOK bool // every product carries the schema fields
// CoveredSources is the number of distinct source chunk IDs referenced by
// level-0 leaf clusters via their source_chunk_ids meta. It measures how
// completely the input corpus is represented by the tree, independent of
// the tree's structural well-formedness.
CoveredSources int
// covered is the working set of distinct source chunk IDs seen so far.
covered map[string]bool
}
// AnalyzeTreeProducts validates tree integrity and computes structural
// metrics from a flat chunk list (the compiled tree output, expressed as
// schema.ChunkDoc values). Used by the 缺口 C golden gate.
//
// validSourceIDs, when provided, limits coverage counting to source chunk IDs
// that actually belong to the input corpus. This prevents an untrusted
// source_chunk_ids (e.g. a leaked/garbage ID) from inflating CoveredSources
// past nChunks and pushing CoverageFraction above 1.0. When empty, all
// source_chunk_ids are counted (backward compatible for unit tests that build
// synthetic trees).
func AnalyzeTreeProducts(chunks []schema.ChunkDoc, validSourceIDs ...string) TreeMetrics {
ids := make(map[string]bool, len(chunks))
for _, c := range chunks {
if id, ok := c.GetExtraString("id"); ok {
ids[id] = true
}
}
validSet := make(map[string]bool, len(validSourceIDs))
for _, id := range validSourceIDs {
validSet[id] = true
}
checkValid := len(validSet) > 0
m := TreeMetrics{ProductCount: len(chunks), AllParented: true, VectorOK: true, SchemaOK: true, covered: make(map[string]bool)}
maxLevel := -1
for _, c := range chunks {
kind, _ := c.GetExtraString("kc_kind")
level := 0
if lf, ok := extraFloat(c, "kc_level"); ok {
level = int(lf)
}
switch kind {
case "root":
m.RootCount++
case "summary":
if level == 0 {
m.LeafClusters++
// Accumulate the distinct source chunk IDs this leaf cluster
// was built from. Every input chunk is assigned to exactly one
// level-0 cluster in buildTree, so the union of these sets is
// the set of covered source chunks. Only IDs that belong to the
// input corpus count, so an unknown ID cannot inflate coverage.
if src, ok := c.GetExtraStringSlice("source_chunk_ids"); ok {
for _, id := range src {
if checkValid && !validSet[id] {
continue
}
if !m.covered[id] {
m.covered[id] = true
m.CoveredSources++
}
}
}
}
if level > maxLevel {
maxLevel = level
}
}
parent, _ := c.GetExtraString("parent_kwd")
if kind != "root" && parent == "" {
m.AllParented = false
}
if parent != "" && !ids[parent] {
m.AllParented = false
}
if !hasVector(c) {
m.VectorOK = false
}
id, _ := c.GetExtraString("id")
docID, _ := c.GetExtraString("doc_id")
tenant, _ := c.GetExtraString("tenant_id")
ck, _ := c.GetExtraString("compile_kwd")
if id == "" || docID == "" || tenant == "" || c.Text == "" || ck == "" {
m.SchemaOK = false
}
}
m.MaxDepth = maxLevel + 1
return m
}
// CoverageFraction reports how completely the input chunks are represented by
// the tree. It is the ratio of distinct source chunk IDs referenced by the
// level-0 leaf clusters (CoveredSources) to the total input chunk count
// (nChunks). This detects dropped source chunks: a structurally well-formed
// tree that silently omits input chunks will score below 1.0.
func (m TreeMetrics) CoverageFraction(nChunks int) float64 {
if nChunks <= 0 {
return 0
}
if m.CoveredSources <= 0 {
return 0
}
// Coverage can never exceed 1.0: a source chunk is covered at most once,
// and only corpus IDs are counted, so CoveredSources <= nChunks.
frac := float64(m.CoveredSources) / float64(nChunks)
if frac > 1.0 {
return 1.0
}
return frac
}
// extraFloat reads a numeric Extra value by key.
func extraFloat(c schema.ChunkDoc, key string) (float64, bool) {
if c.Extra == nil {
return 0, false
}
raw, ok := c.Extra[key]
if !ok {
return 0, false
}
var f float64
if err := json.Unmarshal(raw, &f); err != nil {
return 0, false
}
return f, true
}
// hasVector reports whether the chunk carries any q_<dim>_vec embedding.
func hasVector(c schema.ChunkDoc) bool {
for k := range c.Extra {
if strings.HasPrefix(k, "q_") && strings.HasSuffix(k, "_vec") {
return true
}
}
return false
}