Skip to content

Commit 95d8fa7

Browse files
committed
feat[Go]: implement chunk REST APIs (list, add, update, switch)
Ports four chunk endpoints from Python to Go (#15668): GET /api/v1/datasets/:dataset_id/documents/:document_id/chunks (ListChunksREST) POST /api/v1/datasets/:dataset_id/documents/:document_id/chunks (AddChunk) PATCH /api/v1/datasets/:dataset_id/documents/:document_id/chunks/:chunk_id (UpdateChunkREST) PATCH /api/v1/datasets/:dataset_id/documents/:document_id/chunks (SwitchChunks) Addresses review feedback (CodeRabbit) and the CI build break: - tokenizer.Tokenize returns (string, error); the bogus contentLtks.(string) assertion on a non-interface value (a compile error breaking CI) is removed — the string is passed straight to FineGrainedTokenize - ListChunksREST page/page_size now parse with strconv.Atoi instead of json.Number(...).Int64() - embedTexts no longer reports len(d.Embedding) (the fixed vector dimension) as token usage; token count is derived from the input text via the project tokenizer (estimateTokenCount), since the embedding driver exposes no provider token usage Rebased onto latest main, resolving the chunk.go import conflict with upstream's RetrievalTest refactor (now using service/nlp): merged the import union (keeping nlp from upstream and xxhash/gorm from this PR).
1 parent 55abf4f commit 95d8fa7

4 files changed

Lines changed: 733 additions & 6 deletions

File tree

internal/handler/chunk.go

Lines changed: 197 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,12 +19,13 @@ package handler
1919
import (
2020
"encoding/json"
2121
"net/http"
22+
"strconv"
2223
"strings"
23-
"ragflow/internal/common"
2424

2525
"github.com/gin-gonic/gin"
2626
"go.uber.org/zap"
2727

28+
"ragflow/internal/common"
2829
"ragflow/internal/service"
2930
)
3031

@@ -35,6 +36,10 @@ type chunkService interface {
3536
List(req *service.ListChunksRequest, userID string) (*service.ListChunksResponse, error)
3637
UpdateChunk(req *service.UpdateChunkRequest, userID string) error
3738
RemoveChunks(req *service.RemoveChunksRequest, userID string) (int64, error)
39+
ListChunksREST(datasetID, documentID, userID string, page, pageSize int, keywords string, available *bool) (*service.ListChunksResponse, error)
40+
AddChunk(datasetID, documentID, userID string, req *service.AddChunkRequest) (map[string]interface{}, error)
41+
UpdateChunkREST(datasetID, documentID, chunkID, userID string, req *service.UpdateChunkRESTRequest) error
42+
SwitchChunks(datasetID, documentID, userID string, chunkIDs []string, available bool) error
3843
}
3944

4045
// ChunkHandler chunk handler
@@ -387,6 +392,197 @@ func (h *ChunkHandler) UpdateChunk(c *gin.Context) {
387392
})
388393
}
389394

