Quellcode durchsuchen

增加了QA管理,以及QA的自定义chunk生成

dwp vor 7 Monaten
Ursprung
Commit
534e2f1f75
74 geänderte Dateien mit 4296 neuen und 239 gelöschten Zeilen
  1. 1 1
      server/pom.xml
  2. 1 1
      server/src/main/java/com/giantan/ai/util/JsonUtil.java
  3. 1 1
      server/src/main/java/com/giantan/ai/util/SpringContextUtil.java
  4. 1 1
      server/src/main/java/com/giantan/ai/util/dict/GItem.java
  5. 1 1
      server/src/main/java/com/giantan/ai/util/dict/GLookups.java
  6. 1 1
      server/src/main/java/com/giantan/ai/util/dict/IItem.java
  7. 18 0
      server/src/main/java/com/giantan/data/index/DefaultCollectionMapper.java
  8. 154 1
      server/src/main/java/com/giantan/data/index/HybridSearch.java
  9. 2 5
      server/src/main/java/com/giantan/data/index/HybridSearch2Controller.java
  10. 48 37
      server/src/main/java/com/giantan/data/index/HybridSearchController.java
  11. 8 0
      server/src/main/java/com/giantan/data/index/ICollectionMapper.java
  12. 18 0
      server/src/main/java/com/giantan/data/index/IHybridSearch.java
  13. 5 0
      server/src/main/java/com/giantan/data/index/IndexConfig.java
  14. 33 0
      server/src/main/java/com/giantan/data/index/dto/DocSearchResp.java
  15. 1 2
      server/src/main/java/com/giantan/data/kvs/constant/KvConstants.java
  16. 0 2
      server/src/main/java/com/giantan/data/kvs/controller/KvCollectionsController.java
  17. 1 1
      server/src/main/java/com/giantan/data/kvs/controller/KvDocsController.java
  18. 1 2
      server/src/main/java/com/giantan/data/kvs/controller/KvIndexesController.java
  19. 66 40
      server/src/main/java/com/giantan/data/kvs/repository/GConverter.java
  20. 1 2
      server/src/main/java/com/giantan/data/kvs/repository/GDynamicRepository.java
  21. 25 11
      server/src/main/java/com/giantan/data/kvs/repository/GRepository.java
  22. 9 1
      server/src/main/java/com/giantan/data/kvs/repository/GRowMapper.java
  23. 101 0
      server/src/main/java/com/giantan/data/kvs/repository/GenericRowMapper.java
  24. 900 0
      server/src/main/java/com/giantan/data/kvs/repository/index/GIndexedRepository.java
  25. 35 0
      server/src/main/java/com/giantan/data/kvs/repository/index/IIndexer.java
  26. 1 1
      server/src/main/java/com/giantan/data/kvs/service/ICollectionService.java
  27. 1 1
      server/src/main/java/com/giantan/data/kvs/service/IIndexer.java
  28. 1 1
      server/src/main/java/com/giantan/data/kvs/service/impl/GCollection.java
  29. 3 3
      server/src/main/java/com/giantan/data/kvs/service/impl/GIndexer.java
  30. 1 1
      server/src/main/java/com/giantan/data/kvs/service/impl/KvCollectionService.java
  31. 2 2
      server/src/main/java/com/giantan/data/mds/MdsApplication.java
  32. 1 3
      server/src/main/java/com/giantan/data/mds/bot/GChatClient.java
  33. 17 0
      server/src/main/java/com/giantan/data/mds/constant/MdConstants.java
  34. 2 2
      server/src/main/java/com/giantan/data/mds/controller/ChatController.java
  35. 2 2
      server/src/main/java/com/giantan/data/mds/controller/ChunkController.java
  36. 3 3
      server/src/main/java/com/giantan/data/mds/controller/DownloadController.java
  37. 2 2
      server/src/main/java/com/giantan/data/mds/controller/MdCollectionsController.java
  38. 70 49
      server/src/main/java/com/giantan/data/mds/controller/MdDocsController.java
  39. 102 0
      server/src/main/java/com/giantan/data/mds/controller/MdSearchContoller.java
  40. 2 3
      server/src/main/java/com/giantan/data/mds/controller/TaskController.java
  41. 4 4
      server/src/main/java/com/giantan/data/mds/controller/TaxonomyController.java
  42. 8 1
      server/src/main/java/com/giantan/data/mds/repository/MdDynamicRepository.java
  43. 37 4
      server/src/main/java/com/giantan/data/mds/service/CollectionInstance.java
  44. 2 0
      server/src/main/java/com/giantan/data/mds/service/IMdChunksService.java
  45. 6 4
      server/src/main/java/com/giantan/data/mds/service/IMdDocsService.java
  46. 5 0
      server/src/main/java/com/giantan/data/mds/service/impl/MdChunksService.java
  47. 9 6
      server/src/main/java/com/giantan/data/mds/service/impl/MdCollectionsService.java
  48. 11 4
      server/src/main/java/com/giantan/data/mds/service/impl/MdCores.java
  49. 116 16
      server/src/main/java/com/giantan/data/mds/service/impl/MdDocsService.java
  50. 4 3
      server/src/main/java/com/giantan/data/mds/service/impl/MdTaxonomyService.java
  51. 1 1
      server/src/main/java/com/giantan/data/mds/service/impl/Vectorization.java
  52. 3 3
      server/src/main/java/com/giantan/data/mds/task/impl/ChunksTaskHandler.java
  53. 1 1
      server/src/main/java/com/giantan/data/mds/task/impl/EmbeddingTaskHandler.java
  54. 1 1
      server/src/main/java/com/giantan/data/mds/task/impl/MdsTaskHandler.java
  55. 57 0
      server/src/main/java/com/giantan/data/qa/Readme.java
  56. 36 0
      server/src/main/java/com/giantan/data/qa/constant/QaConstants.java
  57. 79 0
      server/src/main/java/com/giantan/data/qa/controller/QaCollectionsController.java
  58. 292 0
      server/src/main/java/com/giantan/data/qa/controller/QaDocsController.java
  59. 112 0
      server/src/main/java/com/giantan/data/qa/controller/QaSearchContoller.java
  60. 144 0
      server/src/main/java/com/giantan/data/qa/controller/QaTaxonomyController.java
  61. 41 0
      server/src/main/java/com/giantan/data/qa/repository/QaDocRepository.java
  62. 417 0
      server/src/main/java/com/giantan/data/qa/repository/QaIndexer.java
  63. 22 0
      server/src/main/java/com/giantan/data/qa/repository/QaTaxonomyRepository.java
  64. 10 0
      server/src/main/java/com/giantan/data/qa/service/ICollectionService.java
  65. 67 0
      server/src/main/java/com/giantan/data/qa/service/IDocsService.java
  66. 317 0
      server/src/main/java/com/giantan/data/qa/service/QaCollectionService.java
  67. 462 0
      server/src/main/java/com/giantan/data/qa/service/QaDocsService.java
  68. 290 0
      server/src/main/java/com/giantan/data/qa/service/QaTaxonomyService.java
  69. 24 4
      server/src/main/java/com/giantan/data/taxonomy/repository/DynamicTaxonomyRepository.java
  70. 5 0
      server/src/main/java/com/giantan/data/taxonomy/repository/IDynamicTaxonomyRepository.java
  71. 4 1
      server/src/test/java/com/giantan/data/mds/GRepositoryTest.java
  72. 1 1
      server/src/test/java/com/giantan/data/mds/MapDoubleToInt.java
  73. 24 2
      server/src/test/java/com/giantan/data/mds/MdsApplicationTests.java
  74. 42 0
      server/src/test/java/com/giantan/data/mds/SpelChunkGenerator.java

+ 1 - 1
server/pom.xml

@@ -9,7 +9,7 @@
         <version>1.0.0</version>
     </parent>
 
-    <version>2.0.0</version>
+    <version>2.1.1</version>
     <artifactId>mdserver</artifactId>
 
     <properties>

+ 1 - 1
server/src/main/java/com/giantan/ai/common/util/JsonUtil.java → server/src/main/java/com/giantan/ai/util/JsonUtil.java

@@ -1,4 +1,4 @@
-package com.giantan.ai.common.util;
+package com.giantan.ai.util;
 
 import com.fasterxml.jackson.core.JsonProcessingException;
 import com.fasterxml.jackson.core.type.TypeReference;

+ 1 - 1
server/src/main/java/com/giantan/ai/common/util/SpringContextUtil.java → server/src/main/java/com/giantan/ai/util/SpringContextUtil.java

@@ -1,4 +1,4 @@
-package com.giantan.ai.common.util;
+package com.giantan.ai.util;
 
 import org.springframework.beans.BeansException;
 import org.springframework.context.ApplicationContext;

+ 1 - 1
server/src/main/java/com/giantan/ai/common/util/dict/GItem.java → server/src/main/java/com/giantan/ai/util/dict/GItem.java

@@ -1,4 +1,4 @@
-package com.giantan.ai.common.util.dict;
+package com.giantan.ai.util.dict;
 
 import com.fasterxml.jackson.annotation.JsonIgnore;
 

+ 1 - 1
server/src/main/java/com/giantan/ai/common/util/dict/GLookups.java → server/src/main/java/com/giantan/ai/util/dict/GLookups.java

@@ -1,4 +1,4 @@
-package com.giantan.ai.common.util.dict;
+package com.giantan.ai.util.dict;
 
 import com.giantan.ai.util.trie.*;
 

+ 1 - 1
server/src/main/java/com/giantan/ai/common/util/dict/IItem.java → server/src/main/java/com/giantan/ai/util/dict/IItem.java

