mirror of
https://github.com/infiniflow/ragflow.git
synced 2026-08-02 22:07:31 +08:00
847 lines
28 KiB
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{}
|
|
}
|