mirror of
https://github.com/infiniflow/ragflow.git
synced 2026-08-05 07:10:29 +08:00
## Summary
This PR refactors the Go knowledge-compilation ingestion pipeline
(`internal/ingestion/knowledge_compile` +
`internal/ingestion/component/knowledge_compiler`) with three related
changes:
- **Token-budget batching for LLM merge decisions.**
`LLMMergeDecider.DecideBatch` previously stuffed every `(existing,
candidate)` pair into a single LLM call, risking `max_token` overflow.
It now splits pairs into token-bounded sub-batches (budget =
`llmMaxTokens * 0.85`) via `tokenizer.NumTokensFromString`, runs them
concurrently while preserving the global pair index, and never
reindexes.
- **Process-level global compile pool.** Introduces a single vCPU-sized
goroutine pool (`pool.go`, env `KC_COMPILE_CONCURRENCY`) dedicated to
*all* knowledge-compilation stages. KNN search loop, `DecideBatch`
sub-batches, `WriteMerged`/`DeleteMerged` internals, and the
component-level (structure/mindmap) per-call pools are all unified into
it via an injected submitter. No more per-job short-lived goroutines in
`runCompilerJobs` (futures are collected then awaited on the caller).
Fan-out stays bounded by the pool worker count; these stages are
docengine-bounded / LLM-bounded, not CPU-bounded.
- **DocEngine-only deletion.** `Consumer.processBatch` deletion no
longer loads the deleted docs' products into memory. Two sequential
DocEngine calls replace the old in-memory surgery:
- `DeleteDocLevelForDocs` — one `DeleteChunks` over `doc_id IN
deletedDocIDs` (merged rows carry `doc_id == kb`, so only per-doc
products match).
- `StripMergedSources` — one `Search` of `kc_merged=1` rows filtered by
`source_doc_ids IN deletedDocIDs` (intersection pushed down to the
engine), `UpdateChunks` the source array of survivors, and
`DeleteChunks` the rows whose array became empty.
## Changes
- `internal/ingestion/knowledge_compile/pool.go` (new): global
`compilerPool` +
`runCompilerJobs`/`SubmitCompilerJob`/`SubmitCompilerJobs`.
- `internal/ingestion/knowledge_compile/consumer.go`: deletion rewritten
to the two DocEngine calls;
`mergedBase`/`toDelete`/`stripDeletedSources` removed.
- `internal/ingestion/knowledge_compile/writer.go`:
`DeleteDocLevelForDocs` + `StripMergedSources` replace
`DeleteMergedForDoc`/`DeleteMerged`.
- `internal/ingestion/knowledge_compile/reader.go`: drop
`LoadMergedBySourceDoc` + `containsString` (keep `LoadDocProducts` for
the completion branch).
- `internal/ingestion/knowledge_compile/dedup.go`: `NewLLMDeduper` takes
`llmMaxTokens`; wires `SetMaxBatchTokens`/`SetSubmitter`.
- `internal/ingestion/knowledge_compiler/{structure,merge}.go`,
`mindmap/mindmap.go`, `pool_wiring.go`: token-budget split + submitter
injection.
- Tests: `structure_test.go` (token-budget split), `dedup_test.go`,
`consumer_test.go` (tombstone + DocEngine deletion assertions) updated.
## Validation
`bash build.sh --test -race ./internal/ingestion/knowledge_compile/...
./internal/ingestion/component/knowledge_compiler/...` passes (unit
tier, no external services).
🤖 Generated with [CodeBuddy](https://www.codebuddy.ai)
---------
Co-authored-by: yuzhichang <yuzhichang@infiniflow.ai>
123 lines
3.6 KiB
Go
123 lines
3.6 KiB
Go
//
|
|
// Copyright 2026 The InfiniFlow Authors. All Rights Reserved.
|
|
//
|
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
// you may not use this file except in compliance with the License.
|
|
// You may obtain a copy of the License at
|
|
//
|
|
// http://www.apache.org/licenses/LICENSE-2.0
|
|
//
|
|
// Unless required by applicable law or agreed to in writing, software
|
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
// See the License for the specific language governing permissions and
|
|
// limitations under the License.
|
|
|
|
package knowledge_compile
|
|
|
|
import (
|
|
"context"
|
|
"testing"
|
|
|
|
kccommon "ragflow/internal/ingestion/component/knowledge_compiler/common"
|
|
)
|
|
|
|
// recordingDeduper is a fake Deduper that records the Decide/DecideBatch calls
|
|
// and applies a simple policy: a candidate whose content is "dup" is merged
|
|
// into the existing row, otherwise it stays distinct.
|
|
type recordingDeduper struct {
|
|
noopDeduper
|
|
decideBatchCalls int
|
|
lastGroups []MergeGroup
|
|
}
|
|
|
|
func (r *recordingDeduper) DecideBatch(ctx context.Context, groups []MergeGroup) ([]MergeGroup, error) {
|
|
r.decideBatchCalls++
|
|
r.lastGroups = groups
|
|
for gi := range groups {
|
|
existing := groups[gi].Existing
|
|
var distinct []kccommon.Product
|
|
dup := false
|
|
for _, cand := range groups[gi].Candidates {
|
|
if stringOf(cand.Content) == "dup" {
|
|
merged := existing
|
|
merged.Content = "merged-" + existing.Content
|
|
existing = merged
|
|
dup = true
|
|
continue
|
|
}
|
|
c := cand
|
|
c.Merged = true
|
|
c.DocID = existing.DocID
|
|
distinct = append(distinct, c)
|
|
}
|
|
groups[gi].Merged = existing
|
|
groups[gi].Duplicate = dup
|
|
groups[gi].Distinct = distinct
|
|
}
|
|
return groups, nil
|
|
}
|
|
|
|
func stringOf(s any) string {
|
|
if v, ok := s.(string); ok {
|
|
return v
|
|
}
|
|
return ""
|
|
}
|
|
|
|
func TestDeduperDecideBatchFoldsGroups(t *testing.T) {
|
|
d := &recordingDeduper{}
|
|
existing := kccommon.Product{ID: "row-1", DocID: "kb1", Content: "base"}
|
|
groups := []MergeGroup{
|
|
{
|
|
Existing: existing,
|
|
Candidates: []kccommon.Product{{ID: "a", Content: "dup"}, {ID: "b", Content: "fresh"}},
|
|
Score: 1.0,
|
|
},
|
|
}
|
|
out, err := d.DecideBatch(context.Background(), groups)
|
|
if err != nil {
|
|
t.Fatalf("DecideBatch: %v", err)
|
|
}
|
|
if d.decideBatchCalls != 1 {
|
|
t.Errorf("expected exactly one DecideBatch call, got %d", d.decideBatchCalls)
|
|
}
|
|
g := out[0]
|
|
if !g.Duplicate {
|
|
t.Errorf("group with a dup candidate should report Duplicate=true")
|
|
}
|
|
if g.Merged.Content != "merged-base" {
|
|
t.Errorf("merged content = %q, want merged-base", g.Merged.Content)
|
|
}
|
|
if len(g.Distinct) != 1 || stringOf(g.Distinct[0].Content) != "fresh" {
|
|
t.Errorf("distinct = %v, want one fresh candidate", g.Distinct)
|
|
}
|
|
if g.Distinct[0].DocID != "kb1" || !g.Distinct[0].Merged {
|
|
t.Errorf("distinct candidate should be a new merged row under the KB")
|
|
}
|
|
}
|
|
|
|
func TestNoopDeduperDecideBatchNoMerge(t *testing.T) {
|
|
d := NewNoopDeduper()
|
|
existing := kccommon.Product{ID: "row-1", DocID: "kb1", Content: "base"}
|
|
groups := []MergeGroup{
|
|
{Existing: existing, Candidates: []kccommon.Product{{ID: "a", Content: "x"}, {ID: "b", Content: "y"}}},
|
|
}
|
|
out, err := d.DecideBatch(context.Background(), groups)
|
|
if err != nil {
|
|
t.Fatalf("DecideBatch: %v", err)
|
|
}
|
|
g := out[0]
|
|
if g.Duplicate {
|
|
t.Errorf("noop deduper must never merge")
|
|
}
|
|
if len(g.Distinct) != 2 {
|
|
t.Fatalf("noop deduper should report both candidates distinct, got %d", len(g.Distinct))
|
|
}
|
|
for _, c := range g.Distinct {
|
|
if c.DocID != "kb1" || !c.Merged {
|
|
t.Errorf("distinct candidate should be a new merged row: %+v", c)
|
|
}
|
|
}
|
|
}
|