@@ -1,4 +1,4 @@
-package com.giantan.ai.common.util.dict;
+package com.giantan.ai.util.dict;
 
 public interface IItem {
     String getWord();

+ 18 - 0
server/src/main/java/com/giantan/data/index/DefaultCollectionMapper.java

@@ -0,0 +1,18 @@
+package com.giantan.data.index;
+
+public class DefaultCollectionMapper implements ICollectionMapper {
+
+    public DefaultCollectionMapper() {
+    }
+
+    @Override
+    public String mapping(String collection) {
+        return collection;
+    }
+
+    @Override
+    public boolean isOne2One(String collection) {
+        return true;
+    }
+
+}

+ 154 - 1
server/src/main/java/com/giantan/data/index/HybridSearch.java

@@ -1,9 +1,10 @@
 package com.giantan.data.index;
 
 
-import com.giantan.ai.common.util.JsonUtil;
+import com.giantan.ai.util.JsonUtil;
 import com.giantan.data.index.dto.DocReq;
 import com.giantan.data.index.dto.DocResp;
+import com.giantan.data.index.dto.DocSearchResp;
 import org.springframework.beans.factory.annotation.Value;
 import org.springframework.stereotype.Service;
 
@@ -41,6 +42,25 @@ public class HybridSearch implements IHybridSearch {
     public List<DocResp> add(String coll, List<DocReq> docs) throws IOException, InterruptedException {
         String body = JsonUtil.toJsonString(docs);
 
+        HttpRequest request = HttpRequest.newBuilder()
+                .uri(URI.create(url + coll + "/documents"))
+                .header("Content-Type", "application/json")
+                .header("User-Agent", clientInfo)
+                .method("POST", HttpRequest.BodyPublishers.ofString(body))
+                .build();
+        HttpResponse<String> response = HttpClient.newHttpClient().send(request, HttpResponse.BodyHandlers.ofString());
+        //System.out.println(response.body());
+        Map<String, Object> ret = JsonUtil.fromJsonString(response.body());
+        Object o = ret.get("data");
+        List<DocResp> ls = toDocResps(o);
+        return ls;
+    }
+
+    @Override
+    // 已经有了embedding,直接增加
+    public List<DocResp> addDirect(String coll, List<DocReq> docs) throws IOException, InterruptedException {
+        String body = JsonUtil.toJsonString(docs);
+
         HttpRequest request = HttpRequest.newBuilder()
                 .uri(URI.create(url + coll + "/documents/insert"))
                 .header("Content-Type", "application/json")
@@ -127,5 +147,138 @@ public class HybridSearch implements IHybridSearch {
         return IndexUtils.toBoolean(o);
     }
 
+    @Override
+    public boolean drop(String coll) throws IOException, InterruptedException {
+        HttpRequest request = HttpRequest.newBuilder()
+                .uri(URI.create(url + coll))
+                .header("User-Agent", clientInfo)
+                .method("DELETE", HttpRequest.BodyPublishers.noBody())
+                .build();
+        HttpResponse<String> response = HttpClient.newHttpClient().send(request, HttpResponse.BodyHandlers.ofString());
+        //System.out.println(response.body());
+        Map<String, Object> ret = JsonUtil.fromJsonString(response.body());
+        Object o = ret.get("data");
+        return IndexUtils.toBoolean(o);
+    }
+
+    @Override
+    public List<DocResp> getDocumentsByIdFilter(String coll, String filter) throws IOException, InterruptedException {
+        String tmp = """
+                {
+                	"filterExpression": "doc_id LIKE '%s'"
+                }
+                """;
+        String body = String.format(tmp, filter);
 
+        HttpRequest request = HttpRequest.newBuilder()
+                .uri(URI.create(url + coll + "/documents/by-filter"))
+                .header("Content-Type", "application/json")
+                .header("User-Agent", clientInfo)
+                .method("POST", HttpRequest.BodyPublishers.ofString(body))
+                .build();
+        HttpResponse<String> response = HttpClient.newHttpClient().send(request, HttpResponse.BodyHandlers.ofString());
+        //System.out.println(response.body());
+        Map<String, Object> ret = JsonUtil.fromJsonString(response.body());
+        Object o = ret.get("data");
+        List<DocResp> ls = toDocResps(o);
+        return ls;
+    }
+
+    @Override
+    public int deleteDocumentsByIdFilter(String coll, String filter) throws IOException, InterruptedException {
+        String tmp = """
+                {
+                	"filterExpression": "doc_id LIKE '%s'"
+                }
+                """;
+        String body = String.format(tmp, filter);
+        HttpRequest request = HttpRequest.newBuilder()
+                .uri(URI.create(url + coll + "/documents/by-filter"))
+                .header("Content-Type", "application/json")
+                .header("User-Agent", clientInfo)
+                .method("DELETE", HttpRequest.BodyPublishers.ofString(body))
+                .build();
+        HttpResponse<String> response = HttpClient.newHttpClient().send(request, HttpResponse.BodyHandlers.ofString());
+        //System.out.println(response.body());
+        Map<String, Object> ret = JsonUtil.fromJsonString(response.body());
+        Object o = ret.get("data");
+        if (o != null && o instanceof Integer) {
+            return (Integer) o;
+        }
+        return 0;
+    }
+
+
+    //fulltextSearch
+    //hybridSearch
+    //similaritySearch
+
+    @Override
+    public List<DocSearchResp> fulltextSearch(String coll, Map<String, Object> query) throws IOException, InterruptedException {
+        String body = JsonUtil.toJsonString(query);
+        HttpRequest request = HttpRequest.newBuilder()
+                .uri(URI.create(url + coll + "/documents/fulltextSearch"))
+                .header("Content-Type", "application/json")
+                .header("User-Agent", clientInfo)
+                .method("POST", HttpRequest.BodyPublishers.ofString(body))
+                .build();
+        HttpResponse<String> response = HttpClient.newHttpClient().send(request, HttpResponse.BodyHandlers.ofString());
+        Map<String, Object> ret = JsonUtil.fromJsonString(response.body());
+        Object o = ret.get("data");
+        List<DocSearchResp> rets = new ArrayList<>();
+        if (o != null && o instanceof List ls) {
+            for (Object r1 : ls) {
+                if (r1 != null && r1 instanceof Map m) {
+                    rets.add(DocSearchResp.fromMap(m));
+                }
+            }
+        }
+        return rets;
+    }
+
+    @Override
+    public List<DocSearchResp> similaritySearch(String coll, Map<String, Object> query) throws IOException, InterruptedException {
+        String body = JsonUtil.toJsonString(query);
+        HttpRequest request = HttpRequest.newBuilder()
+                .uri(URI.create(url + coll + "/documents/similaritySearch"))
+                .header("Content-Type", "application/json")
+                .header("User-Agent", clientInfo)
+                .method("POST", HttpRequest.BodyPublishers.ofString(body))
+                .build();
+        HttpResponse<String> response = HttpClient.newHttpClient().send(request, HttpResponse.BodyHandlers.ofString());
+        Map<String, Object> ret = JsonUtil.fromJsonString(response.body());
+        Object o = ret.get("data");
+        List<DocSearchResp> rets = new ArrayList<>();
+        if (o != null && o instanceof List ls) {
+            for (Object r1 : ls) {
+                if (r1 != null && r1 instanceof Map m) {
+                    rets.add(DocSearchResp.fromMap(m));
+                }
+            }
+        }
+        return rets;
+    }
+
+    @Override
+    public List<DocSearchResp> hybridSearch(String coll, Map<String, Object> query) throws IOException, InterruptedException {
+        String body = JsonUtil.toJsonString(query);
+        HttpRequest request = HttpRequest.newBuilder()
+                .uri(URI.create(url + coll + "/documents/hybridSearch"))
+                .header("Content-Type", "application/json")
+                .header("User-Agent", clientInfo)
+                .method("POST", HttpRequest.BodyPublishers.ofString(body))
+                .build();
+        HttpResponse<String> response = HttpClient.newHttpClient().send(request, HttpResponse.BodyHandlers.ofString());
+        Map<String, Object> ret = JsonUtil.fromJsonString(response.body());
+        Object o = ret.get("data");
+        List<DocSearchResp> rets = new ArrayList<>();
+        if (o != null && o instanceof List ls) {
+            for (Object r1 : ls) {
+                if (r1 != null && r1 instanceof Map m) {
+                    rets.add(DocSearchResp.fromMap(m));
+                }
+            }
+        }
+        return rets;
+    }
 }

+ 2 - 5
server/src/main/java/com/giantan/data/index/HybridSearch2Controller.java

@@ -2,7 +2,7 @@ package com.giantan.data.index;
 
 import com.giantan.ai.common.reponse.R;
 import com.giantan.data.index.dto.DocResp;
-import com.giantan.data.kvs.constant.KvConstants;
+import com.giantan.data.mds.constant.MdConstants;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.web.bind.annotation.RequestMapping;
 import org.springframework.web.bind.annotation.RestController;
@@ -13,14 +13,11 @@ import java.lang.invoke.MethodHandles;
 
 import org.springframework.http.ResponseEntity;
 import org.springframework.web.bind.annotation.*;
-import org.springframework.web.client.RestTemplate;
 
-import java.lang.invoke.MethodHandles;
-import java.util.Collections;
 import java.util.List;
 
 @RestController
-    @RequestMapping(KvConstants.API_PREFIX + "/collections/{coll}/se")
+    @RequestMapping(IHybridSearch.API_PREFIX + "/collections/{coll}")
     public class HybridSearch2Controller {
         private static final org.slf4j.Logger log
                 = org.slf4j.LoggerFactory.getLogger(MethodHandles.lookup().lookupClass());

+ 48 - 37
server/src/main/java/com/giantan/data/index/HybridSearchController.java

@@ -1,6 +1,6 @@
 package com.giantan.data.index;
 
-import com.giantan.data.kvs.constant.KvConstants;
+import com.giantan.data.qa.constant.QaConstants;
 import jakarta.servlet.http.HttpServletRequest;
 import org.springframework.beans.factory.annotation.Value;
 import org.springframework.http.HttpEntity;
@@ -14,35 +14,11 @@ import java.lang.invoke.MethodHandles;
 import java.util.Collections;
 
 @RestController
-@RequestMapping(KvConstants.API_PREFIX + "/collections/{coll}/indexes")
+//@RequestMapping(IHybridSearch.API_PREFIX + "/collections/{coll}/indexes")
+//@RequestMapping(IHybridSearch.API_PREFIX + "/collections/{coll}")
 public class HybridSearchController {
     private static final org.slf4j.Logger log
             = org.slf4j.LoggerFactory.getLogger(MethodHandles.lookup().lookupClass());
-//    @Autowired
-//    IHybridSearch hybridSearch;
-//
-//    // /by-ids
-//    @PostMapping("/documents/batch")
-//    public ResponseEntity<R> getDocumentsByIds(@PathVariable String coll, @RequestBody List<String> ids) throws IOException, InterruptedException {
-//        List<DocResp> docs = hybridSearch.getDocumentsByIds(coll, ids);
-//        return ResponseEntity.ok(R.data(docs));
-//    }
-//
-//    @GetMapping("/{id}")
-//    public ResponseEntity<R> getDocuments( @PathVariable String coll, @PathVariable String id) throws IOException, InterruptedException {
-//        List<DocResp> docs = hybridSearch.getDocumentsByIds(coll, List.of(id));
-//        if (docs.isEmpty()) {
-//            return ResponseEntity.ok(R.data(null));
-//        }
-//        return ResponseEntity.ok(R.data(docs.get(0)));
-//    }
-//
-//    @GetMapping("/ids")
-//    public ResponseEntity<R> getAllDocumentIds( @PathVariable String coll) {
-//        List<String> ids = hybridSearch.getAllIds(coll);
-//        return ResponseEntity.ok(R.data(ids));
-//
-//    }
 
     @Value("${qas.url}")
     String url = "http://120.78.4.46:7387/v1/collections/";
@@ -52,7 +28,8 @@ public class HybridSearchController {
     //private static final String SRC_PREFIX = "http://127.0.0.1:18211/v1/md";
     private String TARGET_PREFIX = "http://120.78.4.46:7387/v1";
 
-    @RequestMapping("/**")
+
+    @RequestMapping(IHybridSearch.API_PREFIX + "/collections/{coll}/**")
     public ResponseEntity<byte[]> proxyAll(HttpServletRequest request,
                                            @RequestBody(required = false) byte[] body) {
 
@@ -61,16 +38,43 @@ public class HybridSearchController {
 
         // 替换前缀
         //String targetPath = requestUri.replaceFirst("/v1/md", "");
-        int ii = requestUri.indexOf(KvConstants.API_PREFIX);
+        int ii = requestUri.indexOf(IHybridSearch.API_PREFIX);
 
         // 去掉 /indexes
-        String targetPath = requestUri.substring(ii+KvConstants.API_PREFIX.length());
-        targetPath = targetPath.replaceFirst("/indexes", "");
+        String targetPath = requestUri.substring(ii + IHybridSearch.API_PREFIX.length());
+        targetPath = targetPath.replaceFirst("/indexes", "/documents");
 
-        TARGET_PREFIX = url.replaceFirst("/collections","");
+        TARGET_PREFIX = url.replaceFirst("/collections/", "");
+        //TARGET_PREFIX = url.replaceFirst("/collections", "");
         // 拼接目标 URL
         String targetUrl = TARGET_PREFIX + targetPath + (query != null ? "?" + query : "");
 
+        return redirect(request, body, targetUrl);
+    }
+
+//    @RequestMapping(QaConstants.API_PREFIX + "/collections/{coll}/indexes/**")
+//    public ResponseEntity<byte[]> proxyAllQa(HttpServletRequest request,
+//                                           @RequestBody(required = false) byte[] body) {
+//
+//        String requestUri = request.getRequestURI(); // 原始 URI
+//        String query = request.getQueryString();
+//
+//        // 替换前缀
+//        //String targetPath = requestUri.replaceFirst("/v1/md", "");
+//        int ii = requestUri.indexOf(QaConstants.API_PREFIX);
+//
+//        // 去掉 /indexes
+//        String targetPath = requestUri.substring(ii + QaConstants.API_PREFIX.length());
+//        //targetPath = targetPath.replaceFirst("/indexes", "");
+//
+//        TARGET_PREFIX = url.replaceFirst("/collections", "");
+//        // 拼接目标 URL
+//        String targetUrl = TARGET_PREFIX + targetPath + (query != null ? "?" + query : "");
+//
+//        return redirect(request, body, targetUrl);
+//    }
+
+    public ResponseEntity<byte[]> redirect(HttpServletRequest request, byte[] body, String targetUrl) {
 
         // 构造请求头
         HttpHeaders headers = new HttpHeaders();
@@ -82,7 +86,7 @@ public class HybridSearchController {
 
         // 打印请求日志
         int requestSize = body != null ? body.length : 0;
-        log.info("[Qas] " + method + " " + targetUrl + " | request size: " + requestSize + " bytes");
+        log.info("[SE] " + method + " " + targetUrl + " | request size: " + requestSize + " bytes");
 
         // 构造请求实体
         HttpEntity<byte[]> entity = new HttpEntity<>(body, headers);
@@ -97,12 +101,19 @@ public class HybridSearchController {
 
         // 打印响应日志
         int responseSize = response.getBody() != null ? response.getBody().length : 0;
-        log.info("[Qas] response status: " + response.getStatusCode() + " | response size: " + responseSize + " bytes");
-
-        // 返回原始响应
+        log.info("[SE] response status: " + response.getStatusCode() + " | response size: " + responseSize + " bytes");
+
+        // Transfer-Encoding 不去掉,会重复设置
+        HttpHeaders headers1 = response.getHeaders();
+        HttpHeaders headers2 = new HttpHeaders();
+        headers1.forEach((k,v)->{
+            if (!k.equalsIgnoreCase("Transfer-Encoding")){
+                headers2.put(k, v);
+            }
+        });
         return ResponseEntity
                 .status(response.getStatusCode())
-                .headers(response.getHeaders())
+                .headers(headers2)
                 .body(response.getBody());
     }
 }

+ 8 - 0
server/src/main/java/com/giantan/data/index/ICollectionMapper.java

@@ -0,0 +1,8 @@
+package com.giantan.data.index;
+
+public interface ICollectionMapper {
+
+    String mapping(String collection);
+    boolean isOne2One(String collection);
+
+}

+ 18 - 0
server/src/main/java/com/giantan/data/index/IHybridSearch.java

@@ -2,13 +2,19 @@ package com.giantan.data.index;
 
 import com.giantan.data.index.dto.DocReq;
 import com.giantan.data.index.dto.DocResp;
+import com.giantan.data.index.dto.DocSearchResp;
 
 import java.io.IOException;
 import java.util.List;
+import java.util.Map;
 
 public interface IHybridSearch {
+    public static final String API_PREFIX = "/v1/se";
+
     List<DocResp> add(String coll, List<DocReq> docs) throws IOException, InterruptedException;
 
+    List<DocResp> addDirect(String coll, List<DocReq> docs) throws IOException, InterruptedException;
+
     int delete(String coll, List<String> ids) throws IOException, InterruptedException;
 
     List<DocResp> getDocumentsByIds(String coll, List<String> ids) throws IOException, InterruptedException;
@@ -16,4 +22,16 @@ public interface IHybridSearch {
     List<String> getAllIds(String coll) throws IOException, InterruptedException;
 
     boolean deleteAll(String coll) throws IOException, InterruptedException;
+
+    boolean drop(String coll) throws IOException, InterruptedException;
+
+    List<DocResp> getDocumentsByIdFilter(String coll, String filter) throws IOException, InterruptedException;
+
+    int deleteDocumentsByIdFilter(String coll, String filter) throws IOException, InterruptedException;
+
+    List<DocSearchResp> fulltextSearch(String coll, Map<String, Object> query) throws IOException, InterruptedException;
+
+    List<DocSearchResp> similaritySearch(String coll, Map<String, Object> query) throws IOException, InterruptedException;
+
+    List<DocSearchResp> hybridSearch(String coll, Map<String, Object> query) throws IOException, InterruptedException;
 }

+ 5 - 0
server/src/main/java/com/giantan/data/index/IndexConfig.java

@@ -0,0 +1,5 @@
+package com.giantan.data.index;
+
+public class IndexConfig {
+
+}

+ 33 - 0
server/src/main/java/com/giantan/data/index/dto/DocSearchResp.java

@@ -32,4 +32,37 @@ public class DocSearchResp extends DocResp {
                 ", score=" + score +
                 '}';
     }
+
+    public static DocSearchResp fromMap(Map<String, Object> map) {
+        DocSearchResp req = new DocSearchResp();
+        req.id = map.get("id").toString();
+        req.text = (String) map.get("text");
+
+        Object o = map.get("tags");
+        if (o != null) {
+            req.tags = (List<String>) o;
+        }
+
+        o = map.get("metadata");
+        if (o != null) {
+            req.metadata = (Map<String, Object>) o;
+        }
+
+        o = map.get("createTime");
+        if (o != null) {
+            req.createTime = (String) o;
+        }
+        o = map.get("score");
+        if (o != null) {
+            if (o instanceof Double) {
+                req.score = ((Double) o).doubleValue();
+            }else if (o instanceof Integer){
+                req.score = ((Integer) o).doubleValue();
+            }else{
+                req.score = Double.parseDouble((String) o);
+            }
+        }
+        return req;
+    }
+
 }

+ 1 - 2
server/src/main/java/com/giantan/data/kvs/constant/KvConstants.java

@@ -2,8 +2,7 @@ package com.giantan.data.kvs.constant;
 
 public class KvConstants {
 
-    public static final String API_PREFIX = "/v1/md";
-
+    public static final String API_PREFIX = "/v1/kv";
     public static final String FIELD_ENTITY = "entity";
 
     public static final String FIELD_QUERY = "query";

+ 0 - 2
server/src/main/java/com/giantan/data/kvs/controller/KvCollectionsController.java

@@ -3,9 +3,7 @@ package com.giantan.data.kvs.controller;
 
 import com.giantan.ai.common.reponse.R;
 
-import com.giantan.data.kvs.constant.KvConstants;
 import com.giantan.data.kvs.service.ICollectionService;
-import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.http.ResponseEntity;
 import org.springframework.web.bind.annotation.*;
 

+ 1 - 1
server/src/main/java/com/giantan/data/kvs/controller/KvDocsController.java

@@ -1,8 +1,8 @@
 package com.giantan.data.kvs.controller;
 
 import com.giantan.ai.common.reponse.R;
-import com.giantan.data.kvs.repository.GDynamicRepository;
 import com.giantan.data.kvs.constant.KvConstants;
+import com.giantan.data.kvs.repository.GDynamicRepository;
 import com.giantan.data.kvs.kvstore.GBaseKeyValue;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.http.ResponseEntity;

+ 1 - 2
server/src/main/java/com/giantan/data/kvs/controller/KvIndexesController.java

@@ -2,10 +2,9 @@ package com.giantan.data.kvs.controller;
 
 
 import com.giantan.ai.common.reponse.R;
-import com.giantan.ai.common.util.dict.GItem;
+import com.giantan.ai.util.dict.GItem;
 import com.giantan.data.kvs.constant.KvConstants;
 import com.giantan.data.kvs.service.ICollectionService;
-import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.http.ResponseEntity;
 import org.springframework.web.bind.annotation.*;
 

+ 66 - 40
server/src/main/java/com/giantan/data/kvs/repository/GConverter.java

@@ -19,7 +19,7 @@ public class GConverter {
 //    public static final String PATH = "path";
 //    public static final String ATTRIBUTES = "attributes";
 
-    public static List<GEntity> toEntity(List<GBaseKeyValue> data, IdGenerator idGenerator){
+    public static List<GEntity> toEntity(List<GBaseKeyValue> data, IdGenerator idGenerator) {
         List<GEntity> entities = new ArrayList<GEntity>();
         if (data == null || data.isEmpty()) return entities;
         for (GBaseKeyValue kv : data) {
@@ -28,59 +28,85 @@ public class GConverter {
         return entities;
     }
 
-    public static GEntity toEntity(GBaseKeyValue map, IdGenerator idGenerator){
+    public static GEntity toEntity(GBaseKeyValue map, IdGenerator idGenerator) {
         GEntity entity = toEntity(map);
-        if (entity.getGid() == null){
+        if (entity.getGid() == null) {
             entity.setGid(idGenerator.generateId());
         }
         return entity;
     }
 
-    public static GEntity toEntity(GBaseKeyValue map){
+    private static long toLongId(Object o) {
+        if (o == null) return -1l;
+        if (o instanceof String) {
+            return Long.parseLong((String) o);
+        } else if (o instanceof Integer) {
+            return Long.valueOf((Integer) o);
+        } else if (o instanceof Long) {
+            return Long.valueOf((Long) o);
+        }
+        return -1l;
+    }
+
+    public static GEntity toEntity(Map<String, Object> map) {
         GEntity entity = new GEntity();
 
-        if (map.containsKey(GEntityConfig.ID)) {
-            entity.setId(map.getIntId().longValue());
+        Object o = map.get(GEntityConfig.ID);
+        if (o != null) {
+            entity.setId(toLongId(o));
         }
-        if (map.containsKey(GEntityConfig.GID)) {
-            entity.setGid(map.getGid());
+        o = map.get(GEntityConfig.GID);
+        if (o != null) {
+            entity.setGid((String) o);
         }
-        if (map.containsKey(GEntityConfig.NAME)) {
-            entity.setName(map.getName());
+        o = map.get(GEntityConfig.NAME);
+        if (o != null) {
+            entity.setName((String) o);
         }
-        if (map.containsKey(GEntityConfig.ALTLABELS)) {
-            entity.setAltlabels((List<String>) map.get(GEntityConfig.ALTLABELS));
-        }
-        if (map.containsKey(GEntityConfig.MARK)) {
-            entity.setMark((Integer) map.get(GEntityConfig.MARK));
+
+        o = map.get(GEntityConfig.ALTLABELS);
+        if (o != null) {
+            entity.setAltlabels((List<String>) o);
         }
-        if (map.containsKey(GEntityConfig.DESCRIPTION)) {
-            entity.setDescription((String) map.get(GEntityConfig.DESCRIPTION));
+
+        o = map.get(GEntityConfig.MARK);
+        if (o != null) {
+            entity.setMark((Integer) o);
         }
-        if (map.containsKey(GEntityConfig.TAGS)) {
-            entity.setTags((List<String>) map.get(GEntityConfig.TAGS));
+
+        o = map.get(GEntityConfig.DESCRIPTION);
+        if (o != null) {
+            entity.setDescription((String) o);
         }
-        if (map.containsKey(GEntityConfig.PATH)) {
-            entity.setPath((String) map.get(GEntityConfig.PATH));
+
+        o = map.get(GEntityConfig.TAGS);
+        if (o != null) {
+            entity.setTags((List<String>) o);
         }
-        if (map.containsKey(GEntityConfig.ATTRIBUTES)) {
-            entity.setAttributes((Map<String, Object>) map.get(GEntityConfig.ATTRIBUTES));
+
+        o = map.get(GEntityConfig.PATH);
+        if (o != null) {
+            entity.setPath((String) o);
         }
 
+        o = map.get(GEntityConfig.ATTRIBUTES);
+        if (o != null) {
+            entity.setAttributes((Map<String, Object>) o);
+        }
         return entity;
     }
 
-    public static GBaseKeyValue fromEntity(GEntity entity){
+    public static GBaseKeyValue fromEntity(GEntity entity) {
         GBaseKeyValue map = new GBaseKeyValue();
         map.put(GEntityConfig.ID, entity.getId());
         map.put(GEntityConfig.GID, entity.getGid());
-        map.put(GEntityConfig.NAME,entity.getName());
-        map.put(GEntityConfig.ALTLABELS,entity.getAltlabels());
-        map.put(GEntityConfig.MARK,entity.getMark());
-        map.put(GEntityConfig.DESCRIPTION,entity.getDescription());
-        map.put(GEntityConfig.TAGS,entity.getTags());
-        map.put(GEntityConfig.PATH,entity.getPath());
-        map.put(GEntityConfig.ATTRIBUTES,entity.getAttributes());
+        map.put(GEntityConfig.NAME, entity.getName());
+        map.put(GEntityConfig.ALTLABELS, entity.getAltlabels());
+        map.put(GEntityConfig.MARK, entity.getMark());
+        map.put(GEntityConfig.DESCRIPTION, entity.getDescription());
+        map.put(GEntityConfig.TAGS, entity.getTags());
+        map.put(GEntityConfig.PATH, entity.getPath());
+        map.put(GEntityConfig.ATTRIBUTES, entity.getAttributes());
         return map;
     }
 
@@ -90,12 +116,12 @@ public class GConverter {
         return ls2;
     }
 
-    public static Map<String,Object> flatten(Map<String, Object> kv,List<String> fields) {
+    public static Map<String, Object> flatten(Map<String, Object> kv, List<String> fields) {
         if (kv == null || kv.isEmpty()) return kv;
         if (fields == null || fields.isEmpty()) return kv;
-        Map<String,Object> flattened = new HashMap<String,Object>();
+        Map<String, Object> flattened = new HashMap<String, Object>();
 
-        Map<String,Object> attrs = null;
+        Map<String, Object> attrs = null;
         Object oa = kv.get(GEntityConfig.ATTRIBUTES);
         if (oa != null && oa instanceof Map) {
             attrs = (Map<String, Object>) oa;
@@ -103,13 +129,13 @@ public class GConverter {
 
         for (String field : fields) {
             Object o = kv.get(field);
-            if (o != null){
-                flattened.put(field,o);
-            }else{
-                if (attrs != null){
+            if (o != null) {
+                flattened.put(field, o);
+            } else {
+                if (attrs != null) {
                     Object o1 = attrs.get(field);
-                    if (o1 != null){
-                        flattened.put(field,o1);
+                    if (o1 != null) {
+                        flattened.put(field, o1);
                     }
                 }
             }

+ 1 - 2
server/src/main/java/com/giantan/data/kvs/repository/GDynamicRepository.java

@@ -3,13 +3,12 @@ package com.giantan.data.kvs.repository;
 import com.fasterxml.jackson.core.JsonProcessingException;
 import com.fasterxml.jackson.core.type.TypeReference;
 import com.fasterxml.jackson.databind.ObjectMapper;
-import com.giantan.ai.common.util.JsonUtil;
+import com.giantan.ai.util.JsonUtil;
 import com.giantan.data.kvs.kvstore.GBaseKeyValue;
 import com.giantan.data.kvs.kvstore.IGDynamicRepository;
 import com.giantan.ai.util.id.IdGenerator;
 import com.giantan.ai.util.id.UuidGenerator;
 import com.giantan.data.util.JdbcUtils;
-import org.springframework.jdbc.core.ConnectionCallback;
 import org.springframework.jdbc.core.JdbcTemplate;
 
 import java.sql.Array;

+ 25 - 11
server/src/main/java/com/giantan/data/kvs/repository/GRepository.java

@@ -32,7 +32,7 @@ package com.giantan.data.kvs.repository;
 import com.fasterxml.jackson.core.JsonProcessingException;
 import com.fasterxml.jackson.core.type.TypeReference;
 import com.fasterxml.jackson.databind.ObjectMapper;
-import com.giantan.ai.common.util.JsonUtil;
+import com.giantan.ai.util.JsonUtil;
 import com.giantan.data.kvs.kvstore.GBaseKeyValue;
 import com.giantan.data.kvs.kvstore.IGkvRepository;
 import com.giantan.ai.util.id.IdGenerator;
@@ -468,18 +468,32 @@ public class GRepository implements IGkvRepository {
         if (id == null) {
             throw new IllegalArgumentException("ID cannot be null");
         }
-        if (attributes != null && !attributes.isEmpty()) {
-            String js = JsonUtil.toJsonString(attributes);
+//        if (attributes != null && !attributes.isEmpty()) {
+//            String js = JsonUtil.toJsonString(attributes);
+//
+//            //String sql = String.format("UPDATE %s.%s SET attributes = COALESCE(attributes, '{}'::jsonb) || ?::jsonb  WHERE id = ?;", schema, table);// RETURNING *
+//            String sql = String.format("UPDATE %s.%s SET attributes = attributes || ?::jsonb  WHERE id = ?;", schema, table);// RETURNING *
+//            int updated = jdbc.update(sql, js, id);
+//            if (updated == 0) {
+//                return null;
+//            }
+//        }
+//        GBaseKeyValue ret = find(id);
+//        return ret;
+        String js = JsonUtil.toJsonString(attributes);
 
-            //String sql = String.format("UPDATE %s.%s SET attributes = COALESCE(attributes, '{}'::jsonb) || ?::jsonb  WHERE id = ?;", schema, table);// RETURNING *
-            String sql = String.format("UPDATE %s.%s SET attributes = attributes || ?::jsonb  WHERE id = ?;", schema, table);// RETURNING *
-            int updated = jdbc.update(sql, js, id);
-            if (updated == 0) {
-                return null;
-            }
+        String sql = String.format(
+                "UPDATE %s.%s " +
+                        "SET attributes = attributes || ?::jsonb " +
+                        "WHERE id = ? " +
+                        "RETURNING *;", schema, table);
+
+        // 用 queryForObject 一次性返回更新后的对象
+        GEntity entity = jdbc.queryForObject(sql, new Object[]{js, id}, (rs, rowNum) -> toGEntry(rs));
+        if (entity == null) {
+            return null;
         }
-        GBaseKeyValue ret = find(id);
-        return ret;
+        return GConverter.fromEntity(entity);
     }
 
     @Override

+ 9 - 1
server/src/main/java/com/giantan/data/kvs/repository/GRowMapper.java

@@ -14,7 +14,15 @@ import java.util.Map;
 
 public class GRowMapper implements RowMapper<GEntity> {
 
-    private final ObjectMapper mapper = new ObjectMapper();
+    private final ObjectMapper mapper;
+
+    public GRowMapper() {
+        this(new ObjectMapper());
+    }
+
+    public GRowMapper(ObjectMapper mapper) {
+        this.mapper = mapper;
+    }
 
     private static List<String> toList(Array arr) throws SQLException {
         if (arr == null) {

+ 101 - 0
server/src/main/java/com/giantan/data/kvs/repository/GenericRowMapper.java

@@ -0,0 +1,101 @@
+package com.giantan.data.kvs.repository;
+
+import com.fasterxml.jackson.databind.ObjectMapper;
+import org.postgresql.util.PGobject;
+import org.springframework.jdbc.core.RowMapper;
+
+import java.lang.reflect.Array;
+import java.sql.ResultSet;
+import java.sql.ResultSetMetaData;
+import java.sql.SQLException;
+import java.util.*;
+
+/**
+ * 通用行映射:jsonb 自动转 Map/List,SQL Array 自动转 List
+ */
+public class GenericRowMapper implements RowMapper<Map<String, Object>> {
+
+    private final ObjectMapper objectMapper;
+
+    public GenericRowMapper() {
+        this(new ObjectMapper());
+    }
+
+    public GenericRowMapper(ObjectMapper objectMapper) {
+        this.objectMapper = objectMapper;
+    }
+
+    @Override
+    public Map<String, Object> mapRow(ResultSet rs, int rowNum) throws SQLException {
+        Map<String, Object> out = new LinkedHashMap<>();
+        ResultSetMetaData md = rs.getMetaData();
+        int cols = md.getColumnCount();
+
+        for (int i = 1; i <= cols; i++) {
+            String col = md.getColumnLabel(i);
+            Object v = rs.getObject(i);
+
+            if (v instanceof PGobject pg) {
+                String type = pg.getType();
+                if ("json".equalsIgnoreCase(type) || "jsonb".equalsIgnoreCase(type)) {
+                    v = parseJson(pg.getValue()); // JSON → Map/List/…
+                } else {
+                    v = pg.getValue(); // 其它 PGobject (如 citext 等) → 原始字符串
+                }
+            } else if (v instanceof java.sql.Array sqlArray) {
+                v = toList(sqlArray); // SQL Array → List
+            }
+            out.put(col, v);
+        }
+        return out;
+    }
+
+    private Object parseJson(String json) {
+        if (json == null) return null;
+        try {
+            return objectMapper.readValue(json, Object.class);
+        } catch (Exception e) {
+            // 解析失败,退回原始字符串
+            return json;
+        }
+    }
+
+    private List<?> toList(java.sql.Array sqlArray) throws SQLException {
+        try {
+            Object arr = sqlArray.getArray();
+            if (arr == null) return null;
+
+            // 对象数组:直接转 List,并对元素中可能出现的 PGobject(json/jsonb) 递归解析
+            if (arr instanceof Object[] objArr) {
+                List<Object> list = new ArrayList<>(objArr.length);
+                for (Object elem : objArr) {
+                    if (elem instanceof PGobject pg) {
+                        String t = pg.getType();
+                        if ("json".equalsIgnoreCase(t) || "jsonb".equalsIgnoreCase(t)) {
+                            list.add(parseJson(pg.getValue()));
+                        } else {
+                            list.add(pg.getValue());
+                        }
+                    } else {
+                        list.add(elem);
+                    }
+                }
+                return list;
+            }
+
+            // 基本类型数组(如 int[], long[], double[] …)
+            int len = Array.getLength(arr);
+            List<Object> list = new ArrayList<>(len);
+            for (int i = 0; i < len; i++) {
+                list.add(Array.get(arr, i));
+            }
+            return list;
+        } finally {
+            try {
+                sqlArray.free();
+            } catch (Exception ignore) {
+            }
+        }
+    }
+}
+

+ 900 - 0
server/src/main/java/com/giantan/data/kvs/repository/index/GIndexedRepository.java

@@ -0,0 +1,900 @@
+package com.giantan.data.kvs.repository.index;
+
+
+import com.fasterxml.jackson.core.JsonProcessingException;
+import com.fasterxml.jackson.core.type.TypeReference;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.giantan.ai.util.JsonUtil;
+import com.giantan.ai.util.id.IdGenerator;
+import com.giantan.ai.util.id.UlidGenerator;
+import com.giantan.data.kvs.kvstore.GBaseKeyValue;
+import com.giantan.data.kvs.kvstore.IGDynamicRepository;
+import com.giantan.data.kvs.repository.*;
+import com.giantan.data.util.JdbcUtils;
+import org.springframework.jdbc.core.JdbcTemplate;
+
+import java.sql.Array;
+import java.sql.PreparedStatement;
+import java.sql.ResultSet;
+import java.sql.SQLException;
+import java.util.*;
+import java.util.stream.Collectors;
+
+// 增加索引功能
+// copy from GDynamicRepository.java
+public class GIndexedRepository implements IGDynamicRepository {
+
+    private static final org.slf4j.Logger log
+            = org.slf4j.LoggerFactory.getLogger(GDynamicRepository.class);
+
+    private final ObjectMapper mapper = new ObjectMapper();
+
+    protected JdbcTemplate jdbc;
+
+    protected String schema;
+    protected String tablePrefix;
+    protected String indexPrefix = "core";
+
+    private IdGenerator idGenerator = new UlidGenerator();
+
+    private IIndexer indexer;
+
+    public GIndexedRepository() {
+
+    }
+
+    public GIndexedRepository(String schema, String table) {
+        this.schema = schema;
+        this.tablePrefix = table;
+    }
+
+    public GIndexedRepository(String schema, String table, JdbcTemplate jdbc) {
+        this.schema = schema;
+        this.tablePrefix = table;
+        this.jdbc = jdbc;
+    }
+
+    public String getSchema() {
+        return schema;
+    }
+
+    public void setSchema(String schema) {
+        this.schema = schema;
+    }
+
+    public String getTablePrefix() {
+        return tablePrefix;
+    }
+
+    public void setTablePrefix(String tablePrefix) {
+        this.tablePrefix = tablePrefix;
+    }
+
+    public String getIndexPrefix() {
+        return indexPrefix;
+    }
+
+    public void setIndexPrefix(String indexPrefix) {
+        this.indexPrefix = indexPrefix;
+    }
+
+    public IIndexer getIndexer() {
+        return indexer;
+    }
+
+    public void setIndexer(IIndexer indexer) {
+        this.indexer = indexer;
+    }
+
+//    protected String fullTableName(String collId) {
+//        return schema + "." + tablePrefix + "_" + collId;// collId.replace("-", "")
+//    }
+
+    protected String tableName(String collId) {
+        return tablePrefix + "_" + collId;// collId.replace("-", "")
+    }
+
+
+//    public String indexName(String collId) {
+//        //return schema + "_" + indexPrefix + "_" + collId;
+//        return indexPrefix + "_" + collId;
+//    }
+
+    public void setIdGenerator(IdGenerator idGenerator) {
+        this.idGenerator = idGenerator;
+    }
+
+    public void setJdbcTemplate(JdbcTemplate jdbc) {
+        this.jdbc = jdbc;
+    }
+
+
+    private String getGid(GBaseKeyValue kv) {
+        String gid = kv.getGid();
+        if (gid == null) {
+            gid = idGenerator.generateId();
+            kv.setGid(gid);
+        }
+        return gid;
+    }
+
+    private String getName(GBaseKeyValue kv) {
+        String name = kv.getName();
+        if (name == null) {
+            name = "";
+        }
+        return name;
+    }
+
+    private String toJson(Map<String, Object> m) {
+        String s = "{}";
+        if (m == null || m.size() <= 0) {
+            return s;
+        }
+        try {
+            s = mapper.writeValueAsString(m);
+        } catch (JsonProcessingException e) {
+            //throw new RuntimeException(e);
+        }
+        return s;
+    }
+
+    private String[] toArray(List ls) {
+        if (ls == null) {
+            return new String[0];
+        } else {
+            return (String[]) ls.toArray(new String[0]);
+        }
+    }
+
+    private Integer toMark(Integer mark) {
+        if (mark == null) {
+            return 0;
+        }
+        return mark;
+    }
+
+    protected List<String> toList(Array arr) throws SQLException {
+        if (arr == null) {
+            return Arrays.asList(); // 或者返回 null,根据需求
+        }
+        Object[] data = (Object[]) arr.getArray();
+        return Arrays.asList((String[]) data);
+    }
+
+    private Map<String, Object> toMap(String json) {
+        try {
+            Map<String, Object> map = mapper.readValue(json, new TypeReference<Map<String, Object>>() {
+            });
+            return map;
+        } catch (JsonProcessingException e) {
+            throw new RuntimeException(e);
+        }
+    }
+
+    // 这个比用  new GRowMapper() 高效
+    private GEntity toGEntry(ResultSet rs) throws SQLException {
+        GEntity entity = new GEntity();
+        entity.setId(rs.getLong(GEntityConfig.ID));
+        entity.setGid(rs.getString(GEntityConfig.GID));
+        entity.setName(rs.getString(GEntityConfig.NAME));
+        Array altlabels = rs.getArray(GEntityConfig.ALTLABELS);
+        if (altlabels != null) {
+            entity.setAltlabels(toList(altlabels));
+        }
+
+        entity.setMark(rs.getInt(GEntityConfig.MARK));
+        entity.setDescription(rs.getString(GEntityConfig.DESCRIPTION));
+        Array tags = rs.getArray(GEntityConfig.TAGS);
+        if (tags != null) {
+            entity.setTags(toList(tags));
+        }
+        entity.setPath(rs.getString(GEntityConfig.PATH));
+        String s = rs.getString(GEntityConfig.ATTRIBUTES);
+        if (s != null) {
+            entity.setAttributes(toMap(s));
+        }
+        return entity;
+    }
+
+
+    @Override
+    public GBaseKeyValue save(String collId, GBaseKeyValue kvs) throws Throwable {
+        String sql;
+        //String gid = getGid(kvs);
+        GEntity gent = GConverter.toEntity(kvs, idGenerator);
+        GEntity ret = null;
+
+        if (gent.getId() == null) {
+            // 插入新记录
+            sql = String.format("INSERT INTO %s.%s (gid, name, altlabels, mark, description, tags, path, attributes) VALUES (?, ?, ?, ?, ?, ?, ?, ?::jsonb) RETURNING * ;", schema, tableName(collId));
+            ret = jdbc.queryForObject(sql,
+                    new Object[]{gent.getGid(), gent.getName(), toArray(gent.getAltlabels()), gent.getMark(),
+                            gent.getDescription(), toArray(gent.getTags()), gent.getPath(), toJson(gent.getAttributes())},
+                    // new GRowMapper()
+                    (rs, rowNum) -> toGEntry(rs)
+            );
+        } else {
+            // 更新已有记录
+            sql = String.format("UPDATE %s.%s SET gid = ?, name = ?, altlabels = ?, mark = ?, description = ?, tags = ?, path = ?, attributes = ?::jsonb WHERE id = ?  RETURNING * ;", schema, tableName(collId));
+            ret = jdbc.queryForObject(sql,
+                    new Object[]{gent.getGid(), gent.getName(), toArray(gent.getAltlabels()), gent.getMark(),
+                            gent.getDescription(), toArray(gent.getTags()), gent.getPath(), toJson(gent.getAttributes()), gent.getId()},
+                    //new GRowMapper()
+                    (rs, rowNum) -> toGEntry(rs)
+            );
+        }
+        if (ret == null) {
+            return null;
+        }
+        int r2 = indexer.onAdd(collId, ret);
+        GBaseKeyValue ret2 = GConverter.fromEntity(ret);
+        return ret2;
+    }
+
+    @Override
+    public List<Integer> saveAll(String collId, List<GBaseKeyValue> kvs) throws Throwable {
+        //String sql = String.format("INSERT INTO %s.%s (gid, name, attributes) VALUES (?, ?, ?::jsonb)", schema, tablePrefix);
+        String sql = String.format("INSERT INTO %s.%s (gid, name, altlabels, mark, description, tags, path, attributes) VALUES (?, ?, ?, ?, ?, ?, ?, ?::jsonb) ", schema, tableName(collId));
+
+        List<GEntity> entities = GConverter.toEntity(kvs, idGenerator);
+
+        int[][] batched = jdbc.batchUpdate(sql,
+                entities,
+                50,
+                (PreparedStatement ps, GEntity entity) -> {
+                    ps.setString(1, entity.getGid());
+                    ps.setString(2, entity.getName());
+                    //ps.setArray(3, toSqlArray(entity.getAltlabels()));
+                    ps.setInt(4, toMark(entity.getMark()));
+                    ps.setString(5, entity.getDescription());
+                    //ps.setArray(6, toSqlArray(entity.getTags()));
+                    ps.setString(7, entity.getPath());
+                    ps.setString(8, toJson(entity.getAttributes())); // 必须是合法 JSON 字符串
+
+                    JdbcUtils.setStringArray(ps, 3, entity.getAltlabels());
+                    JdbcUtils.setStringArray(ps, 6, entity.getTags());
+                });
+
+        List<Integer> rets = new ArrayList<>();
+        for (int i = 0; i < batched.length; i++) {
+            for (int j = 0; j < batched[i].length; j++) {
+                rets.add(batched[i][j]);
+            }
+        }
+        int r2 = indexer.onAdd(collId, entities);
+        log.info("Save all kvs successfully : {}", rets.size());
+        return rets;
+    }
+
+    @Override
+    public List<GBaseKeyValue> findAll(String collId) throws Throwable {
+        String sql = String.format("SELECT * FROM %s.%s", schema, tableName(collId));
+        List<GEntity> query = jdbc.query(sql, (rs, rowNum) -> toGEntry(rs));
+        List<GBaseKeyValue> rets = GConverter.fromEntity(query);
+        return rets;
+    }
+
+    @Override
+    public List<GBaseKeyValue> findAllByIds(String collId, List<Integer> ids) throws Throwable {
+        if (ids == null || ids.isEmpty()) {
+            return Collections.emptyList();
+        }
+        String placeholders = ids.stream().map(id -> "?").collect(Collectors.joining(","));
+        String sql = String.format("SELECT * FROM %s.%s WHERE id IN (" + placeholders + ")", schema, tableName(collId));
+
+        List<GEntity> query = jdbc.query(
+                sql,
+                ids.toArray(),
+                (rs, rowNum) -> toGEntry(rs)
+        );
+        List<GBaseKeyValue> rets = GConverter.fromEntity(query);
+        return rets;
+    }
+
+    @Override
+    public GBaseKeyValue find(String collId, int id) throws Throwable {
+        String sql = String.format("SELECT * FROM %s.%s WHERE id = ?", schema, tableName(collId));
+        List<GEntity> ret = jdbc.query(
+                sql,
+                new Object[]{id},
+                (rs, rowNum) -> toGEntry(rs) //new EntryRowMapper()
+        );
+        if (ret == null || ret.isEmpty()) {
+            return null;
+        } else {
+            GBaseKeyValue ret2 = GConverter.fromEntity(ret.get(0));
+            return ret2;
+        }
+    }
+
+    @Override
+    public GBaseKeyValue findByGid(String collId, String gid) throws Throwable {
+        String sql = String.format("SELECT * FROM %s.%s WHERE gid = ?", schema, tableName(collId));
+        List<GEntity> ret = jdbc.query(
+                sql,
+                new Object[]{gid},
+                (rs, rowNum) -> toGEntry(rs) //new EntryRowMapper()
+        );
+        if (ret == null || ret.isEmpty()) {
+            return null;
+        } else {
+            GBaseKeyValue ret2 = GConverter.fromEntity(ret.get(0));
+            return ret2;
+        }
+    }
+
+    @Override
+    public List<GBaseKeyValue> findByName(String collId, String name) throws Throwable {
+        String sql = String.format("SELECT * FROM %s.%s WHERE name = ?", schema, tableName(collId));
+        List<GEntity> query = jdbc.query(
+                sql,
+                new Object[]{name},
+                (rs, rowNum) -> toGEntry(rs)
+        );
+//        List<Map<String, Object>> maps = jdbc.queryForList(sql, name);
+
+        List<GBaseKeyValue> rets = GConverter.fromEntity(query);
+        return rets;
+    }
+
+    @Override
+    public List<GBaseKeyValue> findByPath(String collId, String path) {
+        String sql = String.format("SELECT * FROM %s.%s WHERE path = ?", schema, tableName(collId));
+        List<GEntity> query = jdbc.query(
+                sql,
+                new Object[]{path},
+                (rs, rowNum) -> toGEntry(rs)
+        );
+
+        List<GBaseKeyValue> rets = GConverter.fromEntity(query);
+        return rets;
+    }
+
+    @Override
+    public List<GBaseKeyValue> findByPathPrefix(String collId, String prefix) {
+        String sql = String.format("SELECT * FROM %s.%s WHERE path LIKE ?", schema, tableName(collId));
+        List<GEntity> query = jdbc.query(
+                sql,
+                new Object[]{prefix},
+                (rs, rowNum) -> toGEntry(rs)
+        );
+
+        List<GBaseKeyValue> rets = GConverter.fromEntity(query);
+        return rets;
+    }
+
+    @Override
+    public long count(String collId) {
+        String sql = String.format("SELECT COUNT(*) FROM %s.%s", schema, tableName(collId));
+        Long l = jdbc.queryForObject(sql, Long.class);
+        return l;
+    }
+
+    @Override
+    public long delete(String collId, Integer id) {
+//        String sql = String.format("DELETE FROM %s.%s WHERE id = ?", schema, tableName(collId));
+//        return jdbc.update(sql, id);
+        String sql = String.format("DELETE FROM %s.%s WHERE id = ? RETURNING *", schema, tableName(collId));
+        List<GEntity> rs = jdbc.query(sql, new GRowMapper(mapper), id);
+        if (rs == null || rs.isEmpty()) {
+            return 0;
+        }
+        int r2 = indexer.onDelete(collId, rs.get(0));
+        return 1;
+    }
+
+    @Override
+    public long delete(String collId, List<Integer> ids) {
+        if (ids == null || ids.isEmpty()) {
+            return 0L;
+        }
+        String placeholders = ids.stream()
+                .map(id -> "?")
+                .collect(Collectors.joining(","));
+
+        String sql = String.format("DELETE FROM %s.%s WHERE id IN (" + placeholders + ")", schema, tableName(collId));
+        ////QQQ
+        Object[] params = ids.toArray();
+
+        //return jdbc.update(sql, params);
+        List<GEntity> rs = jdbc.query(sql, new GRowMapper(mapper), params);
+        if (rs == null || rs.isEmpty()) {
+            return 0;
+        }
+        int r2 = indexer.onDelete(collId, rs);
+        return rs.size();
+    }
+
+    @Override
+    public long deleteAll(String collId) {
+        String sql = String.format("DELETE FROM %s.%s", schema, tableName(collId));
+        int r = jdbc.update(sql);
+        int r2 = indexer.onDeleteAll(collId);
+        return r;
+    }
+
+    //我给你改造一下 updating 方法,加一个 boolean needOldValue 参数,决定是否在更新前先取原记录。思路如下:
+    //needOldValue = true → 先查询旧记录,再执行 UPDATE
+    //needOldValue = false → 原来的 UPDATE ... RETURNING * 逻辑
+    private GEntity updating(String collId, GBaseKeyValue updateFields) throws Throwable {
+        Object id = updateFields.remove("id");
+        Object gid = updateFields.remove("gid");
+
+        String sql1 = String.format("UPDATE %s.%s SET ", schema, tableName(collId));
+        StringBuilder sql = new StringBuilder(sql1);
+
+        List<String> keys = new ArrayList<>();
+        updateFields.forEach((key, value) -> {
+            if (GEntityConfig.UPDATABLE_FIELDS_SET.contains(key)) {
+                sql.append(key);
+                if ("attributes".equals(key)) {
+                    sql.append(" = ?::jsonb");
+                } else if ("altlabels".equals(key) || "tags".equals(key)) {
+                    sql.append(" = ?");
+                } else {
+                    sql.append(" = ?");
+                }
+                sql.append(", ");
+                keys.add(key);
+            }
+        });
+        sql.setLength(sql.length() - 2); // 移除最后逗号
+
+        if (id != null) {
+            sql.append(" WHERE id = ? RETURNING * ;");
+        } else {
+            sql.append(" WHERE gid = ? RETURNING * ;");
+        }
+
+        List<GEntity> rets = jdbc.query(connection -> {
+            PreparedStatement ps = connection.prepareStatement(sql.toString());
+
+            int index = 1;
+            for (String key : keys) {
+                Object value = updateFields.get(key);
+                if ("attributes".equals(key)) {
+                    ps.setString(index++, toJson((Map<String, Object>) value));
+                } else if ("altlabels".equals(key) || "tags".equals(key)) {
+                    JdbcUtils.setStringArray(ps, index++, (List<String>) value);
+                } else {
+                    ps.setObject(index++, value);
+                }
+            }
+
+            // 最后绑定 id/gid
+            if (id != null) {
+                ps.setObject(index, id);
+            } else {
+                ps.setObject(index, gid);
+            }
+
+            return ps;
+        }, (rs, rowNum) -> toGEntry(rs));
+        if (rets != null && !rets.isEmpty()) {
+            return rets.get(0);
+        }
+        return null;
+    }
+
+
+    @Override
+    public GBaseKeyValue update(String collId, GBaseKeyValue kv) throws Throwable {
+        if (kv.size() == 0) {
+            return null;
+        }
+
+        Integer id = kv.getIntId();
+        String gid = kv.getGid();
+        String name = kv.getName();
+
+        if (id == null && gid == null) {
+            throw new IllegalArgumentException("ID or GID cannot be null");
+        }
+
+        GEntity ret = updating(collId, kv);
+        if (ret == null) {
+            return null;
+        } else {
+            indexer.onUpdate(collId, ret);
+            GBaseKeyValue ret2 = GConverter.fromEntity(ret);
+            return ret2;
+        }
+//        int rows = updating(kv);
+//
+//        if (rows == 0) {
+//            String errorMsg = null;
+//            if (gid != null) {
+//                errorMsg = "Update failed, no record found with gid = " + gid;
+//            } else {
+//                errorMsg = "Update failed, no record found with id = " + id;
+//            }
+//            throw new IllegalStateException(errorMsg);
+//        }
+//
+//        GBaseKeyValue ret = null;
+//        if (gid != null) {
+//            ret = findByGid(gid);
+//        } else {
+//            ret = find(id);
+//        }
+//        return ret;
+    }
+
+    @Override
+    public GBaseKeyValue removeAttribute(String collId, Integer id, List<String> keys) throws Throwable {
+        if (id == null) {
+            throw new IllegalArgumentException("ID cannot be null");
+        }
+        if (keys.size() == 0) {
+            return null;
+        }
+        Object[] params = new Object[keys.size() + 1];
+        for (int i = 0; i < keys.size(); i++) {
+            params[i] = keys.get(i);
+        }
+        params[keys.size()] = id;
+
+        //String placeholders = String.join("-", keys.stream().map(id1 -> "?").toArray(String[]::new));
+        String placeholders = keys.stream().map(k -> "?").collect(Collectors.joining("-"));
+        //String sql = String.format("UPDATE %s.%s SET attributes = attributes - %s WHERE id = ?;", schema, tableName(collId), placeholders);// RETURNING *
+        //        int updated = jdbc.update(sql, params);
+        //        if (updated == 0) {
+        //            return null;
+        //        }
+        //        GBaseKeyValue ret = find(collId, id);
+        String sql = String.format(
+                "UPDATE %s.%s SET attributes = attributes - ARRAY[%s]::text[] WHERE id = ? RETURNING *;",
+                schema, tableName(collId), placeholders
+        );
+        List<GEntity> rets = jdbc.query(connection -> {
+            PreparedStatement ps = connection.prepareStatement(sql);
+            for (int i = 0; i < params.length; i++) {
+                ps.setObject(i + 1, params[i]);
+            }
+            return ps;
+        }, (rs, rowNum) -> toGEntry(rs));
+
+        if (rets != null && !rets.isEmpty()) {
+            GEntity ret = rets.get(0);
+            int r2 = indexer.onUpdateField(collId, ret, "attributes");
+            GBaseKeyValue ret2 = GConverter.fromEntity(ret);
+            return ret2;
+        }
+        return null;
+    }
+
+    @Override
+    public GBaseKeyValue updateAttribute(String collId, Integer id, String key, Object value) throws Throwable {
+        return updateAttribute(collId, id, Map.of(key, value));
+    }
+
+    @Override
+    public GBaseKeyValue updateAttribute(String collId, Integer id, Map<String, Object> attributes) throws Throwable {
+        if (id == null) {
+            throw new IllegalArgumentException("ID cannot be null");
+        }
+        if (attributes != null && !attributes.isEmpty()) {
+            String js = JsonUtil.toJsonString(attributes);
+
+            //String sql = String.format("UPDATE %s.%s SET attributes = COALESCE(attributes, '{}'::jsonb) || ?::jsonb  WHERE id = ?;", schema, table);// RETURNING *
+            String sql = String.format("UPDATE %s.%s SET attributes = attributes || ?::jsonb  WHERE id = ? RETURNING *;", schema, tableName(collId));
+//            int updated = jdbc.update(sql, js, id);
+//            if (updated == 0) {
+//                return null;
+//            }
+//            GBaseKeyValue ret = find(collId, id);
+//            return ret;
+
+            List<GEntity> rs = jdbc.query(sql, new GRowMapper(mapper), js, id);
+            if (rs == null || rs.isEmpty()) {
+                return null;
+            }
+            GEntity entity = rs.get(0);
+            indexer.onUpdateField(collId, entity, "attributes");
+            return GConverter.fromEntity(entity);
+        }
+        return null;
+    }
+
+    @Override
+    public Object findAttribute(String collId, Integer id, String attribute) throws SQLException {
+        String sql = String.format("SELECT attributes->>? FROM %s.%s WHERE id = ?", schema, tableName(collId));
+        Object o = jdbc.queryForObject(sql, Object.class, attribute, id);
+        return o;
+    }
+
+    @Override
+    public long update(String collId, List<GBaseKeyValue> kvs) throws Throwable {
+        long updatedCount = 0;
+        for (GBaseKeyValue kv : kvs) {
+            Integer id = kv.getIntId();
+            String gid = kv.getGid();
+
+            if (id != null || gid != null) {
+                GEntity updated = updating(collId, kv);
+                if (updated != null) {
+                    indexer.onUpdate(collId, updated);
+                    updatedCount += 1;
+                }
+            }
+        }
+        return updatedCount;
+    }
+
+
+//    public List<String> getAllNames() {
+//        String sql = "SELECT name FROM demo1";  // 选择要查询的字段
+//        return jdbcTemplate.queryForList(sql, String.class);  // 返回字段的所有值,类型为 String
+//    }
+
+    @Override
+    public List<Map<String, Object>> getAllEntities(String collId, List<String> fields) {
+        if (fields == null || fields.size() == 0) {
+            return Collections.emptyList();
+        }
+        String placeholders = String.join(", ", fields);
+
+        String sql = String.format("SELECT %s FROM %s.%s ", placeholders, schema, tableName(collId));
+        // 使用 queryForList 获取结果,返回一个 List<Map<String, Object>>
+        return jdbc.queryForList(sql);
+    }
+
+    @Override
+    public List<Map<String, Object>> getAllEntities(String collId, List<String> fields, String whereClause) {
+        if (fields == null || fields.size() == 0) {
+            return Collections.emptyList();
+        }
+        String placeholders = String.join(", ", fields);
+
+        String sql = String.format("SELECT %s FROM %s.%s WHERE %s", placeholders, schema, tableName(collId), whereClause);
+        // 使用 queryForList 获取结果,返回一个 List<Map<String, Object>>
+        return jdbc.queryForList(sql);
+    }
+
+
+//    @Override
+//    public List<String> appendArrayField(String collId, Integer id, String field, List<String> values) {
+//        if (field == null || values == null || values.size() == 0) {
+//            return Collections.emptyList();
+//        }
+//
+//        Array array1 = toSqlArray(values);
+//        String sql = String.format("UPDATE %s.%s SET %s = %s || ?::text[] WHERE id = ? RETURNING %s", schema, tableName(collId), field, field, field);
+//        List<String> rets = jdbc.queryForObject(
+//                sql,
+//                new Object[]{
+//                        array1,
+//                        id
+//                },
+//                (rs, rowNum) -> {
+//                    Array array = rs.getArray(field);
+//                    return toList(array);
+//                }
+//        );
+//        return rets;
+//    }
+
+    @Override
+    public List<String> appendArrayField(String collId, Integer id, String field, List<String> values) {
+        if (field == null || values == null || values.isEmpty()) {
+            return Collections.emptyList();
+        }
+
+        String sql = String.format(
+                "UPDATE %s.%s SET %s = %s || ?::text[] WHERE id = ? RETURNING *",
+                schema,
+                tableName(collId),
+                field,
+                field,
+                field
+        );
+
+        List<GEntity> rets = jdbc.query(
+                con -> {
+                    PreparedStatement ps = con.prepareStatement(sql);
+                    // 创建 SQL Array 并绑定到参数
+                    //Array sqlArray = con.createArrayOf("text", values.toArray(new String[0]));
+                    //ps.setArray(1, sqlArray);
+                    JdbcUtils.setStringArray(ps, 1, values);
+                    ps.setInt(2, id);
+                    return ps;
+                }, (rs, rowNum) -> toGEntry(rs));
+        if (rets == null || rets.isEmpty()) {
+            return null;
+        }
+        GEntity entity = rets.get(0);
+        indexer.onUpdateField(collId, entity, field);
+        return getArrayField(entity, field);
+    }
+
+    private List<String> getArrayField(GEntity entity, String field) {
+        List<String> ret2 = null;
+        if (field.equalsIgnoreCase(GEntityConfig.ALTLABELS)) {
+            ret2 = entity.getAltlabels();
+        } else if (field.equalsIgnoreCase(GEntityConfig.TAGS)) {
+            ret2 = entity.getTags();
+        }
+        return ret2;
+    }
+
+    @Override
+    public List<String> setArrayField(String collId, Integer id, String field, List<String> values) {
+        if (field == null || values == null) {
+            return Collections.emptyList();
+        }
+
+        String sql = String.format(
+                "UPDATE %s.%s SET %s = ? WHERE id = ? RETURNING %s",
+                schema, tableName(collId), field, field
+        );
+
+        List<GEntity> rets = jdbc.query(con -> {
+            PreparedStatement ps = con.prepareStatement(sql);
+            // 第 1 个参数:安全设置 array
+            JdbcUtils.setStringArray(ps, 1, values);
+            // 第 2 个参数:id
+            ps.setInt(2, id);
+            return ps;
+        }, (rs, rowNum) -> toGEntry(rs));
+
+        if (rets == null || rets.isEmpty()) {
+            return null;
+        }
+        GEntity entity = rets.get(0);
+        indexer.onUpdateField(collId, entity, field);
+        return getArrayField(entity, field);
+    }
+
+
+    @Override
+    public List<String> removeArrayField(String collId, Integer id, String field, List<String> values) {
+        if (field == null || values == null || values.size() == 0) {
+            return Collections.emptyList();
+        }
+
+        // 构建动态 SQL:array_remove(array_remove(...), ...)
+        String sql1 = String.format("UPDATE %s.%s SET %s = ", schema, tableName(collId), field);
+        StringBuilder bs = new StringBuilder(sql1);
+
+        StringBuilder bs1 = new StringBuilder();
+        bs1.append("array_remove(").append(field).append(",").append("?)");
+        for (int i = 1; i < values.size(); i++) {
+            bs1.insert(0, "array_remove(");
+            bs1.append(", ?)");
+        }
+
+        bs.append(bs1).append(" WHERE id = ? RETURNING ").append(field);
+
+        // 参数列表
+        Object[] args = new Object[values.size() + 1];
+        for (int i = 0; i < values.size(); i++) {
+            args[i] = values.get(i);
+        }
+        args[values.size()] = id;
+
+        List<GEntity> rets = jdbc.query(
+                bs.toString(),
+                args,
+                (rs, rowNum) -> toGEntry(rs));
+
+        if (rets == null || rets.isEmpty()) {
+            return null;
+        }
+        GEntity entity = rets.get(0);
+        indexer.onUpdateField(collId, entity, field);
+        return getArrayField(entity, field);
+    }
+
+    public int updatePathPrefix(String collId, String oldPrefix, String newPrefix) {
+        String table = tableName(collId);
+        String sql = String.format("""
+                    UPDATE %s.%s
+                    SET path = regexp_replace(path, ?, ?)
+                    WHERE path LIKE ?
+                    RETURNING * ;
+                """, schema, table);
+
+        // 正则必须是 ^/a/b/
+        String regex = "^" + oldPrefix;
+        String like = oldPrefix + "%";
+
+        //return jdbc.update(sql, regex, newPrefix, like);
+        List<GEntity> rets = jdbc.query(connection -> {
+            PreparedStatement ps = connection.prepareStatement(sql);
+            ps.setString(1, regex);
+            ps.setString(2, newPrefix);
+            ps.setString(3, like);
+            return ps;
+        }, (rs, rowNum) -> toGEntry(rs));
+        if (rets == null || rets.isEmpty()) {
+            return 0;
+        }
+        indexer.onUpdateField(collId, rets, "path");
+        return rets.size();
+    }
+
+    public int updatePathAndNamePrefix(String collId, String oldPrefix, String newPrefix) {
+        String table = tableName(collId);
+        // WHERE path LIKE ? OR name LIKE ?;
+//        String sql = String.format("""
+//                    UPDATE %s
+//                    SET
+//                        path = regexp_replace(path, ?::text, ?::text),
+//                        name = regexp_replace(name, ?::text, ?::text)
+//                    WHERE path LIKE ?;
+//                """, table);
+//
+//        String regex = "^" + oldPrefix; // 例如 ^/a/b/
+//        String like = oldPrefix + "%";  // 例如 /a/b/%
+//
+//        return jdbc.update(sql, regex, newPrefix, regex, newPrefix, like);
+        String regex = "^" + oldPrefix; // 例如 ^/a/b/
+        String like = oldPrefix + "%";  // 例如 /a/b/%
+        String sql = String.format("""
+                    UPDATE %s.%s
+                    SET
+                        path = regexp_replace(path, '%s', '%s'),
+                        name = regexp_replace(name, '%s', '%s')
+                    WHERE path LIKE ? RETURNING * 
+                """, schema, table, regex, newPrefix, regex, newPrefix);
+
+        //return jdbc.update(sql, like);
+        List<GEntity> rets = jdbc.query(connection -> {
+            PreparedStatement ps = connection.prepareStatement(sql);
+            ps.setString(1, like);
+            return ps;
+        }, (rs, rowNum) -> toGEntry(rs));
+        if (rets == null || rets.isEmpty()) {
+            return 0;
+        }
+        indexer.onUpdateField(collId, rets, "name");
+        indexer.onUpdateField(collId, rets, "path");
+        return rets.size();
+    }
+
+
+    @Override
+    public int createTable(String collId) {
+        List<String> sqls = GEntityConfig.createGEntityTable(schema, tableName(collId));
+        String sql2 = String.join("\n", sqls);
+        jdbc.execute(sql2);
+
+        int r = indexer.createCollection(collId);
+        return 1;
+    }
+
+    @Override
+    public int deleteTable(String collId) {
+        List<String> sqls = GEntityConfig.deleteGEntityTable(schema, tableName(collId));
+        String sql2 = String.join("\n", sqls);
+        jdbc.execute(sql2);
+        int r = indexer.deleteCollection(collId);
+        return 1;
+    }
+
+    @Override
+    public int deleteByPath(String collId, String path) {
+        String sql = String.format("DELETE FROM %s.%s WHERE path = ? RETURNING *", schema, tableName(collId));
+        //return jdbc.update(sql, path);
+        List<GEntity> rets = jdbc.query(sql, (rs, rowNum) -> toGEntry(rs));
+        if (rets == null || rets.isEmpty()) {
+            return 0;
+        }
+        int r = indexer.onDelete(collId, rets);
+        return rets.size();
+    }
+
+    @Override
+    public int deleteByPathPrefix(String collId, String prefix) {
+        String like = prefix + "%";
+        String sql = String.format("DELETE FROM %s.%s WHERE path LIKE ? RETURNING *", schema, tableName(collId));
+        //return jdbc.update(sql, like);
+        List<GEntity> rets = jdbc.query(sql, (rs, rowNum) -> toGEntry(rs));
+        if (rets == null || rets.isEmpty()) {
+            return 0;
+        }
+        int r = indexer.onDelete(collId, rets);
+        return rets.size();
+    }
+}

+ 35 - 0
server/src/main/java/com/giantan/data/kvs/repository/index/IIndexer.java

@@ -0,0 +1,35 @@
+package com.giantan.data.kvs.repository.index;
+
+import com.giantan.data.kvs.kvstore.GBaseKeyValue;
+import com.giantan.data.kvs.repository.GEntity;
+import com.giantan.data.qa.service.ICollectionService;
+
+import java.io.IOException;
+import java.util.List;
+
+public interface IIndexer {
+
+    int onAdd(String collection, GEntity entity) throws IOException, InterruptedException;
+
+    int onAdd(String collection, List<GEntity> entities) throws IOException, InterruptedException;
+
+    int onDelete(String collection, GEntity entity);
+
+    int onDelete(String collection, List<GEntity> entities);
+
+    int onDeleteAll(String collection);
+
+    int onUpdate(String collection, GEntity entity) throws IOException, InterruptedException;
+
+    int onUpdateField(String collection, GEntity ret, String field) ;
+
+    int onUpdateField(String collection, List<GEntity> rets, String field);
+
+    int createCollection(String collection);
+
+    int deleteCollection(String collection);
+
+
+    // 用于获取collection的attributes 信息
+    void init(ICollectionService collectionService);
+}

+ 1 - 1
server/src/main/java/com/giantan/data/kvs/service/ICollectionService.java

@@ -1,6 +1,6 @@
 package com.giantan.data.kvs.service;
 
-import com.giantan.ai.common.util.dict.GItem;
+import com.giantan.ai.util.dict.GItem;
 import com.giantan.data.kvs.kvstore.GBaseKeyValue;
 
 import java.util.List;

+ 1 - 1
server/src/main/java/com/giantan/data/kvs/service/IIndexer.java

@@ -1,6 +1,6 @@
 package com.giantan.data.kvs.service;
 
-import com.giantan.ai.common.util.dict.GItem;
+import com.giantan.ai.util.dict.GItem;
 import com.giantan.ai.util.trie.Triple;
 
 import java.util.List;

+ 1 - 1
server/src/main/java/com/giantan/data/kvs/service/impl/GCollection.java

@@ -1,6 +1,6 @@
 package com.giantan.data.kvs.service.impl;
 
-import com.giantan.ai.common.util.dict.GItem;
+import com.giantan.ai.util.dict.GItem;
 import com.giantan.data.kvs.constant.KvConstants;
 import com.giantan.data.kvs.service.ICollection;
 import com.giantan.data.kvs.kvstore.GBaseKeyValue;

+ 3 - 3
server/src/main/java/com/giantan/data/kvs/service/impl/GIndexer.java

@@ -1,7 +1,7 @@
 package com.giantan.data.kvs.service.impl;
 
-import com.giantan.ai.common.util.dict.GItem;
-import com.giantan.ai.common.util.dict.GLookups;
+import com.giantan.ai.util.dict.GItem;
+import com.giantan.ai.util.dict.GLookups;
 import com.giantan.data.kvs.service.IIndexer;
 import com.giantan.data.kvs.repository.GEntityConfig;
 import com.giantan.ai.util.trie.Triple;
@@ -9,7 +9,7 @@ import com.giantan.ai.util.trie.Triple;
 import java.util.List;
 import java.util.Map;
 
-import static com.giantan.data.kvs.constant.KvConstants.FIELD_QUERY;
+import static com.giantan.data.mds.constant.MdConstants.FIELD_QUERY;
 
 
 public class GIndexer implements IIndexer {

+ 1 - 1
server/src/main/java/com/giantan/data/kvs/service/impl/KvCollectionService.java

@@ -1,6 +1,6 @@
 package com.giantan.data.kvs.service.impl;
 
-import com.giantan.ai.common.util.dict.GItem;
+import com.giantan.ai.util.dict.GItem;
 
 import com.giantan.data.kvs.constant.KvConfig;
 import com.giantan.data.kvs.service.ICollection;

+ 2 - 2
server/src/main/java/com/giantan/data/mds/MdsApplication.java

@@ -8,7 +8,7 @@ import org.springframework.scheduling.annotation.EnableAsync;
 
 @SpringBootApplication
 @EnableAsync
-@ComponentScan(basePackages = {"com.giantan.data.mds", "com.giantan.data.index"})
+@ComponentScan(basePackages = {"com.giantan.data.mds", "com.giantan.data.index", "com.giantan.data.qa"})
 //@ComponentScan("com.giantan.data.se")
 public class MdsApplication {
     private static final org.slf4j.Logger log
@@ -16,7 +16,7 @@ public class MdsApplication {
 
     public static void main(String[] args) {
         SpringApplication.run(MdsApplication.class, args);
-        log.info("Mds server started. Version 2.0.0");
+        log.info("Mds server started. Version 2.1.1");
     }
 
 }

+ 1 - 3
server/src/main/java/com/giantan/data/mds/bot/GChatClient.java

@@ -1,7 +1,6 @@
 package com.giantan.data.mds.bot;
 
-import com.fasterxml.jackson.core.JsonProcessingException;
-import com.giantan.ai.common.util.JsonUtil;
+import com.giantan.ai.util.JsonUtil;
 import org.springframework.ai.chat.client.ChatClient;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.core.io.DefaultResourceLoader;
@@ -9,7 +8,6 @@ import org.springframework.core.io.Resource;
 import org.springframework.core.io.ResourceLoader;
 import org.springframework.stereotype.Service;
 
-import java.util.List;
 import java.util.Map;
 
 @Service

+ 17 - 0
server/src/main/java/com/giantan/data/mds/constant/MdConstants.java

@@ -0,0 +1,17 @@
+package com.giantan.data.mds.constant;
+
+public class MdConstants {
+
+    public static final String API_PREFIX = "/v1/md";
+
+    public static final String FIELD_ENTITY = "entity";
+
+    public static final String FIELD_QUERY = "query";
+
+    public static final String FIELD_OFFSET = "offset";
+    public static final String FIELD_LENGTH = "length";
+    public static final String FIELD_VALUES = "values";
+
+    public static final String FIELD_RETURN = "returnFields";
+
+}

+ 2 - 2
server/src/main/java/com/giantan/data/mds/controller/ChatController.java

@@ -1,7 +1,7 @@
 package com.giantan.data.mds.controller;
 
 
-import com.giantan.data.kvs.constant.KvConstants;
+import com.giantan.data.mds.constant.MdConstants;
 import com.giantan.data.mds.bot.GChatClient;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.web.bind.annotation.GetMapping;
@@ -14,7 +14,7 @@ import java.util.Map;
 
 //@Slf4j
 @RestController
-@RequestMapping(KvConstants.API_PREFIX+"/bot")
+@RequestMapping(MdConstants.API_PREFIX+"/bot")
 public class ChatController {
     private static final org.slf4j.Logger log
             = org.slf4j.LoggerFactory.getLogger(MethodHandles.lookup().lookupClass());

+ 2 - 2
server/src/main/java/com/giantan/data/mds/controller/ChunkController.java

@@ -1,7 +1,7 @@
 package com.giantan.data.mds.controller;
 
 import com.giantan.ai.common.reponse.R;
-import com.giantan.data.kvs.constant.KvConstants;
+import com.giantan.data.mds.constant.MdConstants;
 import com.giantan.data.mds.chunk.MdChunk;
 import com.giantan.data.mds.service.impl.MdChunksService;
 import org.springframework.beans.factory.annotation.Autowired;
@@ -11,7 +11,7 @@ import java.util.List;
 import java.util.Map;
 
 @RestController
-@RequestMapping(KvConstants.API_PREFIX + "/collections/{coll}/chunks")
+@RequestMapping(MdConstants.API_PREFIX + "/collections/{coll}/chunks")
 public class ChunkController {
 
     @Autowired

+ 3 - 3
server/src/main/java/com/giantan/data/mds/controller/DownloadController.java

@@ -1,7 +1,7 @@
 package com.giantan.data.mds.controller;
 
 
-import com.giantan.data.kvs.constant.KvConstants;
+import com.giantan.data.mds.constant.MdConstants;
 import com.giantan.data.mds.service.IMdFilesService;
 import jakarta.servlet.http.HttpServletResponse;
 import org.springframework.beans.factory.annotation.Autowired;
@@ -15,8 +15,8 @@ import java.net.URLConnection;
 import java.nio.charset.StandardCharsets;
 
 @RestController
-@RequestMapping(KvConstants.API_PREFIX + "/collections/{collId}")
-public class DownloadController {
+@RequestMapping(MdConstants.API_PREFIX + "/collections/{collId}")
+public class  DownloadController {
 
     @Autowired
     IMdFilesService mdFilesService;

+ 2 - 2
server/src/main/java/com/giantan/data/mds/controller/MdCollectionsController.java

@@ -2,7 +2,7 @@ package com.giantan.data.mds.controller;
 
 
 import com.giantan.ai.common.reponse.R;
-import com.giantan.data.kvs.constant.KvConstants;
+import com.giantan.data.mds.constant.MdConstants;
 import com.giantan.data.mds.service.impl.MdCollectionsService;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.http.ResponseEntity;
@@ -13,7 +13,7 @@ import java.util.Map;
 
 @RestController
 //@RequestMapping("/v1/api/entry-groups")
-@RequestMapping(KvConstants.API_PREFIX+"/collections")
+@RequestMapping(MdConstants.API_PREFIX+"/collections")
 @CrossOrigin(origins = "*", maxAge = 3600)
 public class MdCollectionsController {
 

+ 70 - 49
server/src/main/java/com/giantan/data/mds/controller/MdDocsController.java

@@ -2,7 +2,7 @@ package com.giantan.data.mds.controller;
 
 import com.giantan.ai.common.reponse.R;
 import com.giantan.data.kvs.kvstore.GBaseKeyValue;
-import com.giantan.data.kvs.constant.KvConstants;
+import com.giantan.data.mds.constant.MdConstants;
 import com.giantan.data.mds.service.*;
 import com.giantan.data.mds.service.impl.FileProcessingService;
 import com.giantan.data.mds.service.impl.TaskStatusManager;
@@ -50,7 +50,7 @@ import java.util.UUID;
 //- 配合 `mdIds` 或标签、筛选条件等参数进行部分操作
 
 @RestController
-@RequestMapping(KvConstants.API_PREFIX + "/collections/{collId}")
+@RequestMapping(MdConstants.API_PREFIX + "/collections/{collId}")
 public class MdDocsController {
     private static final org.slf4j.Logger log
             = org.slf4j.LoggerFactory.getLogger(MethodHandles.lookup().lookupClass());
@@ -69,8 +69,8 @@ public class MdDocsController {
 
     @PostMapping("/mds/batch")
     public ResponseEntity<Map<String, String>> uploadZip(@PathVariable String collId,
-                                                            @RequestParam("file") MultipartFile file,
-                                                            @RequestParam Map<String, String> params
+                                                         @RequestParam("file") MultipartFile file,
+                                                         @RequestParam Map<String, String> params
     ) throws IOException {
         String taskId = UUID.randomUUID().toString();
         //System.out.println("taskId = " + taskId);
@@ -88,20 +88,20 @@ public class MdDocsController {
     public R<Map<String, Object>> upload(@PathVariable String collId,
                                          @RequestParam("file") MultipartFile file,
                                          @RequestParam Map<String, String> params
-    ){
+    ) {
         String taskId = UUID.randomUUID().toString();
         //System.out.println("taskId = " + taskId);
         //System.out.println("file = " + file.getOriginalFilename());
         log.info("上传文件: {}, taskId: {}", file.getOriginalFilename(), taskId);
 
         //taskStatusManager.putProcessing(taskId, new TaskStatus(collId, taskId, "处理中", "", System.currentTimeMillis(), 0));
-        Map<String,Object> ret = mdDocsService.processAsyncDirect(collId, taskId, file, params);
+        Map<String, Object> ret = mdDocsService.processAsyncDirect(collId, taskId, file, params);
 
         return R.data(ret);
     }
 
     @DeleteMapping("/mds/{gid}")
-    public R<?> delete(@PathVariable String collId,@PathVariable String gid
+    public R<?> delete(@PathVariable String collId, @PathVariable String gid
     ) throws Throwable {
         //String taskId = UUID.randomUUID().toString();
         //System.out.println("taskId = " + taskId);
@@ -110,12 +110,19 @@ public class MdDocsController {
         //taskStatusManager.putProcessing(taskId, new TaskStatus(collId, taskId, "处理中", "", System.currentTimeMillis(), 0));
         //fileProcessingService.processAsyncDirect(collId, taskId, file, params);
         //System.out.println("params = " + params);
-        int deleted = mdDocsService.deleteByMdid(collId,gid);
+        int deleted = mdDocsService.deleteByMdid(collId, gid);
         log.info("删除文件: {}", gid);
         //log.info("taskId = " + taskId);
         return R.data(Map.of("deleted", deleted));
     }
 
+    @GetMapping("/mds/{id}")
+    public R<?> getById(@PathVariable String collId, @PathVariable String id
+    ) throws Throwable {
+        GBaseKeyValue ret = mdDocsService.findByMdid(collId, id);
+        return R.data(ret);
+    }
+
 //    @DeleteMapping("/mds")
 //    public R<Map<String, Object>> deleteByName(
 //            @PathVariable String collId,
@@ -127,18 +134,18 @@ public class MdDocsController {
 //    }
 
     @DeleteMapping("/mds/all")
-    public R<Map<String, Object>> deleteAll( @PathVariable String collId) throws Exception {
+    public R<Map<String, Object>> deleteAll(@PathVariable String collId) throws Exception {
         long removed = mdDocsService.deleteAll(collId);
-        return R.data(Map.of("deleted",removed));
+        return R.data(Map.of("deleted", removed));
     }
 
 
     @GetMapping("/upload-tasks/{taskId}")
-    public ResponseEntity<TaskStatus> getStatus(@PathVariable String collId,@PathVariable String taskId) {
+    public ResponseEntity<TaskStatus> getStatus(@PathVariable String collId, @PathVariable String taskId) {
         TaskStatus status = taskStatusManager.get(taskId);
         if (status == null) {
             return ResponseEntity.status(HttpStatus.NOT_FOUND)
-                    .body(new TaskStatus(collId,taskId,"未知", "任务不存在或已过期", 0, 0));
+                    .body(new TaskStatus(collId, taskId, "未知", "任务不存在或已过期", 0, 0));
         }
         return ResponseEntity.ok(status);
     }
@@ -154,38 +161,52 @@ public class MdDocsController {
 
     /////////////////////
 
+    //    @PostMapping("/mds/locate")
+//    public R<?> locateText(
+//            @PathVariable String collId,
+//            @RequestBody Map<String,Object> req
+//    ) throws Throwable {
+//        Map<String, Object> r = mdDocsService.locateText(collId, req);
+//        return R.data(r);
+//    }
+
+//    @PostMapping("/mds/locateByHeadings")
+//    public R<?> locateByHeading(
+//            @PathVariable String collId,
+//            @RequestBody Map<String, Object> req
+//    ) throws Throwable {
+//        Map<String, Object> r = null;
+//        try {
+//            r = mdDocsService.locateByHeading(collId, req);
+//        } catch (Throwable e) {
+//            log.error(e.getMessage());
+//            r = Map.of("match", "");
+//        }
+//        return R.data(r);
+//    }
+
     @PostMapping("/mds/locate")
     public R<?> locateText(
             @PathVariable String collId,
-            @RequestBody Map<String,Object> req
+            @RequestBody Map<String, Object> req
     ) throws Throwable {
         Map<String, Object> r = mdDocsService.locateText(collId, req);
         return R.data(r);
     }
 
+
     @PostMapping("/mds/locateByHeadings")
-    public R<?> locateByHeading(
+    public R<?> locateByHeadings(
             @PathVariable String collId,
-            @RequestBody Map<String,Object> req
+            @RequestBody Map<String, Object> req
     ) throws Throwable {
-        Map<String, Object> r = null;
-        try {
-            r = mdDocsService.locateByHeading(collId, req);
-        }catch (Throwable e){
-            log.error(e.getMessage());
-            r = Map.of("match", "");
-        }
+        Map<String, Object> r = mdDocsService.locateByHeading(collId, req);
         return R.data(r);
     }
 
     @GetMapping("/all")
-    public R<List<GBaseKeyValue>> getAll(@PathVariable String collId) {
-        List<GBaseKeyValue> ret = null;
-        try {
-            ret = mdDocsService.findAll(collId);
-        } catch (Throwable e) {
-            throw new RuntimeException(e);
-        }
+    public R<List<GBaseKeyValue>> getAll(@PathVariable String collId) throws Throwable {
+        List<GBaseKeyValue> ret = mdDocsService.findAll(collId);
         return R.data(ret);
     }
 
@@ -194,62 +215,62 @@ public class MdDocsController {
     //单个 metadata 字段	/collections/{collId}/mds/{mdId}/metadata/{key}
 
     @GetMapping("/mds/{mdId}/metadata")
-    public R<GBaseKeyValue> getMetadataByMdid(@PathVariable String collId,@PathVariable String mdId) throws Throwable {
-        GBaseKeyValue ret = mdDocsService.findByMdid(collId,mdId);
+    public R<GBaseKeyValue> getMetadataByMdid(@PathVariable String collId, @PathVariable String mdId) throws Throwable {
+        GBaseKeyValue ret = mdDocsService.findByMdid(collId, mdId);
         return R.data(ret);
     }
 
     @GetMapping("/mds/{mdId}/metadata/{key}")
-    public R getMetadataByKey(@PathVariable String collId,@PathVariable String mdId,
-                                             @PathVariable String key
+    public R getMetadataByKey(@PathVariable String collId, @PathVariable String mdId,
+                              @PathVariable String key
     ) throws Throwable {
-        Object ret = mdDocsService.getMetadataByKey(collId,mdId,key);
+        Object ret = mdDocsService.getMetadataByKey(collId, mdId, key);
         return R.data(ret);
     }
 
     @PatchMapping("/mds/{mdId}/metadata")
-    public R patchMetadata(@PathVariable String collId,@PathVariable String mdId,
+    public R patchMetadata(@PathVariable String collId, @PathVariable String mdId,
                            @RequestBody GBaseKeyValue data
     ) throws Throwable {
-        Object ret = mdDocsService.patchMetadata(collId,mdId,data);
+        Object ret = mdDocsService.patchMetadata(collId, mdId, data);
         return R.data(ret);
     }
 
     @GetMapping("/mds/{mdId}/attributes")
-    public R getAttributeByKey(@PathVariable String collId,@PathVariable String mdId
+    public R getAttributeByKey(@PathVariable String collId, @PathVariable String mdId
     ) throws Throwable {
-        Object ret = mdDocsService.getAttributes(collId,mdId);
+        Object ret = mdDocsService.getAttributes(collId, mdId);
         return R.data(ret);
     }
 
     @GetMapping("/mds/{mdId}/attributes/{key}")
-    public R getAttributeByKey(@PathVariable String collId,@PathVariable String mdId,
-                                             @PathVariable String key
+    public R getAttributeByKey(@PathVariable String collId, @PathVariable String mdId,
+                               @PathVariable String key
     ) throws Throwable {
-        Object ret = mdDocsService.getAttributeByKey(collId,mdId,key);
+        Object ret = mdDocsService.getAttributeByKey(collId, mdId, key);
         return R.data(ret);
     }
 
     @PatchMapping("/mds/{mdId}/attributes")
-    public R patchAttributes(@PathVariable String collId,@PathVariable String mdId,
-                           @RequestBody GBaseKeyValue data
+    public R patchAttributes(@PathVariable String collId, @PathVariable String mdId,
+                             @RequestBody GBaseKeyValue data
     ) throws Throwable {
-        Object ret = mdDocsService.patchAttributes(collId,mdId,data);
+        Object ret = mdDocsService.patchAttributes(collId, mdId, data);
         return R.data(ret);
     }
 
     @DeleteMapping("/mds/{mdId}/attributes/{key}")
-    public R deleteAttributeByKey(@PathVariable String collId,@PathVariable String mdId,
-                                @PathVariable String key
+    public R deleteAttributeByKey(@PathVariable String collId, @PathVariable String mdId,
+                                  @PathVariable String key
     ) throws Throwable {
-        Object ret = mdDocsService.deleteAttributeByKey(collId,mdId,key);
+        Object ret = mdDocsService.deleteAttributeByKey(collId, mdId, key);
         return R.data(ret);
     }
 
     @PostMapping("/mds/{mdId}/rename")
-    public R<?> rename(@PathVariable String collId, @PathVariable String mdId,@RequestBody Map<String,Object> req
+    public R<?> rename(@PathVariable String collId, @PathVariable String mdId, @RequestBody Map<String, Object> req
     ) throws Throwable {
-        Map<String, Object> r = mdDocsService.rename(collId, mdId,req);
+        Map<String, Object> r = mdDocsService.rename(collId, mdId, req);
         return R.data(r);
     }
 }

+ 102 - 0
server/src/main/java/com/giantan/data/mds/controller/MdSearchContoller.java

@@ -0,0 +1,102 @@
+package com.giantan.data.mds.controller;
+
+import com.giantan.data.mds.constant.MdConstants;
+import com.giantan.data.mds.service.IMdDocsService;
+import com.giantan.data.mds.service.impl.MdDocsService;
+import com.giantan.data.qa.constant.QaConstants;
+import com.giantan.data.qa.service.QaDocsService;
+import jakarta.servlet.http.HttpServletRequest;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.http.HttpEntity;
+import org.springframework.http.HttpHeaders;
+import org.springframework.http.HttpMethod;
+import org.springframework.http.ResponseEntity;
+import org.springframework.web.bind.annotation.PathVariable;
+import org.springframework.web.bind.annotation.RequestBody;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RestController;
+import org.springframework.web.client.RestTemplate;
+
+import java.lang.invoke.MethodHandles;
+import java.util.Collections;
+
+@RestController
+@RequestMapping(MdConstants.API_PREFIX + "/collections/{coll}")
+public class MdSearchContoller {
+    private static final org.slf4j.Logger log
+            = org.slf4j.LoggerFactory.getLogger(MethodHandles.lookup().lookupClass());
+
+    @Value("${qas.url}")
+    String url = "http://120.78.4.46:7387/v1/collections/";
+
+    @Autowired
+    IMdDocsService mdDocsService;
+
+    private final RestTemplate restTemplate = new RestTemplate();
+
+
+    @RequestMapping("/indexes/**")
+    public ResponseEntity<byte[]> proxyAll(@PathVariable String coll, HttpServletRequest request,
+                                           @RequestBody(required = false) byte[] body) {
+
+        String requestUri = request.getRequestURI(); // 原始 URI
+        String query = request.getQueryString();
+
+        int ii = requestUri.indexOf(MdConstants.API_PREFIX);
+
+        String targetPath = requestUri.substring(ii + MdConstants.API_PREFIX.length());
+        targetPath = targetPath.replaceFirst("/collections","");
+        targetPath = targetPath.replaceFirst("/"+coll,"");
+        targetPath = targetPath.replaceFirst("/indexes", "/documents");
+
+        String collName = mdDocsService.getIndexName(coll);
+
+        String targetUrl = url + collName + targetPath + (query != null ? "?" + query : "");
+
+        return redirect(request, body, targetUrl);
+    }
+
+
+    public ResponseEntity<byte[]> redirect(HttpServletRequest request, byte[] body, String targetUrl) {
+
+        // 构造请求头
+        HttpHeaders headers = new HttpHeaders();
+        Collections.list(request.getHeaderNames())
+                .forEach(name -> headers.add(name, request.getHeader(name)));
+
+        // 读取 HTTP 方法
+        HttpMethod method = HttpMethod.valueOf(request.getMethod());
+
+        // 打印请求日志
+        int requestSize = body != null ? body.length : 0;
+        log.info("[Md] " + method + " " + targetUrl + " | request size: " + requestSize + " bytes");
+
+        // 构造请求实体
+        HttpEntity<byte[]> entity = new HttpEntity<>(body, headers);
+
+        // 转发请求
+        ResponseEntity<byte[]> response = restTemplate.exchange(
+                targetUrl,
+                method,
+                entity,
+                byte[].class
+        );
+
+        // 打印响应日志
+        int responseSize = response.getBody() != null ? response.getBody().length : 0;
+        log.info("[Md] response status: " + response.getStatusCode() + " | response size: " + responseSize + " bytes");
+        HttpHeaders headers1 = response.getHeaders();
+        HttpHeaders headers2 = new HttpHeaders();
+        headers1.forEach((k,v)->{
+            if (!k.equalsIgnoreCase("Transfer-Encoding")){
+                headers2.put(k, v);
+            }
+        });
+
+        return ResponseEntity
+                .status(response.getStatusCode())
+                .headers(headers2)
+                .body(response.getBody());
+    }
+}

+ 2 - 3
server/src/main/java/com/giantan/data/mds/controller/TaskController.java

@@ -1,10 +1,9 @@
 package com.giantan.data.mds.controller;
 
-import com.giantan.data.kvs.constant.KvConstants;
+import com.giantan.data.mds.constant.MdConstants;
 import com.giantan.data.tasks.*;
 import com.giantan.data.tasks.repository.TaskStatusHistory;
 import org.springframework.beans.factory.annotation.Autowired;
-import org.springframework.format.annotation.DateTimeFormat;
 import org.springframework.web.bind.annotation.RequestMapping;
 import org.springframework.web.bind.annotation.RestController;
 import org.springframework.web.bind.annotation.*;
@@ -13,7 +12,7 @@ import java.time.LocalDateTime;
 import java.util.*;
 
 @RestController
-@RequestMapping(KvConstants.API_PREFIX + "/collections/{collId}/tasks")
+@RequestMapping(MdConstants.API_PREFIX + "/collections/{collId}/tasks")
 public class TaskController {
 
     @Autowired

+ 4 - 4
server/src/main/java/com/giantan/data/mds/controller/TaxonomyController.java

@@ -2,8 +2,8 @@ package com.giantan.data.mds.controller;
 
 import com.giantan.ai.common.reponse.R;
 import com.giantan.data.kvs.kvstore.GBaseKeyValue;
-import com.giantan.data.kvs.constant.KvConstants;
-import com.giantan.data.mds.service.impl.DynamicTaxonomyService2;
+import com.giantan.data.mds.constant.MdConstants;
+import com.giantan.data.mds.service.impl.MdTaxonomyService;
 import com.giantan.data.taxonomy.model.TaxonomyNode;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.http.ResponseEntity;
@@ -16,13 +16,13 @@ import java.util.Map;
 import java.util.UUID;
 
 @RestController
-@RequestMapping(KvConstants.API_PREFIX + "/collections/{collName}/taxonomy")
+@RequestMapping(MdConstants.API_PREFIX + "/collections/{collName}/taxonomy")
 public class TaxonomyController {
     private static final org.slf4j.Logger log
             = org.slf4j.LoggerFactory.getLogger(MethodHandles.lookup().lookupClass());
 
     @Autowired
-    private DynamicTaxonomyService2 dynamicTaxonomyService;
+    private MdTaxonomyService dynamicTaxonomyService;
 
     @GetMapping("/all")
     public ResponseEntity<?> listAll(@PathVariable String collName) {

+ 8 - 1
server/src/main/java/com/giantan/data/mds/repository/MdDynamicRepository.java

@@ -14,9 +14,12 @@ public class MdDynamicRepository extends GDynamicRepository {
     @Autowired
     private JdbcTemplate jdbc;
 
+    private String indexPrefix;
+
     public MdDynamicRepository(){
          this.schema = "mddb";
          this.tablePrefix = "mds";
+         this.indexPrefix = "mds";
     }
 
     @PostConstruct
@@ -25,5 +28,9 @@ public class MdDynamicRepository extends GDynamicRepository {
         setJdbcTemplate(this.jdbc);
     }
 
-
+    public String indexName(String coll) {
+        //return schema + "_" + indexPrefix + "_" + collId;
+        //return this.indexPrefix + "_" + coll;
+        return coll;
+    }
 }

+ 37 - 4
server/src/main/java/com/giantan/data/mds/service/CollectionInstance.java

@@ -1,6 +1,10 @@
 package com.giantan.data.mds.service;
 
+import com.giantan.data.qa.constant.QaConstants;
+
 import java.io.Serializable;
+import java.util.HashMap;
+import java.util.Map;
 
 // memory中的Collection
 public class CollectionInstance implements Serializable {
@@ -8,14 +12,16 @@ public class CollectionInstance implements Serializable {
     String name;
     String gid;
     int id;
+    Map<String,Object> attributes;
 
     public CollectionInstance(){
-
     }
-    public CollectionInstance(int id,String gid,String name){
+
+    public CollectionInstance(int id,String gid,String name, Map<String,Object> attributes){
         this.id = id;
         this.gid = gid;
         this.name = name;
+        this.attributes = attributes;
     }
 
     public String getName() {
@@ -42,18 +48,45 @@ public class CollectionInstance implements Serializable {
         this.id = id;
     }
 
+    public Map<String, Object> getAttributes() {
+        return attributes;
+    }
+
+    public void setAttributes(Map<String, Object> attributes) {
+        this.attributes = attributes;
+    }
+
+    public synchronized void updateAttributes(Object attributes) {
+        Map<String,Object> attr2 = null;
+        if (attributes == null){
+            //attr2 = new HashMap<String,Object>();
+        }else{
+            attr2 = (Map<String,Object>)attributes;
+            this.attributes = attr2;
+        }
+    }
+
+
     @Override
     public String toString() {
         return "CollectionInstance{" +
                 "name='" + name + '\'' +
                 ", gid='" + gid + '\'' +
                 ", id=" + id +
+                ", attributes=" + attributes +
                 '}';
     }
 
-    public static CollectionInstance build(int id,String gid,String name){
-        CollectionInstance instance = new CollectionInstance(id,gid,name);
+    public static CollectionInstance build(int id, String gid, String name, Object attributes){
+        Map<String,Object> attr2 = null;
+        if (attributes == null){
+            attr2 = new HashMap<String,Object>();
+        }else{
+            attr2 = (Map<String,Object>)attributes;
+        }
+        CollectionInstance instance = new CollectionInstance(id,gid,name,attr2);
         return instance;
     }
 
+
 }

+ 2 - 0
server/src/main/java/com/giantan/data/mds/service/IMdChunksService.java

@@ -30,6 +30,8 @@ public interface IMdChunksService {
 
     long deleteByIds(String coll, List<String> ids) throws IOException, InterruptedException;
 
+    String getIndexName(String coll);
+
     //String getEmbedding(String coll, Long id);
 
 }

+ 6 - 4
server/src/main/java/com/giantan/data/mds/service/IMdDocsService.java

@@ -18,10 +18,6 @@ public interface IMdDocsService {
 
     int deleteByPathPrefix(String coll, String prefix) throws Throwable;
 
-    Map<String, Object> locateText(String coll, Map<String, Object> req) throws Throwable;
-
-    Map<String, Object> locateByHeading(String coll, Map<String, Object> req) throws Throwable;
-
     long deleteAll(String coll) throws Exception;
 
     List<GBaseKeyValue> findAll(String coll) throws Throwable;
@@ -46,4 +42,10 @@ public interface IMdDocsService {
 
     Map<String, Object> rename(String coll, String mdId, Map<String, Object> req) throws Throwable;
 
+    String getIndexName(String coll);
+
+
+    Map<String, Object> locateText(String coll, Map<String, Object> req) throws Throwable;
+
+    Map<String, Object> locateByHeading(String collId, Map<String, Object> req) throws Throwable;
 }

+ 5 - 0
server/src/main/java/com/giantan/data/mds/service/impl/MdChunksService.java

@@ -119,6 +119,11 @@ public class MdChunksService implements IMdChunksService {
         return r;
     }
 
+    @Override
+    public String getIndexName(String coll) {
+        return mdCollectionsService.getIndexName(coll);
+    }
+
     private List<Long> toListLong(List<String> ss) {
         if (ss == null || ss.isEmpty()) {
             return null;

+ 9 - 6
server/src/main/java/com/giantan/data/mds/service/impl/MdCollectionsService.java

@@ -32,8 +32,8 @@ public class MdCollectionsService {   //extends KvCollectionService
 
     protected GRepository collections;
 
-    @Autowired
-    protected MdCores mdCores;
+    //@Autowired
+    protected MdCores mdCores = new MdCores();
     //protected Map<String, CollectionInstance> stores = new ConcurrentHashMap<>();
 
     @Autowired
@@ -80,7 +80,7 @@ public class MdCollectionsService {   //extends KvCollectionService
     }
 
     public int getCollectionId(String name) {
-        CollectionInstance instance = mdCores.get(name);
+        CollectionInstance instance = mdCores.getByName(name);
         if (instance != null) {
             return instance.getId();
         } else {
@@ -88,7 +88,7 @@ public class MdCollectionsService {   //extends KvCollectionService
                 List<GBaseKeyValue> r = collections.findByName(name);
                 if (r != null && r.size() > 0) {
                     GBaseKeyValue kv = r.get(0);
-                    instance = CollectionInstance.build(kv.getIntId(), kv.getGid(), kv.getName());
+                    instance = CollectionInstance.build(kv.getIntId(), kv.getGid(), kv.getName(),kv.get(GEntityConfig.ATTRIBUTES));
                     mdCores.put(name, instance);
                     return instance.getId();
                 }
@@ -101,7 +101,7 @@ public class MdCollectionsService {   //extends KvCollectionService
     }
 
     public int getCollectionOrNew(String name) {
-        CollectionInstance collection = mdCores.get(name);
+        CollectionInstance collection = mdCores.getByName(name);
         if (collection == null) {
             try {
                 List<GBaseKeyValue> keys = collections.findByName(name);
@@ -112,7 +112,7 @@ public class MdCollectionsService {   //extends KvCollectionService
                 } else {
                     r = keys.get(0);
                 }
-                CollectionInstance instance = CollectionInstance.build(r.getIntId(), r.getGid(), r.getName());
+                CollectionInstance instance = CollectionInstance.build(r.getIntId(), r.getGid(), r.getName(),r.get(GEntityConfig.ATTRIBUTES));
                 mdCores.put(name, instance);
                 return instance.getId();
             } catch (Throwable e) {
@@ -359,6 +359,9 @@ public class MdCollectionsService {   //extends KvCollectionService
         return ret;
     }
 
+    public String getIndexName(String coll) {
+        return docsRepository.indexName(coll);
+    }
 
 //    public int deleteEntryTable2(String schema, String tableName) {
 //        List<String> sqls = GEntityConfig.deleteGEntityTable(schema, tableName);

+ 11 - 4
server/src/main/java/com/giantan/data/mds/service/impl/MdCores.java

@@ -1,29 +1,36 @@
 package com.giantan.data.mds.service.impl;
 
 import com.giantan.data.mds.service.CollectionInstance;
-import org.springframework.stereotype.Component;
 
 import java.util.Map;
 import java.util.concurrent.ConcurrentHashMap;
 
-@Component
+//@Component
 public class MdCores {
 
     protected Map<String, CollectionInstance> stores = new ConcurrentHashMap<>();
+    protected  Map<Integer, CollectionInstance> idstores = new ConcurrentHashMap<>();
     public MdCores(){
 
     }
 
-    public CollectionInstance get(String name){
+    public CollectionInstance getByName(String name){
         return stores.get(name);
     }
 
+    public CollectionInstance getById(int id){
+       return idstores.get(id);
+    }
+
     public void put(String name,CollectionInstance instance){
         stores.put(name, instance);
+        idstores.put(instance.getId(), instance);
     }
 
     public CollectionInstance remove(String name){
-        return stores.remove(name);
+        CollectionInstance removed = stores.remove(name);
+        idstores.remove(removed.getId());
+        return removed;
     }
 
 }

+ 116 - 16
server/src/main/java/com/giantan/data/mds/service/impl/MdDocsService.java

@@ -29,6 +29,8 @@ public class MdDocsService implements IMdDocsService {
 
     public static final String FIELD_NAME = "name";
     public static final String FIELD_GID = "gid";
+    public static final String FIELD_MDID = "id";
+    public static final String FIELD_MDID2 = "mdId";
     public static final String FIELD_ATTRIBUTES = "attributes";
     public static final String FIELD_MARK = "mark";
 
@@ -41,9 +43,6 @@ public class MdDocsService implements IMdDocsService {
     public static final String RESP_MATCH = "match";
     public static final String RESP_HEADINGS = "headings";
 
-    //@Autowired
-    //private TaskStatusManager taskStatusManager;
-
     @Autowired
     S3GkbService gkbStorer;
 
@@ -57,10 +56,8 @@ public class MdDocsService implements IMdDocsService {
     MdCache mdCache;
 
     public MdDocsService() {
-
     }
 
-
     protected int getCollId(String coll) {
         int id = mdCollectionsService.getCollectionId(coll);
         return id;
@@ -231,6 +228,8 @@ public class MdDocsService implements IMdDocsService {
         }
     }
 
+
+
     private String getKvObjectName(String coll, Map<String, Object> req) throws Throwable {
         String collId = getStrOfCollId(coll);
         Object o = req.get(FIELD_NAME);
@@ -245,7 +244,12 @@ public class MdDocsService implements IMdDocsService {
                 GBaseKeyValue kv = mdDynamicRepository.findByGid(collId, gid);
                 name = kv.getName();
             } else {
-
+                o = req.get(FIELD_MDID);
+                if (o != null) {
+                    String mdid = (String) o;
+                    GBaseKeyValue kv = mdDynamicRepository.find(collId, isIntId(mdid));
+                    name = kv.getName();
+                }
             }
         }
         return name;
@@ -272,9 +276,12 @@ public class MdDocsService implements IMdDocsService {
     }
 
     @Override
+    // 根据mdId 来获取 内容
     public Map<String, Object> locateText(String coll, Map<String, Object> req) throws Throwable {
-        String name = getKvObjectName(coll, req);
-        MdSearcher searcher = getSearcher(coll, name);
+        String collId = getStrOfCollId(coll);
+
+        String mdid = getKvObjectId(collId, req);
+        MdSearcher searcher = getSearcher2(coll,collId, mdid);
 
         Object o1 = req.get(QUERY_TEXT);
         Map<String, Object> map = searcher.searchAndHeadings((String) o1);
@@ -284,6 +291,23 @@ public class MdDocsService implements IMdDocsService {
         return map;
     }
 
+    private MdSearcher getSearcher2(String coll, String collId,String id) throws Throwable {
+        String key = coll + ":" + id;
+        MdSearcher searcher = mdCache.get(key);
+        if (searcher == null) {
+            GBaseKeyValue kv = mdDynamicRepository.find(collId, isIntId(id));
+            String name = kv.getName();
+            String text = getMdFileContent(coll, name);
+
+            MdSearcher searcher1 = new MdSearcher();
+            searcher1.load(text, BaseParameters.defaultParams());
+            searcher = searcher1;
+            mdCache.put(key, searcher);
+        }
+        return searcher;
+    }
+
+
     private MdSearcher getSearcher(String coll, String name) throws Exception {
         String key = coll + ":" + name;
         MdSearcher searcher = mdCache.get(key);
@@ -298,17 +322,74 @@ public class MdDocsService implements IMdDocsService {
         return searcher;
     }
 
-    @Override
-    public Map<String, Object> locateByHeading(String coll, Map<String, Object> req) throws Throwable {
-        String[] headings = getHeadings(req);
-        String name = getKvObjectName(coll, req);
+    private String getKvObjectId(String collId, Map<String, Object> req) throws Throwable {
+        //String collId = getStrOfCollId(coll);
 
-        MdSearcher searcher = getSearcher(coll, name);
-        Map<String, Object> ret = searcher.searchByHeadings(headings);
-        ret.put(QUERY_HEADINGS, headings);
-        return ret;
+        String id = null;
+        Object o = req.get(FIELD_MDID);
+
+        if (o != null) {
+            id = (String) o;
+        } else {
+            o = req.get(FIELD_MDID2);
+            if (o!=null){
+                id = (String) o;
+            }else {
+                o = req.get(FIELD_GID);
+                String gid = null;
+                if (o != null) {
+                    gid = (String) o;
+                    GBaseKeyValue kv = mdDynamicRepository.findByGid(collId, gid);
+                    id = kv.getId();
+                } else {
+                    o = req.get(FIELD_NAME);
+                    if (o != null) {
+                        String name = (String) o;
+                        List<GBaseKeyValue> kvs = mdDynamicRepository.findByName(collId, name);
+                        id = kvs.get(0).getId();
+                    }
+                }
+            }
+        }
+        return id;
+    }
+
+
+//    @Override
+//    public Map<String, Object> locateText(String coll, Map<String, Object> req) throws Throwable {
+//        String name = getKvObjectName(coll, req);
+//        MdSearcher searcher = getSearcher(coll, name);
+//
+//        Object o1 = req.get(QUERY_TEXT);
+//        Map<String, Object> map = searcher.searchAndHeadings((String) o1);
+//        if (map == null) {
+//            map = Map.of(RESP_MATCH, "");
+//        }
+//        return map;
+//    }
+
+
+
+    private String getMdFileContentById(String coll, String fn) throws Exception {
+        String repository = coll;
+        String fromObject = getObjectPath("", fn);
+        try (InputStream stream = gkbStorer.download(repository, fromObject)) {
+            String s = IOUtils.toString(stream, "UTF-8");
+            return s;
+        }
     }
 
+//    @Override
+//    public Map<String, Object> locateByHeading(String coll, Map<String, Object> req) throws Throwable {
+//        String[] headings = getHeadings(req);
+//        String name = getKvObjectName(coll, req);
+//
+//        MdSearcher searcher = getSearcher(coll, name);
+//        Map<String, Object> ret = searcher.searchByHeadings(headings);
+//        ret.put(QUERY_HEADINGS, headings);
+//        return ret;
+//    }
+
     @Override
     public long deleteAll(String coll) throws Exception {
         String collId = getStrOfCollId(coll);
@@ -474,6 +555,25 @@ public class MdDocsService implements IMdDocsService {
         return Map.of("error", "No supported fields to update");
     }
 
+    @Override
+    public String getIndexName(String coll) {
+        return mdDynamicRepository.indexName(coll);
+    }
+
+    @Override
+    public Map<String, Object> locateByHeading(String coll, Map<String, Object> req) throws Throwable {
+        String[] headings = getHeadings(req);
+        //String name = getKvObjectName(coll, req);
+        //MdSearcher searcher = getSearcher(coll, name);
+        String collId = getStrOfCollId(coll);
+        String mdid = getKvObjectId(collId, req);
+        MdSearcher searcher = getSearcher2(coll,collId, mdid);
+
+        Map<String, Object> ret = searcher.searchByHeadings(headings);
+        ret.put(QUERY_HEADINGS, headings);
+        return ret;
+    }
+
     ///////////////////////
     // renameByPath / deleteByPath
 

+ 4 - 3
server/src/main/java/com/giantan/data/mds/service/impl/DynamicTaxonomyService2.java → server/src/main/java/com/giantan/data/mds/service/impl/MdTaxonomyService.java

@@ -2,6 +2,7 @@ package com.giantan.data.mds.service.impl;
 
 import com.giantan.data.kvs.kvstore.GBaseKeyValue;
 import com.giantan.data.mds.repository.MdDynamicRepository;
+import com.giantan.data.mds.repository.MdDynamicTaxonomyRepository;
 import com.giantan.data.mds.service.IMdDocsService;
 import com.giantan.data.taxonomy.model.TaxonomyNode;
 import com.giantan.data.taxonomy.repository.DynamicTaxonomyRepository;
@@ -15,12 +16,12 @@ import java.util.List;
 import java.util.Map;
 
 @Service
-public class DynamicTaxonomyService2 {
+public class MdTaxonomyService {
     private static final org.slf4j.Logger log
-            = org.slf4j.LoggerFactory.getLogger(DynamicTaxonomyService2.class);
+            = org.slf4j.LoggerFactory.getLogger(MdTaxonomyService.class);
 
     @Autowired
-    DynamicTaxonomyRepository dynamicTaxonomyRepository;
+    MdDynamicTaxonomyRepository dynamicTaxonomyRepository;
 
     @Autowired
     MdDynamicRepository mdDynamicRepository;

+ 1 - 1
server/src/main/java/com/giantan/data/mds/service/impl/Vectorization.java

@@ -1,6 +1,6 @@
 package com.giantan.data.mds.service.impl;
 
-import com.giantan.ai.common.util.JsonUtil;
+import com.giantan.ai.util.JsonUtil;
 import com.giantan.data.mds.service.IVectorization;
 import org.springframework.beans.factory.annotation.Value;
 import org.springframework.stereotype.Service;

+ 3 - 3
server/src/main/java/com/giantan/data/mds/task/impl/ChunksTaskHandler.java

@@ -1,7 +1,7 @@
 package com.giantan.data.mds.task.impl;
 
 import com.fasterxml.jackson.core.JsonProcessingException;
-import com.giantan.ai.common.util.JsonUtil;
+import com.giantan.ai.util.JsonUtil;
 import com.giantan.data.mds.bot.GChatClient;
 import com.giantan.data.mds.chunk.MdChunk;
 import com.giantan.data.index.IHybridSearch;
@@ -315,8 +315,8 @@ public class ChunksTaskHandler extends BaseTaskHandler {
         doc.setText(chunk.getPlainText());
         doc.setEmbedding(chunk.getEmbedding());
         doc.setMetadata(metadata);
-
-        List<DocResp> ret = hybridSearch.add(coll, List.of(doc));
+        String indexName = mdChunksService.getIndexName(coll);
+        List<DocResp> ret = hybridSearch.addDirect(indexName, List.of(doc));
         //System.out.println(ret);
     }
 

+ 1 - 1
server/src/main/java/com/giantan/data/mds/task/impl/EmbeddingTaskHandler.java

@@ -1,6 +1,6 @@
 package com.giantan.data.mds.task.impl;
 
-import com.giantan.ai.common.util.JsonUtil;
+import com.giantan.ai.util.JsonUtil;
 import com.giantan.data.mds.chunk.MdChunk;
 import com.giantan.data.mds.service.IMdChunksService;
 import com.giantan.data.mds.service.IVectorization;

+ 1 - 1
server/src/main/java/com/giantan/data/mds/task/impl/MdsTaskHandler.java

@@ -1,6 +1,6 @@
 package com.giantan.data.mds.task.impl;
 
-import com.giantan.ai.common.util.JsonUtil;
+import com.giantan.ai.util.JsonUtil;
 import com.giantan.data.kvs.kvstore.GBaseKeyValue;
 import com.giantan.data.mds.chunk.MdChunk;
 import com.giantan.data.mds.service.IMdChunksService;

+ 57 - 0
server/src/main/java/com/giantan/data/qa/Readme.java

@@ -0,0 +1,57 @@
+package com.giantan.data.qa;
+
+
+// 1、首先 要 创建 collection table:
+//    GRepositoryTest.java --  void createTable()
+//    手工创建 schema : qadb
+// 2、构建 QaCollectionService.java
+// 3、构建 repository/
+// 4、构建 QaTaxonomyService.java
+
+import java.util.ArrayList;
+import java.util.List;
+
+/*
+CREATE TABLE IF NOT EXISTS gkg_catalog.qa_collections (id SERIAL PRIMARY KEY,    gid  VARCHAR(36) NOT NULL UNIQUE,    name TEXT NOT NULL UNIQUE,    altlabels TEXT[],    mark INT DEFAULT 0,    description TEXT,    tags TEXT[],    path TEXT,    attributes JSONB);
+CREATE INDEX IF NOT EXISTS idx_qa_collections_gid ON gkg_catalog.QA_COLLECTIONS (gid);
+CREATE INDEX IF NOT EXISTS idx_qa_collections_name ON gkg_catalog.QA_COLLECTIONS (name);
+CREATE INDEX IF NOT EXISTS idx_qa_collections_tags ON gkg_catalog.qa_collections (tags);
+CREATE INDEX IF NOT EXISTS idx_qa_collections_path ON gkg_catalog.qa_collections (path);
+
+ */
+public class Readme {
+
+    public static List<String> createGEntityTable(String schema, String tableName) {
+        List<String> ls = new ArrayList<String>();
+        String createTable = String.format(" CREATE TABLE IF NOT EXISTS %s.%s (id SERIAL PRIMARY KEY," +
+                "    gid  VARCHAR(36) NOT NULL UNIQUE," +
+                "    name TEXT NOT NULL UNIQUE," +
+                "    altlabels TEXT[]," +
+                "    mark INT DEFAULT 0," +
+                "    description TEXT," +
+                "    tags TEXT[]," +
+                "    path TEXT," +
+                "    attributes JSONB);", schema, tableName);
+
+        String gidIndex = String.format("CREATE INDEX IF NOT EXISTS idx_%s_gid ON %s.%S (gid);",
+                tableName, schema, tableName);
+
+        String nameIndex = String.format("CREATE INDEX IF NOT EXISTS idx_%s_name ON %s.%S (name);",
+                tableName, schema, tableName);
+
+        String tagsIndex = String.format("CREATE INDEX IF NOT EXISTS idx_%s_tags ON %s.%s (tags);"
+                , tableName, schema, tableName);
+        String pathIndex = String.format("CREATE INDEX IF NOT EXISTS idx_%s_path ON %s.%s (path);"
+                , tableName, schema, tableName);
+
+        ls.add(createTable);
+        ls.add(gidIndex);
+        ls.add(nameIndex);
+        ls.add(tagsIndex);
+        ls.add(pathIndex);
+
+        return ls;
+    }
+
+
+}

+ 36 - 0
server/src/main/java/com/giantan/data/qa/constant/QaConstants.java

@@ -0,0 +1,36 @@
+package com.giantan.data.qa.constant;
+
+public class QaConstants {
+
+    public static final String API_PREFIX = "/v1/qa";
+    public static final String FIELD_ENTITY = "entity";
+
+    public static final String FIELD_QUERY = "query";
+
+    public static final String FIELD_OFFSET = "offset";
+    public static final String FIELD_LENGTH = "length";
+    public static final String FIELD_VALUES = "values";
+
+    public static final String FIELD_RETURN = "returnFields";
+
+
+    //可配置项(用户可控)
+    //字段选择:["name", "description", "tags", "attributes.color"]
+    //模式:single | per-field
+    //连接符:=, :, -> 等(输出格式)
+    //空值处理:跳过 | 输出 "null"
+    //public static final String CHUNK_TEMPLATE = "chunkTemplate";
+    public static final String CHUNK_MODE = "chunkMode";
+    public static final String CHUNK_MODE_SINGLE = "single";
+    public static final String CHUNK_MODE_MULTIPLE = "multiple";
+    public static final String CHUNK_MODE_CUSTOM = "custom";
+    public static final String CHUNK_TEMPLATES = "chunkTemplates";
+    
+    public static final String INDEX_STRATEGY = "indexStrategy";
+    public static final String INDEX_MODE = "indexMode";  //  、singleCollection
+    public static final String INDEX_MODE_T2C = "table2collection";
+    public static final String INDEX_MODE_SINGLE = "singleCollection";
+    public static final String INDEX_COLLECTION_PREFIX = "collectionPrefix";
+    public static final String INDEX_GLOBAL_COLLECTION = "globalCollection";
+
+}

+ 79 - 0
server/src/main/java/com/giantan/data/qa/controller/QaCollectionsController.java

@@ -0,0 +1,79 @@
+package com.giantan.data.qa.controller;
+
+
+import com.giantan.ai.common.reponse.R;
+import com.giantan.data.qa.constant.QaConstants;
+import com.giantan.data.qa.service.QaCollectionService;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.http.ResponseEntity;
+import org.springframework.web.bind.annotation.*;
+
+import java.util.List;
+import java.util.Map;
+
+//修改某个字段,推荐 PATCH。
+//整体替换,才用 PUT。
+//新增才用 POST。
+
+@RestController
+@RequestMapping(QaConstants.API_PREFIX+"/collections")
+@CrossOrigin(origins = "*", maxAge = 3600)
+public class QaCollectionsController {
+
+    @Autowired
+    QaCollectionService qaCollectionService;
+
+    @PostMapping
+    public ResponseEntity<R> createCollection(@RequestParam String name) throws Throwable {
+        return ResponseEntity.ok(R.data(qaCollectionService.createCollection(name)));
+    }
+
+    @GetMapping
+    public ResponseEntity<R> getAllCollections() throws Throwable {
+        return ResponseEntity.ok(R.data(qaCollectionService.getAllCollections()));
+    }
+
+    @GetMapping("/{name}")
+    public ResponseEntity<R> getCollectionById(@PathVariable String name) throws Throwable {
+        return ResponseEntity.ok(R.data(qaCollectionService.getKvByName(name)));
+    }
+
+    @DeleteMapping("/{name}")
+    public ResponseEntity<R> deleteCollection(@PathVariable String name) throws Throwable {
+       // long ret = collectionService.deleteCollection(id);
+        long ret = qaCollectionService.deleteCollection(name);
+        return ResponseEntity.ok(R.data(ret));
+    }
+
+    @DeleteMapping("/{name}/all")
+    public ResponseEntity<R> clearCollection(@PathVariable String name) throws Throwable {
+        // long ret = collectionService.deleteCollection(id);
+        long ret = qaCollectionService.clearCollection(name);
+        return ResponseEntity.ok(R.data(ret));
+    }
+
+    @PutMapping("/{name}")
+    public ResponseEntity<R> update(@PathVariable String name,  @RequestBody Map<String,Object> kvs) throws Throwable {
+        return ResponseEntity.ok(R.data(qaCollectionService.updateCollection(name,kvs)));
+    }
+
+    @GetMapping("/{name}/attributes")
+    public ResponseEntity<R> getAttributes(@PathVariable String name) throws Throwable {
+        return ResponseEntity.ok(R.data(qaCollectionService.getCollectionAttributes(name)));
+    }
+
+    @PutMapping("/{name}/attributes")
+    public ResponseEntity<R> updateAttributes(@PathVariable String name,  @RequestBody Map<String,Object> attributes) throws Throwable {
+        return ResponseEntity.ok(R.data(qaCollectionService.updateCollectionAttributes(name,attributes)));
+    }
+
+    @DeleteMapping("/{name}/attributes")
+    public ResponseEntity<R> removeAttribute(@PathVariable String name,@RequestBody List<String> keys) throws Throwable {
+        return ResponseEntity.ok(R.data(qaCollectionService.removeCollectionAttribute(name,keys)));
+    }
+
+//    @PutMapping("/{name}/attributes")
+//    public ResponseEntity<R> updateChunkMode(@PathVariable String name, @RequestBody Map<String,Object> attributes) throws Throwable {
+//        return ResponseEntity.ok(R.data(qaCollectionService.updateChunkMode(name,attributes)));
+//    }
+}

+ 292 - 0
server/src/main/java/com/giantan/data/qa/controller/QaDocsController.java

@@ -0,0 +1,292 @@
+package com.giantan.data.qa.controller;
+
+import com.giantan.ai.common.reponse.R;
+import com.giantan.data.kvs.kvstore.GBaseKeyValue;
+import com.giantan.data.qa.constant.QaConstants;
+import com.giantan.data.qa.service.QaDocsService;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.http.ResponseEntity;
+import org.springframework.web.bind.annotation.*;
+
+import java.lang.invoke.MethodHandles;
+import java.util.List;
+import java.util.Map;
+
+// | 上传文件       | `POST`   | `/collections/{collId}/mds`                  |
+//| 上传 ZIP(批量) | `POST`   | `/collections/{collId}/docs/batch`(或 `/bulk`) |
+
+//| 提交新任务  | `POST`   | `/collections/{collId}/tasks`                                                                             |
+//| 获取任务状态 | `GET`    | `/collections/{collId}/tasks/{taskId}/status`  |
+//| 取消任务   | `DELETE` | `/collections/{collId}/tasks/{taskId}`                                                                    |
+//| 获取任务详情 | `GET`    | `/collections/{collId}/tasks/{taskId}`
+
+
+// deleteByName DELETE /collections/{collId}/mds?name=someName
+
+// 批处理任务的统一提交方式
+// POST /collections/{collId}/tasks
+//Content-Type: application/json
+//
+//{
+//  "type": "chunk",  // 或 "keywords", "summary"
+//  "objectIds": ["md1", "md2"],  // 可选:为空则全量
+//  "params": {
+//    "chunkSize": 500,
+//    "language": "zh"
+//  }
+//}
+
+//`/collections/{collId}/docs/actions/keywords`
+
+// - `POST /collections/{collId}/docs/actions/{action}`:简洁直观,表示“对资源集合执行动作”
+//- 配合 `mdIds` 或标签、筛选条件等参数进行部分操作
+
+@RestController
+@RequestMapping(QaConstants.API_PREFIX + "/collections/{collId}")
+public class QaDocsController {
+    private static final org.slf4j.Logger log
+            = org.slf4j.LoggerFactory.getLogger(MethodHandles.lookup().lookupClass());
+
+    @Autowired
+    QaDocsService qaDocsService;
+
+    @PostMapping("/docs")
+    public ResponseEntity createEntry(@PathVariable String collId, @RequestBody Map<String, Object> data) throws Throwable {
+        GBaseKeyValue r = qaDocsService.save(collId, GBaseKeyValue.build(data));
+        return ResponseEntity.ok(r);
+    }
+
+    @PostMapping("/docs/batch")
+    public ResponseEntity<R> createBatch(@PathVariable String collId, @RequestBody List<GBaseKeyValue> kvs) throws Throwable {
+        List<Integer> ret = qaDocsService.saveAll(collId, kvs);
+        return ResponseEntity.ok(R.data(ret));
+    }
+
+    @DeleteMapping("/docs/{gid}")
+    public R<?> delete(@PathVariable String collId, @PathVariable String gid
+    ) throws Throwable {
+        //String taskId = UUID.randomUUID().toString();
+        //System.out.println("taskId = " + taskId);
+        //System.out.println("file = " + file.getOriginalFilename());
+
+        //taskStatusManager.putProcessing(taskId, new TaskStatus(collId, taskId, "处理中", "", System.currentTimeMillis(), 0));
+        //fileProcessingService.processAsyncDirect(collId, taskId, file, params);
+        //System.out.println("params = " + params);
+        int deleted = qaDocsService.deleteByMdid(collId, gid);
+        log.info("删除文件: {}", gid);
+        //log.info("taskId = " + taskId);
+        return R.data(Map.of("deleted", deleted));
+    }
+
+    @DeleteMapping("/docs/by-name")
+    public R<Map<String, Object>> deleteByName(@PathVariable String collId, @RequestParam("name") String name
+    ) throws Throwable {
+        // 根据 name 删除对应 md
+        int deleted = qaDocsService.deleteByName(collId, name);
+        return R.data(Map.of("deleted", deleted));
+    }
+
+    @DeleteMapping("/docs/all")
+    public R<Map<String, Object>> deleteAll(@PathVariable String collId) throws Exception {
+        long removed = qaDocsService.deleteAll(collId);
+        return R.data(Map.of("deleted", removed));
+    }
+
+    @PutMapping("/docs")
+    public R update(@PathVariable String collId, @RequestBody GBaseKeyValue kv) throws Throwable {
+        GBaseKeyValue updatedKv = qaDocsService.update(collId, kv);
+        return R.data(updatedKv);
+    }
+    /////////////////////
+
+//    @PostMapping("/docs/locate")
+//    public R<?> locateText(
+//            @PathVariable String collId,
+//            @RequestBody Map<String,Object> req
+//    ) throws Throwable {
+//        Map<String, Object> r = mdDocsService.locateText(collId, req);
+//        return R.data(r);
+//    }
+//
+//    @PostMapping("/docs/locateByHeadings")
+//    public R<?> locateByHeading(
+//            @PathVariable String collId,
+//            @RequestBody Map<String,Object> req
+//    ) throws Throwable {
+//        Map<String, Object> r = null;
+//        try {
+//            r = mdDocsService.locateByHeading(collId, req);
+//        }catch (Throwable e){
+//            log.error(e.getMessage());
+//            r = Map.of("match", "");
+//        }
+//        return R.data(r);
+//    }
+
+    @GetMapping("/docs/all")
+    public R<List<GBaseKeyValue>> getAll(@PathVariable String collId) {
+        List<GBaseKeyValue> ret = null;
+        try {
+            ret = qaDocsService.findAll(collId);
+        } catch (Throwable e) {
+            throw new RuntimeException(e);
+        }
+        return R.data(ret);
+    }
+
+    //////////////////
+    //整体 metadata 管理	/collections/{collId}/docs/{docId}/metadata
+    //单个 metadata 字段	/collections/{collId}/docs/{docId}/metadata/{key}
+
+//    @GetMapping("/docs/{docId}/metadata")
+//    public R<GBaseKeyValue> getMetadataByMdid(@PathVariable String collId, @PathVariable String docId) throws Throwable {
+//        GBaseKeyValue ret = mdDocsService.findByMdid(collId, docId);
+//        return R.data(ret);
+//    }
+//
+//    @GetMapping("/docs/{docId}/metadata/{key}")
+//    public R getMetadataByKey(@PathVariable String collId, @PathVariable String docId,
+//                              @PathVariable String key
+//    ) throws Throwable {
+//        Object ret = mdDocsService.getMetadataByKey(collId, docId, key);
+//        return R.data(ret);
+//    }
+//
+//    @PatchMapping("/docs/{docId}/metadata")
+//    public R patchMetadata(@PathVariable String collId, @PathVariable String docId,
+//                           @RequestBody GBaseKeyValue data
+//    ) throws Throwable {
+//        Object ret = mdDocsService.patchMetadata(collId, docId, data);
+//        return R.data(ret);
+//    }
+
+
+    @PostMapping("/docs/{docId}/rename")
+    public R<?> rename(@PathVariable String collId, @PathVariable String docId, @RequestBody Map<String, Object> req
+    ) throws Throwable {
+        Map<String, Object> r = qaDocsService.rename(collId, docId, req);
+        return R.data(r);
+    }
+
+
+    ////////////////
+    // 更新属性
+
+    @GetMapping("/docs/{docId}/attributes")
+    public R getAttributeByKey(@PathVariable String collId, @PathVariable String docId
+    ) throws Throwable {
+        Object ret = qaDocsService.getAttributes(collId, docId);
+        return R.data(ret);
+    }
+
+    @GetMapping("/docs/{docId}/attributes/{key}")
+    public R getAttributeByKey(@PathVariable String collId, @PathVariable String docId,
+                               @PathVariable String key
+    ) throws Throwable {
+        Object ret = qaDocsService.getAttributeByKey(collId, docId, key);
+        return R.data(ret);
+    }
+
+    @PatchMapping("/docs/{docId}/attributes")
+    public R patchAttributes(@PathVariable String collId, @PathVariable String docId,
+                             @RequestBody GBaseKeyValue data
+    ) throws Throwable {
+        Object ret = qaDocsService.patchAttributes(collId, docId, data);
+        return R.data(ret);
+    }
+
+    @DeleteMapping("/docs/{docId}/attributes/{key}")
+    public R deleteAttributeByKey(@PathVariable String collId, @PathVariable String docId,
+                                  @PathVariable String key
+    ) throws Throwable {
+        Object ret = qaDocsService.deleteAttributeByKey(collId, docId, key);
+        return R.data(ret);
+    }
+
+    // 移除属性
+    @DeleteMapping("/docs/{docId}/attributes")
+    public R removeAttribute(@PathVariable String collId, @PathVariable String docId, @RequestBody List<String> keys) throws Throwable {
+        GBaseKeyValue updatedKv = qaDocsService.deleteAttributeByKeys(collId, docId, keys);
+        return R.data(updatedKv);
+    }
+
+    @PutMapping("/docs/{docId}/attributes")
+    public R updateAttribute(@PathVariable String collId, @PathVariable Integer docId, @RequestBody Map<String, Object> attributes) throws Throwable {
+        GBaseKeyValue updatedKv = qaDocsService.updateAttribute(collId, docId, attributes);
+        return R.data(updatedKv);
+    }
+
+    // 获取某些字段的所有值
+    @PostMapping("/docs/fields")
+    public R getAllEntities(@PathVariable String collId, @RequestBody List<String> fields) throws Throwable {
+        List<Map<String, Object>> entities = qaDocsService.getAllEntities(collId, fields);
+        return R.data(entities);
+    }
+
+//    @PostMapping("/fieldsByWhere")
+//    public ResponseEntity<R> getAllEntitiesByWhere(@PathVariable String collId,@RequestBody List<String> fields) throws Throwable {
+//        List<Map<String, Object>> entities = collectionService.getAllEntities(collId,fields);
+//        return ResponseEntity.ok(R.data(entities));
+//    }
+
+    // 获取记录数
+    @GetMapping("/docs/count")
+    public R getCount(@PathVariable String collId) {
+        long count = qaDocsService.count(collId);
+        return R.data(count);
+    }
+
+    @PostMapping("/docs/{docId}/tags/add")
+    public ResponseEntity<R> appendTag(@PathVariable String collId, @PathVariable int docId, @RequestBody List<String> values) {
+        List<String> ret = qaDocsService.appendArrayField(collId, docId, "tags", values);
+        return ResponseEntity.ok(R.data(ret));
+    }
+
+    @PostMapping("/docs/{docId}/tags/set")
+    public ResponseEntity<R> setTag(@PathVariable String collId, @PathVariable int docId, @RequestBody List<String> values) {
+        List<String> ret = qaDocsService.setArrayField(collId, docId, "tags", values);
+        return ResponseEntity.ok(R.data(ret));
+    }
+
+    @PostMapping("/docs/{docId}/tags/remove")
+    public ResponseEntity<R> removeTag(@PathVariable String collId, @PathVariable int docId, @RequestBody List<String> values) {
+        List<String> ret = qaDocsService.removeArrayField(collId, docId, "tags", values);
+        return ResponseEntity.ok(R.data(ret));
+    }
+
+    @PostMapping("/docs/{docId}/altlabels/add")
+    public ResponseEntity<R> appendAltlabels(@PathVariable String collId, @PathVariable int docId, @RequestBody List<String> values) {
+        List<String> ret = qaDocsService.appendArrayField(collId, docId, "altlabels", values);
+        return ResponseEntity.ok(R.data(ret));
+    }
+
+    @PostMapping("/docs/{docId}/altlabels/set")
+    public ResponseEntity<R> setAltlabels(@PathVariable String collId, @PathVariable int docId, @RequestBody List<String> values) {
+        List<String> ret = qaDocsService.setArrayField(collId, docId, "altlabels", values);
+        return ResponseEntity.ok(R.data(ret));
+    }
+
+    @PostMapping("/docs/{docId}/altlabels/remove")
+    public ResponseEntity<R> removeAltlabels(@PathVariable String collId, @PathVariable int docId, @RequestBody List<String> values) {
+        List<String> ret = qaDocsService.removeArrayField(collId, docId, "altlabels", values);
+        return ResponseEntity.ok(R.data(ret));
+    }
+
+    @PostMapping("/docs/fulltextSearch")
+    public ResponseEntity<R> fulltextSearch(@PathVariable String collId, @RequestBody Map<String, Object> query) throws Throwable {
+        List<GBaseKeyValue> rets = qaDocsService.fulltextSearch(collId, query);
+        return ResponseEntity.ok(R.data(rets));
+    }
+
+    @PostMapping("/docs/similaritySearch")
+    public ResponseEntity<R> similaritySearch(@PathVariable String collId, @RequestBody Map<String, Object> query) throws Throwable {
+        List<GBaseKeyValue> rets = qaDocsService.similaritySearch(collId, query);
+        return ResponseEntity.ok(R.data(rets));
+    }
+
+    @PostMapping("/docs/hybridSearch")
+    public ResponseEntity<R> hybridSearch(@PathVariable String collId, @RequestBody Map<String, Object> query) throws Throwable {
+        List<GBaseKeyValue> rets = qaDocsService.hybridSearch(collId, query);
+        return ResponseEntity.ok(R.data(rets));
+    }
+}

+ 112 - 0
server/src/main/java/com/giantan/data/qa/controller/QaSearchContoller.java

@@ -0,0 +1,112 @@
+package com.giantan.data.qa.controller;
+
+import com.giantan.ai.common.reponse.R;
+import com.giantan.data.index.IHybridSearch;
+import com.giantan.data.kvs.kvstore.GBaseKeyValue;
+import com.giantan.data.qa.constant.QaConstants;
+import com.giantan.data.qa.repository.QaDocRepository;
+import com.giantan.data.qa.service.QaDocsService;
+import jakarta.servlet.http.HttpServletRequest;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.http.HttpEntity;
+import org.springframework.http.HttpHeaders;
+import org.springframework.http.HttpMethod;
+import org.springframework.http.ResponseEntity;
+import org.springframework.web.bind.annotation.*;
+import org.springframework.web.client.RestTemplate;
+
+import java.lang.invoke.MethodHandles;
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+
+@RestController
+@RequestMapping(QaConstants.API_PREFIX + "/collections/{coll}")
+public class QaSearchContoller {
+    private static final org.slf4j.Logger log
+            = org.slf4j.LoggerFactory.getLogger(MethodHandles.lookup().lookupClass());
+
+    @Value("${qas.url}")
+    String url = "http://120.78.4.46:7387/v1/collections/";
+
+    @Autowired
+    //QaDocRepository qaDocRepository;
+    QaDocsService qaDocsService;
+
+    private final RestTemplate restTemplate = new RestTemplate();
+
+    //private static final String SRC_PREFIX = "http://127.0.0.1:18211/v1/md";
+    private String TARGET_PREFIX = "http://120.78.4.46:7387/v1";
+
+
+    @RequestMapping("/indexes/**")
+    public ResponseEntity<byte[]> proxyAll(@PathVariable String coll, HttpServletRequest request,
+                                           @RequestBody(required = false) byte[] body) {
+
+        String requestUri = request.getRequestURI(); // 原始 URI
+        String query = request.getQueryString();
+
+        // 替换前缀
+        //String targetPath = requestUri.replaceFirst("/v1/md", "");
+        int ii = requestUri.indexOf(QaConstants.API_PREFIX);
+
+        // 去掉 /indexes
+        String targetPath = requestUri.substring(ii + QaConstants.API_PREFIX.length());
+        targetPath = targetPath.replaceFirst("/collections", "");
+        targetPath = targetPath.replaceFirst("/" + coll, "");
+        targetPath = targetPath.replaceFirst("/indexes", "/documents");
+
+        //TARGET_PREFIX = url.replaceFirst("/collections", "");
+        // 拼接目标 URL
+        String collName = qaDocsService.getIndexName(coll);
+
+        String targetUrl = url + collName + targetPath + (query != null ? "?" + query : "");
+
+        return redirect(request, body, targetUrl);
+    }
+
+
+    public ResponseEntity<byte[]> redirect(HttpServletRequest request, byte[] body, String targetUrl) {
+
+        // 构造请求头
+        HttpHeaders headers = new HttpHeaders();
+        Collections.list(request.getHeaderNames())
+                .forEach(name -> headers.add(name, request.getHeader(name)));
+
+        // 读取 HTTP 方法
+        HttpMethod method = HttpMethod.valueOf(request.getMethod());
+
+        // 打印请求日志
+        int requestSize = body != null ? body.length : 0;
+        log.info("[Qas] " + method + " " + targetUrl + " | request size: " + requestSize + " bytes");
+
+        // 构造请求实体
+        HttpEntity<byte[]> entity = new HttpEntity<>(body, headers);
+
+        // 转发请求
+        ResponseEntity<byte[]> response = restTemplate.exchange(
+                targetUrl,
+                method,
+                entity,
+                byte[].class
+        );
+
+        // 打印响应日志
+        int responseSize = response.getBody() != null ? response.getBody().length : 0;
+        log.info("[Qas] response status: " + response.getStatusCode() + " | response size: " + responseSize + " bytes");
+        HttpHeaders headers1 = response.getHeaders();
+        HttpHeaders headers2 = new HttpHeaders();
+        headers1.forEach((k, v) -> {
+            if (!k.equalsIgnoreCase("Transfer-Encoding")) {
+                headers2.put(k, v);
+            }
+        });
+        return ResponseEntity
+                .status(response.getStatusCode())
+                .headers(headers2)
+                .body(response.getBody());
+    }
+
+
+}

+ 144 - 0
server/src/main/java/com/giantan/data/qa/controller/QaTaxonomyController.java

@@ -0,0 +1,144 @@
+package com.giantan.data.qa.controller;
+
+import com.giantan.ai.common.reponse.R;
+import com.giantan.data.kvs.kvstore.GBaseKeyValue;
+
+import com.giantan.data.qa.constant.QaConstants;
+import com.giantan.data.qa.service.QaTaxonomyService;
+import com.giantan.data.taxonomy.model.TaxonomyNode;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.http.ResponseEntity;
+import org.springframework.web.bind.annotation.*;
+
+import java.lang.invoke.MethodHandles;
+import java.util.List;
+import java.util.Map;
+import java.util.UUID;
+
+@RestController
+@RequestMapping(QaConstants.API_PREFIX + "/collections/{collName}")
+public class QaTaxonomyController {
+    private static final org.slf4j.Logger log
+            = org.slf4j.LoggerFactory.getLogger(MethodHandles.lookup().lookupClass());
+
+    @Autowired
+    private QaTaxonomyService dynamicTaxonomyService;
+
+    @GetMapping("/taxonomy/all")
+    public ResponseEntity<?> listAll(@PathVariable String collName) {
+        List<TaxonomyNode> r = dynamicTaxonomyService.listAll(collName);
+        return ResponseEntity.ok(r);
+    }
+
+    @GetMapping("/taxonomy/tree")
+    public ResponseEntity<?> listTree(@PathVariable String collName) {
+        List<TaxonomyNode> r = dynamicTaxonomyService.listTree(collName);
+        return ResponseEntity.ok(r);
+    }
+
+    @GetMapping("/taxonomy/{nodeId}/children")
+    public ResponseEntity<?> findChildren(@PathVariable String collName, @PathVariable String nodeId) throws Exception {
+        List<TaxonomyNode> r = dynamicTaxonomyService.findChildren(collName, nodeId);
+        return ResponseEntity.ok(r);
+    }
+
+    @GetMapping("/taxonomy/{nodeId}/docs")
+    public ResponseEntity<?> findMds(@PathVariable String collName, @PathVariable String nodeId) throws Exception {
+        List<GBaseKeyValue> r = dynamicTaxonomyService.findMds(collName, nodeId);
+        return ResponseEntity.ok(r);
+    }
+
+    @PostMapping("/taxonomy")
+    public ResponseEntity<?> createNode(@PathVariable String collName, @RequestBody Map<String, Object> data) throws Exception {
+        TaxonomyNode r = dynamicTaxonomyService.createNode(collName, data);
+        return ResponseEntity.ok(r);
+    }
+
+//    @PostMapping("/{nodeId}/mds")
+//    public R<Map<String, Object>> upload(@PathVariable String collName,
+//                                         @PathVariable String nodeId,
+//                                         @RequestParam("file") MultipartFile file,
+//                                         @RequestParam Map<String, String> params
+//    ) throws Exception {
+//        String taskId = UUID.randomUUID().toString();
+//        //System.out.println("taskId = " + taskId);
+//        //System.out.println("file = " + file.getOriginalFilename());
+//        log.info("上传文件: {}, taskId: {}", file.getOriginalFilename(), taskId);
+//
+//        //taskStatusManager.putProcessing(taskId, new TaskStatus(collId, taskId, "处理中", "", System.currentTimeMillis(), 0));
+//        Map<String, Object> ret = dynamicTaxonomyService.processAsyncDirect(collName, nodeId, taskId, file, params);
+//
+//        return R.data(ret);
+//    }
+
+    @DeleteMapping("/taxonomy/{nodeId}/docs")
+    public R deleteFolder(@PathVariable String collName, @PathVariable String nodeId) throws Exception {
+        String taskId = UUID.randomUUID().toString();
+        //System.out.println("taskId = " + taskId);
+        //System.out.println("file = " + file.getOriginalFilename());
+        //log.info("上传文件: {}, taskId: {}", file.getOriginalFilename(), taskId);
+
+        //taskStatusManager.putProcessing(taskId, new TaskStatus(collId, taskId, "处理中", "", System.currentTimeMillis(), 0));
+        int ret = dynamicTaxonomyService.deleteFolder(collName, nodeId, taskId);
+
+        return R.data(ret);
+    }
+
+    @PostMapping("/taxonomy/{nodeId}/rename")
+    public R renameFolder(@PathVariable String collName, @PathVariable String nodeId, @RequestBody Map<String, Object> req) throws Exception {
+        String taskId = UUID.randomUUID().toString();
+        //System.out.println("taskId = " + taskId);
+        //System.out.println("file = " + file.getOriginalFilename());
+        //log.info("上传文件: {}, taskId: {}", file.getOriginalFilename(), taskId);
+
+        //taskStatusManager.putProcessing(taskId, new TaskStatus(collId, taskId, "处理中", "", System.currentTimeMillis(), 0));
+        int ret = dynamicTaxonomyService.renameFolder(collName, nodeId, req, taskId);
+
+        return R.data(ret);
+    }
+
+    @PostMapping("/taxonomy/{nodeId}/move")
+    public R moveTo(@PathVariable String collName, @PathVariable String nodeId, @RequestBody Map<String, Object> req) throws Exception {
+        String taskId = UUID.randomUUID().toString();
+        //System.out.println("taskId = " + taskId);
+        //System.out.println("file = " + file.getOriginalFilename());
+        //log.info("上传文件: {}, taskId: {}", file.getOriginalFilename(), taskId);
+
+        //taskStatusManager.putProcessing(taskId, new TaskStatus(collId, taskId, "处理中", "", System.currentTimeMillis(), 0));
+        int ret = dynamicTaxonomyService.moveTo(collName, nodeId, req, taskId);
+
+        return R.data(ret);
+    }
+
+    @GetMapping("/taxonomy/{nodeId}/subtree")
+    public R getSubtree(@PathVariable String collName, @PathVariable String nodeId) throws Exception {
+        List<TaxonomyNode> r = dynamicTaxonomyService.findSubtree(collName, nodeId);
+        return R.data(r);
+    }
+
+    @GetMapping("/taxonomy/{nodeId}/descendants")
+    public R getDescendants(@PathVariable String collName, @PathVariable String nodeId) throws Exception {
+        List<TaxonomyNode> r = dynamicTaxonomyService.findDescendants(collName, nodeId);
+        return R.data(r);
+    }
+
+    @GetMapping("/taxonomy/{nodeId}/ancestors")
+    public R getAncestors(@PathVariable String collName, @PathVariable String nodeId) throws Exception {
+        List<TaxonomyNode> r = dynamicTaxonomyService.findAncestors(collName, nodeId);
+        return R.data(r);
+    }
+
+    @PostMapping("/taxonomy/{nodeId}/docs/by-path")
+    public R createDocByPath(@PathVariable String collName, @PathVariable String nodeId, @RequestBody Map<String, Object> data) throws Throwable {
+        GBaseKeyValue ret = dynamicTaxonomyService.createQa(collName, nodeId, GBaseKeyValue.build(data));
+        return R.data(ret);
+    }
+
+    ////////////
+
+    @PostMapping("/docs/by-path")
+    public R createEntryByPath(@PathVariable String collName, @RequestBody Map<String, Object> data) throws Throwable {
+        GBaseKeyValue ret = dynamicTaxonomyService.createEntryByPath(collName, GBaseKeyValue.build(data));
+        return R.data(ret);
+    }
+}

+ 41 - 0
server/src/main/java/com/giantan/data/qa/repository/QaDocRepository.java

@@ -0,0 +1,41 @@
+package com.giantan.data.qa.repository;
+
+import com.giantan.data.kvs.repository.index.GIndexedRepository;
+
+import jakarta.annotation.PostConstruct;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.jdbc.core.JdbcTemplate;
+import org.springframework.stereotype.Repository;
+
+@Repository
+public class QaDocRepository extends GIndexedRepository {
+    private static final org.slf4j.Logger log
+            = org.slf4j.LoggerFactory.getLogger(QaDocRepository.class);
+
+    @Autowired
+    private JdbcTemplate jdbc;
+
+    @Autowired
+    QaIndexer qaIndexer;
+
+    public QaDocRepository() {
+        this.schema = "qadb";
+        this.tablePrefix = "qas";
+        this.indexPrefix = "qas";
+    }
+
+    @PostConstruct
+    public void init() {
+        setJdbcTemplate(this.jdbc);
+        setIndexer(qaIndexer);
+    }
+
+//    public String indexName(String coll) {
+//        //return schema + "_" + indexPrefix + "_" + collId;
+//        return this.indexPrefix + "_" + coll;
+//    }
+
+    public String getMappingCollection(String collId) {
+        return qaIndexer.getMappedIndexName(collId);
+    }
+}

+ 417 - 0
server/src/main/java/com/giantan/data/qa/repository/QaIndexer.java

@@ -0,0 +1,417 @@
+package com.giantan.data.qa.repository;
+
+import com.giantan.data.index.IHybridSearch;
+import com.giantan.data.index.dto.DocReq;
+import com.giantan.data.index.dto.DocResp;
+import com.giantan.data.kvs.repository.GEntity;
+import com.giantan.data.kvs.repository.GEntityConfig;
+import com.giantan.data.kvs.repository.index.IIndexer;
+import com.giantan.data.qa.constant.QaConstants;
+import com.giantan.data.qa.service.ICollectionService;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.expression.Expression;
+import org.springframework.expression.ExpressionParser;
+import org.springframework.expression.spel.standard.SpelExpressionParser;
+import org.springframework.expression.spel.support.StandardEvaluationContext;
+import org.springframework.stereotype.Repository;
+
+import java.io.IOException;
+import java.lang.invoke.MethodHandles;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+@Repository
+public class QaIndexer implements IIndexer {
+    private static final org.slf4j.Logger log
+            = org.slf4j.LoggerFactory.getLogger(MethodHandles.lookup().lookupClass());
+
+    public static final String COLL_ID = "__cid";
+    public static final String DOC_ID = "__did";
+
+    String defaultIndexPrefix = "qas";
+    String defaultChunkMode = "single";
+
+    String splitter = "\n";
+
+    @Autowired
+    IHybridSearch hybridSearch;
+
+
+    //    ICollectionMapper collMapper;
+    ICollectionService collectionService;
+
+    public QaIndexer() {
+        //collMapper = new DefaultCollectionMapper();
+    }
+
+    @Override
+    public void init(ICollectionService collectionService) {
+        // 根据 params的attributes
+        setCollectionService(collectionService);
+    }
+
+    public ICollectionService getCollectionService() {
+        return collectionService;
+    }
+
+    public void setCollectionService(ICollectionService collectionService) {
+        this.collectionService = collectionService;
+    }
+
+//    public ICollectionMapper getCollMapper() {
+//        return collMapper;
+//    }
+//
+//    public void setCollMapper(ICollectionMapper collMapper) {
+//        this.collMapper = collMapper;
+//    }
+
+    private void buildMetadata(String collection, GEntity entity, DocReq req) {
+        Map<String, Object> metadata = req.getMetadata();
+        if (metadata == null) {
+            metadata = new HashMap<>();
+            req.setMetadata(metadata);
+        }
+        metadata.put(DOC_ID, entity.getId().toString());
+        metadata.put(COLL_ID, collection);
+    }
+
+    public String indexName(String coll) {
+        return this.defaultIndexPrefix + "_" + coll;
+    }
+
+    public String indexName(String prefix, String coll) {
+        return prefix + coll;
+    }
+
+    public String getMappedIndexName(String collId) {
+        String mapped = indexName(collId);
+        Map<String, Object> attibutes = collectionService.getAttibutes(collId);
+        if (attibutes != null && !attibutes.isEmpty()) {
+            Object o = attibutes.get(QaConstants.INDEX_STRATEGY);
+            if (o != null && o instanceof Map strategy) {
+                Object o1 = strategy.get(QaConstants.INDEX_MODE);
+                if (o1 != null && o1 instanceof String mode) {
+                    if (mode.equals(QaConstants.INDEX_MODE_T2C)) {
+                        Object o2 = strategy.get(QaConstants.INDEX_COLLECTION_PREFIX);
+                        if (o2 != null && o2 instanceof String prefix) {
+                            mapped = indexName(prefix, collId);
+                        }
+                    } else if (mode.equals(QaConstants.INDEX_MODE_SINGLE)) {
+                        Object o2 = strategy.get(QaConstants.INDEX_GLOBAL_COLLECTION);
+                        if (o2 != null && o2 instanceof String collection) {
+                            mapped = collection;
+                        }
+                    }
+                }
+            }
+        }
+        return mapped;
+    }
+
+    public boolean isOne2One(String collId) {
+        boolean isOne2One = true;
+        Map<String, Object> attibutes = collectionService.getAttibutes(collId);
+        if (attibutes != null && !attibutes.isEmpty()) {
+            Object o = attibutes.get(QaConstants.INDEX_STRATEGY);
+            if (o != null && o instanceof Map strategy) {
+                Object o1 = strategy.get(QaConstants.INDEX_MODE);
+                if (o1 != null && o1 instanceof String mode) {
+                    if (mode.equals(QaConstants.INDEX_MODE_SINGLE)) {
+//                        Object o2 = strategy.get(QaConstants.INDEX_GLOBAL_COLLECTION);
+//                        if (o2 != null && o2 instanceof String collection) {
+//                            mapped = collection;
+//                        }
+                        isOne2One = true;
+                    }
+                }
+            }
+        }
+        return isOne2One;
+    }
+
+    private String getChunkMode(String coll) {
+        String chunkMode = defaultChunkMode;
+
+        Map<String, Object> attibutes = collectionService.getAttibutes(coll);
+        if (attibutes != null && !attibutes.isEmpty()) {
+            Object o = attibutes.get(QaConstants.CHUNK_MODE);
+            if (o != null && o instanceof String mode) {
+                chunkMode = mode;
+            }
+        }
+        return chunkMode;
+    }
+
+    private List<String> getChunkTemplates(String coll) {
+        List<String> rets = null;
+        Map<String, Object> attibutes = collectionService.getAttibutes(coll);
+        if (attibutes != null && !attibutes.isEmpty()) {
+            Object o = attibutes.get(QaConstants.CHUNK_TEMPLATES);
+            if (o != null && o instanceof List<?> templates) {
+                rets = new ArrayList<>(templates.size());
+                for (Object o1 : templates) {
+                    rets.add(o1.toString());
+                }
+            }
+        }
+        return rets;
+    }
+
+    private List<DocReq> toDocReq(String coll, GEntity entity) {
+        String chunkMode = getChunkMode(coll);
+        List<DocReq> rets = null;
+        if (chunkMode.equals(QaConstants.CHUNK_MODE_MULTIPLE)) {
+            rets = toDocReq2(coll, entity);
+        } else if (chunkMode.equals(QaConstants.CHUNK_MODE_CUSTOM)) {
+            rets = toDocReq3(coll, entity);
+        } else {
+            rets = toDocReq1(coll, entity);
+        }
+        return rets;
+    }
+
+    private List<DocReq> toDocReq1(String coll, GEntity entity) {
+        List<DocReq> lst = new ArrayList<DocReq>();
+        DocReq dr1 = new DocReq();
+        dr1.setId(entity.getGid());
+
+        dr1.setTags(entity.getTags());
+        buildMetadata(coll, entity, dr1);
+
+        StringBuilder sb = new StringBuilder();
+        sb.append(entity.getName());
+        if (entity.getDescription() != null) {
+            sb.append(entity.getDescription()).append(entity.getDescription());
+        }
+
+        List<String> altlabels = entity.getAltlabels();
+        if (altlabels != null && !altlabels.isEmpty()) {
+            for (int i = 0; i < altlabels.size(); i++) {
+                sb.append(splitter).append(altlabels.get(i));
+            }
+        }
+        dr1.setText(sb.toString());
+        lst.add(dr1);
+        return lst;
+    }
+
+    private List<DocReq> toDocReq2(String coll, GEntity entity) {
+        List<DocReq> lst = new ArrayList<DocReq>();
+        DocReq dr1 = new DocReq();
+        dr1.setId(entity.getGid() + "-0");
+        dr1.setText(entity.getName());
+        dr1.setTags(entity.getTags());
+        buildMetadata(coll, entity, dr1);
+
+        lst.add(dr1);
+
+        if (entity.getDescription() != null) {
+            DocReq dr2 = new DocReq();
+            dr2.setId(entity.getGid() + "-1");
+            dr2.setText(entity.getDescription());
+            dr2.setTags(entity.getTags());
+            buildMetadata(coll, entity, dr2);
+            lst.add(dr2);
+        }
+
+        List<String> altlabels = entity.getAltlabels();
+        if (altlabels != null && !altlabels.isEmpty()) {
+            for (int i = 0; i < altlabels.size(); i++) {
+                int idx = i + 2;
+                DocReq dr2 = new DocReq();
+                dr2.setId(entity.getGid() + "-" + idx);
+                dr2.setText(altlabels.get(i));
+                dr2.setTags(entity.getTags());
+                buildMetadata(coll, entity, dr2);
+                lst.add(dr2);
+            }
+        }
+        return lst;
+    }
+
+    //  ExpressionParser parser = new SpelExpressionParser();
+    //        Expression expression = parser.parseExpression(expressionString);
+    //
+    //        // 上下文
+    //        StandardEvaluationContext context = new StandardEvaluationContext();
+    //        context.setVariables(variables);
+    //
+    //        // 计算结果
+    //        String chunk = expression.getValue(context, String.class);
+    private StandardEvaluationContext buildContext(GEntity entity) {
+        Map<String, Object> variables = new HashMap<>();
+        variables.put(GEntityConfig.ATTRIBUTES, entity.getAttributes());
+        variables.put(GEntityConfig.NAME, entity.getName());
+        variables.put(GEntityConfig.ALTLABELS, entity.getAltlabels());
+        variables.put(GEntityConfig.DESCRIPTION, entity.getDescription());
+
+        StandardEvaluationContext context = new StandardEvaluationContext();
+        context.setVariables(variables);
+        return context;
+    }
+
+
+    private List<DocReq> toDocReq3(String coll, GEntity entity) {
+        List<String> templates = getChunkTemplates(coll);
+        if (templates == null || templates.isEmpty()) {
+            return toDocReq1(coll, entity);
+        }
+
+        List<DocReq> lst = new ArrayList<DocReq>();
+        StandardEvaluationContext context = buildContext(entity);
+        ExpressionParser parser = new SpelExpressionParser();
+
+        if (templates.size() == 1) {
+            Expression expression = parser.parseExpression(templates.get(0));
+            String chunk = expression.getValue(context, String.class);
+            DocReq dr1 = new DocReq();
+            dr1.setId(entity.getGid());
+            dr1.setTags(entity.getTags());
+            dr1.setText(chunk);
+            buildMetadata(coll, entity, dr1);
+            lst.add(dr1);
+        }else {
+            for (int i = 0; i < templates.size(); i++) {
+                Expression expression = parser.parseExpression(templates.get(i));
+                String chunk = expression.getValue(context, String.class);
+                DocReq dr1 = new DocReq();
+                dr1.setId(entity.getGid() + "-" + i);
+                dr1.setTags(entity.getTags());
+                dr1.setText(chunk);
+                buildMetadata(coll, entity, dr1);
+                lst.add(dr1);
+            }
+        }
+        return lst;
+    }
+
+    @Override
+    public int onAdd(String collection, GEntity entity) throws IOException, InterruptedException {
+        log.info("{} onAdd: {}", collection, entity.getName());
+        List<DocResp> rs = hybridSearch.add(getMappedIndexName(collection), toDocReq(collection, entity));
+        return rs == null ? 0 : rs.size();
+    }
+
+    @Override
+    public int onAdd(String collection, List<GEntity> entities) throws IOException, InterruptedException {
+        log.info("{} onAdd: size = {}", collection, entities.size());
+        List<DocReq> ls = new ArrayList<>();
+        for (GEntity e : entities) {
+            List<DocReq> docReqs = toDocReq(collection, e);
+            ls.addAll(docReqs);
+        }
+        List<DocResp> rs = hybridSearch.add(getMappedIndexName(collection), ls);
+        return rs == null ? 0 : rs.size();
+    }
+
+    @Override
+    public int onDelete(String collection, GEntity entity) {
+        log.info("{} onDelete: {}", collection, entity.getName());
+        String gid = entity.getGid();
+        int i = 0;
+        try {
+            i = hybridSearch.deleteDocumentsByIdFilter(getMappedIndexName(collection), gid);
+        } catch (Exception e) {
+            throw new RuntimeException(e);
+        }
+        return i;
+    }
+
+    @Override
+    public int onDelete(String collection, List<GEntity> entities) {
+        log.info("{} onDelete: size = {}", collection, entities.size());
+        int count = 0;
+        try {
+            for (GEntity e : entities) {
+                String gid = e.getGid();
+                count = count + hybridSearch.deleteDocumentsByIdFilter(getMappedIndexName(collection), gid);
+            }
+        } catch (Exception e) {
+            throw new RuntimeException(e);
+        }
+        return count;
+    }
+
+    @Override
+    public int onUpdate(String collection, GEntity entity) throws IOException, InterruptedException {
+        log.info("{} onUpdate: {}", collection, entity.getName());
+
+        int i = hybridSearch.deleteDocumentsByIdFilter(getMappedIndexName(collection), entity.getGid());
+        List<DocResp> rs = hybridSearch.add(getMappedIndexName(collection), toDocReq(collection, entity));
+        return rs == null ? 0 : rs.size();
+    }
+
+    @Override
+    public int onUpdateField(String collection, GEntity entity, String field) {
+        log.info("{} onUpdateField: ret = {}, field = {}", collection, entity.getGid(), field);
+        int count = 0;
+        try {
+            int i = hybridSearch.deleteDocumentsByIdFilter(getMappedIndexName(collection), entity.getGid());
+            List<DocResp> rs = hybridSearch.add(getMappedIndexName(collection), toDocReq(collection, entity));
+            count += rs == null ? 0 : rs.size();
+        } catch (Exception e) {
+            throw new RuntimeException(e);
+        }
+
+        return count;
+    }
+
+    @Override
+    public int onUpdateField(String collection, List<GEntity> entities, String field) {
+        log.info("{} onUpdateField: size = {}, field = {}", collection, entities.size(), field);
+        int count = 0;
+        try {
+            for (GEntity e : entities) {
+                int i = hybridSearch.deleteDocumentsByIdFilter(getMappedIndexName(collection), e.getGid());
+                List<DocResp> rs = hybridSearch.add(getMappedIndexName(collection), toDocReq(collection, e));
+                count += rs == null ? 0 : rs.size();
+            }
+        } catch (Exception e) {
+            throw new RuntimeException(e);
+        }
+        return count;
+    }
+
+    @Override
+    public int createCollection(String collection) {
+        return 0;
+    }
+
+
+    @Override
+    public int onDeleteAll(String collection) {
+        boolean b = false;
+        try {
+            if (isOne2One(collection)) {
+                b = hybridSearch.deleteAll(getMappedIndexName(collection));
+            } else {
+                //TODO 特殊处理
+                //  "filterExpression": "metadata[\"__cid\"] == \"qas_2\""
+            }
+        } catch (Exception e) {
+            throw new RuntimeException(e);
+        }
+        return 0;
+    }
+
+    @Override
+    public int deleteCollection(String collection) {
+        boolean b = false;
+        try {
+            if (isOne2One(collection)) {
+                b = hybridSearch.drop(getMappedIndexName(collection));
+            } else {
+                //TODO 特殊处理
+                //  "filterExpression": "metadata[\"__cid\"] == \"qas_2\""
+            }
+        } catch (Exception e) {
+            throw new RuntimeException(e);
+        }
+        return b ? 0 : 1;
+    }
+
+
+}

+ 22 - 0
server/src/main/java/com/giantan/data/qa/repository/QaTaxonomyRepository.java

@@ -0,0 +1,22 @@
+package com.giantan.data.qa.repository;
+
+import com.giantan.data.taxonomy.repository.DynamicTaxonomyRepository;
+import jakarta.annotation.PostConstruct;
+import org.springframework.jdbc.core.JdbcTemplate;
+import org.springframework.stereotype.Repository;
+
+@Repository
+public class QaTaxonomyRepository extends DynamicTaxonomyRepository {
+
+    //private JdbcTemplate jdbc;
+
+    public QaTaxonomyRepository(JdbcTemplate jdbc) {
+        super(jdbc);
+    }
+
+    @PostConstruct
+    public void init() {
+        setSchema("qadb", "taxonomy_");
+    }
+
+}

+ 10 - 0
server/src/main/java/com/giantan/data/qa/service/ICollectionService.java

@@ -0,0 +1,10 @@
+package com.giantan.data.qa.service;
+
+import java.util.Map;
+
+public interface ICollectionService {
+
+    Map<String,Object> getAttibutes(String collId);
+    Map<String,Object> getAttributesByName(String name);
+
+}

+ 67 - 0
server/src/main/java/com/giantan/data/qa/service/IDocsService.java

@@ -0,0 +1,67 @@
+package com.giantan.data.qa.service;
+
+import com.giantan.data.kvs.kvstore.GBaseKeyValue;
+
+import java.io.IOException;
+import java.util.List;
+import java.util.Map;
+
+public interface IDocsService {
+    long deleteAll(String coll) throws Exception;
+
+    List<GBaseKeyValue> findAll(String coll) throws Throwable;
+
+    GBaseKeyValue findByGid(String coll, String gid) throws Throwable;
+
+    GBaseKeyValue findByMdid(String coll, String mdId) throws Throwable;
+
+    int delete(String coll, String gid) throws Throwable;
+
+    int deleteByMdid(String coll, String gid) throws Throwable;
+
+    int deleteByName(String coll, String name) throws Throwable;
+
+    int deleteByPathPrefix(String coll, String prefix) throws Throwable;
+
+    //Object getMetadataByKey(String coll, String mdId, String key) throws Throwable;
+
+    Object getAttributes(String coll, String mdId) throws Throwable;
+
+    Object getAttributeByKey(String coll, String mdId, String key) throws Throwable;
+
+    //Object patchMetadata(String coll, String mdId, GBaseKeyValue data) throws Throwable;
+
+    Object patchAttributes(String coll, String mdId, Map<String, Object> data) throws Throwable;
+
+    Object deleteAttributeByKey(String coll, String mdId, String key) throws Throwable;
+
+    GBaseKeyValue deleteAttributeByKeys(String coll, String mdId, List<String> keys) throws Throwable;
+
+    long deleteCollection(String coll) throws Throwable;
+
+    Map<String, Object> rename(String coll, String mdId, Map<String, Object> req) throws Throwable;
+
+    GBaseKeyValue save(String coll, GBaseKeyValue entity) throws Throwable;
+
+    List<Integer> saveAll(String coll, List<GBaseKeyValue> kvs) throws Throwable;
+
+    GBaseKeyValue update(String coll, GBaseKeyValue kv) throws Throwable;
+
+    GBaseKeyValue updateAttribute(String coll, Integer docId, Map<String, Object> attributes) throws Throwable;
+
+    List<Map<String, Object>> getAllEntities(String coll, List<String> fields);
+
+    long count(String coll);
+
+    List<String> appendArrayField(String coll, int docId, String tags, List<String> values);
+
+    List<String> setArrayField(String coll, int docId, String tags, List<String> values);
+
+    List<String> removeArrayField(String coll, int docId, String tags, List<String> values);
+
+    String getIndexName(String coll);
+
+    List<GBaseKeyValue> fulltextSearch(String coll, Map<String, Object> query) throws Throwable;
+    List<GBaseKeyValue> similaritySearch(String coll, Map<String, Object> query) throws Throwable;
+    List<GBaseKeyValue> hybridSearch(String coll, Map<String, Object> query) throws Throwable;
+}

+ 317 - 0
server/src/main/java/com/giantan/data/qa/service/QaCollectionService.java

@@ -0,0 +1,317 @@
+package com.giantan.data.qa.service;
+
+import com.giantan.data.kvs.kvstore.GBaseKeyValue;
+import com.giantan.data.kvs.repository.GEntityConfig;
+import com.giantan.data.kvs.repository.GRepository;
+import com.giantan.data.mds.service.CollectionInstance;
+import com.giantan.data.mds.service.impl.MdCores;
+import com.giantan.data.qa.repository.QaDocRepository;
+import com.giantan.data.qa.repository.QaTaxonomyRepository;
+import com.giantan.data.qa.repository.QaIndexer;
+import jakarta.annotation.PostConstruct;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.http.HttpStatus;
+import org.springframework.jdbc.core.JdbcTemplate;
+import org.springframework.stereotype.Service;
+import org.springframework.web.server.ResponseStatusException;
+
+import java.util.List;
+import java.util.Map;
+
+@Service
+public class QaCollectionService implements ICollectionService {
+    private static final org.slf4j.Logger log
+            = org.slf4j.LoggerFactory.getLogger(QaCollectionService.class);
+
+    public static final String GKG_CATALOG = "gkg_catalog";
+    // 在 GKG_CATALOG 系统目录下的 table name
+    public static final String GKG_ENTRY_COLLECTIONS = "qa_collections";
+
+    @Autowired
+    protected JdbcTemplate jdbcTemplate;
+
+    protected GRepository collections;
+
+    @Autowired
+    QaDocRepository qaRepository;
+
+    @Autowired
+    QaTaxonomyRepository qaTaxonomyRepository;
+
+    @Autowired
+    QaIndexer qaIndexer;
+
+    protected MdCores mdCores;
+
+    public QaCollectionService() {
+
+    }
+
+    @PostConstruct
+    public void init() {
+        collections = new GRepository(GKG_CATALOG, GKG_ENTRY_COLLECTIONS, jdbcTemplate);
+        mdCores = new MdCores();
+        qaIndexer.init(this);
+    }
+
+    private int isIntId(String mdId) {
+        if (mdId.length() > 12) {
+            return -1;
+        } else {
+            try {
+                int i = Integer.parseInt(mdId);
+                return i;
+            } catch (Exception e) {
+
+            }
+        }
+        return -1;
+    }
+
+    public int getCollectionId(String name) {
+        CollectionInstance instance = mdCores.getByName(name);
+        if (instance != null) {
+            return instance.getId();
+        } else {
+            try {
+                List<GBaseKeyValue> r = collections.findByName(name);
+                if (r != null && r.size() > 0) {
+                    GBaseKeyValue kv = r.get(0);
+                    //qaIndexer.init(kv);
+
+                    instance = CollectionInstance.build(kv.getIntId(), kv.getGid(), kv.getName(), kv.get(GEntityConfig.ATTRIBUTES));
+                    mdCores.put(name, instance);
+                    return instance.getId();
+                }
+            } catch (Throwable e) {
+
+            }
+            throw new ResponseStatusException(HttpStatus.INTERNAL_SERVER_ERROR,
+                    "Collection with name '" + name + "' not exist.");
+        }
+    }
+
+    public int getCollectionOrNew(String name) {
+        CollectionInstance collection = mdCores.getByName(name);
+        if (collection == null) {
+            try {
+                List<GBaseKeyValue> keys = collections.findByName(name);
+                GBaseKeyValue r = null;
+                if (isEmpty(keys)) {
+                    //r = createCollection(name);
+                    r = creatingCollection(name);
+                } else {
+                    r = keys.get(0);
+                }
+                CollectionInstance instance = CollectionInstance.build(r.getIntId(), r.getGid(), r.getName(), r.get(GEntityConfig.ATTRIBUTES));
+                mdCores.put(name, instance);
+                return instance.getId();
+            } catch (Throwable e) {
+                throw new RuntimeException(e);
+            }
+            // ICollection collection1 = buildCollection(id);
+            //stores.put(name, collection1);
+            //collection = collection1;
+        }
+        return collection.getId();
+    }
+
+    public boolean exists(String name) throws Throwable {
+        List<GBaseKeyValue> keys = collections.findByName(name);
+        if (isEmpty(keys)) {
+            return false;
+        }
+        return true;
+    }
+
+    public GBaseKeyValue getKvByName(String name) throws Throwable {
+        List<GBaseKeyValue> keys = collections.findByName(name);
+        if (isEmpty(keys)) {
+            return null;
+        }
+        return keys.get(0);
+    }
+
+    public List<GBaseKeyValue> getAllCollections() throws Throwable {
+        List<GBaseKeyValue> rets = collections.findAll();
+        return rets;
+    }
+
+    protected boolean isEmpty(List ls) {
+        if (ls == null || ls.size() == 0) {
+            return true;
+        }
+        return false;
+    }
+
+    private void onCollectionUpdate(String name, GBaseKeyValue entity) {
+        if (entity != null) {
+            CollectionInstance info = mdCores.getByName(name);
+            info.updateAttributes(entity.get(GEntityConfig.ATTRIBUTES));
+        }
+    }
+
+    //@Override
+    public GBaseKeyValue updateCollection(String name, Map<String, Object> entity) throws Throwable {
+        //GBaseKeyValue kv = getKvByName(name);
+        //if (kv == null) {
+        //    return null;
+        //}
+
+        int collectionId = getCollectionId(name);
+        GBaseKeyValue gkv = new GBaseKeyValue(entity);
+        gkv.put("id", collectionId);
+        GBaseKeyValue updated = collections.update(gkv);
+
+        onCollectionUpdate(name, updated);
+        return updated;
+    }
+
+    public Map<String, Object> getCollectionAttributes(String name) throws Throwable {
+        GBaseKeyValue ret = getKvByName(name);
+        if (ret == null) {
+            return null;
+        }
+        return (Map<String, Object>) ret.get(GEntityConfig.ATTRIBUTES);
+    }
+
+    public Map<String, Object> updateCollectionAttributes(String name, Map<String, Object> attributes) throws Throwable {
+        int id = getCollectionId(name);
+        GBaseKeyValue ret = collections.updateAttribute(id, attributes);
+        onCollectionUpdate(name, ret);
+        return (Map<String, Object>) ret.get(GEntityConfig.ATTRIBUTES);
+    }
+
+    public GBaseKeyValue removeCollectionAttribute(String name, List<String> keys) throws Throwable {
+//        GBaseKeyValue kv = getKvByName(name);
+//        if (kv == null) {
+//            return null;
+//        }
+//        GBaseKeyValue ret = collections.removeAttribute(kv.getIntId(), keys);
+        int id = getCollectionId(name);
+        GBaseKeyValue ret = collections.removeAttribute(id, keys);
+        onCollectionUpdate(name, ret);
+        return ret;
+    }
+
+    private void removeFromStore(String name) {
+        CollectionInstance remove = mdCores.remove(name);
+    }
+
+    public long deleteCollection(String name) throws Throwable {
+
+        GBaseKeyValue kv = getKvByName(name);
+        Integer intId = kv.getIntId();
+        String id = kv.getId();
+
+        long deleted = collections.delete(intId);
+
+        removeFromStore(name);
+
+        //deleteEntryTable2(KvConfig.ENTRY_SCHEMA, getTableName(intId.toString()));
+        try {
+            qaRepository.deleteTable(id);
+        } catch (Exception e) {
+            log.error(e.getMessage());
+        }
+        try {
+            qaTaxonomyRepository.deleteTable(id);
+        } catch (Exception e) {
+            log.error(e.getMessage());
+        }
+
+        try {
+            qaIndexer.deleteCollection(id);
+        } catch (Exception e) {
+            log.error(e.getMessage());
+        }
+
+        return deleted;
+    }
+
+    public long clearCollection(String name) throws Throwable {
+        GBaseKeyValue kv = getKvByName(name);
+        //Integer intId = kv.getIntId();
+        String id = kv.getId();
+        try {
+            qaRepository.deleteAll(id);
+        } catch (Exception e) {
+            log.error(e.getMessage());
+        }
+        try {
+            qaTaxonomyRepository.deleteAll(id);
+        } catch (Exception e) {
+            log.error(e.getMessage());
+        }
+
+        try {
+            qaIndexer.onDeleteAll(id);
+        } catch (Exception e) {
+            log.error(e.getMessage());
+        }
+        return 1;
+    }
+
+    public GBaseKeyValue createCollection(String name) throws Throwable {
+        if (exists(name)) {
+            throw new ResponseStatusException(HttpStatus.CONFLICT,
+                    "Collection with name '" + name + "' already exists.");
+        }
+        return creatingCollection(name);
+    }
+
+    protected GBaseKeyValue creatingCollection(String name) throws Throwable {
+        GBaseKeyValue kv = GBaseKeyValue.build();
+        kv.setName(name);
+        GBaseKeyValue ret = collections.save(kv);
+
+        //// 创建 table
+        String id = ret.getId();
+        if (id == null) {
+            throw new ResponseStatusException(HttpStatus.INTERNAL_SERVER_ERROR,
+                    "Collection with name '" + name + "' create failed.");
+        }
+
+        //createEntryTable(KvConfig.ENTRY_SCHEMA, getTableName(id));
+        qaRepository.createTable(id);
+        qaTaxonomyRepository.createTable(id);
+        qaIndexer.createCollection(id);
+        //
+        return ret;
+    }
+
+    @Override
+    public Map<String, Object> getAttibutes(String collId) {
+        CollectionInstance instance = mdCores.getById(Integer.parseInt(collId));
+        if (instance == null) {
+            return null;
+        }
+        return instance.getAttributes();
+    }
+
+    @Override
+    public Map<String, Object> getAttributesByName(String name) {
+        CollectionInstance instance = mdCores.getByName(name);
+        if (instance == null) {
+            return null;
+        }
+        return instance.getAttributes();
+    }
+
+//    public GBaseKeyValue updateChunkMode(String name, Map<String, Object> attributes) throws Throwable {
+//        GBaseKeyValue ret = getKvByName(name);
+//        if (ret == null) {
+//            return null;
+//        }
+//        int id = ret.getIntId();
+//
+//
+//        Object o = attributes.get(QaConstants.CHUNK_MODE);
+//        if (o != null){
+//            GBaseKeyValue r = collections.updateAttribute(id, QaConstants.CHUNK_MODE, o);
+//            return r;
+//        }
+//        //return getCollectionAttributes(id);
+//        return collections.find(id);
+//    }
+}

+ 462 - 0
server/src/main/java/com/giantan/data/qa/service/QaDocsService.java

@@ -0,0 +1,462 @@
+package com.giantan.data.qa.service;
+
+import com.giantan.data.index.IHybridSearch;
+import com.giantan.data.index.dto.DocSearchResp;
+import com.giantan.data.kvs.kvstore.GBaseKeyValue;
+import com.giantan.data.qa.repository.QaDocRepository;
+import com.giantan.data.qa.repository.QaIndexer;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+@Service
+public class QaDocsService implements IDocsService {
+    private static final org.slf4j.Logger log
+            = org.slf4j.LoggerFactory.getLogger(QaDocsService.class);
+
+    public static final String FIELD_NAME = "name";
+    public static final String FIELD_GID = "gid";
+    public static final String FIELD_ATTRIBUTES = "attributes";
+    public static final String FIELD_MARK = "mark";
+
+    @Autowired
+    QaCollectionService qaCollectionService;
+
+    @Autowired
+    QaDocRepository qaDocRepository;
+
+    @Autowired
+    IHybridSearch hybridSearch;
+
+    public QaDocsService() {
+
+    }
+
+    protected String getStrOfCollId(String coll) {
+        int id = qaCollectionService.getCollectionId(coll);
+        if (id <= 0) {
+            return null;
+        }
+        return Integer.toString(id);
+    }
+
+    protected String normalizePath(String path) {
+        if (path == null || path.length() == 0) {
+            return "/";
+        }
+        path = path.replaceAll("\\\\", "/");
+        if (path.startsWith("/")) {
+            return path;
+        } else {
+            return "/" + path;
+        }
+    }
+
+
+    private Map<String, Object> toObjectMap(Map<String, String> params) {
+        Map<String, Object> map2 = new HashMap<>();
+        map2.putAll(params);
+        return map2;
+    }
+
+    private int isIntId(String mdId) {
+        if (mdId.length() > 12) {
+            return -1;
+        } else {
+            try {
+                int i = Integer.parseInt(mdId);
+                return i;
+            } catch (Exception e) {
+
+            }
+        }
+        return -1;
+    }
+
+    private int getIntId(String coll, String mdId) throws Throwable {
+        if (mdId.length() > 12) {
+            return -1;
+        } else {
+            try {
+                int i = Integer.parseInt(mdId);
+                return i;
+            } catch (Exception e) {
+
+            }
+        }
+        String collId = getStrOfCollId(coll);
+        GBaseKeyValue r = qaDocRepository.findByGid(collId, mdId);
+        if (r != null) {
+            return r.getIntId();
+        }
+        return -1;
+    }
+
+    @Override
+    public long deleteAll(String coll) throws Exception {
+        String collId = getStrOfCollId(coll);
+        long l = qaDocRepository.deleteAll(collId);
+        //gkbStorer.delete(collId);
+        return l;
+    }
+
+    @Override
+    public List<GBaseKeyValue> findAll(String coll) throws Throwable {
+        String collId = getStrOfCollId(coll);
+        if (collId != null) {
+            List<GBaseKeyValue> rets = qaDocRepository.findAll(collId);
+            return rets;
+        }
+        return null;
+    }
+
+
+    @Override
+    public GBaseKeyValue findByMdid(String coll, String mdId) throws Throwable {
+        String collId = getStrOfCollId(coll);
+        int id = isIntId(mdId);
+        GBaseKeyValue r = null;
+        if (id >= 0) {
+            r = qaDocRepository.find(collId, id);
+        } else {
+            r = qaDocRepository.findByGid(collId, mdId);
+        }
+        return r;
+    }
+
+    @Override
+    public int delete(String coll, String gid) throws Throwable {
+        String collId = getStrOfCollId(coll);
+        GBaseKeyValue kv = qaDocRepository.findByGid(collId, gid);
+        if (kv == null) {
+            log.error("该 {} 不存在", gid);
+            return -1;
+        }
+        return doDelete(coll, kv);
+    }
+
+    @Override
+    public int deleteByMdid(String coll, String gid) throws Throwable {
+        GBaseKeyValue kv = findByMdid(coll, gid);
+        if (kv == null) {
+            log.error("该 {} 不存在", gid);
+            return -1;
+        }
+        return doDelete(coll, kv);
+    }
+
+    @Override
+    public int deleteByName(String coll, String name) throws Throwable {
+        String collId = getStrOfCollId(coll);
+        List<GBaseKeyValue> kvs = qaDocRepository.findByName(collId, name);
+        if (kvs == null || kvs.size() == 0) {
+            log.error("该 {} 不存在", name);
+            return -1;
+        }
+        GBaseKeyValue kv = kvs.get(0);
+        return doDelete(collId, kv);
+    }
+
+    @Override
+    public int deleteByPathPrefix(String coll, String prefix) throws Throwable {
+        String collId = getStrOfCollId(coll);
+        List<GBaseKeyValue> kvs = qaDocRepository.findByPath(collId, prefix);
+        if (kvs == null || kvs.size() <= 0) {
+            return 0;
+        }
+        int count = qaDocRepository.deleteByPathPrefix(collId, prefix);
+        //int deleted = gkbStorer.delete(coll, prefix);
+        //System.out.println("deleted="+deleted);
+        log.info("{} 已删除", prefix);
+        return count;
+    }
+
+    public int doDelete(String coll, GBaseKeyValue kv) throws Throwable {
+        String collId = getStrOfCollId(coll);
+        long d1 = qaDocRepository.delete(collId, kv.getIntId());
+        String name = kv.getName();
+        //String objectKey = gkbStorer.getObjectKey(collId, SOURCE, name);
+        //int deleted = gkbStorer.delete(coll, name);
+        //System.out.println("deleted="+deleted);
+        log.info("{} 已删除", name);
+        int deleted = (int) d1;
+        return deleted;
+    }
+
+//    private String getMdFileContent(String coll, String fn) throws Exception {
+//        String repository = coll;
+//        String fromObject = getObjectPath("", fn);
+//        try (InputStream stream = gkbStorer.download(repository, fromObject)) {
+//            String s = IOUtils.toString(stream, "UTF-8");
+//            return s;
+//        }
+//    }
+
+    private String getKvObjectName(String coll, Map<String, Object> req) throws Throwable {
+        String collId = getStrOfCollId(coll);
+        Object o = req.get(FIELD_NAME);
+        String name = null;
+        if (o != null) {
+            name = (String) o;
+        } else {
+            o = req.get(FIELD_GID);
+            String gid = null;
+            if (o != null) {
+                gid = (String) o;
+                GBaseKeyValue kv = qaDocRepository.findByGid(collId, gid);
+                name = kv.getName();
+            } else {
+
+            }
+        }
+        return name;
+    }
+
+    @Override
+    public GBaseKeyValue findByGid(String coll, String gid) throws Throwable {
+        String collId = getStrOfCollId(coll);
+        return qaDocRepository.findByGid(collId, gid);
+    }
+
+//    @Override
+//    public Object getMetadataByKey(String coll, String mdId, String key) throws Throwable {
+//        GBaseKeyValue kv = findByMdid(coll, mdId);
+//        return kv.get(key);
+//    }
+
+    @Override
+    public Object getAttributes(String coll, String mdId) throws Throwable {
+        GBaseKeyValue kv = findByMdid(coll, mdId);
+        Object o = kv.get(FIELD_ATTRIBUTES);
+        return o;
+    }
+
+    @Override
+    public Object getAttributeByKey(String coll, String mdId, String key) throws Throwable {
+        GBaseKeyValue kv = findByMdid(coll, mdId);
+        Object o = kv.get(FIELD_ATTRIBUTES);
+        if (o == null) {
+            return null;
+        }
+        Map<String, Object> m = (Map<String, Object>) o;
+        return m.get(key);
+    }
+
+
+//    @Override
+//    public Object patchMetadata(String coll, String mdId, GBaseKeyValue data) throws Throwable {
+//        int intId = getIntId(coll, mdId);
+//        data.put("id", intId);
+//        String collId = getStrOfCollId(coll);
+//        GBaseKeyValue ret = mdDynamicRepository.update(collId, data);
+//        return ret;
+//    }
+
+    @Override
+    public Object patchAttributes(String coll, String mdId, Map<String, Object> data) throws Throwable {
+        int intId = getIntId(coll, mdId);
+        String collId = getStrOfCollId(coll);
+        GBaseKeyValue value = qaDocRepository.updateAttribute(collId, intId, data);
+        return value;
+    }
+
+    @Override
+    public Object deleteAttributeByKey(String coll, String mdId, String key) throws Throwable {
+        int intId = getIntId(coll, mdId);
+        String collId = getStrOfCollId(coll);
+        GBaseKeyValue r = qaDocRepository.removeAttribute(collId, intId, List.of(key));
+        return r;
+    }
+
+    @Override
+    public GBaseKeyValue deleteAttributeByKeys(String coll, String mdId, List<String> keys) throws Throwable {
+        int intId = getIntId(coll, mdId);
+        String collId = getStrOfCollId(coll);
+        GBaseKeyValue r = qaDocRepository.removeAttribute(collId, intId, keys);
+        return r;
+    }
+
+
+    @Override
+    public long deleteCollection(String coll) throws Throwable {
+        String collId = getStrOfCollId(coll);
+        // 1、先把 所有数据删除
+        long l = deleteAll(collId);
+
+        // 2、再把 collection 的 kv 删除,同时要将 相应的 table都删除
+        qaCollectionService.deleteCollection(collId);
+        return l;
+    }
+
+    @Override
+    public Map<String, Object> rename(String coll, String mdId, Map<String, Object> req) throws Throwable {
+        //if (body.containsKey("name")) {
+        //        String newName = body.get("name").toString();
+        //        MdDocument updated = mdService.rename(collId, mdId, newName);
+        //        return ResponseEntity.ok(updated);
+        //    }
+        //    return ResponseEntity.badRequest().body(Map.of("error", "No supported fields to update"));
+        if (req.containsKey("newName")) {
+            String newName = req.get("newName").toString();
+            String collId = getStrOfCollId(coll);
+            int intId = getIntId(coll, mdId);
+            GBaseKeyValue kv = qaDocRepository.find(collId, intId);
+            String oldName = kv.getName();
+
+            //String r = gkbStorer.renameFile(coll, oldName, newName);
+            GBaseKeyValue value = GBaseKeyValue.build();
+            int i1 = newName.lastIndexOf('/');
+            String path = newName.substring(0, i1 + 1);
+            value.put("path", path);
+            value.setName(newName);
+            value.setId(intId);
+
+            GBaseKeyValue updated = qaDocRepository.update(collId, value);
+
+            return updated;
+        } else {
+
+        }
+        return Map.of("error", "No supported fields to update");
+    }
+
+    @Override
+    public GBaseKeyValue save(String coll, GBaseKeyValue entity) throws Throwable {
+        String collId = getStrOfCollId(coll);
+        GBaseKeyValue r = qaDocRepository.save(collId, entity);
+        return r;
+    }
+
+    @Override
+    public List<Integer> saveAll(String coll, List<GBaseKeyValue> kvs) throws Throwable {
+        String collId = getStrOfCollId(coll);
+        List<Integer> rets = qaDocRepository.saveAll(collId, kvs);
+        return rets;
+    }
+
+    @Override
+    public GBaseKeyValue update(String coll, GBaseKeyValue kv) throws Throwable {
+        String collId = getStrOfCollId(coll);
+        GBaseKeyValue ret = qaDocRepository.update(collId, kv);
+        return ret;
+    }
+
+    @Override
+    public GBaseKeyValue updateAttribute(String coll, Integer docId, Map<String, Object> attributes) throws Throwable {
+        String collId = getStrOfCollId(coll);
+        GBaseKeyValue ret = qaDocRepository.updateAttribute(collId, docId, attributes);
+        return ret;
+    }
+
+    @Override
+    public List<Map<String, Object>> getAllEntities(String coll, List<String> fields) {
+        String collId = getStrOfCollId(coll);
+        List<Map<String, Object>> entities = qaDocRepository.getAllEntities(collId, fields);
+        return entities;
+    }
+
+    @Override
+    public long count(String coll) {
+        String collId = getStrOfCollId(coll);
+        return qaDocRepository.count(collId);
+    }
+
+    @Override
+    public List<String> appendArrayField(String coll, int docId, String tags, List<String> values) {
+        String collId = getStrOfCollId(coll);
+        List<String> rets = qaDocRepository.appendArrayField(collId, docId, tags, values);
+        return rets;
+    }
+
+    @Override
+    public List<String> setArrayField(String coll, int docId, String tags, List<String> values) {
+        String collId = getStrOfCollId(coll);
+        List<String> rets = qaDocRepository.setArrayField(collId, docId, tags, values);
+        return rets;
+    }
+
+    @Override
+    public List<String> removeArrayField(String coll, int docId, String tags, List<String> values) {
+        String collId = getStrOfCollId(coll);
+        List<String> rets = qaDocRepository.removeArrayField(collId, docId, tags, values);
+        return rets;
+    }
+
+    @Override
+    public String getIndexName(String coll) {
+        String collId = getStrOfCollId(coll);
+        return qaDocRepository.getMappingCollection(collId);
+    }
+
+    @Override
+    public List<GBaseKeyValue> fulltextSearch(String coll, Map<String, Object> query) throws Throwable {
+        String collId = getStrOfCollId(coll);
+        String qasName = qaDocRepository.getMappingCollection(collId);
+        List<DocSearchResp> resps = hybridSearch.fulltextSearch(qasName, query);
+
+        List<GBaseKeyValue> rets = getEntitiesBySearch(collId, resps);
+        return rets;
+    }
+
+    @Override
+    public List<GBaseKeyValue> similaritySearch(String coll, Map<String, Object> query) throws Throwable {
+        String collId = getStrOfCollId(coll);
+        String qasName = qaDocRepository.getMappingCollection(collId);
+        List<DocSearchResp> resps = hybridSearch.similaritySearch(qasName, query);
+
+        List<GBaseKeyValue> rets = getEntitiesBySearch(collId, resps);
+        return rets;
+    }
+
+    @Override
+    public List<GBaseKeyValue> hybridSearch(String coll, Map<String, Object> query) throws Throwable {
+        String collId = getStrOfCollId(coll);
+        String qasName = qaDocRepository.getMappingCollection(collId);
+        List<DocSearchResp> resps = hybridSearch.hybridSearch(qasName, query);
+
+        List<GBaseKeyValue> rets = getEntitiesBySearch(collId, resps);
+        return rets;
+    }
+
+    protected List<GBaseKeyValue> getEntitiesBySearch(String collId, List<DocSearchResp> resps) throws Throwable {
+        if (resps.isEmpty()) {
+            return List.of();
+        }
+        List<GBaseKeyValue> rets = new ArrayList<>();
+        for (DocSearchResp resp : resps) {
+            Map<String, Object> metadata = resp.getMetadata();
+            if (metadata != null) {
+                Object o = metadata.get(QaIndexer.COLL_ID);
+                if (o!= null && o instanceof String collId1) {
+                    if (collId.equals(collId1)) {
+                        o = metadata.get(QaIndexer.DOC_ID);
+                        if (o != null) {
+                            GBaseKeyValue r1 = qaDocRepository.find(collId, toDocId(o));
+                            r1.put("score",resp.getScore());
+                            rets.add(r1);
+                        }
+                    }
+//                    if (collName1.equals(indexName)) {
+//                    }
+
+
+                }
+            }
+        }
+        return rets;
+    }
+
+    private int toDocId(Object o) {
+        if (o instanceof Integer) {
+            return ((Integer)o).intValue();
+        }else {
+            return Integer.parseInt(o.toString());
+        }
+    }
+
+}

+ 290 - 0
server/src/main/java/com/giantan/data/qa/service/QaTaxonomyService.java

@@ -0,0 +1,290 @@
+package com.giantan.data.qa.service;
+
+import com.giantan.data.kvs.kvstore.GBaseKeyValue;
+import com.giantan.data.kvs.repository.GEntityConfig;
+import com.giantan.data.qa.repository.QaDocRepository;
+import com.giantan.data.qa.repository.QaTaxonomyRepository;
+import com.giantan.data.taxonomy.model.TaxonomyNode;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+
+import java.util.*;
+
+@Service
+public class QaTaxonomyService {
+    private static final org.slf4j.Logger log
+            = org.slf4j.LoggerFactory.getLogger(QaTaxonomyService.class);
+
+    @Autowired
+    QaCollectionService qaCollectionsService;
+
+    @Autowired
+    QaTaxonomyRepository qaTaxonomyRepository;
+
+    @Autowired
+    QaDocRepository qaDynamicRepository;
+
+    public QaTaxonomyService() {
+
+    }
+
+    protected String getStrOfCollId(String coll) {
+        int id = qaCollectionsService.getCollectionId(coll);
+        if (id <= 0) {
+            return null;
+        }
+        return Integer.toString(id);
+    }
+
+    private int getIntId(String coll, String nodeId) throws Exception {
+        int id = -1;
+        if (nodeId.length() > 12) {
+            //return -1;
+        } else {
+            try {
+                int i = Integer.parseInt(nodeId);
+                id = i;
+            } catch (Exception e) {
+
+            }
+        }
+//        String collId = getStrOfCollId(coll);
+//        GBaseKeyValue r = mdDynamicRepository.findByGid(collId, nodeId);
+//        if (r != null) {
+//            return r.getIntId();
+//        }
+        if (id < 0) {
+            log.error("该 Taxonomy Node ID {} 无效", nodeId);
+            throw new Exception("该 Taxonomy Node ID 无效: " + nodeId);
+        }
+        return id;
+    }
+
+
+    public List<TaxonomyNode> listAll(String coll) {
+        String collId = getStrOfCollId(coll);
+        return qaTaxonomyRepository.listAll(collId);
+    }
+
+    public List<TaxonomyNode> listTree(String coll) {
+        String collId = getStrOfCollId(coll);
+        return qaTaxonomyRepository.listTree(collId);
+    }
+
+    public TaxonomyNode createNode(String coll, Map<String, Object> data) throws Exception {
+        String collId = getStrOfCollId(coll);
+        TaxonomyNode node = qaTaxonomyRepository.createNode(collId, data);
+        return node;
+    }
+
+    public List<TaxonomyNode> findChildren(String coll, String nodeId) throws Exception {
+        String collId = getStrOfCollId(coll);
+        int intId = getIntId(coll, nodeId);
+
+        List<TaxonomyNode> ret = qaTaxonomyRepository.findChildren(collId, intId);
+        return ret;
+    }
+
+    protected String getPath(String collId, int nodeId) {
+        // nodeId不存在时 ancestors size=0
+        List<TaxonomyNode> ancestors = qaTaxonomyRepository.findAncestors(collId, nodeId, true);
+
+        Collections.reverse(ancestors);
+        StringBuilder sb = new StringBuilder();
+        for (int i = 0; i < ancestors.size(); i++) {
+            sb.append(ancestors.get(i).getName()).append("/");
+        }
+        String p = sb.toString();
+        if (p.startsWith("//")) {
+            p = p.substring(1);
+        }
+        return p;
+    }
+
+    public List<GBaseKeyValue> findMds(String coll, String nodeId) throws Exception {
+        // 获取父节点
+        String collId = getStrOfCollId(coll);
+        int intId = getIntId(coll, nodeId);
+        String path = getPath(collId, intId);
+        List<GBaseKeyValue> ret = qaDynamicRepository.findByPath(collId, path);
+        return ret;
+    }
+
+//    public Map<String, Object> processAsyncDirect(String coll, String nodeId, String taskId, MultipartFile file, Map<String, String> params) throws Exception {
+//        try {
+//            String collId = getStrOfCollId(coll);
+//            int intId = getIntId(coll, nodeId);
+//            String path = getPath(collId, intId);
+//            params.put("objectPath", path);
+//            Map<String, Object> ret = mdDocsService.processAsyncDirect(coll, taskId, file, params);
+//            return ret;
+//        } catch (Exception e) {
+//
+//        }
+//        return null;
+//
+//    }
+
+    public int deleteFolder(String coll, String nodeId, String taskId) throws Exception {
+        String collId = getStrOfCollId(coll);
+        try {
+            int intId = getIntId(coll, nodeId);
+            String path = getPath(collId, intId);
+            //int count = mdDocsService.deleteByPathPrefix(coll, path);
+
+            List<GBaseKeyValue> kvs = qaDynamicRepository.findByPath(collId, path);
+            if (kvs == null || kvs.size() <= 0) {
+                return 0;
+            }
+            int count = qaDynamicRepository.deleteByPathPrefix(collId, path);
+            qaTaxonomyRepository.deleteSubtree(collId, intId);
+            return count;
+        } catch (Throwable e) {
+            return 0;
+        }
+    }
+
+    public static String replaceLastPathSegment(String path, String newSegment) {
+        if (path == null || !path.endsWith("/")) {
+            throw new IllegalArgumentException("路径为null 或者 路径没有以 '/' 结尾");
+        }
+
+        String trimmed = path.substring(0, path.length() - 1); // 去掉末尾的 "/"
+        int lastSlashIndex = trimmed.lastIndexOf("/");
+
+        if (lastSlashIndex == -1) {
+            // 说明没有上级路径,例如 "/cc/"
+            return "/" + newSegment + "/";
+        }
+
+        String prefix = trimmed.substring(0, lastSlashIndex + 1); // 保留最后一个斜杠
+        return prefix + newSegment + "/";
+    }
+
+    public int renameFolder(String coll, String nodeId, Map<String, Object> req, String taskId) throws Exception {
+        if (req.containsKey("newName")) {
+            String newName = req.get("newName").toString();
+            String collId = getStrOfCollId(coll);
+
+            int intId = getIntId(coll, nodeId);
+            String oldPath = getPath(collId, intId);
+            String newPath = replaceLastPathSegment(oldPath, newName);
+
+            int count = qaDynamicRepository.updatePathAndNamePrefix(collId, oldPath, newPath);
+            //gkbStorer.renameFolder(coll, oldPath, newPath);
+
+            Map<String, Object> nkv = Map.of("name", newName);
+            TaxonomyNode taxonomyNode = qaTaxonomyRepository.updateFields(collId, intId, nkv);
+
+            return count;
+        }
+        return 0;
+    }
+
+
+    public int moveTo(String coll, String nodeId, Map<String, Object> req, String taskId) throws Exception {
+        if (req.containsKey("newParentId")) {
+            Object o = req.get("newParentId");
+
+            String collId = getStrOfCollId(coll);
+
+            int intId = getIntId(coll, nodeId);
+            int newId = getIntId(coll, o.toString());
+
+            if (!qaTaxonomyRepository.isValidParentId(collId, intId, newId)) {
+                throw new Exception("Node " + intId + " cannot move to it's descendant " + newId + ".");
+            }
+
+            String oldPath = getPath(collId, intId);
+            String newPrefix = getPath(collId, newId);
+            TaxonomyNode node = qaTaxonomyRepository.findById(collId, intId);
+            String newPath = newPrefix + node.getName() + "/";
+
+            int count = qaDynamicRepository.updatePathAndNamePrefix(collId, oldPath, newPath);
+            //gkbStorer.renameFolder(coll, oldPath, newPath);
+
+            TaxonomyNode taxonomyNode = qaTaxonomyRepository.updateParentId(collId, intId, newId);
+
+            return count;
+        }
+        return 0;
+    }
+
+    public List<TaxonomyNode> findSubtree(String coll, String nodeId) throws Exception {
+        String collId = getStrOfCollId(coll);
+        int intId = getIntId(coll, nodeId);
+        return qaTaxonomyRepository.findSubtree(collId, intId);
+    }
+
+
+    public List<TaxonomyNode> findDescendants(String coll, String nodeId) throws Exception {
+        String collId = getStrOfCollId(coll);
+        int intId = getIntId(coll, nodeId);
+        return qaTaxonomyRepository.findAllDescendants(collId, intId);
+    }
+
+    public List<TaxonomyNode> findAncestors(String coll, String nodeId) throws Exception {
+        String collId = getStrOfCollId(coll);
+        int intId = getIntId(coll, nodeId);
+        return qaTaxonomyRepository.findAncestors(collId, intId, true);
+    }
+
+
+    protected List<String> toPathList(String ps) {
+        String[] ss = ps.split("/");
+        List<String> ls = new ArrayList<String>();
+        for (int i = 0; i < ss.length; i++) {
+            if (ss[i] != null && ss[i].length() > 0) {
+                ls.add(ss[i]);
+            }
+        }
+        return ls;
+    }
+
+    protected TaxonomyNode buildPath(String collId, List<String> ls) throws Exception {
+        int parentId = qaTaxonomyRepository.getRoot();
+
+        TaxonomyNode node = null;
+        for (int i = 0; i < ls.size(); i++) {
+            node = qaTaxonomyRepository.findNode(collId, parentId, ls.get(i));
+            if (node == null) {
+                Map<String, Object> kv = new HashMap<String, Object>();
+                kv.put("name", ls.get(i));
+                kv.put("parentId", parentId);
+
+                node = qaTaxonomyRepository.createNode(collId, kv);
+                parentId = node.getId();
+            }
+        }
+        return node;
+    }
+
+    // 根据 目录id 建立qa
+    public GBaseKeyValue createQa(String coll, String nodeId, GBaseKeyValue entity) throws Throwable {
+        String collId = getStrOfCollId(coll);
+        int intNodeId = getIntId(coll, nodeId);
+        String path = getPath(collId, intNodeId);
+        entity.put(GEntityConfig.PATH, path);
+        GBaseKeyValue r = qaDynamicRepository.save(collId, entity);
+        return r;
+    }
+
+    // 根据 entity中的path字段建taxonomy的节点,并新增entity
+    public GBaseKeyValue createEntryByPath(String coll, GBaseKeyValue entity) throws Throwable {
+        String collId = getStrOfCollId(coll);
+        Object o = entity.get(GEntityConfig.PATH);
+        if (o != null && o instanceof String s) {
+            List<String> ps = toPathList(s);
+            TaxonomyNode node = buildPath(collId, ps);
+            String path = "/" + String.join("/", ps);
+            entity.put(GEntityConfig.PATH, path);
+            GBaseKeyValue ret = qaDynamicRepository.save(collId, entity);
+            return ret;
+        }else{
+            GBaseKeyValue ret = qaDynamicRepository.save(collId, entity);
+            return ret;
+        }
+    }
+}
+
+

+ 24 - 4
server/src/main/java/com/giantan/data/taxonomy/repository/DynamicTaxonomyRepository.java

@@ -5,10 +5,8 @@ import com.fasterxml.jackson.databind.ObjectMapper;
 import com.giantan.ai.util.id.IdGenerator;
 import com.giantan.ai.util.id.UuidGenerator;
 import com.giantan.data.taxonomy.model.TaxonomyNode;
-import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.jdbc.core.JdbcTemplate;
 import org.springframework.jdbc.core.RowMapper;
-import org.springframework.stereotype.Repository;
 
 import java.util.ArrayList;
 import java.util.HashMap;
@@ -85,7 +83,7 @@ public class DynamicTaxonomyRepository implements IDynamicTaxonomyRepository {
     @Override
     public boolean isValidParentId(String collId, int nowId, int parentId) {
         boolean ok = true;
-        if(nowId == parentId) return false;
+        if (nowId == parentId) return false;
         TaxonomyNode kv = findById(collId, parentId);
         if (kv == null) {
             ok = false;
@@ -167,6 +165,7 @@ public class DynamicTaxonomyRepository implements IDynamicTaxonomyRepository {
         return findAncestors(collId, nodeId, true);
     }
 
+
     @Override
     public List<TaxonomyNode> findAncestors(String collId, int nodeId, boolean includeSelf) {
         // 查询结果顺序
@@ -264,7 +263,6 @@ public class DynamicTaxonomyRepository implements IDynamicTaxonomyRepository {
     @Override
     public TaxonomyNode updateParentId(String collId, int id, int parentId) {
         String table = tableName(collId);
-        List<String> sets = new ArrayList<>();
         List<Object> args = new ArrayList<>();
 
         String sql = String.format("""
@@ -278,6 +276,22 @@ public class DynamicTaxonomyRepository implements IDynamicTaxonomyRepository {
         return jdbc.queryForObject(sql, args.toArray(), taxonomyMapper);
     }
 
+    @Override
+    public TaxonomyNode findNode(String collId, Integer parentId, String name) {
+        String table = tableName(collId);
+
+        String sql = String.format("""
+                    SELECT id, gid, name, parent_id, description, attributes
+                    FROM %s
+                    WHERE parent_id = ? AND name = ?
+                """, table);
+
+        //return jdbc.queryForObject(sql, new Object[]{parentId, name}, taxonomyMapper);
+        List<TaxonomyNode> results = jdbc.query(sql, taxonomyMapper, parentId, name);
+        return results.isEmpty() ? null : results.get(0);
+    }
+
+
     public List<TaxonomyNode> toTree(List<TaxonomyNode> flatList) { //,int parentId
         Map<Integer, TaxonomyNode> map = new HashMap<>();
         List<TaxonomyNode> roots = new ArrayList<>();
@@ -348,6 +362,11 @@ public class DynamicTaxonomyRepository implements IDynamicTaxonomyRepository {
         return schema + "." + tablePrefix + collId;// collId.replace("-", "")
     }
 
+    @Override
+    public int getRoot(){
+        return ROOT;
+    }
+
     public TaxonomyNode insert(String collId, TaxonomyNode t) {
         String sql = """
                     INSERT INTO %s (gid, name, parent_id, description, attributes)
@@ -360,6 +379,7 @@ public class DynamicTaxonomyRepository implements IDynamicTaxonomyRepository {
         return ret;
     }
 
+
     private String toJson(Map<String, Object> map) {
         try {
             return map != null ? mapper.writeValueAsString(map) : "{}";

+ 5 - 0
server/src/main/java/com/giantan/data/taxonomy/repository/IDynamicTaxonomyRepository.java

@@ -38,6 +38,11 @@ public interface IDynamicTaxonomyRepository {
 
     TaxonomyNode updateParentId(String collId, int id, int parentId);
 
+    // 根据parentId 和name 找到节点
+    TaxonomyNode findNode(String collId, Integer parentId, String name);
+
+    int getRoot();
+
     int createTable(String collId);
 
     int deleteTable(String collId);

+ 4 - 1
server/src/test/java/com/giantan/data/mds/GRepositoryTest.java

@@ -4,6 +4,7 @@ import com.giantan.data.kvs.constant.KvConfig;
 import com.giantan.data.kvs.kvstore.GBaseKeyValue;
 import com.giantan.data.kvs.repository.GEntityConfig;
 import com.giantan.data.kvs.repository.GRepository;
+import com.giantan.data.qa.service.QaCollectionService;
 import org.junit.jupiter.api.Test;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.boot.test.context.SpringBootTest;
@@ -94,8 +95,10 @@ public class GRepositoryTest {
     @Test
     void createTable() throws Throwable {
         //List<String> sqls = GEntityConfig.createGEntityTable(GKG_CATALOG, EntryConfig.GKG_ENTRY_COLLECTIONS);
-        List<String> sqls = GEntityConfig.createGEntityTable(KvConfig.GKG_CATALOG, KvConfig.GKG_ENTRY_COLLECTIONS);
+        //List<String> sqls = GEntityConfig.createGEntityTable(KvConfig.GKG_CATALOG, KvConfig.GKG_ENTRY_COLLECTIONS);
+        List<String> sqls = GEntityConfig.createGEntityTable(KvConfig.GKG_CATALOG, QaCollectionService.GKG_ENTRY_COLLECTIONS);
         String result = String.join("\n", sqls);
+        System.out.println(result);
         jdbcTemplate.execute(result);
     }
 

+ 1 - 1
server/src/test/java/com/giantan/data/mds/MapDoubleToInt.java

@@ -1,7 +1,7 @@
 package com.giantan.data.mds;
 
 import com.fasterxml.jackson.core.type.TypeReference;
-import com.giantan.ai.common.util.JsonUtil;
+import com.giantan.ai.util.JsonUtil;
 import com.giantan.data.tasks.TaskObjectStatus;
 import com.giantan.data.tasks.TaskOperationsStatus;
 import com.giantan.data.tasks.TaskState;

+ 24 - 2
server/src/test/java/com/giantan/data/mds/MdsApplicationTests.java

@@ -1,9 +1,14 @@
 package com.giantan.data.mds;
 
+import com.giantan.data.index.dto.DocResp;
+import com.giantan.data.index.dto.DocSearchResp;
+import com.giantan.data.kvs.kvstore.GBaseKeyValue;
 import com.giantan.data.mds.bot.GChatClient;
 import com.giantan.data.index.HybridSearch;
 import com.giantan.data.mds.service.impl.MdChunksService;
 import com.giantan.data.mds.service.impl.Vectorization;
+import com.giantan.data.qa.service.IDocsService;
+import com.giantan.data.qa.service.QaDocsService;
 import org.junit.jupiter.api.Test;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.boot.test.context.SpringBootTest;
@@ -27,6 +32,8 @@ class MdsApplicationTests {
 	@Autowired
 	HybridSearch hybridSearch;
 
+	@Autowired
+	IDocsService qaDocsService;
 
 	private void testDeepseek(){
 		String s = """
@@ -44,7 +51,7 @@ GLiNER 使用的是 DeBERTA v3 large,它对原始 DeBERTA 模型进行了更
 	void contextLoads() throws IOException, InterruptedException {
 		System.out.println("Hello World");
 
-		testDeepseek();
+		//testDeepseek();
 
 		//fetchInfo();
 //		boolean demo12 = hybridSearch.deleteAll("demo11");
@@ -91,6 +98,21 @@ GLiNER 使用的是 DeBERTA v3 large,它对原始 DeBERTA 模型进行了更
 		//"01k2p4henhmydpraw76ktnjzdg"
 //		int deleted = hybridSearch.delete("demo11", List.of("01k2p4c41mbprwk822r2b86ntd"));
 //		System.out.println(deleted);
-	}
+
+		//int r1 = hybridSearch.deleteDocumentsByFilter("demo11", "01k2zv5r5f75cz9k%");
+		//System.out.println(r1);
+//		List<DocResp> rs = hybridSearch.getDocumentsByIdFilter("demo11", "01k2zv5r5f75cz9k%");
+//		System.out.println(rs);
+
+//		List<DocSearchResp> rets = hybridSearch.hybridSearch("qas_2", Map.of("query", "乙烯价格走势"));
+//		System.out.println(rets);
+
+        try {
+            List<GBaseKeyValue> kvs = qaDocsService.hybridSearch("tools1", Map.of("query", "乙烯价格走势"));
+			System.out.println(kvs);
+        } catch (Throwable e) {
+            throw new RuntimeException(e);
+        }
+    }
 
 }

+ 42 - 0
server/src/test/java/com/giantan/data/mds/SpelChunkGenerator.java

@@ -0,0 +1,42 @@
+package com.giantan.data.mds;
+
+import org.springframework.expression.Expression;
+import org.springframework.expression.ExpressionParser;
+import org.springframework.expression.spel.standard.SpelExpressionParser;
+import org.springframework.expression.spel.support.StandardEvaluationContext;
+
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+//SpEL 表达式里不仅能直接用变量名,还能访问 Map 里的值。
+//Spring Expression Language (SpEL) 天生支持 Map 访问,有两种常见方式:
+//点语法:如果 key 是合法的 Java 标识符,可以直接写 #map.key。
+//下标语法:如果 key 不规则(比如有空格、符号),就用 #map['key name']。
+
+public class SpelChunkGenerator {
+
+    public static void main(String[] args) {
+        // 模拟变量
+        Map<String, Object> variables = new HashMap<>();
+        variables.put("标题", "中医基础");
+        variables.put("author", List.of("zhangsan", "lisi"));
+        variables.put("content", "气虚主要表现为乏力、懒言、面色苍白");
+
+        // 定义组合规则(SpEL 表达式)
+        String expressionString = "'标题: \n' + #标题 + ' 作者: ' + #author + ' 内容: ' + #content";
+
+        // 解析器
+        ExpressionParser parser = new SpelExpressionParser();
+        Expression expression = parser.parseExpression(expressionString);
+
+        // 上下文
+        StandardEvaluationContext context = new StandardEvaluationContext();
+        context.setVariables(variables);
+
+        // 计算结果
+        String chunk = expression.getValue(context, String.class);
+
+        System.out.println("生成的 chunk: " + chunk);
+    }
+}