Files
ragflow/internal/ingestion/task/chunk_process.go

254 lines
6.7 KiB
Go
Raw Normal View History

//
// Copyright 2026 The InfiniFlow Authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
package task
import (
"fmt"
"regexp"
"strings"
"time"
"ragflow/internal/common"
"ragflow/internal/tokenizer"
"ragflow/internal/utility"
"github.com/pkoukk/tiktoken-go"
)
var keywordsSplitRE = regexp.MustCompile(`[,;;、\r\n]+`)
// TruncateTexts truncates each text by token count using cl100k_base encoding.
// maxLength is reduced by 10 as a safety margin, matching Python.
// Mirrors Python: EmbeddingUtils.truncate_texts()
func TruncateTexts(texts []string, maxLength int) []string {
if texts == nil {
return nil
}
safeMax := maxLength - 10
if safeMax < 0 {
safeMax = 0
}
enc, err := tiktoken.GetEncoding("cl100k_base")
if err != nil {
// Fallback: if tiktoken fails, return as-is.
// NOTE: this path cannot be triggered in unit tests because
// tiktoken.GetEncoding always succeeds in normal environments.
// The fallback is simple (make+copy) and verified by code review.
result := make([]string, len(texts))
copy(result, texts)
return result
}
result := make([]string, len(texts))
for i, t := range texts {
tokens := enc.Encode(t, nil, nil)
if len(tokens) > safeMax {
result[i] = enc.Decode(tokens[:safeMax])
} else {
result[i] = t
}
}
return result
}
// SplitQuestions splits a questions string by newline, keeping all elements.
// Mirrors Python: ck["questions"].split("\n") — keeps empty strings
func SplitQuestions(questions string) []string {
return strings.Split(questions, "\n")
}
// SplitKeywords splits a keywords string by common delimiters.
// Mirrors Python: re.split(r"[,;;、\r\n]+", keywords)
func SplitKeywords(keywords string) []string {
if keywords == "" {
return nil
}
parts := keywordsSplitRE.Split(keywords, -1)
result := make([]string, 0, len(parts))
for _, p := range parts {
if p != "" {
result = append(result, p)
}
}
if len(result) == 0 {
return nil
}
return result
}
// CreateChunkTime returns the current timestamp as a formatted string and float Unix timestamp.
// The float has sub-second precision, matching Python: datetime.now().timestamp()
func CreateChunkTime() (string, float64) {
now := time.Now()
timeStr := now.Format("2006-01-02 15:04:05")
return timeStr, float64(now.UnixMicro()) / 1e6
}
// RenameTextToContentWithWeight renames the "text" key to "content_with_weight".
// If "content_with_weight" already exists, the "text" key is simply removed.
// Mirrors Python: ck["content_with_weight"] = ck["text"]; del ck["text"]
func RenameTextToContentWithWeight(chunk map[string]any) {
if _, exists := chunk["content_with_weight"]; !exists {
if text, ok := chunk["text"]; ok {
chunk["content_with_weight"] = text
}
}
delete(chunk, "text")
}
// GetEmbeddingTokenConsumption extracts the embedding token consumption from pipeline output.
// Handles both int (Go native) and float64 (after JSON round-trip).
func GetEmbeddingTokenConsumption(output map[string]any) int {
if output == nil {
return 0
}
switch v := output[EmbeddingTokenConsumptionKey].(type) {
case int:
return v
case float64:
return int(v)
default:
common.Warn(fmt.Sprintf("unexpected type %T for embedding token consumption, key=%q", v, EmbeddingTokenConsumptionKey))
return 0
}
}
// ProcessChunksForPipeline mutates chunks into the pre-index structure used by
// the pipeline and returns merged metadata.
func ProcessChunksForPipeline(
chunks []map[string]any,
docID string,
kbID string,
docName string,
now time.Time,
) map[string]any {
if chunks == nil {
return nil
}
metadata := make(map[string]any)
timeStr := now.Format("2006-01-02 15:04:05")
timestamp := float64(now.UnixMicro()) / 1e6
for _, ck := range chunks {
ck["doc_id"] = docID
ck["kb_id"] = []string{kbID}
ck["docnm_kwd"] = docName
ck["create_time"] = timeStr
ck["create_timestamp_flt"] = timestamp
if _, exists := ck["id"]; !exists {
text, err := MustGetChunkTextString(ck)
if err != nil {
common.Error("unexpected error", err)
}
ck["id"] = ChunkID(text, docID)
}
processChunkQuestions(ck)
processChunkKeywords(ck)
processChunkSummary(ck)
metadata = mergeChunkMetadata(metadata, ck)
RenameTextToContentWithWeight(ck)
processChunkPositions(ck)
removeInternalChunkFields(ck)
}
return metadata
}
func removeInternalChunkFields(ck map[string]any) {
delete(ck, "_pdf_positions")
delete(ck, "image")
}
func processChunkQuestions(ck map[string]any) {
if _, exists := ck["questions"]; !exists {
return
}
if _, hasTks := ck["question_tks"]; !hasTks {
q, _ := ck["questions"].(string)
ck["question_kwd"] = strings.Split(q, "\n")
tks, err := tokenizer.Tokenize(q)
if err == nil {
ck["question_tks"] = tks
} else {
ck["question_tks"] = q
}
}
delete(ck, "questions")
}
func processChunkKeywords(ck map[string]any) {
if _, exists := ck["keywords"]; !exists {
return
}
if _, hasTks := ck["important_tks"]; !hasTks {
kws, _ := ck["keywords"].(string)
ck["important_kwd"] = SplitKeywords(kws)
tks, err := tokenizer.Tokenize(kws)
if err == nil {
ck["important_tks"] = tks
} else {
ck["important_tks"] = kws
}
}
delete(ck, "keywords")
}
func processChunkSummary(ck map[string]any) {
if _, exists := ck["summary"]; !exists {
return
}
if _, hasLtks := ck["content_ltks"]; !hasLtks {
smmry, _ := ck["summary"].(string)
ltks, err := tokenizer.Tokenize(smmry)
if err == nil {
ck["content_ltks"] = ltks
} else {
ck["content_ltks"] = smmry
}
smLtks, err := tokenizer.FineGrainedTokenize(ck["content_ltks"].(string))
if err == nil {
ck["content_sm_ltks"] = smLtks
} else {
ck["content_sm_ltks"] = ck["content_ltks"].(string)
}
}
delete(ck, "summary")
}
func mergeChunkMetadata(metadata map[string]any, ck map[string]any) map[string]any {
metaVal, exists := ck["metadata"]
if !exists {
return metadata
}
if metaMap, ok := metaVal.(map[string]any); ok {
metadata = utility.UpdateMetadataTo(metadata, metaMap)
}
delete(ck, "metadata")
return metadata
}
func processChunkPositions(ck map[string]any) {
poss, exists := ck["positions"]
if !exists {
return
}
if positions, ok := poss.([]float64); ok {
AddPositions(ck, positions)
}
delete(ck, "positions")
}