Files
ragflow/internal/ingestion/component/chunker/one_test.go
Jack 9b0719fa94 fix: Go ingestion migration batch 5 (Parser 1.1/1.7/2.11, Chunker 1.7/1.8/2.6/2.7, Tokenizer 6x fixes) (#17419)
## Summary

Continuation of the Python→Go ingestion pipeline migration (File →
Parser → Chunker → Extractor → Tokenizer). Fixes cover Parser, Chunker,
and Tokenizer gaps identified. Fix page number (0-indexed and 1-index
mixed before fix; use 1-indexed after fix) and chunk order issues.

### Parser
- **Slides TCADP (1.7):** `pptx_tcadp.go` + TCADP branch in
`pptx_parser.go`/`ppt_parser.go` — PowerPoint files now support
`parse_method="tcadp"` via the TCADP cloud service, matching the
spreadsheet-family TCADP pattern. PPT containers pass `"PPT"` as
fileType (not hardcoded `"PPTX"`).
- **Audio default output_format (2.11):** `defaultSetups()` audio
default changed from `"text"` to `"json"`, aligning with Python
`parser.py:232` and `AllowedOutputFormat["audio"]={"json"}`.
- **PDF VLM enhancement (1.1):** `maybeDispatchPDFVisionEnhancement` in
`pdf_vision_dispatch.go` enriches image/table items with IMAGE2TEXT
model descriptions after PDF parsing, mirroring Python
`enhance_media_sections_with_vision`. Semaphore fix: acquire before
goroutine start to prevent unbounded goroutine creation.
- **json family (2.3):** reclassified as Keep Go — `json_parser.go` is a
functional enhancement, not a parity gap.
- **page number:** changed from "mixed use of 1-indexed & 0-indexed" to
"1-indexed"

### Chunker
- **BULLET_PATTERN fallback (1.7):** 4th-level fallback in
`resolveTitleLevels` (`title.go`) detects bullet/numbered-list patterns
(Chinese legal, numbering, English) when outline + regex levels produce
only bodyLevel. Guarded by `allBodyLevel` to never override existing
structure.
- **Tag/One chunker fields (1.8):** `tag.go` sets `TopInt` from source
row index; `one.go` preserves `Positions`/`PDFPositions` from source
items. TSV multi-line RowNum fix: tracks `contentStart` for correct row
attribution.
- **Overlapped_percent normalization (2.6):**
`NormalizeOverlappedPercent` in `schema/chunker.go` mirrors Python
`common/float_utils.py:50-58` — accepts `[0,1)` fraction or `[0,90]`
percent, normalizes to canonical `[0,90]`.
- **Paragraph splitting (2.7):** aligned to Python flow `naive_merge` —
`CRLF` normalization, `splitKeepingDelimiter` preserves sentence
delimiters, single-section merge with token-budget-governed chunking.
- **chunk order:** sort by reading order

### Tokenizer
- **Phantom chunk filtering (Omission 2):** `isPhantomChunk` + filter
loop in `chunksFromTokenizerUpstream` skips zero-value ChunkDocs.
- **Batch size env var (Omission 3):** `embeddingBatchSize()` reads
`TOKENIZER_EMBEDDING_BATCH_SIZE`, defaults to 16.
- **Summary empty check (Diff 5):** `TrimSpace(s) != ""` → `s != ""`,
matching Python truthy check.
- **chunk_order_int all paths (Diff 8):** set unconditionally before
full_text/embedding branching.
- **Timeout default (Diff 10):** `600s` → `60s`, matching Python
`@timeout(60)`.
- **Small maxTokens truncation (Diff 14):** `truncateForEmbedding`
returns `""` when `maxTokens <= 10`, matching Python.

### Code review fixes
- Semaphore acquire moved before goroutine in `pdf_vision_dispatch.go`
(concurrency control)
- Context propagation in `pptx_tcadp.go` (cancellation support)
- Test resolver leak fix in `media_dispatch_test.go` (defer restore)
- Migration history comments removed per AGENTS.md

## Test plan
```
bash build.sh --test ./internal/parser/parser/... ./internal/ingestion/component/...
```

## Notes
- Migration diff tracking: `docs/migration_python_go_diff.md`
- Remaining gaps: Extractor component only (21 items)
2026-07-28 11:12:52 +08:00

144 lines
4.7 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 chunker
import (
"context"
"testing"
)
// oneChunksOf drives OneChunker.Invoke and returns the emitted chunk maps.
func oneChunksOf(t *testing.T, inputs map[string]any) []map[string]any {
t.Helper()
comp, err := NewOneChunker(nil)
if err != nil {
t.Fatalf("NewOneChunker: %v", err)
}
out, err := comp.Invoke(context.Background(), nil, inputs)
if err != nil {
t.Fatalf("OneChunker.Invoke: %v", err)
}
chunks, ok := out["chunks"].([]map[string]any)
if !ok {
t.Fatalf("chunks not []map[string]any: %T", out["chunks"])
}
return chunks
}
// TestOneChunker_Text emits exactly one chunk for a text payload,
// faithful to rag/app/one.py (whole file = one chunk).
func TestOneChunker_Text(t *testing.T) {
chunks := oneChunksOf(t, map[string]any{
"name": "doc.txt",
"output_format": "text",
"text": "first paragraph\n\nsecond paragraph",
})
if len(chunks) != 1 {
t.Fatalf("want 1 chunk, got %d", len(chunks))
}
if got := chunks[0]["text"]; got != "first paragraph\n\nsecond paragraph" {
t.Errorf("text = %q", got)
}
}
// TestOneChunker_JSONSingleItem carries the image/media context through
// for the picture/audio methods, which TokenChunker in "one" mode drops.
func TestOneChunker_JSONSingleItem(t *testing.T) {
chunks := oneChunksOf(t, map[string]any{
"name": "pic.png",
"output_format": "json",
"json": []map[string]any{
{"text": "a cat sitting on a mat", "image": "data:image/png;base64,AAAA", "doc_type_kwd": "image"},
},
})
if len(chunks) != 1 {
t.Fatalf("want 1 chunk, got %d", len(chunks))
}
if got := chunks[0]["image"]; got != "data:image/png;base64,AAAA" {
t.Errorf("image = %q, want preserved media context", got)
}
if got := chunks[0]["text"]; got != "a cat sitting on a mat" {
t.Errorf("text = %q", got)
}
}
// TestOneChunker_JSONMultipleMerges collapses many upstream items into a
// single chunk, preserving the first available image (picture/audio
// one-chunk-per-file behavior).
func TestOneChunker_JSONMultipleMerges(t *testing.T) {
chunks := oneChunksOf(t, map[string]any{
"name": "clip.mp4",
"output_format": "json",
"json": []map[string]any{
{"text": "frame one transcript"},
{"text": "frame two transcript", "image": "data:image/png;base64,BBBB"},
},
})
if len(chunks) != 1 {
t.Fatalf("want 1 chunk, got %d", len(chunks))
}
if got := chunks[0]["text"]; got != "frame one transcript\nframe two transcript" {
t.Errorf("merged text = %q", got)
}
if got := chunks[0]["image"]; got != "data:image/png;base64,BBBB" {
t.Errorf("image = %q, want first available media context", got)
}
}
// TestOneChunker_PreservesPositions verifies that when a single upstream
// item carries PDF coordinates, the OneChunker preserves both Positions
// and PDFPositions (with their coordinate values) on the output chunk.
func TestOneChunker_PreservesPositions(t *testing.T) {
chunks := oneChunksOf(t, map[string]any{
"name": "page.pdf",
"output_format": "json",
"json": []map[string]any{
{
"text": "page text",
"positions": []any{[]any{10.0, 20.0, 30.0, 40.0}},
"_pdf_positions": []any{[]any{1.0, 2.0, 3.0, 4.0, 5.0}},
},
},
})
if len(chunks) != 1 {
t.Fatalf("want 1 chunk, got %d", len(chunks))
}
assertCoordTuple(t, "positions", chunks[0]["positions"], []float64{10.0, 20.0, 30.0, 40.0})
assertCoordTuple(t, "_pdf_positions", chunks[0]["_pdf_positions"], []float64{1.0, 2.0, 3.0, 4.0, 5.0})
}
// assertCoordTuple verifies a positions/_pdf_positions field round-tripped
// as a [][]float64 with the expected single-row coordinate tuple.
func assertCoordTuple(t *testing.T, key string, got any, want []float64) {
t.Helper()
rows, ok := got.([][]float64)
if !ok {
t.Fatalf("%s = %v, want [][]float64 (got %T)", key, got, got)
}
if len(rows) != 1 {
t.Fatalf("%s has %d rows, want 1", key, len(rows))
}
if len(rows[0]) != len(want) {
t.Fatalf("%s[0] = %v, want %v (len %d vs %d)", key, rows[0], want, len(rows[0]), len(want))
}
for i, w := range want {
if rows[0][i] != w {
t.Errorf("%s[0][%d] = %v, want %v", key, i, rows[0][i], w)
}
}
}