395+
// ListChunksREST lists chunks for a document inside a dataset.
396+
// @Summary List Chunks
397+
// @Description List chunks for a document (dataset_id and document_id from path).
398+
// @Tags chunks
399+
// @Produce json
400+
// @Param dataset_id path string true "Dataset ID"
401+
// @Param document_id path string true "Document ID"
402+
// @Param page query int false "Page number (default 1)"
403+
// @Param page_size query int false "Items per page (default 30)"
404+
// @Param keywords query string false "Keyword filter"
405+
// @Param available query bool false "Filter by available status"
406+
// @Success 200 {object} map[string]interface{}
407+
// @Router /api/v1/datasets/{dataset_id}/documents/{document_id}/chunks [get]
408+
func (h *ChunkHandler) ListChunksREST(c *gin.Context) {
409+
user, errorCode, errorMessage := GetUser(c)
410+
if errorCode != common.CodeSuccess {
411+
jsonError(c, errorCode, errorMessage)
412+
return
413+
}
414+
415+
datasetID := c.Param("dataset_id")
416+
documentID := c.Param("document_id")
417+
if datasetID == "" || documentID == "" {
418+
c.JSON(http.StatusOK, gin.H{"code": common.CodeDataError, "data": false, "message": "dataset_id and document_id are required"})
419+
return
420+
}
421+
422+
page := 1
423+
if v := c.Query("page"); v != "" {
424+
if p, err := strconv.Atoi(v); err == nil && p > 0 {
425+
page = p
426+
}
427+
}
428+
pageSize := 30
429+
if v := c.Query("page_size"); v != "" {
430+
if ps, err := strconv.Atoi(v); err == nil && ps > 0 {
431+
if ps > 100 {
432+
ps = 100
433+
}
434+
pageSize = ps
435+
}
436+
}
437+
keywords := c.Query("keywords")
438+
439+
var available *bool
440+
if v := c.Query("available"); v != "" {
441+
b := v == "true" || v == "1"
442+
available = &b
443+
}
444+
445+
resp, err := h.chunkService.ListChunksREST(datasetID, documentID, user.ID, page, pageSize, keywords, available)
446+
if err != nil {
447+
c.JSON(http.StatusOK, gin.H{"code": common.CodeDataError, "data": false, "message": err.Error()})
448+
return
449+
}
450+
451+
c.JSON(http.StatusOK, gin.H{"code": common.CodeSuccess, "data": resp, "message": "success"})
452+
}
453+
454+
// AddChunk adds a manually created chunk to a document.
455+
// @Summary Add Chunk
456+
// @Description Create a new chunk for a document with content, keywords, and questions.
457+
// @Tags chunks
458+
// @Accept json
459+
// @Produce json
460+
// @Param dataset_id path string true "Dataset ID"
461+
// @Param document_id path string true "Document ID"
462+
// @Param request body service.AddChunkRequest true "chunk content"
463+
// @Success 200 {object} map[string]interface{}
464+
// @Router /api/v1/datasets/{dataset_id}/documents/{document_id}/chunks [post]
465+
func (h *ChunkHandler) AddChunk(c *gin.Context) {
466+
user, errorCode, errorMessage := GetUser(c)
467+
if errorCode != common.CodeSuccess {
468+
jsonError(c, errorCode, errorMessage)
469+
return
470+
}
471+
472+
datasetID := c.Param("dataset_id")
473+
documentID := c.Param("document_id")
474+
if datasetID == "" || documentID == "" {
475+
c.JSON(http.StatusOK, gin.H{"code": common.CodeDataError, "data": false, "message": "dataset_id and document_id are required"})
476+
return
477+
}
478+
479+
var req service.AddChunkRequest
480+
if err := c.ShouldBindJSON(&req); err != nil {
481+
c.JSON(http.StatusOK, gin.H{"code": common.CodeDataError, "data": false, "message": err.Error()})
482+
return
483+
}
484+
485+
data, err := h.chunkService.AddChunk(datasetID, documentID, user.ID, &req)
486+
if err != nil {
487+
c.JSON(http.StatusOK, gin.H{"code": common.CodeDataError, "data": false, "message": err.Error()})
488+
return
489+
}
490+
491+
c.JSON(http.StatusOK, gin.H{"code": common.CodeSuccess, "data": data, "message": "success"})
492+
}
493+
494+
// UpdateChunkREST updates a chunk's content, keywords, and availability.
495+
// Re-embeds the chunk when content or questions change.
496+
// @Summary Update Chunk (REST)
497+
// @Description Partially update a chunk by ID, re-embedding on content/question changes.
498+
// @Tags chunks
499+
// @Accept json
500+
// @Produce json
501+
// @Param dataset_id path string true "Dataset ID"
502+
// @Param document_id path string true "Document ID"
503+
// @Param chunk_id path string true "Chunk ID"
504+
// @Param request body service.UpdateChunkRESTRequest true "fields to update"
505+
// @Success 200 {object} map[string]interface{}
506+
// @Router /api/v1/datasets/{dataset_id}/documents/{document_id}/chunks/{chunk_id} [patch]
507+
func (h *ChunkHandler) UpdateChunkREST(c *gin.Context) {
508+
user, errorCode, errorMessage := GetUser(c)
509+
if errorCode != common.CodeSuccess {
510+
jsonError(c, errorCode, errorMessage)
511+
return
512+
}
513+
514+
datasetID := c.Param("dataset_id")
515+
documentID := c.Param("document_id")
516+
chunkID := c.Param("chunk_id")
517+
if datasetID == "" || documentID == "" || chunkID == "" {
518+
c.JSON(http.StatusOK, gin.H{"code": common.CodeDataError, "data": false, "message": "dataset_id, document_id and chunk_id are required"})
519+
return
520+
}
521+
522+
var req service.UpdateChunkRESTRequest
523+
if err := c.ShouldBindJSON(&req); err != nil {
524+
c.JSON(http.StatusOK, gin.H{"code": common.CodeDataError, "data": false, "message": err.Error()})
525+
return
526+
}
527+
528+
if err := h.chunkService.UpdateChunkREST(datasetID, documentID, chunkID, user.ID, &req); err != nil {
529+
c.JSON(http.StatusOK, gin.H{"code": common.CodeDataError, "data": false, "message": err.Error()})
530+
return
531+
}
532+
533+
c.JSON(http.StatusOK, gin.H{"code": common.CodeSuccess, "data": true, "message": "success"})
534+
}
535+
536+
// SwitchChunks bulk-toggles the available status for a list of chunks.
537+
// @Summary Switch Chunks Availability
538+
// @Description Toggle available_int for a set of chunk IDs.
539+
// @Tags chunks
540+
// @Accept json
541+
// @Produce json
542+
// @Param dataset_id path string true "Dataset ID"
543+
// @Param document_id path string true "Document ID"
544+
// @Param request body object true "chunk_ids + available"
545+
// @Success 200 {object} map[string]interface{}
546+
// @Router /api/v1/datasets/{dataset_id}/documents/{document_id}/chunks [patch]
547+
func (h *ChunkHandler) SwitchChunks(c *gin.Context) {
548+
user, errorCode, errorMessage := GetUser(c)
549+
if errorCode != common.CodeSuccess {
550+
jsonError(c, errorCode, errorMessage)
551+
return
552+
}
553+
554+
datasetID := c.Param("dataset_id")
555+
documentID := c.Param("document_id")
556+
if datasetID == "" || documentID == "" {
557+
c.JSON(http.StatusOK, gin.H{"code": common.CodeDataError, "data": false, "message": "dataset_id and document_id are required"})
558+
return
559+
}
560+
561+
var body struct {
562+
ChunkIDs []string `json:"chunk_ids"`
563+
Available *bool `json:"available"`
564+
}
565+
if err := c.ShouldBindJSON(&body); err != nil {
566+
c.JSON(http.StatusOK, gin.H{"code": common.CodeDataError, "data": false, "message": err.Error()})
567+
return
568+
}
569+
if len(body.ChunkIDs) == 0 {
570+
c.JSON(http.StatusOK, gin.H{"code": common.CodeDataError, "data": false, "message": "`chunk_ids` is required."})
571+
return
572+
}
573+
if body.Available == nil {
574+
c.JSON(http.StatusOK, gin.H{"code": common.CodeDataError, "data": false, "message": "`available` is required."})
575+
return
576+
}
577+
578+
if err := h.chunkService.SwitchChunks(datasetID, documentID, user.ID, body.ChunkIDs, *body.Available); err != nil {
579+
c.JSON(http.StatusOK, gin.H{"code": common.CodeDataError, "data": false, "message": err.Error()})
580+
return
581+
}
582+
583+
c.JSON(http.StatusOK, gin.H{"code": common.CodeSuccess, "data": true, "message": "success"})
584+
}
585+
390586
// RemoveChunks handles chunk removal requests
391587
// @Summary Remove Chunks
392588
// @Description Remove chunks from a document

