mirror of
https://github.com/infiniflow/ragflow.git
synced 2026-07-09 21:04:49 +08:00
## Summary Add `EnrichChunksWithDocMetadata` as a method on `MetadataService` that attaches document metadata to retrieval chunks in-place. Equivalent to Python's `enrich_chunks_with_document_metadata()` from `api/utils/reference_metadata_utils.py`. ### Usage ```go metadataSvc.EnrichChunksWithDocMetadata(chunks, tenantID, metadataFields) ``` ### Changes - **`service/metadata.go`**: Added `EnrichChunksWithDocMetadata` method - **`service/enrich_metadata_test.go`** (new): 7 test cases ### Algorithm 1. Collect unique `(kb_id, doc_id)` pairs from chunks 2. Fetch metadata from ES via `SearchMetadata(kbID, tenantID, docIDs)` 3. Attach `document_metadata` field to each matching chunk 4. Optionally filter to specified `metadataFields` ### Testing All 7 tests pass: ``` === RUN TestEnrichChunksWithDocMetadata_NoChunks --- PASS === RUN TestEnrichChunksWithDocMetadata_EmptyChunks --- PASS === RUN TestEnrichChunksWithDocMetadata_EmptyDocID --- PASS === RUN TestEnrichChunksWithDocMetadata_DuplicateDocIDs --- PASS === RUN TestEnrichChunksWithDocMetadata_MultipleKBs --- PASS === RUN TestEnrichChunksWithDocMetadata_WithMetadataFields --- PASS === RUN TestEnrichChunksWithDocMetadata_MixedFields --- PASS ``` Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
267 lines
8.0 KiB
Go
267 lines
8.0 KiB
Go
//
|
|
// Copyright 2026 The InfiniFlow Authors. All Rights Reserved.
|
|
//
|
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
// you may not use this file except in compliance with the License.
|
|
// You may obtain a copy of the License at
|
|
//
|
|
// http://www.apache.org/licenses/LICENSE-2.0
|
|
//
|
|
// Unless required by applicable law or agreed to in writing, software
|
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
// See the License for the specific language governing permissions and
|
|
// limitations under the License.
|
|
//
|
|
|
|
package service
|
|
|
|
import (
|
|
"testing"
|
|
)
|
|
|
|
// --- extractDocID ---
|
|
|
|
func TestExtractDocID_FromID(t *testing.T) {
|
|
chunk := map[string]interface{}{"id": "doc1", "doc_id": "doc2"}
|
|
if got := extractDocID(chunk); got != "doc1" {
|
|
t.Errorf("expected doc1, got %q", got)
|
|
}
|
|
}
|
|
|
|
func TestExtractDocID_FromDocID(t *testing.T) {
|
|
chunk := map[string]interface{}{"doc_id": "doc2"}
|
|
if got := extractDocID(chunk); got != "doc2" {
|
|
t.Errorf("expected doc2, got %q", got)
|
|
}
|
|
}
|
|
|
|
func TestExtractDocID_Empty(t *testing.T) {
|
|
chunk := map[string]interface{}{"title": "no id"}
|
|
if got := extractDocID(chunk); got != "" {
|
|
t.Errorf("expected empty, got %q", got)
|
|
}
|
|
}
|
|
|
|
// --- ConvertSearchResultToDocMeta ---
|
|
|
|
func TestConvertSearchResultToDocMeta_Empty(t *testing.T) {
|
|
result := ConvertSearchResultToDocMeta(nil)
|
|
if len(result) != 0 {
|
|
t.Errorf("expected empty, got %d", len(result))
|
|
}
|
|
}
|
|
|
|
func TestConvertSearchResultToDocMeta_Single(t *testing.T) {
|
|
chunks := []map[string]interface{}{
|
|
{"id": "doc1", "meta_fields": map[string]interface{}{"author": "Zhang San"}},
|
|
}
|
|
result := ConvertSearchResultToDocMeta(chunks)
|
|
if len(result) != 1 {
|
|
t.Fatalf("expected 1 doc, got %d", len(result))
|
|
}
|
|
if result["doc1"]["author"] != "Zhang San" {
|
|
t.Errorf("expected 'Zhang San', got %v", result["doc1"]["author"])
|
|
}
|
|
}
|
|
|
|
func TestConvertSearchResultToDocMeta_Multiple(t *testing.T) {
|
|
chunks := []map[string]interface{}{
|
|
{"id": "doc1", "meta_fields": map[string]interface{}{"author": "Zhang San"}},
|
|
{"id": "doc2", "meta_fields": map[string]interface{}{"author": "Li Si"}},
|
|
}
|
|
result := ConvertSearchResultToDocMeta(chunks)
|
|
if len(result) != 2 {
|
|
t.Fatalf("expected 2 docs, got %d", len(result))
|
|
}
|
|
}
|
|
|
|
func TestConvertSearchResultToDocMeta_SkipEmptyDocID(t *testing.T) {
|
|
chunks := []map[string]interface{}{
|
|
{"meta_fields": map[string]interface{}{"author": "Zhang San"}},
|
|
}
|
|
result := ConvertSearchResultToDocMeta(chunks)
|
|
if len(result) != 0 {
|
|
t.Errorf("expected empty for missing doc_id, got %d", len(result))
|
|
}
|
|
}
|
|
|
|
func TestConvertSearchResultToDocMeta_SkipEmptyMeta(t *testing.T) {
|
|
chunks := []map[string]interface{}{
|
|
{"id": "doc1"},
|
|
}
|
|
result := ConvertSearchResultToDocMeta(chunks)
|
|
if len(result) != 0 {
|
|
t.Errorf("expected empty for missing meta_fields, got %d", len(result))
|
|
}
|
|
}
|
|
|
|
func TestConvertSearchResultToDocMeta_LastWins(t *testing.T) {
|
|
chunks := []map[string]interface{}{
|
|
{"id": "doc1", "meta_fields": map[string]interface{}{"author": "Zhang San"}},
|
|
{"id": "doc1", "meta_fields": map[string]interface{}{"author": "Li Si"}},
|
|
}
|
|
result := ConvertSearchResultToDocMeta(chunks)
|
|
if result["doc1"]["author"] != "Li Si" {
|
|
t.Errorf("expected last value 'Li Si', got %v", result["doc1"]["author"])
|
|
}
|
|
}
|
|
|
|
// --- CollectDocIDsByKB ---
|
|
|
|
func TestCollectDocIDsByKB_Empty(t *testing.T) {
|
|
result := CollectDocIDsByKB(nil)
|
|
if len(result) != 0 {
|
|
t.Errorf("expected empty, got %v", result)
|
|
}
|
|
}
|
|
|
|
func TestCollectDocIDsByKB_Single(t *testing.T) {
|
|
chunks := []map[string]interface{}{
|
|
{"doc_id": "doc1", "kb_id": "kb1"},
|
|
}
|
|
result := CollectDocIDsByKB(chunks)
|
|
if len(result) != 1 || len(result["kb1"]) != 1 || result["kb1"][0] != "doc1" {
|
|
t.Errorf("unexpected: %v", result)
|
|
}
|
|
}
|
|
|
|
func TestCollectDocIDsByKB_Dedup(t *testing.T) {
|
|
chunks := []map[string]interface{}{
|
|
{"doc_id": "doc1", "kb_id": "kb1"},
|
|
{"doc_id": "doc1", "kb_id": "kb1"},
|
|
{"doc_id": "doc1", "kb_id": "kb1"},
|
|
}
|
|
result := CollectDocIDsByKB(chunks)
|
|
if len(result["kb1"]) != 1 {
|
|
t.Errorf("expected 1 doc after dedup, got %v", result["kb1"])
|
|
}
|
|
}
|
|
|
|
func TestCollectDocIDsByKB_MultipleKBs(t *testing.T) {
|
|
chunks := []map[string]interface{}{
|
|
{"doc_id": "doc1", "kb_id": "kb1"},
|
|
{"doc_id": "doc2", "kb_id": "kb2"},
|
|
}
|
|
result := CollectDocIDsByKB(chunks)
|
|
if len(result) != 2 {
|
|
t.Errorf("expected 2 KBs, got %d", len(result))
|
|
}
|
|
if len(result["kb1"]) != 1 || result["kb1"][0] != "doc1" {
|
|
t.Errorf("unexpected kb1: %v", result["kb1"])
|
|
}
|
|
if len(result["kb2"]) != 1 || result["kb2"][0] != "doc2" {
|
|
t.Errorf("unexpected kb2: %v", result["kb2"])
|
|
}
|
|
}
|
|
|
|
func TestCollectDocIDsByKB_UsesIDField(t *testing.T) {
|
|
chunks := []map[string]interface{}{
|
|
{"id": "doc1", "kb_id": "kb1"},
|
|
}
|
|
result := CollectDocIDsByKB(chunks)
|
|
if len(result["kb1"]) != 1 || result["kb1"][0] != "doc1" {
|
|
t.Errorf("expected doc1 from id field, got %v", result["kb1"])
|
|
}
|
|
}
|
|
|
|
func TestCollectDocIDsByKB_PrefersIDOverDocID(t *testing.T) {
|
|
chunks := []map[string]interface{}{
|
|
{"id": "doc-from-id", "doc_id": "doc-from-doc-id", "kb_id": "kb1"},
|
|
}
|
|
result := CollectDocIDsByKB(chunks)
|
|
if result["kb1"][0] != "doc-from-id" {
|
|
t.Errorf("expected doc-from-id (id takes precedence), got %v", result["kb1"])
|
|
}
|
|
}
|
|
|
|
func TestCollectDocIDsByKB_SkipEmpty(t *testing.T) {
|
|
chunks := []map[string]interface{}{
|
|
{"doc_id": "", "kb_id": "kb1"},
|
|
{"doc_id": "doc1", "kb_id": ""},
|
|
{},
|
|
}
|
|
result := CollectDocIDsByKB(chunks)
|
|
if len(result) != 0 {
|
|
t.Errorf("expected empty, got %v", result)
|
|
}
|
|
}
|
|
|
|
// --- AttachDocMetaToChunks ---
|
|
|
|
func TestAttachDocMetaToChunks_NoMatch(t *testing.T) {
|
|
chunks := []map[string]interface{}{{"doc_id": "doc1"}}
|
|
metaByDoc := DocMetaMap{"doc2": {"author": "Zhang San"}}
|
|
AttachDocMetaToChunks(chunks, metaByDoc, nil)
|
|
if _, ok := chunks[0]["document_metadata"]; ok {
|
|
t.Error("should not attach metadata for no match")
|
|
}
|
|
}
|
|
|
|
func TestAttachDocMetaToChunks_Match(t *testing.T) {
|
|
chunks := []map[string]interface{}{{"doc_id": "doc1"}}
|
|
metaByDoc := DocMetaMap{"doc1": {"author": "Zhang San", "date": "2024-01-01"}}
|
|
AttachDocMetaToChunks(chunks, metaByDoc, nil)
|
|
meta, ok := chunks[0]["document_metadata"].(map[string]interface{})
|
|
if !ok {
|
|
t.Fatal("expected document_metadata")
|
|
}
|
|
if meta["author"] != "Zhang San" {
|
|
t.Errorf("expected 'Zhang San', got %v", meta["author"])
|
|
}
|
|
if meta["date"] != "2024-01-01" {
|
|
t.Errorf("expected '2024-01-01', got %v", meta["date"])
|
|
}
|
|
}
|
|
|
|
func TestAttachDocMetaToChunks_FilterFields(t *testing.T) {
|
|
chunks := []map[string]interface{}{{"doc_id": "doc1"}}
|
|
metaByDoc := DocMetaMap{"doc1": {"author": "Zhang San", "date": "2024-01-01", "category": "A"}}
|
|
AttachDocMetaToChunks(chunks, metaByDoc, []string{"author", "date"})
|
|
meta, ok := chunks[0]["document_metadata"].(map[string]interface{})
|
|
if !ok {
|
|
t.Fatal("expected document_metadata")
|
|
}
|
|
if len(meta) != 2 {
|
|
t.Errorf("expected 2 fields, got %d: %v", len(meta), meta)
|
|
}
|
|
if _, ok := meta["category"]; ok {
|
|
t.Error("category should be filtered out")
|
|
}
|
|
}
|
|
|
|
func TestAttachDocMetaToChunks_UsesIDField(t *testing.T) {
|
|
chunks := []map[string]interface{}{{"id": "doc1"}}
|
|
metaByDoc := DocMetaMap{"doc1": {"author": "Zhang San"}}
|
|
AttachDocMetaToChunks(chunks, metaByDoc, nil)
|
|
meta, ok := chunks[0]["document_metadata"].(map[string]interface{})
|
|
if !ok {
|
|
t.Fatal("expected document_metadata when chunk uses id field")
|
|
}
|
|
if meta["author"] != "Zhang San" {
|
|
t.Errorf("expected 'Zhang San', got %v", meta["author"])
|
|
}
|
|
}
|
|
|
|
func TestAttachDocMetaToChunks_EmptyMeta(t *testing.T) {
|
|
chunks := []map[string]interface{}{{"doc_id": "doc1"}}
|
|
AttachDocMetaToChunks(chunks, nil, nil)
|
|
if _, ok := chunks[0]["document_metadata"]; ok {
|
|
t.Error("should not attach when metaByDoc is empty")
|
|
}
|
|
}
|
|
|
|
// --- EnrichChunksWithDocMetadata (integration) ---
|
|
|
|
func TestEnrichChunksWithDocMetadata_NoChunks(t *testing.T) {
|
|
svc := NewMetadataService()
|
|
svc.EnrichChunksWithDocMetadata(nil, "tenant-1", nil)
|
|
// Should not panic
|
|
}
|
|
|
|
func TestEnrichChunksWithDocMetadata_EmptyChunks(t *testing.T) {
|
|
svc := NewMetadataService()
|
|
svc.EnrichChunksWithDocMetadata([]map[string]interface{}{}, "tenant-1", nil)
|
|
// Should not panic
|
|
}
|