mirror of
https://github.com/infiniflow/ragflow.git
synced 2026-07-09 04:44:49 +08:00
### Summary 1. update doc 2. refactor route code --------- Signed-off-by: Jin Hai <haijin.chn@gmail.com>
444 lines
14 KiB
Go
444 lines
14 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 graph
|
|
|
|
import (
|
|
"context"
|
|
"testing"
|
|
|
|
"ragflow/internal/engine"
|
|
"ragflow/internal/engine/types"
|
|
modelModule "ragflow/internal/entity/models"
|
|
)
|
|
|
|
// --- buildEntitySearchRequest ---
|
|
|
|
func TestBuildEntitySearchRequest_TextOnly(t *testing.T) {
|
|
req := buildEntitySearchRequest([]string{"kb1"}, "Elon Musk", nil, 10)
|
|
if req == nil {
|
|
t.Fatal("expected non-nil request")
|
|
}
|
|
if req.Filter["knowledge_graph_kwd"] != "entity" {
|
|
t.Errorf("expected 'entity' filter, got %v", req.Filter["knowledge_graph_kwd"])
|
|
}
|
|
if req.Limit != 10 {
|
|
t.Errorf("expected limit 10, got %d", req.Limit)
|
|
}
|
|
if len(req.MatchExprs) != 1 {
|
|
t.Fatalf("expected 1 MatchExpr (text only), got %d", len(req.MatchExprs))
|
|
}
|
|
if _, ok := req.MatchExprs[0].(*types.MatchTextExpr); !ok {
|
|
t.Error("expected MatchTextExpr")
|
|
}
|
|
}
|
|
|
|
func TestBuildEntitySearchRequest_EmptyQuestion(t *testing.T) {
|
|
req := buildEntitySearchRequest([]string{"kb1"}, "", nil, 10)
|
|
if len(req.MatchExprs) != 0 {
|
|
t.Error("expected no MatchExprs for empty question")
|
|
}
|
|
}
|
|
|
|
func TestBuildEntitySearchRequest_Hybrid(t *testing.T) {
|
|
dense := &types.MatchDenseExpr{
|
|
VectorColumnName: "q_768_vec",
|
|
EmbeddingData: []float64{0.1, 0.2, 0.3},
|
|
EmbeddingDataType: "float",
|
|
DistanceType: "cosine",
|
|
TopN: 10,
|
|
}
|
|
req := buildEntitySearchRequest([]string{"kb1"}, "test question", dense, 10)
|
|
if len(req.MatchExprs) != 3 {
|
|
t.Fatalf("expected 3 MatchExprs (dense + text + fusion), got %d", len(req.MatchExprs))
|
|
}
|
|
if _, ok := req.MatchExprs[0].(*types.MatchDenseExpr); !ok {
|
|
t.Error("expected MatchDenseExpr at [0]")
|
|
}
|
|
if _, ok := req.MatchExprs[1].(*types.MatchTextExpr); !ok {
|
|
t.Error("expected MatchTextExpr at [1]")
|
|
}
|
|
fusion, ok := req.MatchExprs[2].(*types.FusionExpr)
|
|
if !ok {
|
|
t.Fatal("expected FusionExpr at [2]")
|
|
}
|
|
if fusion.Method != "weighted_sum" {
|
|
t.Errorf("expected 'weighted_sum', got %q", fusion.Method)
|
|
}
|
|
}
|
|
|
|
// --- buildEntityTypeSearchRequest ---
|
|
|
|
func TestBuildEntityTypeSearchRequest_Basic(t *testing.T) {
|
|
req := buildEntityTypeSearchRequest([]string{"kb1"}, []string{"PERSON", "ORGANIZATION"}, 10)
|
|
if req.Filter["knowledge_graph_kwd"] != "entity" {
|
|
t.Errorf("expected 'entity' filter, got %v", req.Filter["knowledge_graph_kwd"])
|
|
}
|
|
filter, ok := req.Filter["entity_type_kwd"].([]interface{})
|
|
if !ok || len(filter) != 2 {
|
|
t.Errorf("expected 2 entity_type filters, got %v", filter)
|
|
}
|
|
}
|
|
|
|
func TestBuildEntityTypeSearchRequest_EmptyTypes(t *testing.T) {
|
|
req := buildEntityTypeSearchRequest([]string{"kb1"}, nil, 10)
|
|
if _, ok := req.Filter["entity_type_kwd"]; ok {
|
|
t.Error("expected no entity_type_kwd filter for empty types")
|
|
}
|
|
}
|
|
|
|
// --- buildRelationSearchRequest ---
|
|
|
|
func TestBuildRelationSearchRequest_Basic(t *testing.T) {
|
|
req := buildRelationSearchRequest([]string{"kb1"}, "test", nil, 10)
|
|
if req.Filter["knowledge_graph_kwd"] != "relation" {
|
|
t.Errorf("expected 'relation' filter, got %v", req.Filter["knowledge_graph_kwd"])
|
|
}
|
|
}
|
|
|
|
func TestBuildRelationSearchRequest_EmptyQuestion(t *testing.T) {
|
|
req := buildRelationSearchRequest([]string{"kb1"}, "", nil, 10)
|
|
if len(req.MatchExprs) != 0 {
|
|
t.Error("expected no MatchExprs for empty question")
|
|
}
|
|
}
|
|
|
|
func TestBuildRelationSearchRequest_Hybrid(t *testing.T) {
|
|
dense := &types.MatchDenseExpr{
|
|
VectorColumnName: "q_768_vec",
|
|
EmbeddingData: []float64{0.1, 0.2},
|
|
TopN: 5,
|
|
}
|
|
req := buildRelationSearchRequest([]string{"kb1"}, "test", dense, 5)
|
|
if len(req.MatchExprs) != 3 {
|
|
t.Fatalf("expected 3 MatchExprs (dense + text + fusion), got %d", len(req.MatchExprs))
|
|
}
|
|
}
|
|
|
|
// --- buildCommunitySearchRequest ---
|
|
|
|
func TestBuildCommunitySearchRequest_Basic(t *testing.T) {
|
|
req := buildCommunitySearchRequest([]string{"kb1"}, []string{"Elon Musk"}, 5)
|
|
if req.Filter["knowledge_graph_kwd"] != "community_report" {
|
|
t.Errorf("expected 'community_report' filter, got %v", req.Filter["knowledge_graph_kwd"])
|
|
}
|
|
if req.OrderBy == nil {
|
|
t.Error("expected OrderBy")
|
|
}
|
|
}
|
|
|
|
func TestBuildCommunitySearchRequest_EmptyNames(t *testing.T) {
|
|
req := buildCommunitySearchRequest([]string{"kb1"}, nil, 5)
|
|
if _, ok := req.Filter["entities_kwd"]; ok {
|
|
t.Error("expected no entities_kwd filter for empty names")
|
|
}
|
|
}
|
|
|
|
// --- buildTypeSamplesSearchRequest ---
|
|
|
|
func TestBuildTypeSamplesSearchRequest(t *testing.T) {
|
|
req := buildTypeSamplesSearchRequest([]string{"kb1"})
|
|
if req.Filter["knowledge_graph_kwd"] != "ty2ents" {
|
|
t.Errorf("expected 'ty2ents' filter, got %v", req.Filter["knowledge_graph_kwd"])
|
|
}
|
|
if req.Limit != 10000 {
|
|
t.Errorf("expected 10000, got %d", req.Limit)
|
|
}
|
|
}
|
|
|
|
// --- ParseEntityChunks ---
|
|
|
|
func TestParseEntityChunks_Basic(t *testing.T) {
|
|
chunks := []map[string]interface{}{
|
|
{"entity_kwd": "Elon Musk", "entity_type_kwd": "PERSON", "rank_flt": 0.9, "_score": 0.85, "content_with_weight": "Founder of SpaceX"},
|
|
}
|
|
entities := ParseEntityChunks(chunks)
|
|
if len(entities) != 1 {
|
|
t.Fatalf("expected 1, got %d", len(entities))
|
|
}
|
|
if entities[0].Name != "Elon Musk" || entities[0].Type != "PERSON" || entities[0].PageRank != 0.9 || entities[0].Similarity != 0.85 {
|
|
t.Errorf("unexpected entity fields: %+v", entities[0])
|
|
}
|
|
}
|
|
|
|
func TestParseEntityChunks_List(t *testing.T) {
|
|
chunks := []map[string]interface{}{
|
|
{"entity_kwd": []interface{}{"Elon Musk", "elon_musk"}},
|
|
}
|
|
entities := ParseEntityChunks(chunks)
|
|
if len(entities) != 1 || entities[0].Name != "Elon Musk" {
|
|
t.Errorf("expected first list element, got %q", entities[0].Name)
|
|
}
|
|
}
|
|
|
|
func TestParseEntityChunks_EmptyName(t *testing.T) {
|
|
chunks := []map[string]interface{}{{"entity_type_kwd": "PERSON"}}
|
|
if len(ParseEntityChunks(chunks)) != 0 {
|
|
t.Error("expected 0 for missing name")
|
|
}
|
|
}
|
|
|
|
func TestParseEntityChunks_ScoreFallback(t *testing.T) {
|
|
chunks := []map[string]interface{}{{"entity_kwd": "Test", "score": 0.75}}
|
|
if ParseEntityChunks(chunks)[0].Similarity != 0.75 {
|
|
t.Error("expected 0.75 from score field")
|
|
}
|
|
}
|
|
|
|
func TestParseEntityChunks_NilInput(t *testing.T) {
|
|
if len(ParseEntityChunks(nil)) != 0 {
|
|
t.Error("expected 0 for nil input")
|
|
}
|
|
}
|
|
|
|
// --- ParseRelationChunks ---
|
|
|
|
func TestParseRelationChunks_Basic(t *testing.T) {
|
|
chunks := []map[string]interface{}{
|
|
{"from_entity_kwd": "Elon Musk", "to_entity_kwd": "SpaceX", "weight_int": float64(5), "content_with_weight": "Founder"},
|
|
}
|
|
relations := ParseRelationChunks(chunks)
|
|
if len(relations) != 1 || relations[0].From != "Elon Musk" || relations[0].PageRank != 5 {
|
|
t.Errorf("unexpected: %+v", relations[0])
|
|
}
|
|
}
|
|
|
|
func TestParseRelationChunks_IntWeight(t *testing.T) {
|
|
chunks := []map[string]interface{}{{"from_entity_kwd": "A", "to_entity_kwd": "B", "weight_int": 3}}
|
|
if ParseRelationChunks(chunks)[0].PageRank != 3 {
|
|
t.Error("expected weight 3")
|
|
}
|
|
}
|
|
|
|
func TestParseRelationChunks_EmptyFrom(t *testing.T) {
|
|
if len(ParseRelationChunks([]map[string]interface{}{{"to_entity_kwd": "B"}})) != 0 {
|
|
t.Error("expected 0 for missing from")
|
|
}
|
|
}
|
|
|
|
func TestParseRelationChunks_NilInput(t *testing.T) {
|
|
if len(ParseRelationChunks(nil)) != 0 {
|
|
t.Error("expected 0 for nil")
|
|
}
|
|
}
|
|
|
|
// --- ParseCommunityReportChunks ---
|
|
|
|
func TestParseCommunityReportChunks_Basic(t *testing.T) {
|
|
chunks := []map[string]interface{}{
|
|
{"docnm_kwd": "Report 1", "content_with_weight": "content", "weight_flt": 0.95, "entities_kwd": "A, B"},
|
|
}
|
|
reports := ParseCommunityReportChunks(chunks)
|
|
if len(reports) != 1 || reports[0].Title != "Report 1" || reports[0].Weight != 0.95 {
|
|
t.Errorf("unexpected: %+v", reports[0])
|
|
}
|
|
}
|
|
|
|
func TestParseCommunityReportChunks_EmptyTitle(t *testing.T) {
|
|
if len(ParseCommunityReportChunks([]map[string]interface{}{{"weight_flt": 0.5}})) != 0 {
|
|
t.Error("expected 0 for empty title and content")
|
|
}
|
|
}
|
|
|
|
func TestParseCommunityReportChunks_NilInput(t *testing.T) {
|
|
if len(ParseCommunityReportChunks(nil)) != 0 {
|
|
t.Error("expected 0 for nil")
|
|
}
|
|
}
|
|
|
|
// --- ParseTypeSamplesChunks ---
|
|
|
|
func TestParseTypeSamplesChunks_ValidJSON(t *testing.T) {
|
|
chunks := []map[string]interface{}{
|
|
{"content_with_weight": `{"PERSON": ["Elon Musk", "Einstein"], "ORGANIZATION": ["SpaceX"]}`},
|
|
}
|
|
result := ParseTypeSamplesChunks(chunks)
|
|
if len(result) != 2 {
|
|
t.Fatalf("expected 2 types, got %d: %v", len(result), result)
|
|
}
|
|
if len(result["PERSON"]) != 2 || result["PERSON"][0] != "Elon Musk" {
|
|
t.Errorf("expected PERSON entities, got %v", result["PERSON"])
|
|
}
|
|
if len(result["ORGANIZATION"]) != 1 || result["ORGANIZATION"][0] != "SpaceX" {
|
|
t.Errorf("expected ORGANIZATION entities, got %v", result["ORGANIZATION"])
|
|
}
|
|
}
|
|
|
|
func TestParseTypeSamplesChunks_InvalidJSON(t *testing.T) {
|
|
chunks := []map[string]interface{}{
|
|
{"content_with_weight": "not json"},
|
|
}
|
|
result := ParseTypeSamplesChunks(chunks)
|
|
if len(result) != 0 {
|
|
t.Error("expected empty for invalid JSON")
|
|
}
|
|
}
|
|
|
|
func TestParseTypeSamplesChunks_Empty(t *testing.T) {
|
|
result := ParseTypeSamplesChunks(nil)
|
|
if len(result) != 0 {
|
|
t.Error("expected empty for nil")
|
|
}
|
|
}
|
|
|
|
// --- NhopEntityNames ---
|
|
|
|
func TestNhopEntityNames_ValidJSON(t *testing.T) {
|
|
input := `[{"path": ["A", "B", "C"], "weights": [0.8, 0.5]}, {"path": ["C", "D"], "weights": [0.3]}]`
|
|
names := NhopEntityNames(input)
|
|
if len(names) != 4 {
|
|
t.Fatalf("expected 4 unique names, got %d: %v", len(names), names)
|
|
}
|
|
}
|
|
|
|
func TestNhopEntityNames_Dedup(t *testing.T) {
|
|
input := `[{"path": ["A", "B"], "weights": [0.9]}, {"path": ["A", "C"], "weights": [0.8]}]`
|
|
names := NhopEntityNames(input)
|
|
if len(names) != 3 {
|
|
t.Errorf("expected 3 unique names (A,B,C), got %d: %v", len(names), names)
|
|
}
|
|
}
|
|
|
|
func TestNhopEntityNames_InvalidJSON(t *testing.T) {
|
|
result := NhopEntityNames("not json")
|
|
if result != nil {
|
|
t.Error("expected nil for invalid JSON")
|
|
}
|
|
}
|
|
|
|
// --- Mock engine ---
|
|
|
|
type mockKGEngine struct {
|
|
engine.DocEngine
|
|
searchFunc func(ctx context.Context, req *types.SearchRequest) (*types.SearchResult, error)
|
|
}
|
|
|
|
func (m *mockKGEngine) Search(ctx context.Context, req *types.SearchRequest) (*types.SearchResult, error) {
|
|
if m.searchFunc != nil {
|
|
return m.searchFunc(ctx, req)
|
|
}
|
|
return &types.SearchResult{}, nil
|
|
}
|
|
|
|
// --- Mock model driver for Embed tests ---
|
|
|
|
type fakeEmbedDriver struct {
|
|
modelModule.ModelDriver
|
|
name string
|
|
vector []float64
|
|
}
|
|
|
|
func (f *fakeEmbedDriver) Name() string { return f.name }
|
|
|
|
func (f *fakeEmbedDriver) Embed(modelName *string, texts []string, apiConfig *modelModule.APIConfig, config *modelModule.EmbeddingConfig) ([]modelModule.EmbeddingData, error) {
|
|
return []modelModule.EmbeddingData{{Embedding: f.vector}}, nil
|
|
}
|
|
|
|
func TestBuildKGDenseExpr_WithModel(t *testing.T) {
|
|
embModel := &modelModule.EmbeddingModel{
|
|
ModelName: strPtr("test-model"),
|
|
ModelDriver: &fakeEmbedDriver{
|
|
name: "test",
|
|
vector: []float64{0.1, 0.2, 0.3},
|
|
},
|
|
APIConfig: &modelModule.APIConfig{},
|
|
}
|
|
dense, err := buildDenseExpr(embModel, "test question", 10)
|
|
if err != nil {
|
|
t.Fatalf("buildDenseExpr failed: %v", err)
|
|
}
|
|
if dense == nil {
|
|
t.Fatal("expected non-nil MatchDenseExpr")
|
|
}
|
|
if dense.VectorColumnName != "q_3_vec" {
|
|
t.Errorf("expected 'q_3_vec', got %q", dense.VectorColumnName)
|
|
}
|
|
if dense.TopN != 10 {
|
|
t.Errorf("expected TopN 10, got %d", dense.TopN)
|
|
}
|
|
}
|
|
|
|
func TestBuildKGDenseExpr_NilModel(t *testing.T) {
|
|
dense, err := buildDenseExpr(nil, "test", 10)
|
|
if dense != nil || err != nil {
|
|
t.Errorf("expected nil,nil for nil model, got dense=%v err=%v", dense, err)
|
|
}
|
|
}
|
|
|
|
func TestBuildKGDenseExpr_EmptyQuestion(t *testing.T) {
|
|
dense, err := buildDenseExpr(&modelModule.EmbeddingModel{}, "", 10)
|
|
if dense != nil || err != nil {
|
|
t.Errorf("expected nil,nil for empty question, got dense=%v err=%v", dense, err)
|
|
}
|
|
}
|
|
|
|
// --- Search integration with mock ---
|
|
|
|
func TestSearchEntities_WithMock(t *testing.T) {
|
|
mock := &mockKGEngine{
|
|
searchFunc: func(ctx context.Context, req *types.SearchRequest) (*types.SearchResult, error) {
|
|
if req.Filter["knowledge_graph_kwd"] != "entity" {
|
|
t.Error("expected entity filter")
|
|
}
|
|
return &types.SearchResult{
|
|
Chunks: []map[string]interface{}{
|
|
{"entity_kwd": "Elon Musk", "entity_type_kwd": "PERSON"},
|
|
},
|
|
}, nil
|
|
},
|
|
}
|
|
entities, err := SearchEntities(context.Background(), mock, []string{"kb1"}, "Elon", nil, 10)
|
|
if err != nil {
|
|
t.Fatalf("SearchEntities failed: %v", err)
|
|
}
|
|
if len(entities) != 1 || entities[0].Name != "Elon Musk" {
|
|
t.Errorf("expected [Elon Musk], got %v", entities)
|
|
}
|
|
}
|
|
|
|
func TestSearchEntitiesByTypes_WithMock(t *testing.T) {
|
|
mock := &mockKGEngine{
|
|
searchFunc: func(ctx context.Context, req *types.SearchRequest) (*types.SearchResult, error) {
|
|
return &types.SearchResult{
|
|
Chunks: []map[string]interface{}{
|
|
{"entity_kwd": "SpaceX", "entity_type_kwd": "ORGANIZATION"},
|
|
},
|
|
}, nil
|
|
},
|
|
}
|
|
entities, err := SearchEntitiesByTypes(context.Background(), mock, []string{"kb1"}, []string{"ORGANIZATION"}, 10)
|
|
if err != nil {
|
|
t.Fatalf("SearchEntitiesByTypes failed: %v", err)
|
|
}
|
|
if len(entities) != 1 || entities[0].Type != "ORGANIZATION" {
|
|
t.Errorf("expected ORGANIZATION, got %v", entities)
|
|
}
|
|
}
|
|
|
|
func TestSearchTypeSamples_WithMock(t *testing.T) {
|
|
mock := &mockKGEngine{}
|
|
samples, err := SearchTypeSamples(context.Background(), mock, []string{"kb1"})
|
|
if err != nil {
|
|
t.Fatalf("SearchTypeSamples failed: %v", err)
|
|
}
|
|
if samples == nil {
|
|
samples = map[string][]string{}
|
|
}
|
|
if len(samples) != 0 {
|
|
t.Errorf("expected empty, got %d", len(samples))
|
|
}
|
|
}
|