2026-07-05 20:43:52 +08:00
|
|
|
//
|
|
|
|
|
// 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.
|
|
|
|
|
//
|
|
|
|
|
|
|
|
|
|
// Tokenizer ingestion component (Phase 2.4 of
|
|
|
|
|
// port-rag-flow-pipeline-to-go.md §4). Port of Python
|
|
|
|
|
// `rag/flow/tokenizer/tokenizer.py`. Computes (a) full-text token
|
|
|
|
|
// counts via the Go tokenizer package and (b) embedding vectors via
|
|
|
|
|
// the tenant's embedding model.
|
|
|
|
|
//
|
|
|
|
|
// SCOPE (honest):
|
|
|
|
|
//
|
|
|
|
|
// - TOKEN COUNTING: matched at the wire level. Each chunk gets
|
|
|
|
|
// `content_ltks` (tokenized string via `tokenizer.Tokenize`) and
|
|
|
|
|
// `content_sm_ltks` (fine-grained variant) when `search_method`
|
|
|
|
|
// includes `full_text`. `title_tks` / `title_sm_tks` mirror the
|
|
|
|
|
// upstream `name` field. Python uses C++ RAGAnalyzer via
|
|
|
|
|
// `rag_tokenizer`; the Go side goes through `internal/tokenizer`
|
|
|
|
|
// which itself calls into the same C++ binding (`internal/binding`).
|
|
|
|
|
// For non-ASCII (CJK) input, Python's `rag_tokenizer.tokenize`
|
|
|
|
|
// falls back gracefully; the Go path uses the CGo analyzer
|
|
|
|
|
// when initialized, otherwise an empty string — see
|
|
|
|
|
// `internal/tokenizer/tokenizer.go:Tokenize` (Infinity engine
|
|
|
|
|
// returns input unchanged; otherwise the C++ binding is used).
|
|
|
|
|
//
|
|
|
|
|
// - EMBEDDING MODEL RESOLUTION: mirrored. Python uses
|
|
|
|
|
// `LLMBundle(tenant_id, embd_id).encode([...])` from
|
|
|
|
|
// `rag/flow/tokenizer/tokenizer.py:54-66`; the Go port goes
|
|
|
|
|
// through `service.ModelProviderService.GetEmbeddingModel`
|
2026-07-10 15:46:45 +08:00
|
|
|
// (callers inject the resolver, see `DefaultEmbedderResolver`).
|
2026-07-05 20:43:52 +08:00
|
|
|
// The component does NOT directly construct a model driver —
|
|
|
|
|
// the resolution path depends on tenant/DAO context that lives
|
|
|
|
|
// in `internal/service`, and importing `internal/service` from
|
|
|
|
|
// `internal/ingestion/component` would invert the dependency
|
|
|
|
|
// direction (plan §3 import graph: ingestion → agent/runtime
|
2026-07-10 15:46:45 +08:00
|
|
|
// only). The injection point is `DefaultEmbedderResolver`
|
|
|
|
|
// (package-level var); the ingestion task package wires it in
|
|
|
|
|
// its init() and tests inject a stub via the test-only
|
|
|
|
|
// NewTokenizerComponentWithResolver. When no resolver is
|
|
|
|
|
// available the component short-circuits the embedding branch
|
|
|
|
|
// with a clear error — the same fail-loud contract the Python
|
|
|
|
|
// side enforces via `LLMBundle` constructor.
|
2026-07-05 20:43:52 +08:00
|
|
|
//
|
|
|
|
|
// - BATCHED EMBEDDING (plan §AD-5a): matched. The Python path
|
|
|
|
|
// chunks calls by `settings.EMBEDDING_BATCH_SIZE` (default 16)
|
|
|
|
|
// and uses an async semaphore (`embed_limiter`). The Go port
|
|
|
|
|
// issues ONE `Encode([]string)` call with the entire chunk
|
2026-07-13 13:51:40 +08:00
|
|
|
// list (AD-5a calls out "embedding calls batched, not fanned").
|
|
|
|
|
// Drivers that need to chunk internally can do so — the wire
|
|
|
|
|
// call is one round-trip.
|
2026-07-05 20:43:52 +08:00
|
|
|
//
|
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
|
|
|
// - TRACKING: TrackProgress, TrackElapsed. See
|
|
|
|
|
// `internal/agent/runtime/helpers.go` (plan §1 Phase 1).
|
2026-07-05 20:43:52 +08:00
|
|
|
// `internal/agent/runtime/helpers.go` (plan §1 Phase 1).
|
|
|
|
|
//
|
|
|
|
|
// - WHAT IS NOT PORTED:
|
|
|
|
|
//
|
|
|
|
|
// - The python `finalize_pdf_chunk` post-step — that
|
|
|
|
|
// normalizes PDF bbox metadata; it lives in
|
|
|
|
|
// `rag/flow/parser/pdf_chunk_metadata.py` and is the Parser
|
|
|
|
|
// component's concern (Phase 2.2).
|
|
|
|
|
//
|
|
|
|
|
// - `rag.flow.tokenizer` `thread_pool_exec` async batching +
|
|
|
|
|
// `embed_limiter` semaphore — replaced by the single
|
|
|
|
|
// batched `Encode` call.
|
|
|
|
|
package component
|
|
|
|
|
|
|
|
|
|
import (
|
|
|
|
|
"context"
|
|
|
|
|
"encoding/json"
|
|
|
|
|
"fmt"
|
2026-07-09 16:24:39 +08:00
|
|
|
"log"
|
2026-07-13 16:32:34 +08:00
|
|
|
"os"
|
2026-07-05 20:43:52 +08:00
|
|
|
"regexp"
|
2026-07-09 16:24:39 +08:00
|
|
|
"slices"
|
2026-07-13 16:32:34 +08:00
|
|
|
"strconv"
|
2026-07-05 20:43:52 +08:00
|
|
|
"strings"
|
|
|
|
|
|
fix: docx/email parsing, extractor LLM driver, and chunker alignment (#17144)
## Summary
Three groups of changes across the Go ingestion pipeline:
### 1. DOCX parsing improvements
- **docx_parser.go**: Enhanced DOCX parsing with better structure
extraction and media handling
- **docx_parser_cgo_test.go**, **docx_parser_test.go**: Companion tests
- **office_parsers_no_cgo.go**: Stub sync for non-CGO builds
### 2. Email (.eml) parsing: base64 Content-Transfer-Encoding decoding
- **email_parser.go** (`decodeCTE`): Added Content-Transfer-Encoding
decoding for base64 and quoted-printable. Go's `mime/multipart.Reader`
does not decode Content-Transfer-Encoding automatically, so attachments
with `Content-Transfer-Encoding: base64` remained base64-encoded in the
output. The new `decodeCTE` helper is called after reading each
multipart part's raw bytes in `readMailBody`, mirroring Python's
`part.get_payload(decode=True)`.
- **email_parser_test.go**: Two new tests — simple base64 attachment and
nested multipart/alternative with base64 attachment.
### 3. Extractor LLM driver fix + ModelDriver consolidation
- **extractor.go**: Fixed a bug where the Extractor component used
`ModelFactory.CreateModelDriver()`, which creates bare model instances
without API keys or provider configuration. Switched to
`models.GetPreconfiguredDriver()` which resolves the actual
pre-configured driver from `ProviderManager`, matching the codepath used
by `llm.go`. This fixes auto keyword/question extraction in DSL
pipelines that require LLM calls.
- **get_driver.go** (new): Extracted shared `GetPreconfiguredDriver()`
from `llm.go:newChatModelDriver()` so both `llm.go` and `extractor.go`
use the same codepath.
- **get_driver_test.go** (new): Tests for the shared driver resolution.
- **llm.go**: Replaced inline driver resolution with
`models.GetPreconfiguredDriver()`.
### 4. Chunker fixes and observability
- **group.go** (`extractLineRecords`): Fixed to also read `markdown` and
`html` payload keys — previously it only read `text`/`content`, causing
GroupTitleChunker to silently return empty results for markdown-format
parser output.
- **common.go** (`compileDelimPattern`): Aligned with Python's
`_compile_delimiter_pattern` — only backtick-wrapped delimiters produce
an active regex pattern; plain delimiters are not compiled into the
split regex.
- **token.go** (`applyChildrenDelim`): Set `DocType` and `CKType` to
`"text"` on created ChunkDocs so the token-size merge path correctly
identifies and merges text segments.
- **parser.go**, **extractor.go**, **tokenizer.go**, **group.go**,
**hierarchy.go**: Added debug-level logging for pipeline diagnostics.
- **parser_dispatch_test.go**, **group_test.go**: New tests.
## Verification
- All Go tests pass: `bash build.sh --test ./internal/parser/parser/...`
and `bash build.sh --test ./internal/ingestion/component/...`
- Build succeeds: `bash build.sh --go`
2026-07-21 13:51:17 +08:00
|
|
|
"go.uber.org/zap"
|
2026-07-27 10:20:16 +08:00
|
|
|
"gorm.io/gorm"
|
fix: docx/email parsing, extractor LLM driver, and chunker alignment (#17144)
## Summary
Three groups of changes across the Go ingestion pipeline:
### 1. DOCX parsing improvements
- **docx_parser.go**: Enhanced DOCX parsing with better structure
extraction and media handling
- **docx_parser_cgo_test.go**, **docx_parser_test.go**: Companion tests
- **office_parsers_no_cgo.go**: Stub sync for non-CGO builds
### 2. Email (.eml) parsing: base64 Content-Transfer-Encoding decoding
- **email_parser.go** (`decodeCTE`): Added Content-Transfer-Encoding
decoding for base64 and quoted-printable. Go's `mime/multipart.Reader`
does not decode Content-Transfer-Encoding automatically, so attachments
with `Content-Transfer-Encoding: base64` remained base64-encoded in the
output. The new `decodeCTE` helper is called after reading each
multipart part's raw bytes in `readMailBody`, mirroring Python's
`part.get_payload(decode=True)`.
- **email_parser_test.go**: Two new tests — simple base64 attachment and
nested multipart/alternative with base64 attachment.
### 3. Extractor LLM driver fix + ModelDriver consolidation
- **extractor.go**: Fixed a bug where the Extractor component used
`ModelFactory.CreateModelDriver()`, which creates bare model instances
without API keys or provider configuration. Switched to
`models.GetPreconfiguredDriver()` which resolves the actual
pre-configured driver from `ProviderManager`, matching the codepath used
by `llm.go`. This fixes auto keyword/question extraction in DSL
pipelines that require LLM calls.
- **get_driver.go** (new): Extracted shared `GetPreconfiguredDriver()`
from `llm.go:newChatModelDriver()` so both `llm.go` and `extractor.go`
use the same codepath.
- **get_driver_test.go** (new): Tests for the shared driver resolution.
- **llm.go**: Replaced inline driver resolution with
`models.GetPreconfiguredDriver()`.
### 4. Chunker fixes and observability
- **group.go** (`extractLineRecords`): Fixed to also read `markdown` and
`html` payload keys — previously it only read `text`/`content`, causing
GroupTitleChunker to silently return empty results for markdown-format
parser output.
- **common.go** (`compileDelimPattern`): Aligned with Python's
`_compile_delimiter_pattern` — only backtick-wrapped delimiters produce
an active regex pattern; plain delimiters are not compiled into the
split regex.
- **token.go** (`applyChildrenDelim`): Set `DocType` and `CKType` to
`"text"` on created ChunkDocs so the token-size merge path correctly
identifies and merges text segments.
- **parser.go**, **extractor.go**, **tokenizer.go**, **group.go**,
**hierarchy.go**: Added debug-level logging for pipeline diagnostics.
- **parser_dispatch_test.go**, **group_test.go**: New tests.
## Verification
- All Go tests pass: `bash build.sh --test ./internal/parser/parser/...`
and `bash build.sh --test ./internal/ingestion/component/...`
- Build succeeds: `bash build.sh --go`
2026-07-21 13:51:17 +08:00
|
|
|
|
2026-07-05 20:43:52 +08:00
|
|
|
"ragflow/internal/agent/runtime"
|
fix: docx/email parsing, extractor LLM driver, and chunker alignment (#17144)
## Summary
Three groups of changes across the Go ingestion pipeline:
### 1. DOCX parsing improvements
- **docx_parser.go**: Enhanced DOCX parsing with better structure
extraction and media handling
- **docx_parser_cgo_test.go**, **docx_parser_test.go**: Companion tests
- **office_parsers_no_cgo.go**: Stub sync for non-CGO builds
### 2. Email (.eml) parsing: base64 Content-Transfer-Encoding decoding
- **email_parser.go** (`decodeCTE`): Added Content-Transfer-Encoding
decoding for base64 and quoted-printable. Go's `mime/multipart.Reader`
does not decode Content-Transfer-Encoding automatically, so attachments
with `Content-Transfer-Encoding: base64` remained base64-encoded in the
output. The new `decodeCTE` helper is called after reading each
multipart part's raw bytes in `readMailBody`, mirroring Python's
`part.get_payload(decode=True)`.
- **email_parser_test.go**: Two new tests — simple base64 attachment and
nested multipart/alternative with base64 attachment.
### 3. Extractor LLM driver fix + ModelDriver consolidation
- **extractor.go**: Fixed a bug where the Extractor component used
`ModelFactory.CreateModelDriver()`, which creates bare model instances
without API keys or provider configuration. Switched to
`models.GetPreconfiguredDriver()` which resolves the actual
pre-configured driver from `ProviderManager`, matching the codepath used
by `llm.go`. This fixes auto keyword/question extraction in DSL
pipelines that require LLM calls.
- **get_driver.go** (new): Extracted shared `GetPreconfiguredDriver()`
from `llm.go:newChatModelDriver()` so both `llm.go` and `extractor.go`
use the same codepath.
- **get_driver_test.go** (new): Tests for the shared driver resolution.
- **llm.go**: Replaced inline driver resolution with
`models.GetPreconfiguredDriver()`.
### 4. Chunker fixes and observability
- **group.go** (`extractLineRecords`): Fixed to also read `markdown` and
`html` payload keys — previously it only read `text`/`content`, causing
GroupTitleChunker to silently return empty results for markdown-format
parser output.
- **common.go** (`compileDelimPattern`): Aligned with Python's
`_compile_delimiter_pattern` — only backtick-wrapped delimiters produce
an active regex pattern; plain delimiters are not compiled into the
split regex.
- **token.go** (`applyChildrenDelim`): Set `DocType` and `CKType` to
`"text"` on created ChunkDocs so the token-size merge path correctly
identifies and merges text segments.
- **parser.go**, **extractor.go**, **tokenizer.go**, **group.go**,
**hierarchy.go**: Added debug-level logging for pipeline diagnostics.
- **parser_dispatch_test.go**, **group_test.go**: New tests.
## Verification
- All Go tests pass: `bash build.sh --test ./internal/parser/parser/...`
and `bash build.sh --test ./internal/ingestion/component/...`
- Build succeeds: `bash build.sh --go`
2026-07-21 13:51:17 +08:00
|
|
|
"ragflow/internal/common"
|
2026-07-10 15:46:45 +08:00
|
|
|
"ragflow/internal/ingestion/component/globals"
|
2026-07-05 20:43:52 +08:00
|
|
|
"ragflow/internal/ingestion/component/schema"
|
|
|
|
|
"ragflow/internal/tokenizer"
|
2026-07-15 14:53:16 +08:00
|
|
|
"ragflow/internal/utility"
|
2026-07-05 20:43:52 +08:00
|
|
|
)
|
|
|
|
|
|
|
|
|
|
const ComponentNameTokenizer = "Tokenizer"
|
|
|
|
|
|
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
|
|
|
// embeddingBatchSize returns the embedding batch size, matching Python's
|
|
|
|
|
// settings.EMBEDDING_BATCH_SIZE. Reads TOKENIZER_EMBEDDING_BATCH_SIZE env
|
|
|
|
|
// var; defaults to 16. Invalid / non-positive values fall back to the
|
|
|
|
|
// default (diff Tokenizer Omission-3).
|
|
|
|
|
func embeddingBatchSize() int {
|
|
|
|
|
if v := os.Getenv("TOKENIZER_EMBEDDING_BATCH_SIZE"); v != "" {
|
|
|
|
|
if n, err := strconv.Atoi(v); err == nil && n > 0 {
|
|
|
|
|
return n
|
2026-07-13 16:32:34 +08:00
|
|
|
}
|
|
|
|
|
}
|
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
|
|
|
return 16
|
2026-07-13 16:32:34 +08:00
|
|
|
}
|
|
|
|
|
|
2026-07-05 20:43:52 +08:00
|
|
|
// titleExtRE strips a trailing file-extension (e.g. ".pdf") from the
|
|
|
|
|
// upstream document name before tokenizing it. Mirrors the python
|
|
|
|
|
// `re.sub(r"\.[a-zA-Z]+$", "", name)` in tokenizer.py:137.
|
|
|
|
|
var titleExtRE = regexp.MustCompile(`\.[a-zA-Z]+$`)
|
|
|
|
|
|
|
|
|
|
// htmlTableRE matches HTML table-cell tags so the embedded text fed
|
|
|
|
|
// to the embedding model doesn't carry raw markup. Mirrors the python
|
|
|
|
|
// `re.sub(r"</?(table|td|caption|tr|th)( [^<>]{0,12})?>", " ", txt)` at
|
|
|
|
|
// tokenizer.py:79.
|
|
|
|
|
var htmlTableRE = regexp.MustCompile(`</?(table|td|caption|tr|th)( [^<>]{0,12})?>`)
|
|
|
|
|
|
2026-07-09 16:24:39 +08:00
|
|
|
// EmbeddingResult carries a vector plus the model-reported token usage
|
|
|
|
|
// for that input batch entry.
|
|
|
|
|
type EmbeddingResult struct {
|
|
|
|
|
Vector []float64
|
|
|
|
|
TokenCount int
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Embedder is the testability seam for the embedding branch.
|
2026-07-05 20:43:52 +08:00
|
|
|
type Embedder interface {
|
2026-07-09 16:24:39 +08:00
|
|
|
MaxTokens() int
|
2026-07-22 22:30:57 +08:00
|
|
|
Encode(ctx context.Context, texts []string) ([]EmbeddingResult, error)
|
2026-07-05 20:43:52 +08:00
|
|
|
}
|
|
|
|
|
|
2026-07-09 16:24:39 +08:00
|
|
|
// EmbedderResolver resolves the embedder for one tokenizer invocation.
|
2026-07-10 15:46:45 +08:00
|
|
|
// embeddingModel is the Tokenizer-scoped embedding-model identifier (from the
|
|
|
|
|
// component's setups); an empty value tells the resolver to fall back to the
|
|
|
|
|
// dataset's configured model.
|
2026-07-27 10:20:16 +08:00
|
|
|
type EmbedderResolver func(ctx context.Context, tenantID, kbID, embeddingModel string) (Embedder, error)
|
2026-07-10 15:46:45 +08:00
|
|
|
|
|
|
|
|
// DefaultEmbedderResolver is the production embedder resolver. It is nil in
|
|
|
|
|
// this leaf package — which must not import internal/service (see the
|
|
|
|
|
// EMBEDDING MODEL RESOLUTION note above) — and is injected by the composition
|
|
|
|
|
// root: the ingestion task package wires a resolver backed by the model
|
|
|
|
|
// provider in its init(). NewTokenizerComponent falls back to this resolver
|
|
|
|
|
// when no explicit (test-only) resolver is supplied.
|
|
|
|
|
var DefaultEmbedderResolver EmbedderResolver
|
2026-07-05 20:43:52 +08:00
|
|
|
|
|
|
|
|
// TokenizerComponent computes token counts and (optionally) embedding
|
|
|
|
|
// vectors for an upstream chunk list. Mirrors python
|
|
|
|
|
// rag/flow/tokenizer/tokenizer.py:Tokenizer.
|
|
|
|
|
//
|
|
|
|
|
// Inputs:
|
|
|
|
|
//
|
|
|
|
|
// tenant_id (string, optional) — used to resolve the embedding model
|
2026-07-10 15:46:45 +08:00
|
|
|
// kb_id (string, optional) — dataset whose embd_id is used when the
|
|
|
|
|
// setups embedding_model is unset
|
2026-07-05 20:43:52 +08:00
|
|
|
// output_format (string) — one of json/markdown/text/html/chunks
|
|
|
|
|
// chunks (list[map]) — chunk list when output_format == "chunks"
|
|
|
|
|
// json (list[map]) — structured parser payload when output_format == "json" or unset
|
|
|
|
|
// markdown/text/html — scalar payload matching output_format
|
|
|
|
|
//
|
|
|
|
|
// Outputs:
|
|
|
|
|
//
|
|
|
|
|
// chunks — the chunk list with tokenized fields
|
|
|
|
|
// and (when embedding is requested)
|
|
|
|
|
// q_<n>_vec vector fields
|
|
|
|
|
// embedding_token_consumption — non-negative int (matches the python
|
|
|
|
|
// `embedding_token_consumption` output)
|
|
|
|
|
// output_format — always "chunks" (matches python set_output)
|
|
|
|
|
// _created_time / _elapsed_time — TrackElapsed bookkeeping
|
|
|
|
|
type TokenizerComponent struct {
|
2026-07-10 15:46:45 +08:00
|
|
|
param schema.TokenizerParam
|
|
|
|
|
resolver EmbedderResolver
|
|
|
|
|
embeddingModel string
|
2026-07-05 20:43:52 +08:00
|
|
|
}
|
|
|
|
|
|
2026-07-10 15:46:45 +08:00
|
|
|
// NewTokenizerComponent constructs a production TokenizerComponent from DSL
|
2026-07-05 20:43:52 +08:00
|
|
|
// params. Mirrors python `TokenizerParam` defaults (search_method =
|
2026-07-10 15:46:45 +08:00
|
|
|
// ["full_text","embedding"], filename_embd_weight=0.1, fields=["text"]). The
|
|
|
|
|
// embedding branch resolves its embedder via the injected
|
|
|
|
|
// DefaultEmbedderResolver (wired by the ingestion task package).
|
2026-07-05 20:43:52 +08:00
|
|
|
func NewTokenizerComponent(params map[string]any) (runtime.Component, error) {
|
2026-07-10 15:46:45 +08:00
|
|
|
return newTokenizerComponent(params, nil)
|
2026-07-09 16:24:39 +08:00
|
|
|
}
|
|
|
|
|
|
2026-07-10 15:46:45 +08:00
|
|
|
// NewTokenizerComponentWithResolver is TEST-ONLY. It injects an explicit
|
|
|
|
|
// embedder resolver so unit/integration tests can stub the embedding backend
|
|
|
|
|
// without touching the model provider. Production code MUST use
|
|
|
|
|
// NewTokenizerComponent and rely on DefaultEmbedderResolver instead.
|
2026-07-09 16:24:39 +08:00
|
|
|
func NewTokenizerComponentWithResolver(params map[string]any, resolver EmbedderResolver) (runtime.Component, error) {
|
2026-07-10 15:46:45 +08:00
|
|
|
return newTokenizerComponent(params, resolver)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func newTokenizerComponent(params map[string]any, resolver EmbedderResolver) (runtime.Component, error) {
|
2026-07-05 20:43:52 +08:00
|
|
|
p := schema.TokenizerParam{}.Defaults()
|
2026-07-10 15:46:45 +08:00
|
|
|
embeddingModel := ""
|
2026-07-05 20:43:52 +08:00
|
|
|
if params != nil {
|
|
|
|
|
if v, ok := params["search_method"]; ok {
|
|
|
|
|
// Replace (not append) so a caller-supplied
|
|
|
|
|
// search_method = ["full_text"] correctly disables
|
|
|
|
|
// embedding. Python's TokenizerParam similarly treats
|
|
|
|
|
// caller-supplied values as the full set.
|
|
|
|
|
p.SearchMethod = nil
|
|
|
|
|
switch t := v.(type) {
|
|
|
|
|
case []any:
|
|
|
|
|
for _, x := range t {
|
|
|
|
|
if s, ok := x.(string); ok {
|
|
|
|
|
p.SearchMethod = append(p.SearchMethod, s)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
case []string:
|
|
|
|
|
p.SearchMethod = append(p.SearchMethod, t...)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
if v, ok := params["filename_embd_weight"]; ok {
|
|
|
|
|
switch t := v.(type) {
|
|
|
|
|
case float64:
|
|
|
|
|
p.FilenameEmbdWeight = t
|
|
|
|
|
case int:
|
|
|
|
|
p.FilenameEmbdWeight = float64(t)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
if v, ok := params["fields"]; ok {
|
|
|
|
|
switch t := v.(type) {
|
|
|
|
|
case string:
|
|
|
|
|
p.Fields = []string{t}
|
|
|
|
|
case []any:
|
|
|
|
|
for _, x := range t {
|
|
|
|
|
if s, ok := x.(string); ok {
|
|
|
|
|
p.Fields = append(p.Fields, s)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
case []string:
|
|
|
|
|
p.Fields = append(p.Fields, t...)
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-07-10 15:46:45 +08:00
|
|
|
embeddingModel = embeddingModelFromSetups(params)
|
2026-07-05 20:43:52 +08:00
|
|
|
}
|
|
|
|
|
if err := p.Validate(); err != nil {
|
|
|
|
|
return nil, fmt.Errorf("Tokenizer: param check: %w", err)
|
|
|
|
|
}
|
2026-07-10 15:46:45 +08:00
|
|
|
return &TokenizerComponent{param: p, resolver: resolver, embeddingModel: embeddingModel}, nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// embeddingModelFromSetups extracts the embedding-model identifier from the
|
|
|
|
|
// component's setups map (params["setups"]["embedding_model"]). The embedding
|
|
|
|
|
// model id is a Tokenizer-scoped setup rather than a run-level global so it is
|
|
|
|
|
// never mistaken for, e.g., a chat model id shared across components. Empty
|
|
|
|
|
// when unset — the resolver then falls back to the dataset's configured model.
|
|
|
|
|
func embeddingModelFromSetups(params map[string]any) string {
|
|
|
|
|
setups, ok := params["setups"].(map[string]any)
|
|
|
|
|
if !ok {
|
|
|
|
|
return ""
|
|
|
|
|
}
|
|
|
|
|
if v, ok := setups["embedding_model"].(string); ok {
|
|
|
|
|
return strings.TrimSpace(v)
|
|
|
|
|
}
|
|
|
|
|
return ""
|
2026-07-05 20:43:52 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Inputs returns the parameter metadata.
|
|
|
|
|
func (c *TokenizerComponent) Inputs() map[string]string {
|
|
|
|
|
return map[string]string{
|
|
|
|
|
"tenant_id": "Tenant identifier used to resolve the embedding model (mirrors python self._canvas._tenant_id).",
|
2026-07-10 15:46:45 +08:00
|
|
|
"kb_id": "Optional knowledgebase identifier used to resolve the bound embedding model when the setups embedding_model is unset.",
|
2026-07-05 20:43:52 +08:00
|
|
|
"output_format": "Upstream payload discriminator: json / markdown / text / html / chunks.",
|
|
|
|
|
"chunks": "List of chunk maps when output_format == \"chunks\".",
|
|
|
|
|
"json": "Structured parser payload when output_format == \"json\" or unset.",
|
|
|
|
|
"text": "Plain-text payload when output_format == \"text\".",
|
|
|
|
|
"markdown": "Markdown payload when output_format == \"markdown\".",
|
|
|
|
|
"html": "HTML payload when output_format == \"html\".",
|
|
|
|
|
"name": "Upstream document name (used for title_tks and the title-blended embedding).",
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Outputs returns the parameter metadata. Mirrors python set_output
|
|
|
|
|
// contract for Tokenizer.
|
|
|
|
|
func (c *TokenizerComponent) Outputs() map[string]string {
|
|
|
|
|
return map[string]string{
|
|
|
|
|
"chunks": "Tokenized chunk list (each entry gains content_ltks / content_sm_ltks / title_tks and, when embedding is requested, q_<n>_vec).",
|
|
|
|
|
"embedding_token_consumption": "Non-negative token count consumed by the embedding call. Omitted when no embedding ran.",
|
|
|
|
|
"output_format": "Always \"chunks\" (matches python set_output).",
|
|
|
|
|
"_created_time": "RFC3339Nano creation timestamp (TrackElapsed).",
|
|
|
|
|
"_elapsed_time": "Wall-clock seconds (TrackElapsed).",
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Invoke computes tokens + embeddings for the upstream chunks.
|
|
|
|
|
//
|
|
|
|
|
// Failure modes:
|
|
|
|
|
//
|
2026-07-09 16:24:39 +08:00
|
|
|
// - "embedding" requested but resolver is nil → returns an
|
2026-07-05 20:43:52 +08:00
|
|
|
// error (fail-loud: same contract as python when LLMBundle is
|
|
|
|
|
// unconstructable).
|
|
|
|
|
// - Empty chunks list → returns an empty chunks output without
|
|
|
|
|
// panicking (python tokenizer.py:121 treats this as valid).
|
|
|
|
|
// - Per-chunk empty cleaned text → chunk is skipped from the
|
|
|
|
|
// embedding batch (python tokenizer.py:80-82 `if not cleaned_txt:
|
|
|
|
|
// continue`), but the chunk still carries tokenized fields if
|
|
|
|
|
// `full_text` is in `search_method`.
|
2026-07-27 10:20:16 +08:00
|
|
|
func (c *TokenizerComponent) Invoke(ctx context.Context, db *gorm.DB, inputs map[string]any) (map[string]any, error) {
|
2026-07-10 15:46:45 +08:00
|
|
|
// Run-level metadata lives in the workflow-wide CanvasState.Globals
|
|
|
|
|
// bag (seeded at pipeline start, published by the File component),
|
|
|
|
|
// not in the upstream output map — see GlobalOrInput.
|
|
|
|
|
name := globals.GlobalOrInput(ctx, inputs, "name", "")
|
|
|
|
|
tenantID := globals.GlobalOrInput(ctx, inputs, "tenant_id", "")
|
|
|
|
|
kbID := globals.GlobalOrInput(ctx, inputs, "kb_id", "")
|
|
|
|
|
// The embedding-model id is a Tokenizer-scoped setup (params["setups"]),
|
|
|
|
|
// resolved at construction, not a run-level global — see
|
|
|
|
|
// embeddingModelFromSetups.
|
|
|
|
|
embeddingModel := c.embeddingModel
|
|
|
|
|
|
|
|
|
|
// decodeTokenizerFromUpstream validates `name`; carry the resolved
|
|
|
|
|
// name into the decode input so both a Globals-backed run and a
|
|
|
|
|
// headless run (no Globals attached) satisfy it.
|
|
|
|
|
decInputs := inputs
|
|
|
|
|
if name != "" {
|
|
|
|
|
decInputs = cloneInputs(inputs)
|
|
|
|
|
decInputs["name"] = name
|
|
|
|
|
}
|
|
|
|
|
upstream, err := decodeTokenizerFromUpstream(decInputs)
|
2026-07-05 20:43:52 +08:00
|
|
|
if err != nil {
|
|
|
|
|
return nil, err
|
|
|
|
|
}
|
|
|
|
|
chunks := chunksFromTokenizerUpstream(upstream)
|
fix: docx/email parsing, extractor LLM driver, and chunker alignment (#17144)
## Summary
Three groups of changes across the Go ingestion pipeline:
### 1. DOCX parsing improvements
- **docx_parser.go**: Enhanced DOCX parsing with better structure
extraction and media handling
- **docx_parser_cgo_test.go**, **docx_parser_test.go**: Companion tests
- **office_parsers_no_cgo.go**: Stub sync for non-CGO builds
### 2. Email (.eml) parsing: base64 Content-Transfer-Encoding decoding
- **email_parser.go** (`decodeCTE`): Added Content-Transfer-Encoding
decoding for base64 and quoted-printable. Go's `mime/multipart.Reader`
does not decode Content-Transfer-Encoding automatically, so attachments
with `Content-Transfer-Encoding: base64` remained base64-encoded in the
output. The new `decodeCTE` helper is called after reading each
multipart part's raw bytes in `readMailBody`, mirroring Python's
`part.get_payload(decode=True)`.
- **email_parser_test.go**: Two new tests — simple base64 attachment and
nested multipart/alternative with base64 attachment.
### 3. Extractor LLM driver fix + ModelDriver consolidation
- **extractor.go**: Fixed a bug where the Extractor component used
`ModelFactory.CreateModelDriver()`, which creates bare model instances
without API keys or provider configuration. Switched to
`models.GetPreconfiguredDriver()` which resolves the actual
pre-configured driver from `ProviderManager`, matching the codepath used
by `llm.go`. This fixes auto keyword/question extraction in DSL
pipelines that require LLM calls.
- **get_driver.go** (new): Extracted shared `GetPreconfiguredDriver()`
from `llm.go:newChatModelDriver()` so both `llm.go` and `extractor.go`
use the same codepath.
- **get_driver_test.go** (new): Tests for the shared driver resolution.
- **llm.go**: Replaced inline driver resolution with
`models.GetPreconfiguredDriver()`.
### 4. Chunker fixes and observability
- **group.go** (`extractLineRecords`): Fixed to also read `markdown` and
`html` payload keys — previously it only read `text`/`content`, causing
GroupTitleChunker to silently return empty results for markdown-format
parser output.
- **common.go** (`compileDelimPattern`): Aligned with Python's
`_compile_delimiter_pattern` — only backtick-wrapped delimiters produce
an active regex pattern; plain delimiters are not compiled into the
split regex.
- **token.go** (`applyChildrenDelim`): Set `DocType` and `CKType` to
`"text"` on created ChunkDocs so the token-size merge path correctly
identifies and merges text segments.
- **parser.go**, **extractor.go**, **tokenizer.go**, **group.go**,
**hierarchy.go**: Added debug-level logging for pipeline diagnostics.
- **parser_dispatch_test.go**, **group_test.go**: New tests.
## Verification
- All Go tests pass: `bash build.sh --test ./internal/parser/parser/...`
and `bash build.sh --test ./internal/ingestion/component/...`
- Build succeeds: `bash build.sh --go`
2026-07-21 13:51:17 +08:00
|
|
|
common.Debug("tokenizer stage",
|
|
|
|
|
zap.String("component", "Tokenizer"),
|
|
|
|
|
zap.Int("input_chunks", len(chunks)),
|
|
|
|
|
)
|
2026-07-05 20:43:52 +08:00
|
|
|
titleStem := titleExtRE.ReplaceAllString(name, "")
|
|
|
|
|
|
2026-07-13 13:51:40 +08:00
|
|
|
normalizeChunkTextFallback(chunks)
|
2026-07-05 20:43:52 +08:00
|
|
|
|
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
|
|
|
// Diff Tokenizer-8: chunk_order_int must be set on all paths
|
|
|
|
|
// (Python sets it unconditionally). tokenizeChunks also sets it
|
|
|
|
|
// for the full_text path; this covers the embedding-only path.
|
|
|
|
|
for i := range chunks {
|
|
|
|
|
if chunks[i].ChunkOrderInt == nil {
|
|
|
|
|
chunks[i].ChunkOrderInt = intPtr(i)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-07-21 17:46:23 +08:00
|
|
|
language := globals.GlobalOrInput(ctx, inputs, "lang", "English")
|
|
|
|
|
|
2026-07-13 13:51:40 +08:00
|
|
|
if contains(c.param.SearchMethod, "full_text") {
|
2026-07-21 17:46:23 +08:00
|
|
|
if err := tokenizeChunks(chunks, titleStem, language); err != nil {
|
2026-07-13 13:51:40 +08:00
|
|
|
return nil, err
|
2026-07-05 20:43:52 +08:00
|
|
|
}
|
2026-07-13 13:51:40 +08:00
|
|
|
}
|
2026-07-05 20:43:52 +08:00
|
|
|
|
2026-07-13 13:51:40 +08:00
|
|
|
out := map[string]any{
|
|
|
|
|
"output_format": "chunks",
|
|
|
|
|
"chunks": schema.ChunkDocsToMaps(chunks),
|
|
|
|
|
}
|
2026-07-05 20:43:52 +08:00
|
|
|
|
2026-07-13 13:51:40 +08:00
|
|
|
if contains(c.param.SearchMethod, "embedding") {
|
|
|
|
|
chunks, tokenCount, err := c.embedChunks(ctx, tenantID, kbID, embeddingModel, name, chunks)
|
|
|
|
|
if err != nil {
|
2026-07-09 16:24:39 +08:00
|
|
|
return nil, err
|
|
|
|
|
}
|
2026-07-13 13:51:40 +08:00
|
|
|
out["embedding_token_consumption"] = tokenCount
|
|
|
|
|
out["chunks"] = schema.ChunkDocsToMaps(chunks)
|
|
|
|
|
}
|
|
|
|
|
if err := validateTokenizerOutputs(chunks, c.param.SearchMethod, c.param.Fields); err != nil {
|
|
|
|
|
return nil, err
|
|
|
|
|
}
|
2026-07-05 20:43:52 +08:00
|
|
|
|
fix: docx/email parsing, extractor LLM driver, and chunker alignment (#17144)
## Summary
Three groups of changes across the Go ingestion pipeline:
### 1. DOCX parsing improvements
- **docx_parser.go**: Enhanced DOCX parsing with better structure
extraction and media handling
- **docx_parser_cgo_test.go**, **docx_parser_test.go**: Companion tests
- **office_parsers_no_cgo.go**: Stub sync for non-CGO builds
### 2. Email (.eml) parsing: base64 Content-Transfer-Encoding decoding
- **email_parser.go** (`decodeCTE`): Added Content-Transfer-Encoding
decoding for base64 and quoted-printable. Go's `mime/multipart.Reader`
does not decode Content-Transfer-Encoding automatically, so attachments
with `Content-Transfer-Encoding: base64` remained base64-encoded in the
output. The new `decodeCTE` helper is called after reading each
multipart part's raw bytes in `readMailBody`, mirroring Python's
`part.get_payload(decode=True)`.
- **email_parser_test.go**: Two new tests — simple base64 attachment and
nested multipart/alternative with base64 attachment.
### 3. Extractor LLM driver fix + ModelDriver consolidation
- **extractor.go**: Fixed a bug where the Extractor component used
`ModelFactory.CreateModelDriver()`, which creates bare model instances
without API keys or provider configuration. Switched to
`models.GetPreconfiguredDriver()` which resolves the actual
pre-configured driver from `ProviderManager`, matching the codepath used
by `llm.go`. This fixes auto keyword/question extraction in DSL
pipelines that require LLM calls.
- **get_driver.go** (new): Extracted shared `GetPreconfiguredDriver()`
from `llm.go:newChatModelDriver()` so both `llm.go` and `extractor.go`
use the same codepath.
- **get_driver_test.go** (new): Tests for the shared driver resolution.
- **llm.go**: Replaced inline driver resolution with
`models.GetPreconfiguredDriver()`.
### 4. Chunker fixes and observability
- **group.go** (`extractLineRecords`): Fixed to also read `markdown` and
`html` payload keys — previously it only read `text`/`content`, causing
GroupTitleChunker to silently return empty results for markdown-format
parser output.
- **common.go** (`compileDelimPattern`): Aligned with Python's
`_compile_delimiter_pattern` — only backtick-wrapped delimiters produce
an active regex pattern; plain delimiters are not compiled into the
split regex.
- **token.go** (`applyChildrenDelim`): Set `DocType` and `CKType` to
`"text"` on created ChunkDocs so the token-size merge path correctly
identifies and merges text segments.
- **parser.go**, **extractor.go**, **tokenizer.go**, **group.go**,
**hierarchy.go**: Added debug-level logging for pipeline diagnostics.
- **parser_dispatch_test.go**, **group_test.go**: New tests.
## Verification
- All Go tests pass: `bash build.sh --test ./internal/parser/parser/...`
and `bash build.sh --test ./internal/ingestion/component/...`
- Build succeeds: `bash build.sh --go`
2026-07-21 13:51:17 +08:00
|
|
|
common.Debug("tokenizer stage",
|
|
|
|
|
zap.String("component", "Tokenizer"),
|
|
|
|
|
zap.Int("output_chunks", len(chunks)),
|
|
|
|
|
)
|
2026-07-13 13:51:40 +08:00
|
|
|
return out, nil
|
2026-07-09 16:24:39 +08:00
|
|
|
}
|
2026-07-05 20:43:52 +08:00
|
|
|
|
2026-07-10 15:46:45 +08:00
|
|
|
func (c *TokenizerComponent) embedChunks(ctx context.Context, tenantID, kbID, embeddingModel, name string, chunks []schema.ChunkDoc) ([]schema.ChunkDoc, int, error) {
|
2026-07-09 16:24:39 +08:00
|
|
|
if len(chunks) == 0 {
|
|
|
|
|
return chunks, 0, nil
|
|
|
|
|
}
|
2026-07-10 15:46:45 +08:00
|
|
|
// An explicit (test-only) resolver wins; production wiring leaves it nil
|
|
|
|
|
// and falls back to the injected DefaultEmbedderResolver.
|
|
|
|
|
resolver := c.resolver
|
|
|
|
|
if resolver == nil {
|
|
|
|
|
resolver = DefaultEmbedderResolver
|
|
|
|
|
}
|
|
|
|
|
if resolver == nil {
|
|
|
|
|
return nil, 0, fmt.Errorf("Tokenizer: embedding requested but no embedder resolver configured")
|
2026-07-09 16:24:39 +08:00
|
|
|
}
|
2026-07-27 10:20:16 +08:00
|
|
|
embedder, err := resolver(ctx, tenantID, kbID, embeddingModel)
|
2026-07-09 16:24:39 +08:00
|
|
|
if err != nil {
|
|
|
|
|
return nil, 0, fmt.Errorf("Tokenizer: resolve embedder: %w", err)
|
|
|
|
|
}
|
|
|
|
|
if embedder == nil {
|
|
|
|
|
return nil, 0, fmt.Errorf("Tokenizer: embedding requested but encoder resolution returned nil")
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
texts := make([]string, 0, len(chunks))
|
|
|
|
|
pairs := make([]int, 0, len(chunks))
|
|
|
|
|
for i, ck := range chunks {
|
|
|
|
|
txt := concatFields(ck, c.param.Fields)
|
|
|
|
|
txt = htmlTableRE.ReplaceAllString(txt, " ")
|
|
|
|
|
txt = strings.TrimSpace(txt)
|
|
|
|
|
if txt == "" {
|
|
|
|
|
continue
|
|
|
|
|
}
|
|
|
|
|
texts = append(texts, truncateForEmbedding(txt, embedder.MaxTokens()))
|
|
|
|
|
pairs = append(pairs, i)
|
|
|
|
|
}
|
|
|
|
|
if len(texts) == 0 {
|
|
|
|
|
return chunks, 0, nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
trimmedName := strings.TrimSpace(name)
|
|
|
|
|
var (
|
|
|
|
|
titleVec []float64
|
|
|
|
|
tokenCount int
|
|
|
|
|
hasTitleVec bool
|
|
|
|
|
)
|
|
|
|
|
if trimmedName == "" {
|
|
|
|
|
log.Printf("Tokenizer: empty name provided from upstream, embedding will skip title weighting")
|
|
|
|
|
} else {
|
Fix(go): align ingestion pipeline with Python (parser/media dispatch + PDF coordinate chain + Chunker) (#17349)
## Summary
Aligns the Go ingestion pipeline with the Python implementation, closing
several behavioral gaps found during the Python→Go migration (tracked in
`docs/migration_python_go_diff.md`). Covers parser/media dispatch
alignment, the PDF coordinate-chain (preview images, outline→title,
chunk coordinate finalization), and the Chunker Token/QA batches below.
Commits are grouped as follows.
### 1. Fix parser params (c524f450e)
Fixes parser/media wiring and several dispatch gaps:
- **docx/pdf vision dispatch**: correct parameter handling and VLM
invocation.
- **markdown vision (diff 2.5)**: also enhance items whose
`doc_type_kwd` is `table`, not only `image` (parser/utils.py:181).
- **media audio (diff 2.11)**: when `output_format` is `json`, carry the
ASR transcription as a JSON item instead of only the `Text` field (the
Invoke switch had no `json` branch and dropped it).
- **email (diff 2.2)**: default `output_format` is `json`
(parser.py:212), not `text`.
- **tokenizer**: handle empty/whitespace-only names; trim before
embedding.
- **extractor**: tag-matching parameter wiring.
- **split**: keyword-split regex now covers CJK/English separators.
- **parser.go**: parser-param plumbing.
### 2. fix parser gap (373537da1)
Image dispatch now mirrors `rag/app/picture.py:chunk()`:
- Always OCR the image (PaddleOCR or local ONNX).
- When OCR text is short, also call VLM (`describe`) and combine `OCR +
VLM` text.
- Emits a **structured JSON item** carrying the image data-URI and
`doc_type_kwd:"image"`, instead of a bare `Text` string. This fixes the
payload being rejected downstream by OneChunker/TokenChunker (JSON=nil).
### 3. PDF coordinate-chain fixes (55367a820, 727f8167c)
Closes three items from the migration tracker in the
chunker/tokenizer/task layer:
- **(Chunker-1.3) `restore_pdf_text_previews`** — `needsCrop` now also
returns true for `text` chunks that carry PDF positions
(`pdfcrop_cgo.go`), so text blocks get a rendered preview image uploaded
to storage via `imageUploadDecorator`/`ChunkImageUploader`, matching
Python `restore_pdf_text_previews` + `image2id`.
- **(Chunker-1.5) PDF outline → title levels** — `title.go` adds
`outlineSimilarity` (rune-bigram Jaccard, mirroring
`common.py:_outline_similarity`), `resolveOutlineLevels` (matches text
lines to outline entries at similarity > 0.8, with a sparse guard
`len(outline)/len(records) <= 0.03`), and `outlineFromInputs` (reads
`file.outline`). Wired into `newLevelContext` in both `group.go` and
`hierarchy.go`; falls back to the title-shape heuristic when no outline
is present.
- **(Tokenizer-(T)1) `finalize_pdf_chunk`** — the coordinate →
`position_int`/`page_num_int`/`top_int` conversion is owned by the task
layer (`processChunkPositions`→`AddPositions`), which runs *after* the
tokenizer and consumes the tokenizer-owned fields. The tokenizer only
preserves the raw `positions`/`_pdf_positions` (no duplicate
conversion), pinned by `TestChunkDocsToMaps_PreservesPDFPositions`.
### 4. Integration test made environment-free
(`internal/ingestion/task/pipeline_real_integration_test.go`)
- Removed the `//go:build integration` tag so the contract tests run
under the default `build.sh --test` (which does not pass `-tags
integration`).
- External dependencies replaced with in-memory substitutes so no
MySQL/MinIO/ES is required:
- MySQL → on-disk sqlite (`glebarez/sqlite`) with the needed tables
auto-migrated.
- MinIO → `storage.NewMemoryStorage()`.
- Elasticsearch → chunks captured via `WithInsertFunc` instead of
`engine.InsertChunks`/`Search`.
- `requireTokenizerPool` still skips gracefully when the native
tokenizer pool is unavailable; `WithLogCreateFunc(noop)` avoids
depending on the operation-log table.
- Added `taskChunkFieldEqualsStr` to tolerate `kb_id` being a
`[]string`/`[]any` in the raw chunk payload (the search engine flattens
it to a string on read).
### 5. TokenChunker alignment — Batch 1
(`internal/ingestion/component/chunker/token.go`)
Closes four Chunker items from the migration tracker:
- **(Chunker-2.1) sentence delimiter** — the boundary regex now also
breaks on ASCII `!`/`?`. Extracted to a package-level `var
sentenceDelimiter` and used in `mergeByTokenSize`, matching Python's
full delimiter set.
- **(Chunker-2.2) overlap tag leakage** — when a new chunk starts, its
overlap prefix is taken from the previous chunk *after* `removeTag`, in
both the text path (`mergeByTokenSize`) and the JSON path
(`mergeByTokenSizeFromJSON`). Parser tags (`@@…##`) no longer leak into
the overlap region (mirrors `nlp/__init__.py:1181`).
- **(Chunker-2.11) empty-text merge** — merging a non-empty chunk into
an empty previous chunk now assigns the text directly instead of being
skipped (`mergeByTokenSizeFromJSON`), mirroring
`token_chunker.py:236-239`.
- **(Chunker-2.4) overlap token counting** —
`takeFromEnd`/`takeFromStart` now count tokens exactly via `tokenizeStr`
instead of the 4-bytes/token heuristic, fixing over-counting for CJK
text.
### 6. QA Chunker alignment — Batch 2
(`internal/ingestion/component/chunker/qa.go` + `schema`)
Closes three Chunker items from the migration tracker:
- **(Chunker-2.13) default language** — an empty `lang` now defaults to
Chinese prefixes (`问题:`/`回答:`) instead of English, matching `qa.py:299`.
- **(Chunker-2.12) `rmQAPrefix` regex** — the separator is changed to
`[\t:: ]+` (one-or-more), matching `qa.py:241`, so multiple separators
(e.g. `Q:: answer`) are fully stripped.
- **(Chunker-1.8 QA) missing chunk fields** — QA chunks now preserve:
- `top_int` — the source row/record index, threaded through the
tab/csv/markdown extractors (mirrors `qa.py` `beAdoc(..., row_num=i)`);
- `image` + `doc_type_kwd:"image"`;
- `_pdf_positions` / `positions` carried from the upstream JSON item.
`schema.ChunkDoc` gains a `TopInt []int` field (serialized as `top_int`,
registered in `UnmarshalJSON`). Note: the Tag/Table/Presentation/One
chunker field gaps under 1.8 remain pending.
## Test plan
- Added/updated unit tests: `pdfcrop_cgo_test.go` (`TestNeedsCrop`,
`TestRestorePDFTextPreview`), `title_test.go`
(`TestResolveOutlineLevels`, `TestResolveOutlineLevels_SparseGuard`,
`TestNewLevelContext_OutlineBranch`, `TestOutlineFromInputs`),
`tokenizer_unit_test.go` (`TestChunkDocsToMaps_PreservesPDFPositions`),
`token_pdfpos_test.go`.
- **Batch 1** — `token_batch1_test.go`:
`TestSentenceDelimiterMatchesBangAndQuestion`,
`TestMergeByTokenSizeFromJSON_OverlapStripsTags`,
`TestMergeByTokenSizeFromJSON_EmptyPrevKeepsChunk`,
`TestTakeFromEndRespectsTokenCount`,
`TestTakeFromStartRespectsTokenCount`.
- **Batch 2** — `qa_batch2_test.go`:
`TestQAChunker_DefaultLangIsChinese`,
`TestRmQAPrefixStripsMultipleSeparators`, `TestQAChunker_SetsTopInt`,
`TestQAChunker_CarriesImageAndPositions`. Existing `qa_test.go`
expectations were updated to the corrected language default / separator
behavior.
- `pipeline_real_integration_test.go`
(`TestPipelineExecutor_Run_RealCanvasDSL_UsesGeneralPipeline`,
`TestPipelineExecutor_Run_RealPDF_ProducesIndexedChunks`,
`TestRunPipeline_RealPipelineOutput_ProducesIndexFields`) now runs
without any external service.
- `bash build.sh --test ./internal/ingestion/...` passes.
- No files deleted.
2026-07-24 21:06:38 +08:00
|
|
|
// Encode the raw name (no TrimSpace) to mirror Python
|
|
|
|
|
// tokenizer.py:95 which passes name verbatim to embedding. The
|
|
|
|
|
// empty-name guard above still uses TrimSpace, matching Python's
|
|
|
|
|
// `.strip()==""` check at tokenizer.py:200.
|
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
|
|
|
titleResults, err := embedder.Encode(ctx, []string{name})
|
2026-07-09 16:24:39 +08:00
|
|
|
if err != nil {
|
|
|
|
|
return nil, 0, fmt.Errorf("Tokenizer: encode title: %w", err)
|
|
|
|
|
}
|
|
|
|
|
if len(titleResults) != 1 {
|
|
|
|
|
return nil, 0, fmt.Errorf("Tokenizer: encode title returned %d vectors for 1 chunk", len(titleResults))
|
|
|
|
|
}
|
|
|
|
|
titleVec = titleResults[0].Vector
|
|
|
|
|
tokenCount = titleResults[0].TokenCount
|
|
|
|
|
hasTitleVec = true
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
contentResults := make([]EmbeddingResult, 0, len(texts))
|
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
|
|
|
for start := 0; start < len(texts); start += embeddingBatchSize() {
|
|
|
|
|
end := start + embeddingBatchSize()
|
2026-07-09 16:24:39 +08:00
|
|
|
if end > len(texts) {
|
|
|
|
|
end = len(texts)
|
|
|
|
|
}
|
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
|
|
|
batchResults, err := embedder.Encode(ctx, texts[start:end])
|
2026-07-09 16:24:39 +08:00
|
|
|
if err != nil {
|
|
|
|
|
return nil, 0, fmt.Errorf("Tokenizer: encode: %w", err)
|
|
|
|
|
}
|
|
|
|
|
if len(batchResults) != end-start {
|
|
|
|
|
return nil, 0, fmt.Errorf("Tokenizer: encode returned %d vectors for %d chunks", len(batchResults), end-start)
|
|
|
|
|
}
|
|
|
|
|
for _, result := range batchResults {
|
|
|
|
|
tokenCount += result.TokenCount
|
|
|
|
|
}
|
|
|
|
|
contentResults = append(contentResults, batchResults...)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
titleWeight := c.param.FilenameEmbdWeight
|
|
|
|
|
for i, idx := range pairs {
|
|
|
|
|
merged := append([]float64(nil), contentResults[i].Vector...)
|
|
|
|
|
if hasTitleVec {
|
|
|
|
|
merged, err = mergeEmbeddingVectors(titleVec, contentResults[i].Vector, titleWeight)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, 0, fmt.Errorf("Tokenizer: merge vectors: %w", err)
|
2026-07-05 20:43:52 +08:00
|
|
|
}
|
|
|
|
|
}
|
2026-07-09 16:24:39 +08:00
|
|
|
if err := chunks[idx].SetExtraValue(fmt.Sprintf("q_%d_vec", len(merged)), merged); err != nil {
|
|
|
|
|
return nil, 0, fmt.Errorf("Tokenizer: vector marshal: %w", err)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return chunks, tokenCount, nil
|
|
|
|
|
}
|
2026-07-05 20:43:52 +08:00
|
|
|
|
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
|
|
|
// truncateForEmbedding truncates text to fit within maxTokens,
|
|
|
|
|
// reserving 10 tokens for special/model overhead. Mirrors Python
|
|
|
|
|
// tokenizer.py truncation: when maxTokens <= 10, the text is
|
|
|
|
|
// truncated to empty
|
2026-07-09 16:24:39 +08:00
|
|
|
func truncateForEmbedding(text string, maxTokens int) string {
|
|
|
|
|
if maxTokens <= 10 {
|
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
|
|
|
return ""
|
2026-07-09 16:24:39 +08:00
|
|
|
}
|
|
|
|
|
return tokenizer.TrimContentToTokenLimit(text, maxTokens-10)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func mergeEmbeddingVectors(titleVec, contentVec []float64, titleWeight float64) ([]float64, error) {
|
|
|
|
|
if len(titleVec) == 0 || len(contentVec) == 0 {
|
|
|
|
|
return nil, fmt.Errorf("empty embedding vector")
|
|
|
|
|
}
|
|
|
|
|
if len(titleVec) != len(contentVec) {
|
|
|
|
|
return nil, fmt.Errorf("unexpected embedding dimensions")
|
|
|
|
|
}
|
|
|
|
|
merged := make([]float64, len(titleVec))
|
|
|
|
|
for i := range titleVec {
|
|
|
|
|
merged[i] = titleWeight*titleVec[i] + (1-titleWeight)*contentVec[i]
|
|
|
|
|
}
|
|
|
|
|
return merged, nil
|
2026-07-05 20:43:52 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func decodeTokenizerFromUpstream(inputs map[string]any) (schema.TokenizerFromUpstream, error) {
|
|
|
|
|
var out schema.TokenizerFromUpstream
|
|
|
|
|
if inputs == nil {
|
|
|
|
|
return out, fmt.Errorf("Tokenizer: inputs map is nil")
|
|
|
|
|
}
|
|
|
|
|
data, err := json.Marshal(stripRuntimeTimestamps(inputs))
|
|
|
|
|
if err != nil {
|
|
|
|
|
return out, fmt.Errorf("Tokenizer: encode inputs: %w", err)
|
|
|
|
|
}
|
|
|
|
|
if err := json.Unmarshal(data, &out); err != nil {
|
|
|
|
|
return out, fmt.Errorf("Tokenizer: decode inputs: %w", err)
|
|
|
|
|
}
|
|
|
|
|
if err := out.Validate(); err != nil {
|
|
|
|
|
return out, fmt.Errorf("Tokenizer: input error: %w", err)
|
|
|
|
|
}
|
|
|
|
|
return out, nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func stripRuntimeTimestamps(inputs map[string]any) map[string]any {
|
|
|
|
|
out := make(map[string]any, len(inputs))
|
|
|
|
|
for k, v := range inputs {
|
|
|
|
|
if k == "_created_time" || k == "_elapsed_time" {
|
|
|
|
|
continue
|
|
|
|
|
}
|
|
|
|
|
out[k] = v
|
|
|
|
|
}
|
|
|
|
|
return out
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func chunksFromTokenizerUpstream(in schema.TokenizerFromUpstream) []schema.ChunkDoc {
|
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
|
|
|
var raw []schema.ChunkDoc
|
2026-07-05 20:43:52 +08:00
|
|
|
switch in.OutputFormat {
|
|
|
|
|
case schema.PayloadFormatChunks:
|
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
|
|
|
raw = cloneChunkDocs(in.Chunks)
|
2026-07-05 20:43:52 +08:00
|
|
|
case schema.PayloadFormatMarkdown:
|
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
|
|
|
raw = textPayloadToChunks(in.MarkdownResult)
|
2026-07-05 20:43:52 +08:00
|
|
|
case schema.PayloadFormatText:
|
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
|
|
|
raw = textPayloadToChunks(in.TextResult)
|
2026-07-05 20:43:52 +08:00
|
|
|
case schema.PayloadFormatHTML:
|
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
|
|
|
raw = textPayloadToChunks(in.HTMLResult)
|
2026-07-05 20:43:52 +08:00
|
|
|
default:
|
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
|
|
|
raw = cloneChunkDocs(in.JSONResult)
|
|
|
|
|
}
|
|
|
|
|
// Discard zero-value ChunkDocs (no text, no content_with_weight, no
|
|
|
|
|
// image, no summary) so they don't produce phantom embeddings
|
|
|
|
|
// downstream. Python's pipeline filters none-chunks before the
|
|
|
|
|
// tokenizer.
|
|
|
|
|
filtered := raw[:0]
|
|
|
|
|
for _, ck := range raw {
|
|
|
|
|
if isPhantomChunk(ck) {
|
|
|
|
|
continue
|
|
|
|
|
}
|
|
|
|
|
filtered = append(filtered, ck)
|
2026-07-05 20:43:52 +08:00
|
|
|
}
|
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
|
|
|
return filtered
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// isPhantomChunk returns true when a ChunkDoc has no usable content for
|
|
|
|
|
// downstream tokenization or embedding. A chunk carrying only a Summary
|
|
|
|
|
// (no Text/Image/ContentWithWeight) is kept — tokenizeChunks tokenizes the
|
|
|
|
|
// Summary in that case. Mirrors Python's none-chunk filtering.
|
|
|
|
|
func isPhantomChunk(ck schema.ChunkDoc) bool {
|
|
|
|
|
return ck.Text == "" && ck.Image == "" && ck.ContentWithWeight == "" && ck.Summary == ""
|
2026-07-05 20:43:52 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func textPayloadToChunks(payload *string) []schema.ChunkDoc {
|
|
|
|
|
if payload == nil || strings.TrimSpace(*payload) == "" {
|
|
|
|
|
return []schema.ChunkDoc{}
|
|
|
|
|
}
|
|
|
|
|
return []schema.ChunkDoc{{Text: *payload}}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func cloneChunkDocs(in []schema.ChunkDoc) []schema.ChunkDoc {
|
|
|
|
|
if len(in) == 0 {
|
|
|
|
|
return []schema.ChunkDoc{}
|
|
|
|
|
}
|
|
|
|
|
out := make([]schema.ChunkDoc, len(in))
|
|
|
|
|
for i := range in {
|
|
|
|
|
out[i] = cloneTokenizerChunkDoc(in[i])
|
|
|
|
|
}
|
|
|
|
|
return out
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func cloneTokenizerChunkDoc(in schema.ChunkDoc) schema.ChunkDoc {
|
|
|
|
|
out := in
|
|
|
|
|
if in.TKNums != nil {
|
|
|
|
|
v := *in.TKNums
|
|
|
|
|
out.TKNums = &v
|
|
|
|
|
}
|
|
|
|
|
if in.ChunkOrderInt != nil {
|
|
|
|
|
v := *in.ChunkOrderInt
|
|
|
|
|
out.ChunkOrderInt = &v
|
|
|
|
|
}
|
|
|
|
|
if in.PageNumber != nil {
|
|
|
|
|
v := *in.PageNumber
|
|
|
|
|
out.PageNumber = &v
|
|
|
|
|
}
|
|
|
|
|
if in.Extra != nil {
|
|
|
|
|
out.Extra = make(map[string]json.RawMessage, len(in.Extra))
|
|
|
|
|
for k, v := range in.Extra {
|
|
|
|
|
out.Extra[k] = append(json.RawMessage(nil), v...)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
if len(in.PDFPositions) > 0 {
|
|
|
|
|
out.PDFPositions = append(json.RawMessage(nil), in.PDFPositions...)
|
|
|
|
|
}
|
|
|
|
|
if len(in.Positions) > 0 {
|
|
|
|
|
out.Positions = append(json.RawMessage(nil), in.Positions...)
|
|
|
|
|
}
|
|
|
|
|
return out
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// normalizeChunkTextFallback populates each chunk's "text" key
|
|
|
|
|
// from "content_with_weight" when "text" is absent or empty. Mirrors
|
|
|
|
|
// the python rag/flow/tokenizer.py:111 fallback so a chunk that
|
|
|
|
|
// arrives from the parser path with only the structured
|
|
|
|
|
// content_with_weight field still tokenizes.
|
|
|
|
|
//
|
|
|
|
|
// The function mutates the input slice in place; callers should
|
|
|
|
|
// not retain separate copies of the chunks map. If both fields
|
|
|
|
|
// are present, the existing "text" wins — preserves the python
|
|
|
|
|
// contract where the chunker's emitted text is authoritative.
|
|
|
|
|
func normalizeChunkTextFallback(chunks []schema.ChunkDoc) {
|
|
|
|
|
for i := range chunks {
|
|
|
|
|
if chunks[i].Text != "" {
|
|
|
|
|
continue
|
|
|
|
|
}
|
|
|
|
|
if chunks[i].ContentWithWeight != "" {
|
|
|
|
|
chunks[i].Text = chunks[i].ContentWithWeight
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// tokenizeChunks annotates each chunk with title_tks, content_ltks,
|
|
|
|
|
// and (when applicable) question_tks / important_tks / summary fields.
|
2026-07-21 17:46:23 +08:00
|
|
|
// Mirrors python tokenizer.py:130-185 and rag/nlp/__init__.py tokenize() /
|
|
|
|
|
// tokenize_chunks().
|
|
|
|
|
//
|
|
|
|
|
// language sets the Snowball stemmer language, matching Python's
|
|
|
|
|
// rag_tokenizer.tokenizer.set_language(language) call inside tokenize().
|
|
|
|
|
func tokenizeChunks(chunks []schema.ChunkDoc, titleStem string, language string) error {
|
|
|
|
|
tok := tokenizer.New(language)
|
2026-07-05 20:43:52 +08:00
|
|
|
for i := range chunks {
|
|
|
|
|
ck := &chunks[i]
|
|
|
|
|
ck.ChunkOrderInt = intPtr(i)
|
2026-07-21 17:46:23 +08:00
|
|
|
titleTk, err := tok.Tokenize(titleStem)
|
2026-07-05 20:43:52 +08:00
|
|
|
if err != nil {
|
|
|
|
|
return fmt.Errorf("Tokenizer: title tokenize: %w", err)
|
|
|
|
|
}
|
2026-07-21 17:46:23 +08:00
|
|
|
titleSmTk, err := tok.FineGrainedTokenize(titleTk)
|
2026-07-05 20:43:52 +08:00
|
|
|
if err != nil {
|
|
|
|
|
return fmt.Errorf("Tokenizer: title fine-grain: %w", err)
|
|
|
|
|
}
|
|
|
|
|
ck.TitleTks = titleTk
|
|
|
|
|
ck.TitleSmTks = titleSmTk
|
|
|
|
|
|
|
|
|
|
// Question / keyword / summary fields are optional. The python
|
|
|
|
|
// path branches on each independently.
|
|
|
|
|
if q := ck.Questions; q != "" {
|
|
|
|
|
if err := ck.SetExtraValue("question_kwd", strings.Split(q, "\n")); err != nil {
|
|
|
|
|
return fmt.Errorf("Tokenizer: question keywords marshal: %w", err)
|
|
|
|
|
}
|
2026-07-21 17:46:23 +08:00
|
|
|
qt, err := tok.Tokenize(q)
|
2026-07-05 20:43:52 +08:00
|
|
|
if err != nil {
|
|
|
|
|
return fmt.Errorf("Tokenizer: question tokenize: %w", err)
|
|
|
|
|
}
|
|
|
|
|
if err := ck.SetExtraValue("question_tks", qt); err != nil {
|
|
|
|
|
return fmt.Errorf("Tokenizer: question tokens marshal: %w", err)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
if kw := ck.Keywords; kw != "" {
|
2026-07-15 14:53:16 +08:00
|
|
|
if err := ck.SetExtraValue("important_kwd", utility.SplitKeywords(kw)); err != nil {
|
2026-07-05 20:43:52 +08:00
|
|
|
return fmt.Errorf("Tokenizer: keyword list marshal: %w", err)
|
|
|
|
|
}
|
2026-07-21 17:46:23 +08:00
|
|
|
it, err := tok.Tokenize(kw)
|
2026-07-05 20:43:52 +08:00
|
|
|
if err != nil {
|
|
|
|
|
return fmt.Errorf("Tokenizer: keyword tokenize: %w", err)
|
|
|
|
|
}
|
|
|
|
|
if err := ck.SetExtraValue("important_tks", it); err != nil {
|
|
|
|
|
return fmt.Errorf("Tokenizer: keyword tokens marshal: %w", err)
|
|
|
|
|
}
|
|
|
|
|
}
|
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
|
|
|
// Keep Go: skip whitespace-only summaries so they don't shadow
|
|
|
|
|
// the real Text. Python's truthy check (tokenizer.py:155) treats
|
|
|
|
|
// " " as present and blanks out content_ltks; Go is more sensible.
|
|
|
|
|
if s := strings.TrimSpace(ck.Summary); s != "" {
|
2026-07-21 17:46:23 +08:00
|
|
|
st, err := tok.Tokenize(s)
|
2026-07-05 20:43:52 +08:00
|
|
|
if err != nil {
|
|
|
|
|
return fmt.Errorf("Tokenizer: summary tokenize: %w", err)
|
|
|
|
|
}
|
2026-07-13 11:08:04 +08:00
|
|
|
if st == "" {
|
|
|
|
|
st = s
|
|
|
|
|
}
|
2026-07-05 20:43:52 +08:00
|
|
|
ck.ContentLtks = st
|
2026-07-21 17:46:23 +08:00
|
|
|
smt, err := tok.FineGrainedTokenize(st)
|
2026-07-05 20:43:52 +08:00
|
|
|
if err != nil {
|
|
|
|
|
return fmt.Errorf("Tokenizer: summary fine-grain: %w", err)
|
|
|
|
|
}
|
2026-07-13 11:08:04 +08:00
|
|
|
if smt == "" {
|
|
|
|
|
smt = st
|
|
|
|
|
}
|
2026-07-05 20:43:52 +08:00
|
|
|
ck.ContentSmLtks = smt
|
2026-07-13 11:08:04 +08:00
|
|
|
} else if t := ck.Text; strings.TrimSpace(t) != "" {
|
2026-07-21 17:46:23 +08:00
|
|
|
tt, err := tok.Tokenize(t)
|
2026-07-05 20:43:52 +08:00
|
|
|
if err != nil {
|
|
|
|
|
return fmt.Errorf("Tokenizer: text tokenize: %w", err)
|
|
|
|
|
}
|
2026-07-13 11:08:04 +08:00
|
|
|
if tt == "" {
|
|
|
|
|
tt = t
|
|
|
|
|
}
|
2026-07-05 20:43:52 +08:00
|
|
|
ck.ContentLtks = tt
|
2026-07-21 17:46:23 +08:00
|
|
|
smt, err := tok.FineGrainedTokenize(tt)
|
2026-07-05 20:43:52 +08:00
|
|
|
if err != nil {
|
|
|
|
|
return fmt.Errorf("Tokenizer: text fine-grain: %w", err)
|
|
|
|
|
}
|
2026-07-13 11:08:04 +08:00
|
|
|
if smt == "" {
|
|
|
|
|
smt = tt
|
|
|
|
|
}
|
2026-07-05 20:43:52 +08:00
|
|
|
ck.ContentSmLtks = smt
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// concatFields concatenates the configured fields of a chunk into
|
|
|
|
|
// a single string. Mirrors python tokenizer.py:69-79 which
|
|
|
|
|
// concatenates `param.fields` (string or list-of-strings per chunk).
|
|
|
|
|
func concatFields(ck schema.ChunkDoc, fields []string) string {
|
|
|
|
|
var b strings.Builder
|
|
|
|
|
for _, f := range fields {
|
|
|
|
|
switch f {
|
|
|
|
|
case "text":
|
|
|
|
|
b.WriteString(ck.Text)
|
|
|
|
|
case "content_with_weight":
|
|
|
|
|
b.WriteString(ck.ContentWithWeight)
|
|
|
|
|
case "questions":
|
|
|
|
|
b.WriteString(ck.Questions)
|
|
|
|
|
case "keywords":
|
|
|
|
|
b.WriteString(ck.Keywords)
|
|
|
|
|
case "summary":
|
|
|
|
|
b.WriteString(ck.Summary)
|
|
|
|
|
default:
|
|
|
|
|
if s, ok := ck.GetExtraString(f); ok {
|
|
|
|
|
b.WriteString(s)
|
|
|
|
|
continue
|
|
|
|
|
}
|
|
|
|
|
if values, ok := ck.GetExtraStringSlice(f); ok {
|
|
|
|
|
b.WriteString(strings.Join(values, "\n"))
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return b.String()
|
|
|
|
|
}
|
|
|
|
|
|
2026-07-09 16:24:39 +08:00
|
|
|
func validateTokenizerOutputs(chunks []schema.ChunkDoc, searchMethods, fields []string) error {
|
|
|
|
|
needFullText := contains(searchMethods, "full_text")
|
|
|
|
|
needEmbedding := contains(searchMethods, "embedding")
|
|
|
|
|
if !needFullText && !needEmbedding {
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
for i := range chunks {
|
|
|
|
|
if needFullText && requiresFullTextTokens(chunks[i]) {
|
|
|
|
|
if strings.TrimSpace(chunks[i].ContentLtks) == "" || strings.TrimSpace(chunks[i].ContentSmLtks) == "" {
|
|
|
|
|
return fmt.Errorf("Tokenizer: chunk[%d] missing full_text tokens", i)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
if needEmbedding && requiresEmbeddingVector(chunks[i], fields) {
|
|
|
|
|
if !hasEmbeddingVector(chunks[i]) {
|
|
|
|
|
return fmt.Errorf("Tokenizer: chunk[%d] missing embedding vector", i)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func requiresFullTextTokens(ck schema.ChunkDoc) bool {
|
|
|
|
|
return strings.TrimSpace(ck.Summary) != "" || strings.TrimSpace(ck.Text) != ""
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func requiresEmbeddingVector(ck schema.ChunkDoc, fields []string) bool {
|
|
|
|
|
return strings.TrimSpace(cleanEmbeddingText(concatFields(ck, fields))) != ""
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func cleanEmbeddingText(text string) string {
|
|
|
|
|
return strings.TrimSpace(htmlTableRE.ReplaceAllString(text, " "))
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func hasEmbeddingVector(ck schema.ChunkDoc) bool {
|
|
|
|
|
if len(ck.Extra) == 0 {
|
|
|
|
|
return false
|
|
|
|
|
}
|
|
|
|
|
for key, raw := range ck.Extra {
|
|
|
|
|
if !strings.HasPrefix(key, "q_") || !strings.HasSuffix(key, "_vec") {
|
|
|
|
|
continue
|
|
|
|
|
}
|
|
|
|
|
var vec []float64
|
|
|
|
|
if err := json.Unmarshal(raw, &vec); err != nil {
|
|
|
|
|
continue
|
|
|
|
|
}
|
|
|
|
|
if len(vec) > 0 {
|
|
|
|
|
return true
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return false
|
|
|
|
|
}
|
|
|
|
|
|
2026-07-05 20:43:52 +08:00
|
|
|
func getStringOr(m map[string]any, key, def string) string {
|
2026-07-10 15:46:45 +08:00
|
|
|
if v, ok := m[key].(string); ok && v != "" {
|
2026-07-05 20:43:52 +08:00
|
|
|
return v
|
|
|
|
|
}
|
|
|
|
|
return def
|
|
|
|
|
}
|
|
|
|
|
|
2026-07-10 15:46:45 +08:00
|
|
|
// cloneInputs returns a shallow copy of m with room for one extra key.
|
|
|
|
|
// Used to inject the Globals-resolved `name` into the decode input without
|
|
|
|
|
// mutating the caller's input snapshot.
|
|
|
|
|
func cloneInputs(m map[string]any) map[string]any {
|
|
|
|
|
if m == nil {
|
|
|
|
|
return map[string]any{}
|
2026-07-05 20:43:52 +08:00
|
|
|
}
|
2026-07-10 15:46:45 +08:00
|
|
|
cp := make(map[string]any, len(m)+1)
|
|
|
|
|
for k, v := range m {
|
|
|
|
|
cp[k] = v
|
2026-07-05 20:43:52 +08:00
|
|
|
}
|
2026-07-10 15:46:45 +08:00
|
|
|
return cp
|
2026-07-05 20:43:52 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func contains(s []string, v string) bool {
|
2026-07-09 16:24:39 +08:00
|
|
|
return slices.Contains(s, v)
|
2026-07-05 20:43:52 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func intPtr(v int) *int { return &v }
|
|
|
|
|
|
|
|
|
|
// init registers Tokenizer under CategoryIngestion (plan §4
|
|
|
|
|
// Phase 2.4). The metadata drives Phase 4's GET /api/v1/components
|
|
|
|
|
// listing.
|
|
|
|
|
func init() {
|
|
|
|
|
c := &TokenizerComponent{}
|
|
|
|
|
runtime.MustRegister(ComponentNameTokenizer, runtime.CategoryIngestion,
|
|
|
|
|
func(_ string, params map[string]any) (runtime.Component, error) {
|
|
|
|
|
return NewTokenizerComponent(params)
|
|
|
|
|
},
|
|
|
|
|
runtime.Metadata{
|
|
|
|
|
Version: "1.0.0",
|
|
|
|
|
Inputs: c.Inputs(),
|
|
|
|
|
Outputs: c.Outputs(),
|
|
|
|
|
})
|
|
|
|
|
}
|