Files
ragflow/internal/ingestion/knowledge_compile/service.go
Zhichang Yu 01d667296d refactor(knowledge_compile): global compile pool, token-budget batching, and DocEngine-only deletion (#17679)
## 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>
2026-08-02 17:06:29 +08:00

138 lines
4.4 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"
"crypto/rand"
"encoding/hex"
"fmt"
"time"
"gorm.io/gorm"
"ragflow/internal/engine"
kccommon "ragflow/internal/ingestion/component/knowledge_compiler/common"
)
// Option configures a Consumer.
type Option func(*Consumer)
// WithTTL sets the per-KB claim lease TTL.
func WithTTL(d time.Duration) Option {
return func(c *Consumer) {
if d > 0 {
c.ttl = d
}
}
}
// WithHeartbeat sets the claim heartbeat period (must be < TTL).
func WithHeartbeat(d time.Duration) Option {
return func(c *Consumer) {
if d > 0 {
c.heartbeat = d
}
}
}
// WithPollInterval sets how often the worker poll loop looks for a claimable KB
// when no NATS notify arrives.
func WithPollInterval(d time.Duration) Option {
return func(c *Consumer) {
if d > 0 {
c.pollInterval = d
}
}
}
// WithSweepInterval sets how often the worker tick calls TryClaim, which
// reclaims inflight left by crashed workers before claiming any ready batch.
func WithSweepInterval(d time.Duration) Option {
return func(c *Consumer) {
if d > 0 {
c.sweepInterval = d
}
}
}
// WithReader overrides the chunk Reader (used in tests).
func WithReader(r Reader) Option { return func(c *Consumer) { c.reader = r } }
// WithWriter overrides the chunk Writer (used in tests).
func WithWriter(w Writer) Option { return func(c *Consumer) { c.writer = w } }
// WithDeduperFactory overrides the per-tenant Deduper factory.
func WithDeduperFactory(f DeduperFactory) Option { return func(c *Consumer) { c.factory = f } }
// defaultLLMID / defaultEmbedding are the model ids used when resolving LLM
// deps for the dataset-level deduper. Set via SetModelConfig (typically from the
// server bootstrap). Empty strings make the factory fall back to the noop
// deduper until production wiring supplies real values.
var (
defaultLLMID string
defaultEmbedding string
)
// SetModelConfig records the model ids used to build the LLM deduper.
func SetModelConfig(llmID, embedding string) {
defaultLLMID = llmID
defaultEmbedding = embedding
}
// defaultDeduperFactory resolves the per-tenant LLM deps and builds the
// KB-scoped deduper. On any failure it returns an error so the caller falls
// back to the noop deduper (merged products are still written, just without
// cross-document LLM merging).
func defaultDeduperFactory(tenant string) (Deduper, error) {
deps, err := kccommon.ResolveDeps(tenant, defaultLLMID, defaultEmbedding)
if err != nil {
return nil, err
}
return NewLLMDeduper(deps.Chat, deps.Embed, defaultLLMID, 0.99, deps.LLMMaxLength), nil
}
func generateHolder() string {
b := make([]byte, 8)
if _, err := rand.Read(b); err != nil {
return fmt.Sprintf("knowledgecompile-%d", time.Now().UnixNano())
}
return "knowledgecompile-" + hex.EncodeToString(b)
}
// Provision initializes the dataset-level compile scheduling store and installs
// the package-level Scheduler used by the publishing path. It is called once by
// the owning Ingestor (which then drives the consumer from its own worker pool),
// so the dataset-level consumer shares the ingestor's lifecycle instead of
// being a standalone service. Best-effort: provisioning errors are returned so
// the caller can log and continue (the pipeline still writes available_int=0
// compiled chunks; they just won't be merged until a scheduler is available).
func Provision(ctx context.Context, mq engine.MessageQueue, db *gorm.DB) error {
if db == nil {
return nil
}
s := newScheduler(db, mq, generateHolder(), 2*time.Minute)
// Bound the startup AutoMigrate so a slow/unreachable DB cannot block
// startup indefinitely. The caller's ctx is also honoured (cancelled on
// shutdown).
ctx, cancel := context.WithTimeout(ctx, 30*time.Second)
defer cancel()
if err := s.Provision(ctx); err != nil {
return err
}
SetScheduler(s)
return nil
}