mirror of
https://github.com/infiniflow/ragflow.git
synced 2026-07-21 23:21:04 +08:00
### Summary Refactor dataset.go document.do file.go file2document.go in internal/service.
233 lines
6.8 KiB
Go
233 lines
6.8 KiB
Go
package document
|
|
|
|
import (
|
|
"fmt"
|
|
"strings"
|
|
"time"
|
|
|
|
"ragflow/internal/dao"
|
|
"ragflow/internal/entity"
|
|
)
|
|
|
|
// escapeSQLLikePattern escapes the SQL LIKE wildcards ('%', '_') and
|
|
// the escape character itself ('!') so a literal user-supplied
|
|
// filename can be safely interpolated into a `LIKE ? ESCAPE '!'`
|
|
// pattern. Without this, "%.png" would match any string ending in
|
|
// ".png" and "_" would match a single character — bypassing the
|
|
// filename-specific authorization check. PR review round 5, Major #8.
|
|
func escapeSQLLikePattern(s string) string {
|
|
r := strings.NewReplacer(`!`, `!!`, `%`, `!%`, `_`, `!_`)
|
|
return r.Replace(s)
|
|
}
|
|
|
|
// ListDocuments list documents
|
|
func (s *DocumentService) ListDocuments(page, pageSize int) ([]*DocumentResponse, int64, error) {
|
|
offset := (page - 1) * pageSize
|
|
documents, total, err := s.documentDAO.List(offset, pageSize)
|
|
if err != nil {
|
|
return nil, 0, err
|
|
}
|
|
|
|
responses := make([]*DocumentResponse, len(documents))
|
|
for i, doc := range documents {
|
|
responses[i] = s.toResponse(doc)
|
|
}
|
|
|
|
return responses, total, nil
|
|
}
|
|
|
|
func (s *DocumentService) GetThumbnails(userID string, docIDs []string) (map[string]string, error) {
|
|
if len(docIDs) == 0 {
|
|
return map[string]string{}, nil
|
|
}
|
|
|
|
tenantIDs := []string{userID}
|
|
if userID != "" {
|
|
ids, err := dao.NewUserTenantDAO().GetTenantIDsByUserID(userID)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to fetch user tenants: %w", err)
|
|
}
|
|
tenantIDs = append(tenantIDs, ids...)
|
|
}
|
|
|
|
documents, err := s.documentDAO.GetByIDsAndTenantIDs(docIDs, tenantIDs)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to fetch document thumbnails: %w", err)
|
|
}
|
|
|
|
result := make(map[string]string, len(documents))
|
|
for _, document := range documents {
|
|
if document == nil {
|
|
continue
|
|
}
|
|
|
|
thumbnail := ""
|
|
if document.Thumbnail != nil && *document.Thumbnail != "" {
|
|
if strings.HasPrefix(*document.Thumbnail, imgBase64Prefix) {
|
|
thumbnail = *document.Thumbnail
|
|
} else {
|
|
thumbnail = fmt.Sprintf(
|
|
"/api/v1/documents/images/%s-%s",
|
|
document.KbID,
|
|
*document.Thumbnail,
|
|
)
|
|
}
|
|
}
|
|
|
|
result[document.ID] = thumbnail
|
|
}
|
|
|
|
return result, nil
|
|
}
|
|
|
|
// ListDocumentsByDatasetID list documents by knowledge base ID
|
|
func (s *DocumentService) ListDocumentsByDatasetID(kbID, keywords string, page, pageSize int) ([]*entity.DocumentListItem, int64, error) {
|
|
return s.ListDocumentsByDatasetIDWithOptions(dao.DocumentListOptions{
|
|
KbID: kbID,
|
|
Keywords: keywords,
|
|
OrderBy: "create_time",
|
|
Desc: true,
|
|
}, page, pageSize)
|
|
}
|
|
|
|
// ListDocumentsByDatasetIDWithOptions lists documents by knowledge base ID with filters.
|
|
func (s *DocumentService) ListDocumentsByDatasetIDWithOptions(opts dao.DocumentListOptions, page, pageSize int) ([]*entity.DocumentListItem, int64, error) {
|
|
opts.Offset = (page - 1) * pageSize
|
|
opts.Limit = pageSize
|
|
if opts.OrderBy == "" {
|
|
opts.OrderBy = "create_time"
|
|
}
|
|
documents, total, err := s.documentDAO.ListByKBIDWithOptions(opts)
|
|
if err != nil {
|
|
return nil, 0, err
|
|
}
|
|
|
|
responses := make([]*entity.DocumentListItem, len(documents))
|
|
for i, doc := range documents {
|
|
responses[i] = doc
|
|
}
|
|
|
|
return responses, total, nil
|
|
}
|
|
|
|
// GetDocumentFiltersByDatasetID returns aggregate filter values for documents in a dataset.
|
|
func (s *DocumentService) GetDocumentFiltersByDatasetID(opts dao.DocumentListOptions) (map[string]interface{}, int64, error) {
|
|
filters, total, err := s.documentDAO.GetFilterByKBID(opts)
|
|
if err != nil {
|
|
return nil, 0, err
|
|
}
|
|
docIDs, err := s.documentDAO.ListIDsByKBIDWithOptions(opts)
|
|
if err != nil {
|
|
return nil, 0, err
|
|
}
|
|
metadataFilter, err := s.getDocumentMetadataFilter(opts.KbID, docIDs)
|
|
if err != nil {
|
|
return nil, 0, err
|
|
}
|
|
filters["metadata"] = metadataFilter
|
|
return filters, total, nil
|
|
}
|
|
|
|
func (s *DocumentService) getDocumentMetadataFilter(kbID string, docIDs []string) (map[string]interface{}, error) {
|
|
metadataByKey, err := s.GetMetadataByKBs([]string{kbID})
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
candidateSet := make(map[string]bool, len(docIDs))
|
|
for _, docID := range docIDs {
|
|
candidateSet[docID] = true
|
|
}
|
|
|
|
metadataCounter := map[string]interface{}{}
|
|
docIDsWithMetadata := map[string]bool{}
|
|
for key, rawValues := range metadataByKey {
|
|
values, ok := rawValues.(map[string][]string)
|
|
if !ok {
|
|
continue
|
|
}
|
|
valueCounter := map[string]int64{}
|
|
for value, valueDocIDs := range values {
|
|
for _, docID := range valueDocIDs {
|
|
if !candidateSet[docID] {
|
|
continue
|
|
}
|
|
valueCounter[value]++
|
|
docIDsWithMetadata[docID] = true
|
|
}
|
|
}
|
|
if len(valueCounter) > 0 {
|
|
metadataCounter[key] = valueCounter
|
|
}
|
|
}
|
|
metadataCounter["empty_metadata"] = map[string]int64{"true": int64(len(docIDs) - len(docIDsWithMetadata))}
|
|
return metadataCounter, nil
|
|
}
|
|
|
|
// ListDocumentIDsByDatasetIDWithOptions lists matching document IDs without pagination.
|
|
func (s *DocumentService) ListDocumentIDsByDatasetIDWithOptions(opts dao.DocumentListOptions) ([]string, error) {
|
|
return s.documentDAO.ListIDsByKBIDWithOptions(opts)
|
|
}
|
|
|
|
// GetDocumentsByAuthorID get documents by author ID
|
|
func (s *DocumentService) GetDocumentsByAuthorID(authorID, page, pageSize int) ([]*DocumentResponse, int64, error) {
|
|
offset := (page - 1) * pageSize
|
|
documents, total, err := s.documentDAO.GetByAuthorID(fmt.Sprintf("%d", authorID), offset, pageSize)
|
|
if err != nil {
|
|
return nil, 0, err
|
|
}
|
|
|
|
responses := make([]*DocumentResponse, len(documents))
|
|
for i, doc := range documents {
|
|
responses[i] = s.toResponse(doc)
|
|
}
|
|
|
|
return responses, total, nil
|
|
}
|
|
|
|
// toResponse convert model.Document to DocumentResponse
|
|
func (s *DocumentService) toResponse(doc *entity.Document) *DocumentResponse {
|
|
createdAt := ""
|
|
if doc.CreateTime != nil {
|
|
// Check if timestamp is in milliseconds (13 digits) or seconds (10 digits)
|
|
var ts int64
|
|
if *doc.CreateTime > 1000000000000 {
|
|
// Milliseconds - convert to seconds
|
|
ts = *doc.CreateTime / 1000
|
|
} else {
|
|
ts = *doc.CreateTime
|
|
}
|
|
createdAt = time.Unix(ts, 0).Format("2006-01-02 15:04:05")
|
|
}
|
|
updatedAt := ""
|
|
if doc.UpdateTime != nil {
|
|
// Accept both historical second-based values and current millisecond-based values.
|
|
ts := *doc.UpdateTime
|
|
if ts > 1000000000000 {
|
|
ts /= 1000
|
|
}
|
|
updatedAt = time.Unix(ts, 0).Format("2006-01-02 15:04:05")
|
|
}
|
|
return &DocumentResponse{
|
|
ID: doc.ID,
|
|
Name: doc.Name,
|
|
KbID: doc.KbID,
|
|
ParserID: doc.ParserID,
|
|
PipelineID: doc.PipelineID,
|
|
Type: doc.Type,
|
|
SourceType: doc.SourceType,
|
|
CreatedBy: doc.CreatedBy,
|
|
Location: doc.Location,
|
|
Size: doc.Size,
|
|
TokenNum: doc.TokenNum,
|
|
ChunkNum: doc.ChunkNum,
|
|
Progress: doc.Progress,
|
|
ProgressMsg: doc.ProgressMsg,
|
|
ProcessDuration: doc.ProcessDuration,
|
|
Suffix: doc.Suffix,
|
|
Run: doc.Run,
|
|
Status: doc.Status,
|
|
CreatedAt: createdAt,
|
|
UpdatedAt: updatedAt,
|
|
}
|
|
}
|