mirror of
https://github.com/infiniflow/ragflow.git
synced 2026-08-09 17:07:57 +08:00
refactor(chunker): replace allowBoundaryOverflow bool with MergeStrategy enum (#17851)
## Summary Follow-up to #17835 (merged). The OVER_CAP / UNDER_CAP merge strategy was threaded through `mergeDecision` and `mergeByTokenSizeFromJSON` as an inlined `allowBoundaryOverflow bool` derived from `!c.param.UnderCap` at three call sites. This replaces that with a named `schema.MergeStrategy` enum. ## Why - The `!c.param.UnderCap` inversion was hand-written in three places, so a future strategy addition could silently drift between the JSON path (`invokeTextPayload` / `invokeJSONPayload`) and the text path (`mergeByTokenSize`) — no compile error, and the existing tests don't cover all three sites with both strategies. - The strategy concept was never named; `allowBoundaryOverflow` (true = OVER_CAP) is a double-negation of `UnderCap` and reads opaquely at the 5th positional argument. ## What changed - Add `schema.MergeStrategy` (`MergeOverCap` / `MergeUnderCap`) mirroring Python's `rag/nlp/__init__.py` `MergeStrategy`, so Go and Python stay on the same vocabulary. - Expose `TokenChunkerParam.MergeStrategy()` derived from the wire-facing `UnderCap bool` (existing `"under_cap"` configs keep working — no schema break). - `mergeDecision` and `mergeByTokenSizeFromJSON` now take `schema.MergeStrategy` instead of `allowBoundaryOverflow bool`; the three call sites pass `c.param.MergeStrategy()` (no `!`). - Tests updated to pass the enum; added a guard test for the `UnderCap` -> `MergeStrategy` mapping and an end-to-end test for UNDER_CAP on the JSON path. No behavior change: default remains OVER_CAP, `under_cap=true` still selects UNDER_CAP. ## Test plan `bash build.sh --test ./internal/ingestion/component/chunker/... ./internal/ingestion/component/schema/...` — all green, including `TestMergeByTokenSizeFromJSON_UnderCapNoOverflow`, `TestMergeByTokenSize_UnderCapNoOverflow`, `TestInvokeJSONPayload_UnderCapEndToEnd`, and `TestTokenChunkerParamMergeStrategy`. ## Related issues - Relates to #17835 — wired UNDER_CAP as a tested merge-strategy seam (merged) - Relates to #17799 — contract doc for token-chunker cap/delimiter alignment - Relates to #17808 — related chunker alignment work --------- Co-authored-by: CodeBuddy <noreply@cnb.cool>
This commit is contained in:
@@ -334,7 +334,7 @@ func (c *TokenChunkerComponent) invokeTextPayload(_ context.Context, text string
|
||||
// Split-then-merge: split on delimiters, then greedily merge to
|
||||
// chunk_token_size with optional overlap.
|
||||
perItem := [][]schema.ChunkDoc{docs}
|
||||
merged := mergeByTokenSizeFromJSON(perItem, c.param.ChunkTokenSize, c.param.OverlappedPercent, true, !c.param.UnderCap)
|
||||
merged := mergeByTokenSizeFromJSON(perItem, c.param.ChunkTokenSize, c.param.OverlappedPercent, true, c.param.MergeStrategy())
|
||||
return chunkOutputs(flatten(merged))
|
||||
}
|
||||
|
||||
@@ -496,18 +496,21 @@ const (
|
||||
// is the separator used to project the joined text ("" for the text path, "\n"
|
||||
// for the JSON path). target is the token cap.
|
||||
//
|
||||
// allowBoundaryOverflow=true selects OVER_CAP (Python's default, canonical):
|
||||
// when the joined text exceeds target but the incoming unit still fits target,
|
||||
// it is merged into the previous chunk and that chunk is then closed
|
||||
// (mergeThenClose), forcing the next unit to start a new chunk. An incoming
|
||||
// unit that already exceeds target is never merged — it stands alone as its own
|
||||
// chunk (Python OVER_CAP: an oversized paragraph is never combined with the
|
||||
// previous chunk). allowBoundaryOverflow=false selects UNDER_CAP (strict
|
||||
// no-overflow): an overflowing joined text starts a new chunk instead.
|
||||
// strategy selects the merge strategy (schema.MergeStrategy), mirroring
|
||||
// Python's MergeStrategy:
|
||||
// - MergeOverCap (default, Python OVER_CAP): when the joined text exceeds
|
||||
// target but the incoming unit still fits target, it is merged into the
|
||||
// previous chunk and that chunk is then closed (mergeThenClose), forcing
|
||||
// the next unit to start a new chunk. An incoming unit that already exceeds
|
||||
// target is never merged — it stands alone as its own chunk (Python
|
||||
// OVER_CAP: an oversized paragraph is never combined with the previous
|
||||
// chunk).
|
||||
// - MergeUnderCap (Python UNDER_CAP, strict no-overflow): an overflowing
|
||||
// joined text starts a new chunk instead.
|
||||
//
|
||||
// JSON-only metadata (PDFPositions/Positions/TKNums) is the caller's
|
||||
// responsibility; this helper only returns the merged/new text and the action.
|
||||
func mergeDecision(prevText, incoming, joinSep string, target int, overlapPct float64, allowBoundaryOverflow bool) (string, mergeAction) {
|
||||
func mergeDecision(prevText, incoming, joinSep string, target int, overlapPct float64, strategy schema.MergeStrategy) (string, mergeAction) {
|
||||
incomingTokens := tokenizeStr(incoming)
|
||||
// An incoming unit that already exceeds target can never be merged; it
|
||||
// stands alone as its own chunk.
|
||||
@@ -518,7 +521,7 @@ func mergeDecision(prevText, incoming, joinSep string, target int, overlapPct fl
|
||||
if tokenizeStr(joined) <= target {
|
||||
return joined, mergeIntoPrev
|
||||
}
|
||||
if allowBoundaryOverflow {
|
||||
if strategy == schema.MergeOverCap {
|
||||
// OVER_CAP: merge the overflowing unit but close the chunk so the
|
||||
// next unit starts fresh.
|
||||
return joined, mergeThenClose
|
||||
@@ -593,7 +596,7 @@ func (c *TokenChunkerComponent) mergeByTokenSize(text string, childrenPattern *r
|
||||
tkns = append(tkns, tokenizeStr(out))
|
||||
return
|
||||
}
|
||||
out, act := mergeDecision(cks[len(cks)-1], segment, "", target, overlapPct, !c.param.UnderCap)
|
||||
out, act := mergeDecision(cks[len(cks)-1], segment, "", target, overlapPct, c.param.MergeStrategy())
|
||||
switch act {
|
||||
case mergeIntoPrev, mergeThenClose:
|
||||
cks[len(cks)-1] = out
|
||||
@@ -722,7 +725,7 @@ func (c *TokenChunkerComponent) invokeJSONPayload(ctx context.Context, items []s
|
||||
// chunks across JSON items into one global token budget. Flatten the
|
||||
// per-item structure into a single sequence first so the merge is
|
||||
// global; non-text chunks still break the merge via their CKType.
|
||||
attached = mergeByTokenSizeFromJSON([][]schema.ChunkDoc{flatten(attached)}, c.param.ChunkTokenSize, c.param.OverlappedPercent, false, !c.param.UnderCap)
|
||||
attached = mergeByTokenSizeFromJSON([][]schema.ChunkDoc{flatten(attached)}, c.param.ChunkTokenSize, c.param.OverlappedPercent, false, c.param.MergeStrategy())
|
||||
}
|
||||
|
||||
flat := flatten(attached)
|
||||
@@ -962,11 +965,12 @@ func takeFromStart(text string, tokens int) string {
|
||||
// Oversized text units are sub-split via splitOversizedUnit before merge;
|
||||
// overlap is applied only when overlap+segment still fits the budget.
|
||||
//
|
||||
// allowBoundaryOverflow selects the merge strategy: true = OVER_CAP (Python's
|
||||
// canonical default, a chunk may exceed the target by at most one incoming
|
||||
// unit), false = UNDER_CAP (never exceed the target; a projected overflow
|
||||
// starts a fresh chunk). The TokenChunker threads its UnderCap param here.
|
||||
func mergeByTokenSizeFromJSON(perItem [][]schema.ChunkDoc, chunkTokens int, overlappedPct float64, subSplitOversize bool, allowBoundaryOverflow bool) [][]schema.ChunkDoc {
|
||||
// strategy selects the merge strategy (schema.MergeStrategy): MergeOverCap =
|
||||
// OVER_CAP (Python's canonical default, a chunk may exceed the target by at
|
||||
// most one incoming unit), MergeUnderCap = UNDER_CAP (never exceed the target;
|
||||
// a projected overflow starts a fresh chunk). The TokenChunker threads its
|
||||
// MergeStrategy() here.
|
||||
func mergeByTokenSizeFromJSON(perItem [][]schema.ChunkDoc, chunkTokens int, overlappedPct float64, subSplitOversize bool, strategy schema.MergeStrategy) [][]schema.ChunkDoc {
|
||||
// overlappedPct is a [0,100] percentage. Clamp defensively because this
|
||||
// helper is also exercised directly by tests.
|
||||
if overlappedPct < 0 {
|
||||
@@ -1032,7 +1036,7 @@ func mergeByTokenSizeFromJSON(perItem [][]schema.ChunkDoc, chunkTokens int, over
|
||||
return
|
||||
}
|
||||
// Proactive projected-total merge (joined with "\n").
|
||||
out, act := mergeDecision(prev.Text, ck.Text, "\n", chunkTokens, overlappedPct, allowBoundaryOverflow)
|
||||
out, act := mergeDecision(prev.Text, ck.Text, "\n", chunkTokens, overlappedPct, strategy)
|
||||
switch act {
|
||||
case mergeIntoPrev, mergeThenClose:
|
||||
prev.Text = out
|
||||
|
||||
@@ -91,7 +91,7 @@ func TestMergeByTokenSizeFromJSON_OverlapStripsTags(t *testing.T) {
|
||||
{Text: cText, DocType: "text", CKType: "text", TKNums: intPtr(cN)},
|
||||
},
|
||||
}
|
||||
got := mergeByTokenSizeFromJSON(items, budget, 30.0, true, true)
|
||||
got := mergeByTokenSizeFromJSON(items, budget, 30.0, true, schema.MergeOverCap)
|
||||
merged := got[0]
|
||||
if len(merged) != 2 {
|
||||
t.Fatalf("want 2 chunks (overflow-closed + overlap chunk), got %d (a=%d b=%d c=%d budget=%d)", len(merged), aN, bN, cN, budget)
|
||||
@@ -157,7 +157,7 @@ func TestMergeByTokenSizeFromJSON_NonTextBoundaryResetsPrevClosed(t *testing.T)
|
||||
{Text: t4, DocType: "text", CKType: "text", TKNums: intPtr(tokenizeStr(t4))},
|
||||
},
|
||||
}
|
||||
got := mergeByTokenSizeFromJSON(items, budget, 0.0, true, true)
|
||||
got := mergeByTokenSizeFromJSON(items, budget, 0.0, true, schema.MergeOverCap)
|
||||
merged := got[0]
|
||||
// Expect: chunk0 (T1+T2, overflow-closed), N (non-text), chunk1 (T3+T4 merged).
|
||||
if len(merged) != 3 {
|
||||
@@ -183,7 +183,7 @@ func TestMergeByTokenSizeFromJSON_NonTextBoundaryResetsPrevClosed(t *testing.T)
|
||||
}
|
||||
|
||||
// TestMergeByTokenSizeFromJSON_UnderCapNoOverflow exercises the UNDER_CAP
|
||||
// strategy (allowBoundaryOverflow=false): a projected join that would exceed
|
||||
// strategy (schema.MergeUnderCap): a projected join that would exceed
|
||||
// the target must start a fresh chunk instead of merging-then-closing. This is
|
||||
// the seam that lets Go follow Python's no-overflow (UNDER_CAP) strategy. Under
|
||||
// OVER_CAP the same input merges a+b and overflows chunk0; here a, b, c must
|
||||
@@ -213,7 +213,7 @@ func TestMergeByTokenSizeFromJSON_UnderCapNoOverflow(t *testing.T) {
|
||||
{Text: cText, DocType: "text", CKType: "text", TKNums: intPtr(cN)},
|
||||
},
|
||||
}
|
||||
got := mergeByTokenSizeFromJSON(items, budget, 0.0, true, false)
|
||||
got := mergeByTokenSizeFromJSON(items, budget, 0.0, true, schema.MergeUnderCap)
|
||||
merged := got[0]
|
||||
if len(merged) != 3 {
|
||||
t.Fatalf("UNDER_CAP want 3 chunks (a, b, c separate), got %d", len(merged))
|
||||
@@ -255,12 +255,12 @@ func clampOverlapFixture() [][]schema.ChunkDoc {
|
||||
}
|
||||
|
||||
func TestMergeByTokenSizeFromJSON_ClampsOverlappedPct(t *testing.T) {
|
||||
at100 := mergeByTokenSizeFromJSON(clampOverlapFixture(), 128, 100, true, true)
|
||||
at100 := mergeByTokenSizeFromJSON(clampOverlapFixture(), 128, 100, true, schema.MergeOverCap)
|
||||
if at100 == nil || len(at100) == 0 {
|
||||
t.Fatalf("overlappedPct=100: nil/empty result")
|
||||
}
|
||||
at150 := mergeByTokenSizeFromJSON(clampOverlapFixture(), 128, 150, true, true)
|
||||
atHuge := mergeByTokenSizeFromJSON(clampOverlapFixture(), 128, 1e300, true, true)
|
||||
at150 := mergeByTokenSizeFromJSON(clampOverlapFixture(), 128, 150, true, schema.MergeOverCap)
|
||||
atHuge := mergeByTokenSizeFromJSON(clampOverlapFixture(), 128, 1e300, true, schema.MergeOverCap)
|
||||
if !reflect.DeepEqual(at100, at150) {
|
||||
t.Errorf("overlappedPct=150 should clamp to 100; output differs from 100")
|
||||
}
|
||||
@@ -268,12 +268,12 @@ func TestMergeByTokenSizeFromJSON_ClampsOverlappedPct(t *testing.T) {
|
||||
t.Errorf("overlappedPct=1e300 should clamp to 100; output differs from 100")
|
||||
}
|
||||
|
||||
at0 := mergeByTokenSizeFromJSON(clampOverlapFixture(), 128, 0, true, true)
|
||||
at0 := mergeByTokenSizeFromJSON(clampOverlapFixture(), 128, 0, true, schema.MergeOverCap)
|
||||
if at0 == nil || len(at0) == 0 {
|
||||
t.Fatalf("overlappedPct=0: nil/empty result")
|
||||
}
|
||||
atNeg := mergeByTokenSizeFromJSON(clampOverlapFixture(), 128, -5, true, true)
|
||||
atNegHuge := mergeByTokenSizeFromJSON(clampOverlapFixture(), 128, -1e300, true, true)
|
||||
atNeg := mergeByTokenSizeFromJSON(clampOverlapFixture(), 128, -5, true, schema.MergeOverCap)
|
||||
atNegHuge := mergeByTokenSizeFromJSON(clampOverlapFixture(), 128, -1e300, true, schema.MergeOverCap)
|
||||
if !reflect.DeepEqual(at0, atNeg) {
|
||||
t.Errorf("overlappedPct=-5 should clamp to 0; output differs from 0")
|
||||
}
|
||||
@@ -294,7 +294,7 @@ func TestMergeByTokenSizeFromJSON_EmptyPrevKeepsChunk(t *testing.T) {
|
||||
{Text: "keepme", DocType: "text", CKType: "text", TKNums: intPtr(5)},
|
||||
},
|
||||
}
|
||||
got := mergeByTokenSizeFromJSON(items, 128, 0, true, true)
|
||||
got := mergeByTokenSizeFromJSON(items, 128, 0, true, schema.MergeOverCap)
|
||||
merged := got[0]
|
||||
if len(merged) != 1 {
|
||||
t.Fatalf("want 1 merged chunk, got %d", len(merged))
|
||||
|
||||
@@ -38,7 +38,7 @@ func TestMergeByTokenSizeFromJSON_ExtendsPDFPositions(t *testing.T) {
|
||||
{Text: "beta", DocType: "text", CKType: "text", TKNums: intPtr(5), PDFPositions: posB},
|
||||
},
|
||||
}
|
||||
got := mergeByTokenSizeFromJSON(items, 128, 0, true, true)
|
||||
got := mergeByTokenSizeFromJSON(items, 128, 0, true, schema.MergeOverCap)
|
||||
merged := got[0]
|
||||
if len(merged) != 1 {
|
||||
t.Fatalf("want 1 merged chunk, got %d", len(merged))
|
||||
@@ -63,7 +63,7 @@ func TestMergeByTokenSizeFromJSON_ExtendsPositions(t *testing.T) {
|
||||
{Text: "b", DocType: "text", CKType: "text", TKNums: intPtr(5), Positions: posB},
|
||||
},
|
||||
}
|
||||
got := mergeByTokenSizeFromJSON(items, 128, 0, true, true)
|
||||
got := mergeByTokenSizeFromJSON(items, 128, 0, true, schema.MergeOverCap)
|
||||
combined := string(got[0][0].Positions)
|
||||
if !strings.Contains(combined, "1,2,3") || !strings.Contains(combined, "4,5,6") {
|
||||
t.Errorf("merged chunk dropped/omitted `positions`: %s", combined)
|
||||
@@ -103,7 +103,7 @@ func TestMergeByTokenSizeFromJSON_PositionsDecodeToMatrix(t *testing.T) {
|
||||
{Text: "b", DocType: "text", CKType: "text", TKNums: intPtr(5), Positions: posB},
|
||||
},
|
||||
}
|
||||
got := mergeByTokenSizeFromJSON(items, 128, 0, true, true)
|
||||
got := mergeByTokenSizeFromJSON(items, 128, 0, true, schema.MergeOverCap)
|
||||
m := got[0][0].ToMap()
|
||||
raw, ok := m["positions"]
|
||||
if !ok {
|
||||
|
||||
@@ -108,7 +108,7 @@ func TestMergeByTokenSizeFromJSON_StrictCapNoOvershoot(t *testing.T) {
|
||||
Text: text, DocType: "text", CKType: "text", TKNums: intPtr(tokenizeStr(text)),
|
||||
})
|
||||
}
|
||||
got := mergeByTokenSizeFromJSON([][]schema.ChunkDoc{sections}, budget, 0, true, true)
|
||||
got := mergeByTokenSizeFromJSON([][]schema.ChunkDoc{sections}, budget, 0, true, schema.MergeOverCap)
|
||||
merged := got[0]
|
||||
if len(merged) < 3 {
|
||||
t.Fatalf("want >=3 chunks, got %d", len(merged))
|
||||
@@ -135,7 +135,7 @@ func TestMergeByTokenSizeFromJSON_OverlapDroppedAtOverflow(t *testing.T) {
|
||||
Text: text, DocType: "text", CKType: "text", TKNums: intPtr(tokenizeStr(text)),
|
||||
})
|
||||
}
|
||||
got := mergeByTokenSizeFromJSON([][]schema.ChunkDoc{sections}, budget, 20, true, true)
|
||||
got := mergeByTokenSizeFromJSON([][]schema.ChunkDoc{sections}, budget, 20, true, schema.MergeOverCap)
|
||||
unit := tokenizeStr(sections[0].Text)
|
||||
for i, ck := range got[0] {
|
||||
// OVER_CAP allows one boundary overflow (prev + one unit). The JSON
|
||||
@@ -157,7 +157,7 @@ func TestMergeByTokenSizeFromJSON_OversizedUnitIsSubSplit(t *testing.T) {
|
||||
items := [][]schema.ChunkDoc{{
|
||||
{Text: long, DocType: "text", CKType: "text", TKNums: intPtr(tokenizeStr(long))},
|
||||
}}
|
||||
got := mergeByTokenSizeFromJSON(items, budget, 0, true, true)
|
||||
got := mergeByTokenSizeFromJSON(items, budget, 0, true, schema.MergeOverCap)
|
||||
if len(got[0]) < 2 {
|
||||
t.Fatalf("oversized unit must yield multiple chunks, got %d", len(got[0]))
|
||||
}
|
||||
@@ -350,3 +350,76 @@ func TestInvokeTextPayload_StrictCapEndToEnd(t *testing.T) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TestInvokeJSONPayload_UnderCapEndToEnd exercises the UNDER_CAP strategy on
|
||||
// the JSON path end-to-end (invokeJSONPayload). Many single-line JSON items
|
||||
// are flattened into one global merge sequence, and under_cap=true must keep
|
||||
// every chunk within chunk_token_size. The OVER_CAP control is asserted to
|
||||
// pack fewer chunks than UNDER_CAP, proving the toggle is live on the JSON
|
||||
// path — not just the text path covered by TestInvokeTextPayload_StrictCapEndToEnd.
|
||||
func TestInvokeJSONPayload_UnderCapEndToEnd(t *testing.T) {
|
||||
const budget = 32
|
||||
unit := tokenizeStr(strings.TrimSpace(strings.Repeat("alpha ", 12)))
|
||||
|
||||
// Each item is one ~unit-sized token block; 24 items give the global
|
||||
// merge plenty to accumulate.
|
||||
var items []map[string]any
|
||||
for i := 0; i < 24; i++ {
|
||||
items = append(items, map[string]any{
|
||||
"text": strings.TrimSpace(strings.Repeat("alpha ", 12)),
|
||||
"doc_type_kwd": "text",
|
||||
})
|
||||
}
|
||||
|
||||
run := func(underCap bool) []map[string]any {
|
||||
comp, err := NewTokenChunker(map[string]any{
|
||||
"delimiter_mode": "delimiter",
|
||||
"delimiters": []string{"\n"},
|
||||
"chunk_token_size": budget,
|
||||
"under_cap": underCap,
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("NewTokenChunker: %v", err)
|
||||
}
|
||||
out, err := comp.Invoke(context.Background(), nil, map[string]any{
|
||||
"name": "doc.json",
|
||||
"output_format": "json",
|
||||
"json": items,
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("Invoke: %v", err)
|
||||
}
|
||||
if errMsg, _ := out["_ERROR"].(string); errMsg != "" {
|
||||
t.Fatalf("Invoke error payload: %s", errMsg)
|
||||
}
|
||||
chunks, _ := out["chunks"].([]map[string]any)
|
||||
return chunks
|
||||
}
|
||||
|
||||
// UNDER_CAP: no chunk may exceed the token target.
|
||||
under := run(true)
|
||||
if len(under) < 2 {
|
||||
t.Fatalf("UNDER_CAP: want multiple chunks, got %d", len(under))
|
||||
}
|
||||
for i, ck := range under {
|
||||
text, _ := ck["text"].(string)
|
||||
if n := tokenizeStr(text); n > budget {
|
||||
t.Errorf("UNDER_CAP chunk %d exceeds target: tokens=%d (cap=%d)", i, n, budget)
|
||||
}
|
||||
}
|
||||
|
||||
// OVER_CAP control: the same input packs fewer chunks (it allows one
|
||||
// boundary overflow per chunk), proving the toggle is live on the JSON
|
||||
// path. Equal lengths would mean under_cap is a no-op here.
|
||||
over := run(false)
|
||||
if len(over) >= len(under) {
|
||||
t.Errorf("OVER_CAP should pack fewer chunks than UNDER_CAP (over=%d under=%d); toggle may be a no-op on the JSON path", len(over), len(under))
|
||||
}
|
||||
for i, ck := range over {
|
||||
text, _ := ck["text"].(string)
|
||||
// OVER_CAP may overflow by at most one unit.
|
||||
if n := tokenizeStr(text); n > budget+unit {
|
||||
t.Errorf("OVER_CAP chunk %d overflows by more than one unit: tokens=%d (cap=%d unit=%d)", i, n, budget, unit)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -211,11 +211,38 @@ type TokenChunkerParam struct {
|
||||
// (a boundary overflow then closes the chunk).
|
||||
// - true: UNDER_CAP — strictly never exceed the target; when the
|
||||
// projected join would overflow, start a fresh chunk instead.
|
||||
// This is the seam that lets Go follow Python's no-overflow strategy
|
||||
// without changing the default behavior.
|
||||
// This is the wire-facing bool; the active strategy is exposed as
|
||||
// MergeStrategy so callers never have to invert it.
|
||||
UnderCap bool `json:"under_cap"`
|
||||
}
|
||||
|
||||
// MergeStrategy selects how the TokenChunker greedily accumulates adjacent
|
||||
// units into a chunk. It mirrors Python's rag/nlp/__init__.py MergeStrategy so
|
||||
// the Go and Python implementations stay on the same vocabulary (OVER_CAP /
|
||||
// UNDER_CAP) instead of a bare inlined bool.
|
||||
type MergeStrategy int
|
||||
|
||||
const (
|
||||
// MergeOverCap is the canonical default (Python OVER_CAP): a chunk may
|
||||
// exceed the token target by at most one incoming unit (a boundary
|
||||
// overflow then closes the chunk).
|
||||
MergeOverCap MergeStrategy = iota
|
||||
// MergeUnderCap never exceeds the target; when the projected join would
|
||||
// overflow, a fresh chunk is started instead (Python UNDER_CAP).
|
||||
MergeUnderCap
|
||||
)
|
||||
|
||||
// MergeStrategy reports the active merge strategy for this param. It is derived
|
||||
// from UnderCap so existing configs that set "under_cap" keep working without a
|
||||
// schema break, and callers read the strategy directly instead of inverting a
|
||||
// bool at every call site.
|
||||
func (p TokenChunkerParam) MergeStrategy() MergeStrategy {
|
||||
if p.UnderCap {
|
||||
return MergeUnderCap
|
||||
}
|
||||
return MergeOverCap
|
||||
}
|
||||
|
||||
// Defaults returns the Python default TokenChunkerParam.
|
||||
func (TokenChunkerParam) Defaults() TokenChunkerParam {
|
||||
return TokenChunkerParam{
|
||||
|
||||
@@ -567,3 +567,15 @@ func TestExtractorOutputsJSONRoundTrip(t *testing.T) {
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
func ptrString(s string) *string { return &s }
|
||||
|
||||
// TestTokenChunkerParamMergeStrategy locks the UnderCap bool -> MergeStrategy
|
||||
// mapping so a future refactor cannot silently flip OVER_CAP/UNDER_CAP without
|
||||
// a failing test.
|
||||
func TestTokenChunkerParamMergeStrategy(t *testing.T) {
|
||||
if got := (TokenChunkerParam{}).MergeStrategy(); got != MergeOverCap {
|
||||
t.Errorf("default (UnderCap=false) want MergeOverCap, got %v", got)
|
||||
}
|
||||
if got := (TokenChunkerParam{UnderCap: true}).MergeStrategy(); got != MergeUnderCap {
|
||||
t.Errorf("UnderCap=true want MergeUnderCap, got %v", got)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user