mirror of
https://github.com/infiniflow/ragflow.git
synced 2026-06-29 23:41:12 +08:00
Go: update search (#14023)
### What problem does this PR solve? Update search ### 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:
@@ -155,8 +155,25 @@ func (dao *SearchDAO) DeleteByID(id string) error {
|
||||
// Accessible4Deletion checks if a search can be deleted by a specific user
|
||||
// Reference: Python search_service.py::accessible4deletion
|
||||
// Returns true if the search exists, is valid, and was created by the user
|
||||
func (dao *SearchDAO) Accessible4Deletion(searchID string, userID string) bool {
|
||||
func (dao *SearchDAO) Accessible4Deletion(searchID string, userID string) (bool, error) {
|
||||
var search entity.Search
|
||||
err := DB.Where("id = ? AND created_by = ? AND status = ?", searchID, userID, "1").First(&search).Error
|
||||
return err == nil
|
||||
return err == nil, err
|
||||
}
|
||||
|
||||
// GetByTenantIDAndID gets search by tenant ID and search ID
|
||||
// Reference: Python SearchService.query(tenant_id=tenant_id, id=search_id)
|
||||
func (dao *SearchDAO) GetByTenantIDAndID(tenantID string, searchID string) (*entity.Search, error) {
|
||||
var search entity.Search
|
||||
err := DB.Where("tenant_id = ? AND id = ? AND status = ?", tenantID, searchID, "1").First(&search).Error
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &search, nil
|
||||
}
|
||||
|
||||
// UpdateByID updates search by ID
|
||||
// Reference: Python common_service.py::update_by_id
|
||||
func (dao *SearchDAO) UpdateByID(id string, updates map[string]interface{}) error {
|
||||
return DB.Model(&entity.Search{}).Where("id = ?", id).Updates(updates).Error
|
||||
}
|
||||
|
||||
@@ -309,3 +309,115 @@ func (h *SearchHandler) DeleteSearch(c *gin.Context) {
|
||||
"message": "success",
|
||||
})
|
||||
}
|
||||
|
||||
// UpdateSearch update a search app
|
||||
// @Summary Update Search App
|
||||
// @Description Update a search app by ID
|
||||
// @Tags search
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param search_id path string true "search app ID"
|
||||
// @Param request body service.UpdateSearchRequest true "search update parameters"
|
||||
// @Success 200 {object} map[string]interface{}
|
||||
// @Router /api/v1/searches/{search_id} [put]
|
||||
func (h *SearchHandler) UpdateSearch(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
|
||||
}
|
||||
|
||||
// Parse request body
|
||||
var req service.UpdateSearchRequest
|
||||
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
|
||||
}
|
||||
|
||||
// Validate name (same as Python validation)
|
||||
if err := common.ValidateName(req.Name); err != nil {
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"code": common.CodeDataError,
|
||||
"data": nil,
|
||||
"message": err.Error(),
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
// Update search
|
||||
updatedSearch, err := h.searchService.UpdateSearch(userID, searchID, &req)
|
||||
if err != nil {
|
||||
errMsg := err.Error()
|
||||
switch errMsg {
|
||||
case "no authorization":
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"code": common.CodeAuthenticationError,
|
||||
"data": false,
|
||||
"message": "No authorization.",
|
||||
})
|
||||
case "duplicated search name":
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"code": common.CodeDataError,
|
||||
"data": nil,
|
||||
"message": "Duplicated search name.",
|
||||
})
|
||||
default:
|
||||
// Check if it's a "cannot find search" error
|
||||
if len(errMsg) > 18 && errMsg[:18] == "cannot find search" {
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"code": common.CodeDataError,
|
||||
"data": false,
|
||||
"message": errMsg,
|
||||
})
|
||||
} else {
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"code": common.CodeDataError,
|
||||
"data": nil,
|
||||
"message": errMsg,
|
||||
})
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// Convert to response format (same as Python updated_search.to_dict())
|
||||
result := map[string]interface{}{
|
||||
"id": updatedSearch.ID,
|
||||
"tenant_id": updatedSearch.TenantID,
|
||||
"name": updatedSearch.Name,
|
||||
"description": updatedSearch.Description,
|
||||
"created_by": updatedSearch.CreatedBy,
|
||||
"status": updatedSearch.Status,
|
||||
"create_time": updatedSearch.CreateTime,
|
||||
"update_time": updatedSearch.UpdateTime,
|
||||
"search_config": updatedSearch.SearchConfig,
|
||||
}
|
||||
|
||||
if updatedSearch.Avatar != nil {
|
||||
result["avatar"] = *updatedSearch.Avatar
|
||||
}
|
||||
|
||||
// Return success response
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"code": common.CodeSuccess,
|
||||
"data": result,
|
||||
"message": "success",
|
||||
})
|
||||
}
|
||||
|
||||
@@ -175,17 +175,18 @@ func (r *Router) Setup(engine *gin.Engine) {
|
||||
// message.GET("/:memory_id/:message_id/content", r.memoryHandler.GetMessageContent)
|
||||
// }
|
||||
|
||||
chats := v1.Group("/chats")
|
||||
{
|
||||
chats.GET("", r.chatHandler.ListChats)
|
||||
chats.GET("/:chat_id", r.chatHandler.GetChat)
|
||||
}
|
||||
chats := v1.Group("/chats")
|
||||
{
|
||||
chats.GET("", r.chatHandler.ListChats)
|
||||
chats.GET("/:chat_id", r.chatHandler.GetChat)
|
||||
}
|
||||
|
||||
searches := v1.Group("/searches")
|
||||
{
|
||||
searches.GET("", r.searchHandler.ListSearches)
|
||||
searches.POST("", r.searchHandler.CreateSearch)
|
||||
searches.GET("/:search_id", r.searchHandler.GetSearch)
|
||||
searches.PUT("/:search_id", r.searchHandler.UpdateSearch)
|
||||
searches.DELETE("/:search_id", r.searchHandler.DeleteSearch)
|
||||
}
|
||||
|
||||
|
||||
@@ -149,16 +149,6 @@ type CreateSearchResponse struct {
|
||||
// 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()
|
||||
@@ -238,15 +228,105 @@ func (s *SearchService) GetSearchDetail(userID string, searchID string) (*entity
|
||||
func (s *SearchService) DeleteSearch(userID string, searchID string) error {
|
||||
// Step 1: Check deletion permission (same as Python SearchService.accessible4deletion)
|
||||
// Python: cls.model.select().where(cls.model.id == search_id, cls.model.created_by == user_id, cls.model.status == StatusEnum.VALID.value).first()
|
||||
if !s.searchDAO.Accessible4Deletion(searchID, userID) {
|
||||
|
||||
status, err := s.searchDAO.Accessible4Deletion(searchID, userID)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to check deletion permission: %w", err)
|
||||
}
|
||||
|
||||
if !status {
|
||||
return fmt.Errorf("no authorization")
|
||||
}
|
||||
|
||||
// Step 2: Execute delete (same as Python SearchService.delete_by_id)
|
||||
// Python: cls.model.delete().where(cls.model.id == pid).execute()
|
||||
if err := s.searchDAO.DeleteByID(searchID); err != nil {
|
||||
if err = s.searchDAO.DeleteByID(searchID); err != nil {
|
||||
return fmt.Errorf("failed to delete search App %s: %w", searchID, err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// UpdateSearchRequest update search request
|
||||
// Reference: api/apps/restful_apis/search_api.py::update
|
||||
// Required fields: name, search_config
|
||||
// Optional fields: description
|
||||
// Immutable fields: search_id, tenant_id, created_by, update_time, id (will be removed)
|
||||
type UpdateSearchRequest struct {
|
||||
Name string `json:"name" binding:"required"`
|
||||
Description *string `json:"description,omitempty"`
|
||||
SearchConfig map[string]interface{} `json:"search_config" binding:"required"`
|
||||
}
|
||||
|
||||
func (s *SearchService) UpdateSearch(userID string, searchID string, req *UpdateSearchRequest) (*entity.Search, error) {
|
||||
// Step 1: Check update permission (same as delete - uses accessible4deletion)
|
||||
// Only creator can update
|
||||
|
||||
status, err := s.searchDAO.Accessible4Deletion(searchID, userID)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to check deletion permission: %w", err)
|
||||
}
|
||||
|
||||
if !status {
|
||||
return nil, fmt.Errorf("no authorization")
|
||||
}
|
||||
|
||||
// Step 2: Get existing search
|
||||
// Python: search_app = SearchService.query(tenant_id=current_user.id, id=search_id)[0]
|
||||
search, err := s.searchDAO.GetByTenantIDAndID(userID, searchID)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("cannot find search %s", searchID)
|
||||
}
|
||||
|
||||
// Step 3: Check for duplicate name (if name changed)
|
||||
// Python: if req["name"].lower() != search_app.name.lower() and len(SearchService.query(...)) >= 1
|
||||
trimmedName := req.Name
|
||||
if search.Name != trimmedName {
|
||||
existing, _ := s.searchDAO.GetByNameAndTenant(trimmedName, userID)
|
||||
if len(existing) > 0 {
|
||||
return nil, fmt.Errorf("duplicated search name")
|
||||
}
|
||||
}
|
||||
|
||||
// Step 4: Merge search_config
|
||||
// Python: req["search_config"] = {**current_config, **new_config}
|
||||
currentConfig := search.SearchConfig
|
||||
if currentConfig == nil {
|
||||
currentConfig = make(entity.JSONMap)
|
||||
}
|
||||
mergedConfig := make(entity.JSONMap)
|
||||
// Copy current config
|
||||
for k, v := range currentConfig {
|
||||
mergedConfig[k] = v
|
||||
}
|
||||
// Merge new config
|
||||
for k, v := range req.SearchConfig {
|
||||
mergedConfig[k] = v
|
||||
}
|
||||
|
||||
// Step 5: Prepare updates (excluding immutable fields)
|
||||
// Python removes: search_id, tenant_id, created_by, update_time, id
|
||||
updates := map[string]interface{}{
|
||||
"name": trimmedName,
|
||||
"search_config": mergedConfig,
|
||||
}
|
||||
|
||||
if req.Description != nil {
|
||||
updates["description"] = *req.Description
|
||||
}
|
||||
|
||||
// Step 6: Execute update
|
||||
// Python: SearchService.update_by_id(search_id, req)
|
||||
if err = s.searchDAO.UpdateByID(searchID, updates); err != nil {
|
||||
return nil, fmt.Errorf("failed to update search: %w", err)
|
||||
}
|
||||
|
||||
// Step 7: Fetch updated search
|
||||
// Python: e, updated_search = SearchService.get_by_id(search_id)
|
||||
updatedSearch, err := s.searchDAO.GetByID(searchID)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to fetch updated search: %w", err)
|
||||
}
|
||||
|
||||
return updatedSearch, nil
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user