mirror of
https://github.com/infiniflow/ragflow.git
synced 2026-06-29 15:31:05 +08:00
Go: Add create search (#13998)
### What problem does this PR solve? As title ### Type of change - [x] New Feature (non-breaking change which adds functionality) --------- Signed-off-by: Jin Hai <haijin.chn@gmail.com>
This commit is contained in:
125
internal/common/app_name.go
Normal file
125
internal/common/app_name.go
Normal file
@@ -0,0 +1,125 @@
|
||||
//
|
||||
// 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 common
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"path"
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
// splitNameCounter splits a filename into base name and counter
|
||||
// Handles names in format "filename(123)" pattern
|
||||
//
|
||||
// Parameters:
|
||||
// - filename: The filename to split
|
||||
//
|
||||
// Returns:
|
||||
// - string: The base name without counter
|
||||
// - *int: The counter value, or nil if no counter exists
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// splitNameCounter("test(5)") returns ("test", 5)
|
||||
// splitNameCounter("test") returns ("test", nil)
|
||||
func splitNameCounter(filename string) (string, *int) {
|
||||
re := regexp.MustCompile(`^(.+)\((\d+)\)$`)
|
||||
matches := re.FindStringSubmatch(filename)
|
||||
if len(matches) >= 3 {
|
||||
counter := -1
|
||||
fmt.Sscanf(matches[2], "%d", &counter)
|
||||
stem := strings.TrimRight(matches[1], " ")
|
||||
return stem, &counter
|
||||
}
|
||||
return filename, nil
|
||||
}
|
||||
|
||||
// DuplicateName generates a unique name by appending a counter if the name already exists
|
||||
// It tries up to 1000 times to generate a unique name
|
||||
//
|
||||
// Parameters:
|
||||
// - queryFunc: Function to check if a name already exists (returns true if exists)
|
||||
// - name: The original name
|
||||
// - tenantID: The tenant ID for name uniqueness check
|
||||
//
|
||||
// Returns:
|
||||
// - string: A unique name (either original or with counter appended)
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// DuplicateName(func(name string, tid string) bool { return false }, "test", "tenant1") returns "test"
|
||||
// DuplicateName(func(name string, tid string) bool { return true }, "test", "tenant1") returns "test(1)"
|
||||
func DuplicateName(queryFunc func(name string, tenantID string) bool, name string, tenantID string) (string, error) {
|
||||
const maxRetries = 1000
|
||||
|
||||
originalName := name
|
||||
currentName := name
|
||||
retries := 0
|
||||
|
||||
for retries < maxRetries {
|
||||
if !queryFunc(currentName, tenantID) {
|
||||
return currentName, nil
|
||||
}
|
||||
|
||||
stem, counter := splitNameCounter(currentName)
|
||||
ext := path.Ext(stem)
|
||||
stemBase := strings.TrimSuffix(stem, ext)
|
||||
|
||||
newCounter := 1
|
||||
if counter != nil {
|
||||
newCounter = *counter + 1
|
||||
}
|
||||
|
||||
currentName = fmt.Sprintf("%s(%d)%s", stemBase, newCounter, ext)
|
||||
retries++
|
||||
|
||||
if err := ValidateName(currentName); err != nil {
|
||||
return "", err
|
||||
}
|
||||
}
|
||||
|
||||
return "", fmt.Errorf("failed to generate unique name after %d attempts, conflict name: %s", maxRetries, originalName)
|
||||
}
|
||||
|
||||
const AppNameLimit = 256
|
||||
|
||||
func ValidateName(name string) error {
|
||||
// Validate name is not empty after trimming
|
||||
trimmedName := strings.TrimSpace(name)
|
||||
if trimmedName == "" {
|
||||
return fmt.Errorf("name can't be empty")
|
||||
}
|
||||
|
||||
// Validate name length in bytes (not characters) - same as Python len(search_name.encode("utf-8"))
|
||||
if len([]byte(name)) > AppNameLimit {
|
||||
return fmt.Errorf("name length is %d which is large than %d", len([]byte(name)), AppNameLimit)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// GenerateUUID generates a UUID without dashes
|
||||
func GenerateUUID() string {
|
||||
newID := strings.ReplaceAll(uuid.New().String(), "-", "")
|
||||
if len(newID) > 32 {
|
||||
newID = newID[:32]
|
||||
}
|
||||
return newID
|
||||
}
|
||||
52
internal/common/time.go
Normal file
52
internal/common/time.go
Normal file
@@ -0,0 +1,52 @@
|
||||
//
|
||||
// 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 common
|
||||
|
||||
import (
|
||||
"time"
|
||||
)
|
||||
|
||||
// DeltaSeconds calculates seconds elapsed from a given date string to now.
|
||||
//
|
||||
// Supports multiple time formats:
|
||||
// - "YYYY-MM-DD HH:MM:SS" (e.g., "2024-01-01 12:00:00")
|
||||
// - ISO 8601 / RFC3339 (e.g., "2026-04-09T18:55:46+08:00")
|
||||
//
|
||||
// Args:
|
||||
// dateString: Date string in supported format
|
||||
//
|
||||
// Returns:
|
||||
// float64: Number of seconds between the given date and current time
|
||||
//
|
||||
// Example:
|
||||
// DeltaSeconds("2024-01-01 12:00:00")
|
||||
// DeltaSeconds("2026-04-09T18:55:46+08:00")
|
||||
func DeltaSeconds(dateString string) (float64, error) {
|
||||
// Try RFC3339 format first (ISO 8601 with timezone, e.g., "2026-04-09T18:55:46+08:00")
|
||||
dt, err := time.Parse(time.RFC3339, dateString)
|
||||
if err == nil {
|
||||
return time.Since(dt).Seconds(), nil
|
||||
}
|
||||
|
||||
// Try custom format without timezone (e.g., "2024-01-01 12:00:00")
|
||||
const layout = "2006-01-02 15:04:05"
|
||||
dt, err = time.ParseInLocation(layout, dateString, time.Local)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
return time.Since(dt).Seconds(), nil
|
||||
}
|
||||
@@ -124,3 +124,24 @@ func (dao *SearchDAO) GetByID(id string) (*entity.Search, error) {
|
||||
}
|
||||
return &search, nil
|
||||
}
|
||||
|
||||
// GetByNameAndTenant gets search by name and tenant ID
|
||||
func (dao *SearchDAO) GetByNameAndTenant(name string, tenantID string) ([]*entity.Search, error) {
|
||||
var searches []*entity.Search
|
||||
err := DB.Where("name = ? AND tenant_id = ? AND status = ?", name, tenantID, "1").Find(&searches).Error
|
||||
return searches, err
|
||||
}
|
||||
|
||||
// Create creates a new search
|
||||
func (dao *SearchDAO) Create(search *entity.Search) error {
|
||||
return DB.Create(search).Error
|
||||
}
|
||||
|
||||
// QueryByTenantIDAndID checks if a search exists with given tenant_id and id
|
||||
// Reference: Python SearchService.query(tenant_id=tenant.tenant_id, id=search_id)
|
||||
// Used for permission verification in detail API
|
||||
func (dao *SearchDAO) QueryByTenantIDAndID(tenantID string, searchID string) ([]*entity.Search, error) {
|
||||
var searches []*entity.Search
|
||||
err := DB.Where("tenant_id = ? AND id = ? AND status = ?", tenantID, searchID, "1").Find(&searches).Error
|
||||
return searches, err
|
||||
}
|
||||
|
||||
@@ -114,3 +114,140 @@ func (h *SearchHandler) ListSearches(c *gin.Context) {
|
||||
"message": "success",
|
||||
})
|
||||
}
|
||||
|
||||
// CreateSearch create a new search app
|
||||
// @Summary Create Search App
|
||||
// @Description Create a new search app for the current user
|
||||
// @Tags search
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param request body service.CreateSearchRequest true "search creation parameters"
|
||||
// @Success 200 {object} service.CreateSearchResponse
|
||||
// @Router /api/v1/searches [post]
|
||||
|
||||
type CreateSearchRequest struct {
|
||||
Name string `json:"name" binding:"required"` // required field, max 255 bytes
|
||||
Description *string `json:"description,omitempty"` // optional description
|
||||
}
|
||||
|
||||
func (h *SearchHandler) CreateSearch(c *gin.Context) {
|
||||
// Get current user from context (same as Python current_user)
|
||||
user, errorCode, errorMessage := GetUser(c)
|
||||
if errorCode != common.CodeSuccess {
|
||||
jsonError(c, errorCode, errorMessage)
|
||||
return
|
||||
}
|
||||
userID := user.ID
|
||||
|
||||
// Parse request body (same as Python get_request_json())
|
||||
var req CreateSearchRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{
|
||||
"code": common.CodeBadRequest,
|
||||
"data": nil,
|
||||
"message": "Invalid request body: " + err.Error(),
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
if err := common.ValidateName(req.Name); err != nil {
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"code": common.CodeDataError,
|
||||
"data": nil,
|
||||
"message": err.Error(),
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
// Create search (same as Python SearchService.save within DB.atomic())
|
||||
result, err := h.searchService.CreateSearch(userID, req.Name, req.Description)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{
|
||||
"code": common.CodeServerError,
|
||||
"data": nil,
|
||||
"message": err.Error(),
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
// Return success response (same as Python get_json_result(data={"search_id": req["id"]}))
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"code": common.CodeSuccess,
|
||||
"data": result,
|
||||
"message": "success",
|
||||
})
|
||||
}
|
||||
|
||||
// GetSearch get search app detail
|
||||
// @Summary Get Search App Detail
|
||||
// @Description Get detail of a search app by ID
|
||||
// @Tags search
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param search_id path string true "search app ID"
|
||||
// @Success 200 {object} entity.Search
|
||||
// @Router /api/v1/searches/{search_id} [get]
|
||||
func (h *SearchHandler) GetSearch(c *gin.Context) {
|
||||
// Get current user from context (same as Python current_user)
|
||||
user, errorCode, errorMessage := GetUser(c)
|
||||
if errorCode != common.CodeSuccess {
|
||||
jsonError(c, errorCode, errorMessage)
|
||||
return
|
||||
}
|
||||
userID := user.ID
|
||||
|
||||
// Get search_id from path parameter (same as Python <search_id>)
|
||||
searchID := c.Param("search_id")
|
||||
if searchID == "" {
|
||||
c.JSON(http.StatusBadRequest, gin.H{
|
||||
"code": common.CodeBadRequest,
|
||||
"data": nil,
|
||||
"message": "search_id is required",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
// Get search detail with permission check
|
||||
search, err := h.searchService.GetSearchDetail(userID, searchID)
|
||||
if err != nil {
|
||||
// Check if it's a permission error
|
||||
if err.Error() == "has no permission for this operation" {
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"code": common.CodeOperatingError,
|
||||
"data": false,
|
||||
"message": "Has no permission for this operation.",
|
||||
})
|
||||
return
|
||||
}
|
||||
// Not found error
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"code": common.CodeDataError,
|
||||
"data": nil,
|
||||
"message": err.Error(),
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
// Convert to response format (same as Python get_json_result(data=search))
|
||||
result := map[string]interface{}{
|
||||
"id": search.ID,
|
||||
"tenant_id": search.TenantID,
|
||||
"name": search.Name,
|
||||
"description": search.Description,
|
||||
"created_by": search.CreatedBy,
|
||||
"create_time": search.CreateTime,
|
||||
"update_time": search.UpdateTime,
|
||||
"search_config": search.SearchConfig,
|
||||
}
|
||||
|
||||
if search.Avatar != nil {
|
||||
result["avatar"] = *search.Avatar
|
||||
}
|
||||
|
||||
// Return success response
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"code": common.CodeSuccess,
|
||||
"data": result,
|
||||
"message": "success",
|
||||
})
|
||||
}
|
||||
|
||||
@@ -192,10 +192,12 @@ func (r *Router) Setup(engine *gin.Engine) {
|
||||
chats.GET("", r.chatHandler.ListChats)
|
||||
}
|
||||
|
||||
searches := v1.Group("/searches")
|
||||
{
|
||||
searches.GET("", r.searchHandler.ListSearches)
|
||||
}
|
||||
searches := v1.Group("/searches")
|
||||
{
|
||||
searches.GET("", r.searchHandler.ListSearches)
|
||||
searches.POST("", r.searchHandler.CreateSearch)
|
||||
searches.GET("/:search_id", r.searchHandler.GetSearch)
|
||||
}
|
||||
|
||||
file := v1.Group("/files")
|
||||
{
|
||||
|
||||
@@ -28,8 +28,6 @@ import (
|
||||
"ragflow/internal/utility"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
// KnowledgebaseService service class for managing dataset operations
|
||||
@@ -147,7 +145,7 @@ func (s *KnowledgebaseService) CreateKB(req *CreateKBRequest, tenantID string) (
|
||||
parserConfig["llm_id"] = tenant.LLMID
|
||||
|
||||
// Generate KB ID
|
||||
kbID := strings.ReplaceAll(uuid.New().String(), "-", "")
|
||||
kbID := common.GenerateUUID()
|
||||
|
||||
// Create knowledge base model
|
||||
now := time.Now().Unix()
|
||||
@@ -551,11 +549,6 @@ func mergeParserConfig(base, override map[string]interface{}) map[string]interfa
|
||||
return result
|
||||
}
|
||||
|
||||
// GenerateUUID generates a UUID string without dashes
|
||||
func GenerateUUID() string {
|
||||
return strings.ReplaceAll(uuid.New().String(), "-", "")
|
||||
}
|
||||
|
||||
// GetUserByToken gets user by authorization token
|
||||
func (s *KnowledgebaseService) GetUserByToken(authorization string) (*entity.User, common.ErrorCode, error) {
|
||||
userService := NewUserService()
|
||||
|
||||
@@ -19,15 +19,12 @@ package service
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"path"
|
||||
"ragflow/internal/common"
|
||||
"ragflow/internal/entity"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
|
||||
"ragflow/internal/dao"
|
||||
)
|
||||
|
||||
@@ -241,75 +238,6 @@ func NewMemoryService() *MemoryService {
|
||||
}
|
||||
}
|
||||
|
||||
// splitNameCounter splits a filename into base name and counter
|
||||
// Handles names in format "filename(123)" pattern
|
||||
//
|
||||
// Parameters:
|
||||
// - filename: The filename to split
|
||||
//
|
||||
// Returns:
|
||||
// - string: The base name without counter
|
||||
// - *int: The counter value, or nil if no counter exists
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// splitNameCounter("test(5)") returns ("test", 5)
|
||||
// splitNameCounter("test") returns ("test", nil)
|
||||
func splitNameCounter(filename string) (string, *int) {
|
||||
re := regexp.MustCompile(`^(.+)\((\d+)\)$`)
|
||||
matches := re.FindStringSubmatch(filename)
|
||||
if len(matches) >= 3 {
|
||||
counter := -1
|
||||
fmt.Sscanf(matches[2], "%d", &counter)
|
||||
stem := strings.TrimRight(matches[1], " ")
|
||||
return stem, &counter
|
||||
}
|
||||
return filename, nil
|
||||
}
|
||||
|
||||
// duplicateName generates a unique name by appending a counter if the name already exists
|
||||
// It tries up to 1000 times to generate a unique name
|
||||
//
|
||||
// Parameters:
|
||||
// - queryFunc: Function to check if a name already exists (returns true if exists)
|
||||
// - name: The original name
|
||||
// - tenantID: The tenant ID for name uniqueness check
|
||||
//
|
||||
// Returns:
|
||||
// - string: A unique name (either original or with counter appended)
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// duplicateName(func(name string, tid string) bool { return false }, "test", "tenant1") returns "test"
|
||||
// duplicateName(func(name string, tid string) bool { return true }, "test", "tenant1") returns "test(1)"
|
||||
func duplicateName(queryFunc func(name string, tenantID string) bool, name string, tenantID string) string {
|
||||
const maxRetries = 1000
|
||||
|
||||
originalName := name
|
||||
currentName := name
|
||||
retries := 0
|
||||
|
||||
for retries < maxRetries {
|
||||
if !queryFunc(currentName, tenantID) {
|
||||
return currentName
|
||||
}
|
||||
|
||||
stem, counter := splitNameCounter(currentName)
|
||||
ext := path.Ext(stem)
|
||||
stemBase := strings.TrimSuffix(stem, ext)
|
||||
|
||||
newCounter := 1
|
||||
if counter != nil {
|
||||
newCounter = *counter + 1
|
||||
}
|
||||
|
||||
currentName = fmt.Sprintf("%s(%d)%s", stemBase, newCounter, ext)
|
||||
retries++
|
||||
}
|
||||
|
||||
panic(fmt.Sprintf("Failed to generate unique name within %d attempts. Original: %s", maxRetries, originalName))
|
||||
}
|
||||
|
||||
// CreateMemoryRequest defines the request structure for creating a memory
|
||||
type CreateMemoryRequest struct {
|
||||
// Name is the memory name (required, max 128 characters)
|
||||
@@ -410,14 +338,12 @@ func (s *MemoryService) CreateMemory(tenantID string, req *CreateMemoryRequest)
|
||||
req.TenantEmbdID = &tenantEmbdIDStr
|
||||
}
|
||||
|
||||
memoryName := strings.TrimSpace(req.Name)
|
||||
if len(memoryName) == 0 {
|
||||
return nil, errors.New("memory name cannot be empty or whitespace")
|
||||
}
|
||||
if len(memoryName) > MemoryNameLimit {
|
||||
return nil, fmt.Errorf("memory name '%s' exceeds limit of %d", memoryName, MemoryNameLimit)
|
||||
if err := common.ValidateName(req.Name); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
memoryName := req.Name
|
||||
|
||||
if !isList(req.MemoryType) {
|
||||
return nil, errors.New("memory type must be a list")
|
||||
}
|
||||
@@ -435,13 +361,12 @@ func (s *MemoryService) CreateMemory(tenantID string, req *CreateMemoryRequest)
|
||||
uniqueMemoryTypes = append(uniqueMemoryTypes, mt)
|
||||
}
|
||||
|
||||
memoryName = duplicateName(func(name string, tid string) bool {
|
||||
memoryName, err := common.DuplicateName(func(name string, tid string) bool {
|
||||
existing, _ := s.memoryDAO.GetByNameAndTenant(name, tid)
|
||||
return len(existing) > 0
|
||||
}, memoryName, tenantID)
|
||||
|
||||
if len(memoryName) > MemoryNameLimit {
|
||||
return nil, fmt.Errorf("memory name %s exceeds limit of %d", memoryName, MemoryNameLimit)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
memoryTypeInt := dao.CalculateMemoryType(uniqueMemoryTypes)
|
||||
@@ -449,10 +374,7 @@ func (s *MemoryService) CreateMemory(tenantID string, req *CreateMemoryRequest)
|
||||
|
||||
systemPrompt := PromptAssembler{}.AssembleSystemPrompt(uniqueMemoryTypes)
|
||||
|
||||
newID := strings.ReplaceAll(uuid.New().String(), "-", "")
|
||||
if len(newID) > 32 {
|
||||
newID = newID[:32]
|
||||
}
|
||||
newID := common.GenerateUUID()
|
||||
|
||||
memory := &entity.Memory{
|
||||
ID: newID,
|
||||
@@ -516,18 +438,15 @@ func (s *MemoryService) UpdateMemory(tenantID string, memoryID string, req *Upda
|
||||
|
||||
if req.Name != nil {
|
||||
memoryName := strings.TrimSpace(*req.Name)
|
||||
if len(memoryName) == 0 {
|
||||
return nil, errors.New("memory name cannot be empty or whitespace")
|
||||
if err := common.ValidateName(memoryName); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if len(memoryName) > MemoryNameLimit {
|
||||
return nil, fmt.Errorf("memory name '%s' exceeds limit of %d", memoryName, MemoryNameLimit)
|
||||
}
|
||||
memoryName = duplicateName(func(name string, tid string) bool {
|
||||
memoryName, err := common.DuplicateName(func(name string, tid string) bool {
|
||||
existing, _ := s.memoryDAO.GetByNameAndTenant(name, tid)
|
||||
return len(existing) > 0
|
||||
}, memoryName, tenantID)
|
||||
if len(memoryName) > MemoryNameLimit {
|
||||
return nil, fmt.Errorf("memory name %s exceeds limit of %d", memoryName, MemoryNameLimit)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
updateDict["name"] = memoryName
|
||||
}
|
||||
|
||||
@@ -17,6 +17,8 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"ragflow/internal/common"
|
||||
"ragflow/internal/dao"
|
||||
"ragflow/internal/entity"
|
||||
)
|
||||
@@ -130,3 +132,104 @@ func (s *SearchService) toSearchAppResponse(search *entity.Search) map[string]in
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
// CreateSearchResponse create search response
|
||||
// Reference: api/apps/restful_apis/search_api.py::create - returns {"search_id": req["id"]}
|
||||
type CreateSearchResponse struct {
|
||||
SearchID string `json:"search_id"` // UUID format
|
||||
}
|
||||
|
||||
// CreateSearch creates a new search app
|
||||
// Reference: api/apps/restful_apis/search_api.py::create
|
||||
// Python implementation steps:
|
||||
// 1. Get JSON request body with name (required) and description (optional)
|
||||
// 2. Validate name is string, non-empty, and max 255 bytes
|
||||
// 3. Generate unique name using duplicate_name(SearchService.query, name, tenant_id)
|
||||
// 4. Generate UUID for search ID
|
||||
// 5. Set fields: id, name, description, tenant_id, created_by
|
||||
// 6. Save to database within DB.atomic() transaction
|
||||
// 7. Return {search_id: id} on success
|
||||
//
|
||||
// Error handling from Python:
|
||||
// - Name not string: "Search name must be string."
|
||||
// - Name empty: "Search name can't be empty."
|
||||
// - Name too long: "Search name length is X which is larger than 255."
|
||||
// - Tenant not found: "Authorized identity."
|
||||
// - Save failure: generic get_data_error_result()
|
||||
//
|
||||
// Note: Go implementation validates these in handler layer for cleaner separation
|
||||
// Note: Similar pattern in: CreateMemory (memory.go), CreateDataset (datasets.go)
|
||||
func (s *SearchService) CreateSearch(userID string, name string, description *string) (*CreateSearchResponse, error) {
|
||||
// Generate UUID for search ID (same as Python get_uuid())
|
||||
searchID := common.GenerateUUID()
|
||||
|
||||
// Generate unique name (same as Python duplicate_name)
|
||||
uniqueName, err := common.DuplicateName(func(name string, tid string) bool {
|
||||
existing, _ := s.searchDAO.GetByNameAndTenant(name, tid)
|
||||
return len(existing) > 0
|
||||
}, name, userID)
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Create search entity
|
||||
search := &entity.Search{
|
||||
ID: searchID,
|
||||
TenantID: userID,
|
||||
Name: uniqueName,
|
||||
CreatedBy: userID,
|
||||
SearchConfig: make(entity.JSONMap),
|
||||
}
|
||||
|
||||
if description != nil {
|
||||
search.Description = description
|
||||
}
|
||||
|
||||
// Set default status ("1" = valid/active, same as Python StatusEnum.VALID.value)
|
||||
status := "1"
|
||||
search.Status = &status
|
||||
|
||||
// Save to database
|
||||
if err := s.searchDAO.Create(search); err != nil {
|
||||
return nil, fmt.Errorf("failed to create search: %w", err)
|
||||
}
|
||||
|
||||
return &CreateSearchResponse{
|
||||
SearchID: searchID,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (s *SearchService) GetSearchDetail(userID string, searchID string) (*entity.Search, error) {
|
||||
// Step 1: Get user tenants (same as Python UserTenantService.query(user_id=current_user.id))
|
||||
tenants, err := s.userTenantDAO.GetByUserID(userID)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get user tenants: %w", err)
|
||||
}
|
||||
|
||||
// Step 2: Check if user has permission to access this search
|
||||
// Python: for tenant in tenants: if SearchService.query(tenant_id=tenant.tenant_id, id=search_id): break
|
||||
hasPermission := false
|
||||
for _, tenant := range tenants {
|
||||
searches, err := s.searchDAO.QueryByTenantIDAndID(tenant.TenantID, searchID)
|
||||
if err != nil {
|
||||
continue // Try next tenant
|
||||
}
|
||||
if len(searches) > 0 {
|
||||
hasPermission = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if !hasPermission {
|
||||
return nil, fmt.Errorf("has no permission for this operation")
|
||||
}
|
||||
|
||||
// Step 3: Get search detail (same as Python SearchService.get_detail(search_id))
|
||||
search, err := s.searchDAO.GetByID(searchID)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("can't find this Search App!")
|
||||
}
|
||||
|
||||
return search, nil
|
||||
}
|
||||
|
||||
@@ -19,13 +19,11 @@ package service
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"ragflow/internal/entity"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"ragflow/internal/common"
|
||||
"ragflow/internal/dao"
|
||||
"ragflow/internal/engine"
|
||||
"ragflow/internal/entity"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// TenantService tenant service
|
||||
@@ -232,14 +230,14 @@ func (s *TenantService) GetTenantList(userID string) ([]*TenantListItem, error)
|
||||
}
|
||||
|
||||
result := make([]*TenantListItem, len(tenants))
|
||||
now := time.Now()
|
||||
|
||||
for i, t := range tenants {
|
||||
// Parse update_date and calculate delta_seconds
|
||||
var deltaSeconds float64
|
||||
if t.UpdateDate != "" {
|
||||
if updateTime, err := time.Parse("2006-01-02 15:04:05", t.UpdateDate); err == nil {
|
||||
deltaSeconds = now.Sub(updateTime).Seconds()
|
||||
deltaSeconds, err = common.DeltaSeconds(t.UpdateDate)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user