internal/handler/chunk_test.go

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,18 @@ func (m *mockChunkSvc) UpdateChunk(*service.UpdateChunkRequest, string) error {
4242
func (m *mockChunkSvc) RemoveChunks(*service.RemoveChunksRequest, string) (int64, error) {
4343
panic("not implemented")
4444
}
45+
func (m *mockChunkSvc) ListChunksREST(datasetID, documentID, userID string, page, pageSize int, keywords string, available *bool) (*service.ListChunksResponse, error) {
46+
panic("not implemented")
47+
}
48+
func (m *mockChunkSvc) AddChunk(datasetID, documentID, userID string, req *service.AddChunkRequest) (map[string]interface{}, error) {
49+
panic("not implemented")
50+
}
51+
func (m *mockChunkSvc) UpdateChunkREST(datasetID, documentID, chunkID, userID string, req *service.UpdateChunkRESTRequest) error {
52+
panic("not implemented")
53+
}
54+
func (m *mockChunkSvc) SwitchChunks(datasetID, documentID, userID string, chunkIDs []string, available bool) error {
55+
panic("not implemented")
56+
}
4557

4658
func setupChunkRetrievalTest(userID string) (*gin.Engine, *mockChunkSvc) {
4759
mock := &mockChunkSvc{}

internal/router/router.go

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -260,10 +260,14 @@ func (r *Router) Setup(engine *gin.Engine) {
260260
datasets.DELETE("/:dataset_id/documents", r.documentHandler.DeleteDocuments)
261261

262262
// Dataset document chunk
263+
datasets.GET("/:dataset_id/documents/:document_id/chunks", r.chunkHandler.ListChunksREST)
264+
datasets.POST("/:dataset_id/documents/:document_id/chunks", r.chunkHandler.AddChunk)
263265
datasets.GET("/:dataset_id/documents/:document_id/chunks/:chunk_id", r.chunkHandler.Get)
266+
datasets.PATCH("/:dataset_id/documents/:document_id/chunks/:chunk_id", r.chunkHandler.UpdateChunkREST)
267+
datasets.PATCH("/:dataset_id/documents/:document_id/chunks", r.chunkHandler.SwitchChunks)
268+
datasets.DELETE("/:dataset_id/documents/:document_id/chunks", r.chunkHandler.RemoveChunks)
264269
datasets.POST("/:dataset_id/documents/parse", r.documentHandler.ParseDocuments)
265270
datasets.POST("/:dataset_id/documents/stop", r.documentHandler.StopParseDocuments)
266-
datasets.DELETE("/:dataset_id/documents/:document_id/chunks", r.chunkHandler.RemoveChunks)
267271
}
268272

269273
// Search routes

0 commit comments

Comments
 (0)