Files
ragflow/internal/service/dataset_artifact_service.go

847 lines
28 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.
//
// 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 (
"context"
"encoding/json"
"fmt"
"sort"
"ragflow/internal/engine"
"ragflow/internal/engine/types"
)
// Compile keyword constants used by the knowledge-compilation artifacts stored
// in the document engine.
const (
CompileKwdWikiPage = "wiki_page"
CompileKwdWikiEntity = "wiki_entity"
CompileKwdWikiRelation = "wiki_relation"
CompileKwdWikiAlter = "wiki_alter"
CompileKwdSkill = "skill"
CompileKwdSkillAll = "skill_all"
CompileKwdDatasetNav = "dataset_nav"
CompileKwdRaptorGraph = "raptor_graph"
// Structure / graph compilation keywords.
CompileKwdStructure = "structure"
CompileKwdStructureIndex = "structureIndex"
CompileKwdStructureEntity = "structureEntity"
CompileKwdStructureRelation = "structureRelation"
CompileKwdStructureCommunity = "structureCommunity"
// Field name for the structure index type discriminator.
FieldStructureIndexType = "structure_index_type"
FieldStructureKind = "structure_kind"
FieldPageID = "page_id"
FieldGraphType = "graph_type"
)
// DatasetArtifactService reads knowledge-compilation artifacts (wiki pages,
// graphs, structures, navigation, skills) from the document engine.
type DatasetArtifactService struct{}
// NewDatasetArtifactService creates a DatasetArtifactService.
func NewDatasetArtifactService() *DatasetArtifactService {
return &DatasetArtifactService{}
}
// wikiIndexName returns the tenant document index name.
func wikiIndexName(tenantID string) string {
return fmt.Sprintf("ragflow_%s", tenantID)
}
// searchCompiled runs a filtered search over the tenant's document index for
// the given dataset, returning the matching chunks.
func (s *DatasetArtifactService) searchCompiled(ctx context.Context, tenantID, datasetID string, filter map[string]interface{}, selectFields []string, offset, limit int, orderBy *types.OrderByExpr) ([]map[string]interface{}, int64, error) {
docEngine := engine.Get()
if docEngine == nil {
return nil, 0, fmt.Errorf("document engine is not initialized")
}
// Copy caller filters first, then pin the dataset scope so kb_id always wins
// even if a caller passes its own kb_id.
merged := make(map[string]interface{}, len(filter)+1)
for k, v := range filter {
merged[k] = v
}
merged["kb_id"] = []string{datasetID}
res, err := docEngine.Search(ctx, &types.SearchRequest{
IndexNames: []string{wikiIndexName(tenantID)},
KbIDs: []string{datasetID},
Offset: offset,
Limit: limit,
SelectFields: selectFields,
Filter: merged,
OrderBy: orderBy,
})
if err != nil {
return nil, 0, err
}
if res == nil {
return nil, 0, nil
}
return res.Chunks, res.Total, nil
}
// intValue coerces an engine field value into an int, tolerating the numeric
// types a document engine may decode a number as (float64, int, int64,
// json.Number) as well as a string form and a list-valued form (first element
// wins). It returns 0 for anything else.
func intValue(v interface{}) int {
if v == nil {
return 0
}
switch n := v.(type) {
case int:
return n
case int64:
return int(n)
case int32:
return int(n)
case float64:
return int(n)
case float32:
return int(n)
case json.Number:
if iv, err := n.Int64(); err == nil {
return int(iv)
}
if fv, err := n.Float64(); err == nil {
return int(fv)
}
case string:
var iv int
if _, err := fmt.Sscanf(n, "%d", &iv); err == nil {
return iv
}
case []interface{}:
if len(n) > 0 {
return intValue(n[0])
}
}
return 0
}
// HasWiki reports whether the dataset has any compiled wiki artifact.
func (s *DatasetArtifactService) HasWiki(ctx context.Context, tenantID, datasetID string) (bool, error) {
_, total, err := s.searchCompiled(ctx, tenantID, datasetID,
map[string]interface{}{"compile_kwd": []string{CompileKwdWikiPage}}, nil, 0, 1, nil)
if err != nil {
return false, err
}
return total > 0, nil
}
// HasSkill reports whether the dataset has any compiled skill artifact.
func (s *DatasetArtifactService) HasSkill(ctx context.Context, tenantID, datasetID string) (bool, error) {
_, total, err := s.searchCompiled(ctx, tenantID, datasetID,
map[string]interface{}{"compile_kwd": []string{CompileKwdSkillAll}}, nil, 0, 1, nil)
if err != nil {
return false, err
}
return total > 0, nil
}
// WikiPageItem is a single wiki page summary returned by ListWikiPages.
type WikiPageItem struct {
Slug string `json:"slug"`
Title string `json:"title"`
PageType string `json:"page_type"`
Topic string `json:"topic"`
Summary string `json:"summary"`
}
// ListWikiPages lists wiki pages for a dataset with optional page_type/topic
// filters and pagination.
func (s *DatasetArtifactService) ListWikiPages(ctx context.Context, tenantID, datasetID, pageType, topic string, page, pageSize int) ([]WikiPageItem, int64, error) {
filter := map[string]interface{}{"compile_kwd": []string{CompileKwdWikiPage}}
if pageType != "" {
filter["page_type_kwd"] = []string{pageType}
}
if topic != "" {
filter["topic_kwd"] = []string{topic}
}
offset := (page - 1) * pageSize
chunks, total, err := s.searchCompiled(ctx, tenantID, datasetID, filter,
[]string{"slug_kwd", "title_kwd", "page_type_kwd", "topic_kwd", "outlinks_int", "summary_with_weight"},
offset, pageSize, (&types.OrderByExpr{}).Desc("outlinks_int").Asc("title_kwd"))
if err != nil {
return nil, 0, err
}
items := make([]WikiPageItem, 0, len(chunks))
for _, c := range chunks {
items = append(items, WikiPageItem{
Slug: firstStringValue(c["slug_kwd"]),
Title: firstStringValue(c["title_kwd"]),
PageType: firstStringValue(c["page_type_kwd"]),
Topic: firstStringValue(c["topic_kwd"]),
Summary: firstStringValue(c["summary_with_weight"]),
})
}
return items, total, nil
}
// WikiPageDetail is the full wiki page payload.
type WikiPageDetail struct {
Slug string `json:"slug"`
Title string `json:"title"`
PageType string `json:"page_type"`
Topic string `json:"topic"`
ContentMd string `json:"content_md"`
Summary string `json:"summary"`
EntityNames []string `json:"entity_names"`
Outlinks []string `json:"outlinks"`
RelatedKbPages []string `json:"related_kb_pages"`
SourceChunkIDs []string `json:"source_chunk_ids"`
SourceDocIDs []string `json:"source_doc_ids"`
}
// GetWikiPage returns a single wiki page by page_type and slug.
func (s *DatasetArtifactService) GetWikiPage(ctx context.Context, tenantID, datasetID, pageType, slug string) (*WikiPageDetail, error) {
slugKwd := pageType + "/" + slug
filter := map[string]interface{}{
"compile_kwd": []string{CompileKwdWikiPage},
"page_type_kwd": []string{pageType},
"slug_kwd": []string{slugKwd},
}
chunks, _, err := s.searchCompiled(ctx, tenantID, datasetID, filter,
[]string{"slug_kwd", "title_kwd", "page_type_kwd", "topic_kwd", "content_with_weight",
"summary_with_weight", "entity_names_kwd", "outlinks_kwd", "related_kb_pages_kwd",
"source_chunk_ids", "source_doc_ids"},
0, 1, nil)
if err != nil {
return nil, err
}
if len(chunks) == 0 {
return nil, nil
}
c := chunks[0]
detail := &WikiPageDetail{
Slug: firstStringValue(c["slug_kwd"]),
Title: firstStringValue(c["title_kwd"]),
PageType: firstStringValue(c["page_type_kwd"]),
Topic: firstStringValue(c["topic_kwd"]),
ContentMd: firstStringValue(c["content_with_weight"]),
Summary: firstStringValue(c["summary_with_weight"]),
EntityNames: toStringSlice(c["entity_names_kwd"]),
Outlinks: toStringSlice(c["outlinks_kwd"]),
RelatedKbPages: toStringSlice(c["related_kb_pages_kwd"]),
SourceChunkIDs: toStringSlice(c["source_chunk_ids"]),
SourceDocIDs: toStringSlice(c["source_doc_ids"]),
}
return detail, nil
}
// UpdateWikiPage performs a partial field update of a wiki page's content,
// title and outlinks through the document engine, then returns the refreshed
// page.
func (s *DatasetArtifactService) UpdateWikiPage(ctx context.Context, tenantID, datasetID, pageType, slug, contentMd, title string, outlinks []string) (*WikiPageDetail, error) {
docEngine := engine.Get()
if docEngine == nil {
return nil, fmt.Errorf("document engine is not initialized")
}
slugKwd := pageType + "/" + slug
filter := map[string]interface{}{
"compile_kwd": []string{CompileKwdWikiPage},
"page_type_kwd": []string{pageType},
"slug_kwd": []string{slugKwd},
}
chunks, _, err := s.searchCompiled(ctx, tenantID, datasetID, filter, []string{"id"}, 0, 1, nil)
if err != nil {
return nil, err
}
if len(chunks) == 0 {
return nil, nil
}
id, ok := chunks[0]["id"].(string)
if !ok || id == "" {
return nil, fmt.Errorf("wiki page chunk has no id")
}
update := map[string]interface{}{}
if contentMd != "" {
update["content_with_weight"] = contentMd
}
if title != "" {
update["title_kwd"] = title
}
if len(outlinks) > 0 {
update["outlinks_kwd"] = outlinks
}
if len(update) > 0 {
cond := map[string]interface{}{"id": id, "kb_id": datasetID}
if err := docEngine.UpdateChunks(ctx, cond, update, wikiIndexName(tenantID), datasetID); err != nil {
return nil, err
}
}
return s.GetWikiPage(ctx, tenantID, datasetID, pageType, slug)
}
// WikiTopicItem is a single topic entry returned by ListWikiTopics.
type WikiTopicItem struct {
Topic string `json:"topic"`
Title string `json:"title"`
Slug string `json:"slug"`
PageCount int `json:"page_count"`
}
// ListWikiTopics aggregates wiki topics for a dataset.
func (s *DatasetArtifactService) ListWikiTopics(ctx context.Context, tenantID, datasetID string) ([]WikiTopicItem, int64, error) {
filter := map[string]interface{}{
"compile_kwd": []string{CompileKwdWikiPage},
"page_type_kwd": []string{"concept", "entity"},
}
chunks, _, err := s.searchCompiled(ctx, tenantID, datasetID, filter,
[]string{"topic_kwd", "title_kwd", "slug_kwd"}, 0, 1000, nil)
if err != nil {
return nil, 0, err
}
counts := map[string]int{}
metas := map[string]WikiTopicItem{}
for _, c := range chunks {
t := firstStringValue(c["topic_kwd"])
if t == "" {
continue
}
counts[t]++
if _, ok := metas[t]; !ok {
metas[t] = WikiTopicItem{
Topic: t,
Title: firstStringValue(c["title_kwd"]),
Slug: firstStringValue(c["slug_kwd"]),
}
}
}
items := make([]WikiTopicItem, 0, len(metas))
for t, it := range metas {
it.PageCount = counts[t]
items = append(items, it)
}
sort.Slice(items, func(i, j int) bool { return items[i].Topic < items[j].Topic })
return items, int64(len(items)), nil
}
// WikiGraph is the entity/relation graph for a dataset's wiki artifacts.
type WikiGraph struct {
Entities []WikiGraphEntity `json:"entities"`
Relations []WikiGraphRelation `json:"relations"`
}
// WikiGraphEntity is a single wiki graph entity.
type WikiGraphEntity struct {
Slug string `json:"slug"`
Name string `json:"name"`
Aliases []string `json:"aliases"`
Description string `json:"description"`
Type string `json:"type"`
Weight int `json:"weight"`
SourceChunkIDs []string `json:"source_chunk_ids"`
}
// WikiGraphRelation is a single wiki graph relation.
type WikiGraphRelation struct {
From string `json:"from"`
To string `json:"to"`
}
// GetWikiGraph returns the wiki entity/relation graph for a dataset.
func (s *DatasetArtifactService) GetWikiGraph(ctx context.Context, tenantID, datasetID string) (*WikiGraph, error) {
entityChunks, _, err := s.searchCompiled(ctx, tenantID, datasetID,
map[string]interface{}{"compile_kwd": []string{CompileKwdWikiEntity}},
[]string{"slug_kwd", "title_kwd", "aliases_kwd", "description_with_weight", "entity_type_kwd", "weight_int", "source_chunk_ids"},
0, 1000, (&types.OrderByExpr{}).Desc("weight_int"))
if err != nil {
return nil, err
}
fromKwds := make([]string, 0, len(entityChunks))
for _, c := range entityChunks {
fromKwds = append(fromKwds, firstStringValue(c["slug_kwd"]))
}
graph := &WikiGraph{Entities: []WikiGraphEntity{}, Relations: []WikiGraphRelation{}}
for _, c := range entityChunks {
w := intValue(c["weight_int"])
graph.Entities = append(graph.Entities, WikiGraphEntity{
Slug: firstStringValue(c["slug_kwd"]),
Name: firstStringValue(c["title_kwd"]),
Aliases: toStringSlice(c["aliases_kwd"]),
Description: firstStringValue(c["description_with_weight"]),
Type: firstStringValue(c["entity_type_kwd"]),
Weight: w,
SourceChunkIDs: toStringSlice(c["source_chunk_ids"]),
})
}
if len(fromKwds) > 0 {
relChunks, _, err := s.searchCompiled(ctx, tenantID, datasetID,
map[string]interface{}{"compile_kwd": []string{CompileKwdWikiRelation}, "from_kwd": fromKwds},
[]string{"from_kwd", "to_kwd"}, 0, 10000, nil)
if err != nil {
return nil, err
}
for _, c := range relChunks {
graph.Relations = append(graph.Relations, WikiGraphRelation{
From: firstStringValue(c["from_kwd"]),
To: firstStringValue(c["to_kwd"]),
})
}
}
return graph, nil
}
// WikiAlteration is the alteration summary for a dataset's wiki artifacts.
type WikiAlteration struct {
Removed int `json:"removed"`
NewlyUploaded int `json:"newly_uploaded"`
RemovedDocIDs []string `json:"removed_doc_ids"`
NewlyUploadedDocIDs []string `json:"newly_uploaded_doc_ids"`
InvolvedDocIDs []string `json:"involved_doc_ids"`
EligibleDocIDs []string `json:"eligible_doc_ids"`
}
// GetWikiAlteration returns the wiki alteration summary for a dataset.
func (s *DatasetArtifactService) GetWikiAlteration(ctx context.Context, tenantID, datasetID string) (*WikiAlteration, error) {
chunks, _, err := s.searchCompiled(ctx, tenantID, datasetID,
map[string]interface{}{"compile_kwd": []string{CompileKwdWikiPage}},
[]string{"source_doc_ids"}, 0, 10000, nil)
if err != nil {
return nil, err
}
involved := map[string]struct{}{}
for _, c := range chunks {
for _, d := range toStringSlice(c["source_doc_ids"]) {
involved[d] = struct{}{}
}
}
ids := make([]string, 0, len(involved))
for d := range involved {
ids = append(ids, d)
}
sort.Strings(ids)
return &WikiAlteration{
Removed: 0,
NewlyUploaded: 0,
RemovedDocIDs: []string{},
NewlyUploadedDocIDs: []string{},
InvolvedDocIDs: ids,
EligibleDocIDs: ids,
}, nil
}
// ClearWiki deletes all wiki artifacts for a dataset.
func (s *DatasetArtifactService) ClearWiki(ctx context.Context, tenantID, datasetID string) (map[string]int, error) {
docEngine := engine.Get()
if docEngine == nil {
return nil, fmt.Errorf("document engine is not initialized")
}
kwds := []string{CompileKwdWikiPage, CompileKwdWikiEntity, CompileKwdWikiRelation, CompileKwdWikiAlter}
deleted := map[string]int{}
for _, kwd := range kwds {
chunks, _, err := s.searchCompiled(ctx, tenantID, datasetID,
map[string]interface{}{"compile_kwd": []string{kwd}}, []string{"id"}, 0, 10000, nil)
if err != nil {
return nil, err
}
if len(chunks) == 0 {
deleted[kwd] = 0
continue
}
ids := make([]string, 0, len(chunks))
for _, c := range chunks {
if id, ok := c["id"].(string); ok {
ids = append(ids, id)
}
}
cond := map[string]interface{}{"id": ids, "kb_id": datasetID}
if _, err := docEngine.DeleteChunks(ctx, cond, wikiIndexName(tenantID), datasetID); err != nil {
return nil, err
}
deleted[kwd] = len(ids)
}
return deleted, nil
}
// StructureItem is a single compiled structure entry for a dataset.
type StructureItem struct {
PageID string `json:"page_id"`
StructureKind string `json:"structure_kind"`
StructureIndexType string `json:"structure_index_type"`
Data string `json:"data"`
}
// ListStructures returns the compiled structures of a dataset, filtered by
// optional structure_kind and structure_index_type.
func (s *DatasetArtifactService) ListStructures(ctx context.Context, tenantID, datasetID, structureKind, structureIndexType string) ([]StructureItem, int64, error) {
filter := map[string]interface{}{"compile_kwd": []string{CompileKwdStructure}}
if structureKind != "" {
filter[FieldStructureKind] = []string{structureKind}
}
if structureIndexType != "" {
filter[FieldStructureIndexType] = []string{structureIndexType}
}
chunks, total, err := s.searchCompiled(ctx, tenantID, datasetID, filter,
[]string{FieldPageID, FieldStructureKind, FieldStructureIndexType, "content_with_weight"}, 0, 10000, nil)
if err != nil {
return nil, 0, err
}
items := make([]StructureItem, 0, len(chunks))
for _, c := range chunks {
items = append(items, StructureItem{
PageID: firstStringValue(c[FieldPageID]),
StructureKind: firstStringValue(c[FieldStructureKind]),
StructureIndexType: firstStringValue(c[FieldStructureIndexType]),
Data: firstStringValue(c["content_with_weight"]),
})
}
return items, total, nil
}
// DeleteStructures deletes the compiled structures of a dataset, optionally
// scoped by structure_kind and structure_index_type.
func (s *DatasetArtifactService) DeleteStructures(ctx context.Context, tenantID, datasetID, structureKind, structureIndexType string) (int, error) {
docEngine := engine.Get()
if docEngine == nil {
return 0, fmt.Errorf("document engine is not initialized")
}
filter := map[string]interface{}{"compile_kwd": []string{CompileKwdStructure}}
if structureKind != "" {
filter[FieldStructureKind] = []string{structureKind}
}
if structureIndexType != "" {
filter[FieldStructureIndexType] = []string{structureIndexType}
}
chunks, _, err := s.searchCompiled(ctx, tenantID, datasetID, filter, []string{"id"}, 0, 10000, nil)
if err != nil {
return 0, err
}
if len(chunks) == 0 {
return 0, nil
}
ids := make([]string, 0, len(chunks))
for _, c := range chunks {
if id, ok := c["id"].(string); ok {
ids = append(ids, id)
}
}
cond := map[string]interface{}{"id": ids, "kb_id": datasetID}
if _, err := docEngine.DeleteChunks(ctx, cond, wikiIndexName(tenantID), datasetID); err != nil {
return 0, err
}
return len(ids), nil
}
// DocGraphItem is a single node/edge entry in a document's structure graph.
type DocGraphItem struct {
ID string `json:"id"`
Content string `json:"content"`
SourceID string `json:"source_id"`
}
// GetDocumentGraph returns the structure graph of a single document.
func (s *DatasetArtifactService) GetDocumentGraph(ctx context.Context, tenantID, datasetID, documentID, graphType string) ([]DocGraphItem, int64, error) {
filter := map[string]interface{}{
"doc_id": []string{documentID},
"compiled_graph_kwd": []string{"graph"},
}
if graphType != "" {
filter[FieldGraphType] = []string{graphType}
}
chunks, total, err := s.searchCompiled(ctx, tenantID, datasetID, filter,
[]string{"id", "content_with_weight", "source_id"}, 0, 10000, nil)
if err != nil {
return nil, 0, err
}
items := make([]DocGraphItem, 0, len(chunks))
for _, c := range chunks {
items = append(items, DocGraphItem{
ID: firstStringValue(c["id"]),
Content: firstStringValue(c["content_with_weight"]),
SourceID: firstStringValue(c["source_id"]),
})
}
return items, total, nil
}
// DeleteDocumentGraph deletes the structure graph of a single document.
func (s *DatasetArtifactService) DeleteDocumentGraph(ctx context.Context, tenantID, datasetID, documentID string) (int, error) {
docEngine := engine.Get()
if docEngine == nil {
return 0, fmt.Errorf("document engine is not initialized")
}
filter := map[string]interface{}{
"doc_id": []string{documentID},
"compiled_graph_kwd": []string{"graph"},
}
chunks, _, err := s.searchCompiled(ctx, tenantID, datasetID, filter, []string{"id"}, 0, 10000, nil)
if err != nil {
return 0, err
}
if len(chunks) == 0 {
return 0, nil
}
ids := make([]string, 0, len(chunks))
for _, c := range chunks {
if id, ok := c["id"].(string); ok {
ids = append(ids, id)
}
}
cond := map[string]interface{}{"id": ids, "kb_id": datasetID}
if _, err := docEngine.DeleteChunks(ctx, cond, wikiIndexName(tenantID), datasetID); err != nil {
return 0, err
}
return len(ids), nil
}
// NavigationItem is a single navigation cluster.
type NavigationItem struct {
Name string `json:"name"`
Title string `json:"title"`
Count int `json:"count"`
}
// ListNavClusters returns the navigation clusters of a dataset.
func (s *DatasetArtifactService) ListNavClusters(ctx context.Context, tenantID, datasetID string) ([]NavigationItem, int64, error) {
chunks, total, err := s.searchCompiled(ctx, tenantID, datasetID,
map[string]interface{}{"compile_kwd": []string{CompileKwdDatasetNav}},
[]string{"nav_cluster_kwd", "title_kwd", "count_int"}, 0, 10000, nil)
if err != nil {
return nil, 0, err
}
items := make([]NavigationItem, 0, len(chunks))
for _, c := range chunks {
count := intValue(c["count_int"])
items = append(items, NavigationItem{
Name: firstStringValue(c["nav_cluster_kwd"]),
Title: firstStringValue(c["title_kwd"]),
Count: count,
})
}
return items, total, nil
}
// DeleteNav deletes all navigation clusters of a dataset.
func (s *DatasetArtifactService) DeleteNav(ctx context.Context, tenantID, datasetID string) (int, error) {
docEngine := engine.Get()
if docEngine == nil {
return 0, fmt.Errorf("document engine is not initialized")
}
chunks, _, err := s.searchCompiled(ctx, tenantID, datasetID,
map[string]interface{}{"compile_kwd": []string{CompileKwdDatasetNav}}, []string{"id"}, 0, 10000, nil)
if err != nil {
return 0, err
}
if len(chunks) == 0 {
return 0, nil
}
ids := make([]string, 0, len(chunks))
for _, c := range chunks {
if id, ok := c["id"].(string); ok {
ids = append(ids, id)
}
}
cond := map[string]interface{}{"id": ids, "kb_id": datasetID}
if _, err := docEngine.DeleteChunks(ctx, cond, wikiIndexName(tenantID), datasetID); err != nil {
return 0, err
}
return len(ids), nil
}
// DeleteNavNode deletes a single navigation cluster by name.
func (s *DatasetArtifactService) DeleteNavNode(ctx context.Context, tenantID, datasetID, name string) (int, error) {
docEngine := engine.Get()
if docEngine == nil {
return 0, fmt.Errorf("document engine is not initialized")
}
chunks, _, err := s.searchCompiled(ctx, tenantID, datasetID,
map[string]interface{}{"compile_kwd": []string{CompileKwdDatasetNav}, "nav_cluster_kwd": []string{name}},
[]string{"id"}, 0, 10000, nil)
if err != nil {
return 0, err
}
if len(chunks) == 0 {
return 0, nil
}
ids := make([]string, 0, len(chunks))
for _, c := range chunks {
if id, ok := c["id"].(string); ok {
ids = append(ids, id)
}
}
cond := map[string]interface{}{"id": ids, "kb_id": datasetID}
if _, err := docEngine.DeleteChunks(ctx, cond, wikiIndexName(tenantID), datasetID); err != nil {
return 0, err
}
return len(ids), nil
}
// NavChildItem is a single child entry under a navigation cluster.
type NavChildItem struct {
Name string `json:"name"`
Title string `json:"title"`
Count int `json:"count"`
}
// ListNavChildren returns the children of a navigation cluster.
func (s *DatasetArtifactService) ListNavChildren(ctx context.Context, tenantID, datasetID, name string) ([]NavChildItem, int64, error) {
chunks, total, err := s.searchCompiled(ctx, tenantID, datasetID,
map[string]interface{}{"compile_kwd": []string{CompileKwdDatasetNav}, "nav_cluster_kwd": []string{name}},
[]string{"nav_child_kwd", "title_kwd", "count_int"}, 0, 10000, nil)
if err != nil {
return nil, 0, err
}
items := make([]NavChildItem, 0, len(chunks))
for _, c := range chunks {
count := intValue(c["count_int"])
items = append(items, NavChildItem{
Name: firstStringValue(c["nav_child_kwd"]),
Title: firstStringValue(c["title_kwd"]),
Count: count,
})
}
return items, total, nil
}
// SkillTreeItem is a single skill-tree page summary.
type SkillTreeItem struct {
Kwd string `json:"kwd"`
Title string `json:"title"`
PageType string `json:"page_type"`
Outlinks int `json:"outlinks"`
Inlinks int `json:"inlinks"`
}
// GetSkillTree returns the skill tree of a dataset.
func (s *DatasetArtifactService) GetSkillTree(ctx context.Context, tenantID, datasetID, kwd string) ([]SkillTreeItem, int64, error) {
filter := map[string]interface{}{"compile_kwd": []string{CompileKwdSkillAll}}
if kwd != "" {
filter["kwd"] = []string{kwd}
}
chunks, total, err := s.searchCompiled(ctx, tenantID, datasetID, filter,
[]string{"kwd", "title_kwd", "page_type_kwd", "outlinks_int", "inlinks_int"}, 0, 10000, nil)
if err != nil {
return nil, 0, err
}
items := make([]SkillTreeItem, 0, len(chunks))
for _, c := range chunks {
outlinks, inlinks := intValue(c["outlinks_int"]), intValue(c["inlinks_int"])
items = append(items, SkillTreeItem{
Kwd: firstStringValue(c["kwd"]),
Title: firstStringValue(c["title_kwd"]),
PageType: firstStringValue(c["page_type_kwd"]),
Outlinks: outlinks,
Inlinks: inlinks,
})
}
return items, total, nil
}
// SkillPageDetail is the full skill page payload.
type SkillPageDetail struct {
Kwd string `json:"kwd"`
Title string `json:"title"`
ContentMd string `json:"content_md"`
Outlinks []string `json:"outlinks"`
Inlinks []string `json:"inlinks"`
}
// GetSkillPage returns a single skill page by keyword.
func (s *DatasetArtifactService) GetSkillPage(ctx context.Context, tenantID, datasetID, kwd string) (*SkillPageDetail, error) {
chunks, _, err := s.searchCompiled(ctx, tenantID, datasetID,
map[string]interface{}{"compile_kwd": []string{CompileKwdSkillAll}, "kwd": []string{kwd}},
[]string{"kwd", "title_kwd", "content_with_weight", "outlinks_kwd", "inlinks_kwd"}, 0, 1, nil)
if err != nil {
return nil, err
}
if len(chunks) == 0 {
return nil, nil
}
c := chunks[0]
return &SkillPageDetail{
Kwd: firstStringValue(c["kwd"]),
Title: firstStringValue(c["title_kwd"]),
ContentMd: firstStringValue(c["content_with_weight"]),
Outlinks: toStringSlice(c["outlinks_kwd"]),
Inlinks: toStringSlice(c["inlinks_kwd"]),
}, nil
}
// DeleteSkills deletes skills of a dataset, optionally scoped by keyword.
func (s *DatasetArtifactService) DeleteSkills(ctx context.Context, tenantID, datasetID, kwd string) (int, error) {
docEngine := engine.Get()
if docEngine == nil {
return 0, fmt.Errorf("document engine is not initialized")
}
filter := map[string]interface{}{"compile_kwd": []string{CompileKwdSkillAll}}
if kwd != "" {
filter["kwd"] = []string{kwd}
}
chunks, _, err := s.searchCompiled(ctx, tenantID, datasetID, filter, []string{"id"}, 0, 10000, nil)
if err != nil {
return 0, err
}
if len(chunks) == 0 {
return 0, nil
}
ids := make([]string, 0, len(chunks))
for _, c := range chunks {
if id, ok := c["id"].(string); ok {
ids = append(ids, id)
}
}
cond := map[string]interface{}{"id": ids, "kb_id": datasetID}
if _, err := docEngine.DeleteChunks(ctx, cond, wikiIndexName(tenantID), datasetID); err != nil {
return 0, err
}
return len(ids), nil
}
// firstStringValue returns the first string in a possibly-list ES field value.
func firstStringValue(v interface{}) string {
switch t := v.(type) {
case string:
return t
case []interface{}:
if len(t) > 0 {
if s, ok := t[0].(string); ok {
return s
}
}
case []string:
if len(t) > 0 {
return t[0]
}
}
return ""
}
// toStringSlice normalizes an ES field value (string or list) to []string.
func toStringSlice(v interface{}) []string {
switch t := v.(type) {
case string:
if t == "" {
return []string{}
}
return []string{t}
case []interface{}:
out := make([]string, 0, len(t))
for _, e := range t {
if s, ok := e.(string); ok {
out = append(out, s)
}
}
return out
case []string:
return t
}
return []string{}
}