Răsfoiți Sursa

增加了md的多collection的搜索

dwp 3 luni în urmă
părinte
comite
218446b399
21 a modificat fișierele cu 1234 adăugiri și 20 ștergeri
  1. 43 0
      qacontroller修改1.txt
  2. 1 1
      server/pom.xml
  3. 1 1
      server/src/main/java/com/giantan/data/common/model/CollsSearchRequest.java
  4. 1 1
      server/src/main/java/com/giantan/data/common/model/SearchType.java
  5. 2 1
      server/src/main/java/com/giantan/data/common/service/GoeSimService.java
  6. 2 0
      server/src/main/java/com/giantan/data/index/HybridIndexer.java
  7. 1 1
      server/src/main/java/com/giantan/data/mds/MdsApplication.java
  8. 47 0
      server/src/main/java/com/giantan/data/mds/controller/MdCollsSearchController.java
  9. 4 0
      server/src/main/java/com/giantan/data/mds/repository/MdDynamicRepository.java
  10. 18 0
      server/src/main/java/com/giantan/data/mds/repository/MdIndexer.java
  11. 380 0
      server/src/main/java/com/giantan/data/mds/service/impl/MdCollsSearchService.java
  12. 5 6
      server/src/main/java/com/giantan/data/qa/controller/QaCollsSearchController.java
  13. 76 0
      server/src/main/java/com/giantan/data/qa/controller/v2/V2CollectionsController.java
  14. 26 0
      server/src/main/java/com/giantan/data/qa/controller/v2/V2CollsSearchController.java
  15. 258 0
      server/src/main/java/com/giantan/data/qa/controller/v2/V2DocsController.java
  16. 84 0
      server/src/main/java/com/giantan/data/qa/controller/v2/V2ProxySearchController.java
  17. 113 0
      server/src/main/java/com/giantan/data/qa/controller/v2/V2TaskController.java
  18. 130 0
      server/src/main/java/com/giantan/data/qa/controller/v2/V2TaxonomyController.java
  19. 27 8
      server/src/main/java/com/giantan/data/qa/service/QaCollsSearchService.java
  20. 14 1
      server/src/main/java/com/giantan/data/qa/service/QaDocsService.java
  21. 1 0
      server/src/main/java/com/giantan/data/qa/service/TextSimilarity.java

+ 43 - 0
qacontroller修改1.txt

@@ -0,0 +1,43 @@
+ Collections
+                                                                                                                                                                                                   
+  - POST /collections/search → POST /collections/_search 或 POST /collections?query=...(如果必须复杂 body,保留 POST;建议统一为 /_search 资源)                                                  
+  - DELETE /collections/{name}/all → DELETE /collections/{name}/docs(清空集合中的文档)                                                                                                           
+  - DELETE /collections/{name}/attributes (body keys) → PATCH /collections/{name} 或 PATCH /collections/{name}/attributes(用 patch 表达字段删除)                                                 
+  - PUT /collections/{name}/attributes → PATCH /collections/{name} 或 PUT /collections/{name}/attributes(若整体替换,用 PUT;局部用 PATCH)                                                       
+                                                                                                                                                                                                   
+  Docs(QaDocsController)                                                                                                                                                                         
+                                                                                                                                                                                                   
+  - POST /collections/{collId}/docs/batch → POST /collections/{collId}/docs(body 为数组;或 /docs/batch 仅作兼容)                                                                                
+  - GET /collections/{collId}/docs/all → GET /collections/{collId}/docs                                                                                                                            
+  - DELETE /collections/{collId}/docs/all → DELETE /collections/{collId}/docs                                                                                                                      
+  - DELETE /collections/{collId}/docs/by-name?name=... → DELETE /collections/{collId}/docs?name=...                                                                                                
+  - GET /collections/{collId}/docs/by-path?path=... → GET /collections/{collId}/docs?path=...                                                                                                      
+  - GET /collections/{collId}/docs/by-prefix?prefix=... → GET /collections/{collId}/docs?prefix=...                                                                                                
+  - DELETE /collections/{collId}/docs/by-path?path=... → DELETE /collections/{collId}/docs?path=...                                                                                                
+  - POST /collections/{collId}/docs/fields → POST /collections/{collId}/docs/_fields(派生集合)                                                                                                   
+  - POST /collections/{collId}/docs/{docId}/rename → PATCH /collections/{collId}/docs/{docId}(body: { "name": "..." })                                                                           
+  - POST /collections/{collId}/docs/{docId}/tags/add|set|remove →                                                                                                                                  
+      - PATCH /collections/{collId}/docs/{docId}(统一字段更新)                                                                                                                                   
+      - 或 POST/PUT/DELETE /collections/{collId}/docs/{docId}/tags                                                                                                                                 
+  - POST /collections/{collId}/docs/{docId}/altlabels/add|set|remove →                                                                                                                             
+      - 同 tags 处理                                                                                                                                                                               
+  - POST /collections/{collId}/docs/fulltextSearch → POST /collections/{collId}/docs/_search(body: { "type": "fulltext", ... })                                                                  
+  - POST /collections/{collId}/docs/similaritySearch → 同上(type: "similarity")                                                                                                                  
+  - POST /collections/{collId}/docs/hybridSearch → 同上(type: "hybrid")                                                                                                                          
+  - DELETE /collections/{collId}/docs/indexes → DELETE /collections/{collId}/indexes(将 indexes 作为子资源)                                                                                      
+                                                                                                                                                                                                   
+  Tasks(QaTaskController)                                                                                                                                                                        
+                                                                                                                                                                                                   
+  - POST /collections/{collId}/tasks/submit → POST /collections/{collId}/tasks(创建任务)                                                                                                         
+  - POST /collections/{collId}/tasks/{id}/cancel → PATCH /collections/{collId}/tasks/{id}(body: { "status": "canceled" })                                                                        
+  - DELETE /collections/{collId}/tasks/cleanup → DELETE /collections/{collId}/tasks?expired=true(或 ?cleanup=true)                                                                               
+  - DELETE /collections/{collId}/tasks/history/cleanup → DELETE /collections/{collId}/tasks/history?expired=true                                                                                   
+  - GET /collections/{collId}/tasks/status/{status} → GET /collections/{collId}/tasks?status=...                                                                                                   
+                                                                                                                                                                                                   
+  Taxonomy(QaTaxonomyController)                                                                                                                                                                 
+                                                                                                                                                                                                   
+  - POST /collections/{collName}/taxonomy/by-path → POST /collections/{collName}/taxonomy(body 包含 path)
+  - GET /collections/{collName}/taxonomy/by-path?path=... → GET /collections/{collName}/taxonomy?path=...
+  - PUT /collections/{collName}/taxonomy/by-path → PATCH /collections/{collName}/taxonomy(或 PUT 用于整体替换)
+  - DELETE /collections/{collName}/taxonomy/by-path?path=... → DELETE /collections/{collName}/taxonomy?path=...
+  - POST /collections/{collName}/docs/by-path → POST /collections/{collName}/docs(body 包含 path)

+ 1 - 1
server/pom.xml

@@ -9,7 +9,7 @@
         <version>1.0.0</version>
     </parent>
 
-    <version>3.1.2</version>
+    <version>3.1.3</version>
     <artifactId>mdserver</artifactId>
 
     <properties>

+ 1 - 1
server/src/main/java/com/giantan/data/qa/model/CollsSearchRequest.java → server/src/main/java/com/giantan/data/common/model/CollsSearchRequest.java

@@ -1,4 +1,4 @@
-package com.giantan.data.qa.model;
+package com.giantan.data.common.model;
 
 import lombok.Data;
 

+ 1 - 1
server/src/main/java/com/giantan/data/qa/model/SearchType.java → server/src/main/java/com/giantan/data/common/model/SearchType.java

@@ -1,4 +1,4 @@
-package com.giantan.data.qa.model;
+package com.giantan.data.common.model;
 
 import com.fasterxml.jackson.annotation.JsonCreator;
 

+ 2 - 1
server/src/main/java/com/giantan/data/qa/service/GoeSimService.java → server/src/main/java/com/giantan/data/common/service/GoeSimService.java

@@ -1,8 +1,9 @@
-package com.giantan.data.qa.service;
+package com.giantan.data.common.service;
 
 import cnnlp.semnet.core.OeConfigSet;
 import cnnlp.semnet.core.OeConfigSetFactory;
 import cnnlp.util.FileIOAdapter;
+import com.giantan.data.qa.service.TextSimilarity;
 import jakarta.annotation.PostConstruct;
 //import org.cnnlp.service.app.TextSimilarity;
 import org.cnnlp.service.v2.OntologyBasedService2;

+ 2 - 0
server/src/main/java/com/giantan/data/index/HybridIndexer.java

@@ -8,6 +8,8 @@ public class HybridIndexer {
     public static final String COLL_ID = "__cid";
     public static final String DOC_ID = "__did";
 
+    public static final String COLL_NAME = "collectionName";
+
     public static final String TABLE_ID = "tableId";
 
     protected String defaultIndexPrefix = "qas";

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

@@ -16,7 +16,7 @@ public class MdsApplication {
 
     public static void main(String[] args) {
         SpringApplication.run(MdsApplication.class, args);
-        log.info("Mds server started. Version 3.1.");
+        log.info("Mds server started. Version 3.1.3");
     }
 
 }

+ 47 - 0
server/src/main/java/com/giantan/data/mds/controller/MdCollsSearchController.java

@@ -0,0 +1,47 @@
+package com.giantan.data.mds.controller;
+
+import com.giantan.ai.common.reponse.R;
+import com.giantan.data.index.dto.DocSearchResp;
+import com.giantan.data.mds.constant.MdConstants;
+import com.giantan.data.common.model.CollsSearchRequest;
+import com.giantan.data.mds.service.impl.MdCollsSearchService;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.http.ResponseEntity;
+import org.springframework.web.bind.annotation.PostMapping;
+import org.springframework.web.bind.annotation.RequestBody;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RestController;
+
+import java.util.List;
+
+/*
+public class CollsSearchRequest {
+    String query;
+    List<String> collections;
+    Integer topK = 3;
+    String searchType = "HYBRID"; //"FULLTEXT","SIMILARITY"
+    List<String> tags;
+    String tagsMatch = "any"; // = "any" | "all";
+    String path;
+    String fromTime;
+    String toTime;
+
+    Double similarityThreshold;
+    String filterExpression;
+}
+ */
+
+@RestController
+@RequestMapping(MdConstants.API_PREFIX + "/collections/_search")
+public class MdCollsSearchController {
+
+    @Autowired
+    MdCollsSearchService mdSearchService;
+
+    @PostMapping()
+    public ResponseEntity<R> federatedSearch(@RequestBody CollsSearchRequest query) throws Throwable {
+        List<DocSearchResp> rets = mdSearchService.federatedSearch(query);
+        return ResponseEntity.ok(R.data(rets));
+    }
+
+}

+ 4 - 0
server/src/main/java/com/giantan/data/mds/repository/MdDynamicRepository.java

@@ -31,6 +31,10 @@ public class MdDynamicRepository extends GDynamicRepository {
         setJdbcTemplate(this.jdbc);
     }
 
+    public String getMappingCollection(String collId) {
+        return indexer.getMappedIndexName(collId);
+    }
+
     public String indexName(String coll) {
         //return schema + "_" + indexPrefix + "_" + collId;
         //return this.indexPrefix + "_" + coll;

+ 18 - 0
server/src/main/java/com/giantan/data/mds/repository/MdIndexer.java

@@ -4,6 +4,7 @@ import com.giantan.data.index.HybridIndexer;
 import com.giantan.data.index.IHybridSearch;
 import com.giantan.data.index.dto.DocReq;
 import com.giantan.data.index.dto.DocResp;
+import com.giantan.data.index.dto.DocSearchResp;
 import com.giantan.data.mds.service.impl.MdCollectionsService;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.stereotype.Repository;
@@ -45,6 +46,19 @@ public class MdIndexer extends HybridIndexer {
 //        return prefix + coll;
 //    }
 
+
+    public String getMappedIndexName(String collId) {
+//        String collName = collectionService.getCollectionName(Integer.parseInt(collId));
+//        String mapped = indexName(collName);
+//        Map<String, Object> attibutes = collectionService.getAttibutes(collId);
+//        String configIndexName = getConfigIndexName(collId, attibutes);
+//        if (configIndexName != null) {
+//            mapped = configIndexName;
+//        }
+        return getMappedIndexNameById(collId);
+    }
+
+
     public String getMappedIndexNameById(String collId) {
         String collName = collectionService.getCollectionName(Integer.parseInt(collId));
         String mapped = indexName(collName);
@@ -143,4 +157,8 @@ public class MdIndexer extends HybridIndexer {
         List<DocResp> ret = hybridSearch.addDirect(indexName, docs);
         return ret;
     }
+
+    public Map<String, List<DocSearchResp>> federatedSearch(Map<String, Object> req) throws IOException, InterruptedException {
+        return hybridSearch.federatedSearch(req);
+    }
 }

+ 380 - 0
server/src/main/java/com/giantan/data/mds/service/impl/MdCollsSearchService.java

@@ -0,0 +1,380 @@
+package com.giantan.data.mds.service.impl;
+
+import cnnlp.util.MultiValueHashMap;
+import com.giantan.data.common.model.CollsSearchRequest;
+import com.giantan.data.index.dto.DocSearchResp;
+import com.giantan.data.mds.repository.MdDynamicRepository;
+import com.giantan.data.mds.repository.MdIndexer;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+
+import java.util.*;
+import java.util.stream.Collectors;
+
+@Service
+public class MdCollsSearchService {
+    private static final org.slf4j.Logger log
+            = org.slf4j.LoggerFactory.getLogger(MdCollsSearchService.class);
+
+    @Autowired
+    MdCollectionsService qaCollectionService;
+
+    @Autowired
+    MdDynamicRepository qaDocRepository;
+
+    @Autowired
+    MdIndexer hybridSearch;
+
+//    @Autowired
+//    GoeSimService goeSimService;
+
+//    @Autowired
+//    QaDocsService qaDocsService;
+
+
+    public List<DocSearchResp> federatedSearch(CollsSearchRequest req) throws Throwable {
+        List<String> colls = req.getCollections();
+        if (colls == null || colls.isEmpty()) {
+            throw new RuntimeException("No collections found");
+        }
+        List<DocSearchResp> rets = new ArrayList<>();
+
+        MultiValueHashMap mapping = new MultiValueHashMap();
+
+        List<String> collNames = new ArrayList<>();
+        for (String coll : colls) {
+            String collId = getStrOfCollId(coll);
+            String qasCollName = qaDocRepository.getMappingCollection(collId);
+            collNames.add(qasCollName);
+            mapping.put(qasCollName, collId);
+        }
+        req.setCollections(collNames);
+
+        Map<String, List<DocSearchResp>> rs = hybridSearch.federatedSearch(req.toMap());
+        if (rs != null && !rs.isEmpty()) {
+            String q = req.getQuery();
+            rs.forEach((k, v) -> {
+                String[] collIds = mapping.get(k);
+                try {
+                    List<DocSearchResp> rs1 = getEntitiesBySearch2(collIds, q, v);
+                    if (rs1 != null && !rs1.isEmpty()) {
+                        rets.addAll(rs1);
+                    }
+                } catch (Throwable e) {
+                    throw new RuntimeException(e);
+                }
+            });
+        }
+
+        if (rets != null && !rets.isEmpty()) {
+            // 按 score 降序
+            rets.sort((a, b) -> {
+                double sa = toScore(a.getScore());
+                double sb = toScore(b.getScore());
+                return Double.compare(sb, sa);
+            });
+        }
+        return rets;
+    }
+
+    private String toCollStr(Object o){
+        if (o instanceof String) {
+            return (String) o;
+        }else if (o instanceof Double) {
+            Double d = (Double) o;
+            String s = String.valueOf(d.longValue());
+            return s;
+        }else if (o instanceof Integer) {
+            return String.valueOf(o);
+        }
+        return String.valueOf(o);
+    }
+
+    protected List<DocSearchResp> getEntitiesBySearch2(String[] collIds, String q, List<DocSearchResp> resps) throws Throwable {
+        if (resps.isEmpty()) {
+            return List.of();
+        }
+
+        Set<String> collSet = new HashSet<>(Arrays.asList(collIds));
+        List<DocSearchResp> rets = new ArrayList<>();
+        for (DocSearchResp resp : resps) {
+            Map<String, Object> metadata = resp.getMetadata();
+
+            if (metadata != null) {
+                Object o = metadata.get(MdIndexer.COLL_ID);
+                if (o != null) {
+                    String collId1 = toCollStr(o);
+                    if (collSet.contains(collId1)) {
+                        String collectionName = qaCollectionService.getCollectionName(toDocId(collId1));
+                        metadata.put(MdIndexer.COLL_NAME, collectionName);
+                        rets.add(resp);
+                    }
+                }
+            }
+        }
+        return rets;
+    }
+
+//    public List<GBaseKeyValue> hybridSearch(CollsSearchRequest query) throws Throwable {
+//        List<String> colls = query.getCollections();
+//        if (colls == null || colls.isEmpty()) {
+//            //throw new RuntimeException("No collections found");
+//            colls = new ArrayList<>();
+//            List<GBaseKeyValue> allCollections = qaCollectionService.getAllCollections();
+//            for (GBaseKeyValue coll : allCollections) {
+//                colls.add(coll.getName());
+//            }
+//        }
+//
+//        List<GBaseKeyValue> rs = new ArrayList<>();
+//
+//        for (String coll : colls) {
+//            Map<String, Object> queryMap = query.toMap();
+//            List<GBaseKeyValue> rs1 = hybridSearch(coll, queryMap);
+//            if (rs1 != null) {
+//                rs.addAll(rs1);
+//            }
+//        }
+//        if (rs != null && !rs.isEmpty()) {
+//            // 按 score 降序
+//            rs.sort((a, b) -> {
+//                double sa = toScore(a.get("score"));
+//                double sb = toScore(b.get("score"));
+//                return Double.compare(sb, sa);
+//            });
+//        }
+//        return rs;
+//    }
+
+    private double toScore(Object v) {
+        if (v == null) return Double.NEGATIVE_INFINITY;
+        if (v instanceof Number) return ((Number) v).doubleValue();
+        return Double.parseDouble(v.toString());
+    }
+
+//    public List<GBaseKeyValue> hybridSearch(String coll, Map<String, Object> query) throws Throwable {
+//        String collId = getStrOfCollId(coll);
+//
+//        String qasCollName = qaDocRepository.getMappingCollection(collId);
+//
+//        // 这里要判断 collection 是不是 index 到一个同一个milvus的collection,如果 是的话,就要加上 qasName 做过滤
+//        boolean isIcludeCollName = true;
+//        query.put(HybridIndexer.TABLE_ID, collId);
+//        List<DocSearchResp> resps = hybridSearch.hybridSearch(qasCollName, query, isIcludeCollName);
+//
+//        String q = query.get("query").toString();
+//        List<GBaseKeyValue> rets = getEntitiesBySearch(collId, q, resps);
+//        return rets;
+//    }
+
+    protected String getStrOfCollId(String coll) {
+        int id = qaCollectionService.getCollectionId(coll);
+        if (id <= 0) {
+            return null;
+        }
+        return Integer.toString(id);
+    }
+
+    private int toDocId(Object o) {
+        if (o instanceof Integer) {
+            return ((Integer) o).intValue();
+        } else {
+            return Integer.parseInt(o.toString());
+        }
+    }
+
+
+//    private List<GBaseKeyValue> getCollInfos(List<String> colls) throws Throwable {
+//        List<GBaseKeyValue> allCollections = new ArrayList<GBaseKeyValue>();
+//        if (colls.size() == 1 && (colls.get(0).equals("*") || colls.get(0).equalsIgnoreCase("all"))) {
+//            allCollections = qaCollectionService.getAllCollections();
+//        } else {
+//            for (int i = 0; i < colls.size(); i++) {
+//                GBaseKeyValue collObj = qaCollectionService.getKvByName(colls.get(i));
+//                allCollections.add(collObj);
+//            }
+//        }
+//        return allCollections;
+//    }
+
+
+    private void addIntList(List<Integer> is, Object v) {
+        if (v == null) {
+            return;
+        }
+        if (v instanceof Integer) {
+            int id1 = (Integer) v;
+            if (!is.contains(id1)) {
+                is.add(id1);
+            }
+        } else if (v instanceof String) {
+            int id1 = Integer.parseInt((String) v);
+            if (!is.contains(id1)) {
+                is.add(id1);
+            }
+        }
+    }
+
+//    protected List<GBaseKeyValue> getEntitiesBySearch(String collId, String q, List<DocSearchResp> resps) throws Throwable {
+//        if (resps.isEmpty()) {
+//            return List.of();
+//        }
+//        List<Integer> ids = new ArrayList<>();
+//        List<GBaseKeyValue> rets = new ArrayList<>();
+//        Map<Integer, Double> scoreMap = new HashMap<>();
+//
+//        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);
+//                        addIntList(ids, o);
+//                        scoreMap.putIfAbsent(ids.get(ids.size() - 1), resp.getScore());
+//                    }
+//                }
+//            }
+//        }
+//        if (!ids.isEmpty()) {
+//            List<GBaseKeyValue> rs2 = qaDocRepository.findAllByIds(collId, ids);
+//            if (rs2 != null) {
+//                for (GBaseKeyValue ro : rs2) {
+//                    // 用 oe 计算相似度
+//                    GoeSimService.Pair<String, Double> r1 = getOeSimilarity(q, ro);
+//
+//                    double score1 = scoreMap.get(ro.getIntId());
+//
+//                    if (score1 < r1.right()) {
+//                        score1 = r1.right();
+//                    }
+//                    ro.put("score", score1);
+//                    ro.put("matched", r1.left());
+//                    rets.add(ro);
+//                }
+//            }
+//        }
+//
+//        return rets;
+//    }
+
+//    private GoeSimService.Pair<String, Double> getOeSimilarity(String q, GBaseKeyValue qa) {
+//        List<String> ls = new ArrayList<>();
+//        Object o = qa.get("name");
+//        if (o != null) {
+//            ls.add(o.toString());
+//        }
+//        Object o1 = qa.get("altlabels");
+//        if (o1 != null && o1 instanceof List) {
+//            List<String> ls1 = (List<String>) o1;
+//            ls.addAll(ls1);
+//        }
+//        GoeSimService.Pair r = goeSimService.search(q, ls);
+//        return r;
+//    }
+
+//    public List<GBaseKeyValue> federatedSearch(CollsSearchRequest req) throws Throwable {
+//        List<String> colls = req.getCollections();
+//        if (colls == null || colls.isEmpty()) {
+//            throw new RuntimeException("No collections found");
+//        }
+//        List<GBaseKeyValue> rets = new ArrayList<>();
+//
+//        MultiValueHashMap mapping = new MultiValueHashMap();
+//
+//        List<String> collNames = new ArrayList<>();
+//        for (String coll : colls) {
+//            String collId = getStrOfCollId(coll);
+//            String qasCollName = qaDocRepository.getMappingCollection(collId);
+//            collNames.add(qasCollName);
+//            mapping.put(qasCollName, collId);
+//        }
+//        req.setCollections(collNames);
+//
+//        Map<String, List<DocSearchResp>> rs = hybridSearch.federatedSearch(req.toMap());
+//        if (rs != null && !rs.isEmpty()) {
+//            String q = req.getQuery();
+//            rs.forEach((k, v) -> {
+//                String[] collIds = mapping.get(k);
+//                try {
+//                    List<GBaseKeyValue> rs1 = getEntitiesBySearch2(collIds, q, v);
+//                    if (rs1 != null && !rs1.isEmpty()) {
+//                        rets.addAll(rs1);
+//                    }
+//                } catch (Throwable e) {
+//                    throw new RuntimeException(e);
+//                }
+//            });
+//        }
+//
+//        if (rets != null && !rets.isEmpty()) {
+//            // 按 score 降序
+//            rets.sort((a, b) -> {
+//                double sa = toScore(a.get("score"));
+//                double sb = toScore(b.get("score"));
+//                return Double.compare(sb, sa);
+//            });
+//        }
+//        return rets;
+//    }
+
+
+//    protected List<GBaseKeyValue> getEntitiesBySearch2(String[] collIds, String q, List<DocSearchResp> resps) throws Throwable {
+//        if (resps.isEmpty()) {
+//            return List.of();
+//        }
+//
+//        List<GBaseKeyValue> rets = new ArrayList<>();
+//
+//        MultiValueHashMap mapping = new MultiValueHashMap();
+//        Set<String> collSet = new HashSet<>(Arrays.asList(collIds));
+//
+//        Map<String, Double> scoreMap = new HashMap<>();
+//
+//        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 (collSet.contains(collId1)) {
+//                        o = metadata.get(QaIndexer.DOC_ID);
+//                        //addIntList(ids, o);
+//                        //scoreMap.putIfAbsent(ids.get(ids.size() - 1), resp.getScore());
+//                        String did1 = o.toString();
+//                        mapping.put(collId1, o.toString());
+//                        scoreMap.putIfAbsent(collId1 + ":" + did1, resp.getScore());
+//                    }
+//                }
+//            }
+//        }
+//        Set<Map.Entry<String, String[]>> ens = mapping.entrySet();
+//        for (Map.Entry<String, String[]> e : ens) {
+//            String[] value = e.getValue();
+//            List<Integer> idList = Arrays.stream(value)
+//                    .map(Integer::valueOf)
+//                    .collect(Collectors.toList());
+//
+//            String collId = e.getKey();
+//            List<GBaseKeyValue> rs2 = qaDocRepository.findAllByIds(collId, idList);
+//            if (rs2 != null) {
+//                for (GBaseKeyValue ro : rs2) {
+//                    // 用 oe 计算相似度
+//                    GoeSimService.Pair<String, Double> r1 = getOeSimilarity(q, ro);
+//                    double score1 = scoreMap.get(collId + ":" + ro.getIntId());
+//
+//                    if (score1 < r1.right()) {
+//                        score1 = r1.right();
+//                    }
+//                    ro.put("score", score1);
+//                    ro.put("matched", r1.left());
+//                    rets.add(ro);
+//                }
+//            }
+//
+//        }
+//        return rets;
+//    }
+}

+ 5 - 6
server/src/main/java/com/giantan/data/qa/controller/CollsSearchController.java → server/src/main/java/com/giantan/data/qa/controller/QaCollsSearchController.java

@@ -3,8 +3,8 @@ 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.model.CollsSearchRequest;
-import com.giantan.data.qa.service.CollsSearchService;
+import com.giantan.data.common.model.CollsSearchRequest;
+import com.giantan.data.qa.service.QaCollsSearchService;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.http.ResponseEntity;
 import org.springframework.web.bind.annotation.*;
@@ -13,15 +13,14 @@ import java.util.List;
 
 @RestController
 @RequestMapping(QaConstants.API_PREFIX + "/collections/_search")
-public class CollsSearchController {
+public class QaCollsSearchController {
 
     @Autowired
-    CollsSearchService searchService;
+    QaCollsSearchService qaSearchService;
 
     @PostMapping()
     public ResponseEntity<R> federatedSearch(@RequestBody CollsSearchRequest query) throws Throwable {
-        //List<GBaseKeyValue> rets = searchService.hybridSearch(query);
-        List<GBaseKeyValue> rets = searchService.federatedSearch(query);
+        List<GBaseKeyValue> rets = qaSearchService.federatedSearch(query);
         return ResponseEntity.ok(R.data(rets));
     }
 

+ 76 - 0
server/src/main/java/com/giantan/data/qa/controller/v2/V2CollectionsController.java

@@ -0,0 +1,76 @@
+package com.giantan.data.qa.controller.v2;
+
+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 org.springframework.web.servlet.support.ServletUriComponentsBuilder;
+
+import java.net.URI;
+import java.util.List;
+import java.util.Map;
+
+@RestController
+@RequestMapping(QaConstants.API_PREFIX_V2 + "/collections")
+@CrossOrigin(origins = "*", maxAge = 3600)
+public class V2CollectionsController {
+
+    @Autowired
+    QaCollectionService qaCollectionService;
+
+    @PostMapping
+    public ResponseEntity<R> createCollection(@RequestParam String name) throws Throwable {
+        Object created = qaCollectionService.createCollection(name);
+        URI location = ServletUriComponentsBuilder.fromCurrentRequest()
+                .path("/{name}")
+                .buildAndExpand(name)
+                .toUri();
+        return ResponseEntity.created(location).body(R.data(created));
+    }
+
+    @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 = qaCollectionService.deleteCollection(name);
+        return ResponseEntity.ok(R.data(ret));
+    }
+
+    @DeleteMapping("/{name}/docs")
+    public ResponseEntity<R> clearCollection(@PathVariable String name) throws Throwable {
+        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)));
+    }
+
+    @PatchMapping("/{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,
+                                             @RequestParam List<String> keys) throws Throwable {
+        return ResponseEntity.ok(R.data(qaCollectionService.removeCollectionAttribute(name, keys)));
+    }
+}

+ 26 - 0
server/src/main/java/com/giantan/data/qa/controller/v2/V2CollsSearchController.java

@@ -0,0 +1,26 @@
+package com.giantan.data.qa.controller.v2;
+
+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.common.model.CollsSearchRequest;
+import com.giantan.data.qa.service.QaCollsSearchService;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.http.ResponseEntity;
+import org.springframework.web.bind.annotation.*;
+
+import java.util.List;
+
+@RestController
+@RequestMapping(QaConstants.API_PREFIX_V2 + "/collections/_search")
+public class V2CollsSearchController {
+
+    @Autowired
+    QaCollsSearchService searchService;
+
+    @PostMapping
+    public ResponseEntity<R> hybridSearch(@RequestBody CollsSearchRequest query) throws Throwable {
+        List<GBaseKeyValue> rets = searchService.hybridSearch(query);
+        return ResponseEntity.ok(R.data(rets));
+    }
+}

+ 258 - 0
server/src/main/java/com/giantan/data/qa/controller/v2/V2DocsController.java

@@ -0,0 +1,258 @@
+package com.giantan.data.qa.controller.v2;
+
+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 org.springframework.web.servlet.support.ServletUriComponentsBuilder;
+
+import java.io.IOException;
+import java.net.URI;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+@RestController
+@RequestMapping(QaConstants.API_PREFIX_V2 + "/collections/{collId}/docs")
+public class V2DocsController {
+
+    @Autowired
+    QaDocsService qaDocsService;
+
+    @PostMapping
+    public ResponseEntity<R> createEntry(@PathVariable String collId, @RequestBody Map<String, Object> data)
+            throws Throwable {
+        GBaseKeyValue created = qaDocsService.save(collId, GBaseKeyValue.build(data));
+        URI location = buildDocLocation(created);
+        if (location != null) {
+            return ResponseEntity.created(location).body(R.data(created));
+        }
+        return ResponseEntity.status(201).body(R.data(created));
+    }
+
+    @PostMapping("/_batch")
+    public ResponseEntity<R> createBatch(@PathVariable String collId, @RequestBody List<GBaseKeyValue> kvs)
+            throws Throwable {
+        List<Integer> ret = qaDocsService.saveAll(collId, kvs);
+        return ResponseEntity.status(201).body(R.data(ret));
+    }
+
+    @GetMapping("/{id}")
+    public ResponseEntity<R<?>> getById(@PathVariable String collId, @PathVariable String id) throws Throwable {
+        GBaseKeyValue ret = qaDocsService.findByIdOrGid(collId, id);
+        return ResponseEntity.ok(R.data(ret));
+    }
+
+    @DeleteMapping("/{gid}")
+    public ResponseEntity<R<?>> delete(@PathVariable String collId, @PathVariable String gid) throws Throwable {
+        int deleted = qaDocsService.deleteByMdid(collId, gid);
+        return ResponseEntity.ok(R.data(Map.of("deleted", deleted)));
+    }
+
+    @GetMapping
+    public ResponseEntity<R> getDocs(
+            @PathVariable String collId,
+            @RequestParam(required = false) String path,
+            @RequestParam(required = false) String prefix,
+            @RequestParam(required = false) String parent) throws Throwable {
+        List<GBaseKeyValue> rets;
+        if (path != null) {
+            rets = qaDocsService.findByPath(collId, path);
+        } else if (prefix != null) {
+            rets = qaDocsService.findByPrefix(collId, prefix);
+        } else if (parent != null) {
+            rets = qaDocsService.getChildren(collId, parent);
+        } else {
+            rets = qaDocsService.findAll(collId);
+        }
+        return ResponseEntity.ok(R.data(rets));
+    }
+
+    @DeleteMapping
+    public ResponseEntity<R> deleteDocs(
+            @PathVariable String collId,
+            @RequestParam(required = false) String path,
+            @RequestParam(required = false) String prefix,
+            @RequestParam(required = false) String subtree,
+            @RequestParam(required = false) String name) throws Throwable {
+        int ret;
+        if (path != null) {
+            ret = qaDocsService.deleteByPath(collId, path);
+        } else if (prefix != null) {
+            ret = qaDocsService.deleteByPrefix(collId, prefix);
+        } else if (subtree != null) {
+            ret = qaDocsService.deletePathAndDescendants(collId, subtree);
+        } else if (name != null) {
+            ret = qaDocsService.deleteByName(collId, name);
+        } else {
+            long removed = qaDocsService.deleteAll(collId);
+            return ResponseEntity.ok(R.data(Map.of("deleted", removed)));
+        }
+        return ResponseEntity.ok(R.data(Map.of("deleted", ret)));
+    }
+
+    @PatchMapping("/{docId}")
+    public ResponseEntity<R<?>> patchDoc(@PathVariable String collId,
+                                         @PathVariable String docId,
+                                         @RequestBody Map<String, Object> req) throws Throwable {
+        if (req.containsKey("newName")) {
+            Map<String, Object> r = qaDocsService.rename(collId, docId, req);
+            return ResponseEntity.ok(R.data(r));
+        }
+        if (req.containsKey("attributes")) {
+            Object attrs = req.get("attributes");
+            if (attrs instanceof Map) {
+                Object r = qaDocsService.patchAttributes(collId, docId, (Map<String, Object>) attrs);
+                return ResponseEntity.ok(R.data(r));
+            }
+        }
+        return ResponseEntity.badRequest().body(R.data(Map.of("error", "No supported fields to update")));
+    }
+
+    @GetMapping("/{docId}/attributes")
+    public ResponseEntity<R> getAttributes(@PathVariable String collId, @PathVariable String docId) throws Throwable {
+        Object ret = qaDocsService.getAttributes(collId, docId);
+        return ResponseEntity.ok(R.data(ret));
+    }
+
+    @GetMapping("/{docId}/attributes/{key}")
+    public ResponseEntity<R> getAttributeByKey(@PathVariable String collId, @PathVariable String docId,
+                                               @PathVariable String key) throws Throwable {
+        Object ret = qaDocsService.getAttributeByKey(collId, docId, key);
+        return ResponseEntity.ok(R.data(ret));
+    }
+
+    @PatchMapping("/{docId}/attributes")
+    public ResponseEntity<R> patchAttributes(@PathVariable String collId, @PathVariable String docId,
+                                             @RequestBody Map<String, Object> data) throws Throwable {
+        Object ret = qaDocsService.patchAttributes(collId, docId, data);
+        return ResponseEntity.ok(R.data(ret));
+    }
+
+    @DeleteMapping("/{docId}/attributes/{key}")
+    public ResponseEntity<R> deleteAttributeByKey(@PathVariable String collId, @PathVariable String docId,
+                                                  @PathVariable String key) throws Throwable {
+        Object ret = qaDocsService.deleteAttributeByKey(collId, docId, key);
+        return ResponseEntity.ok(R.data(ret));
+    }
+
+    @DeleteMapping("/{docId}/attributes")
+    public ResponseEntity<R> removeAttributes(@PathVariable String collId, @PathVariable String docId,
+                                              @RequestParam List<String> keys) throws Throwable {
+        GBaseKeyValue updatedKv = qaDocsService.deleteAttributeByKeys(collId, docId, keys);
+        return ResponseEntity.ok(R.data(updatedKv));
+    }
+
+    @PutMapping("/{docId}/attributes")
+    public ResponseEntity<R> updateAttribute(@PathVariable String collId, @PathVariable Integer docId,
+                                             @RequestBody Map<String, Object> attributes) throws Throwable {
+        GBaseKeyValue updatedKv = qaDocsService.updateAttribute(collId, docId, attributes);
+        return ResponseEntity.ok(R.data(updatedKv));
+    }
+
+    @PostMapping("/_fields")
+    public ResponseEntity<R> getAllEntities(@PathVariable String collId,
+                                            @RequestBody List<String> fields) throws Throwable {
+        List<Map<String, Object>> entities = qaDocsService.getAllEntities(collId, fields);
+        return ResponseEntity.ok(R.data(entities));
+    }
+
+    @GetMapping("/count")
+    public ResponseEntity<R> getCount(@PathVariable String collId) {
+        long count = qaDocsService.count(collId);
+        return ResponseEntity.ok(R.data(count));
+    }
+
+    @PostMapping("/{docId}/tags")
+    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));
+    }
+
+    @PutMapping("/{docId}/tags")
+    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));
+    }
+
+    @DeleteMapping("/{docId}/tags")
+    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("/{docId}/altlabels")
+    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));
+    }
+
+    @PutMapping("/{docId}/altlabels")
+    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));
+    }
+
+    @DeleteMapping("/{docId}/altlabels")
+    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("/_search")
+    public ResponseEntity<R> search(@PathVariable String collId, @RequestBody Map<String, Object> query)
+            throws Throwable {
+        Map<String, Object> q = new HashMap<>(query);
+        String type = q.get("type") != null ? q.get("type").toString() : "hybrid";
+        q.remove("type");
+        List<GBaseKeyValue> rets;
+        if ("fulltext".equalsIgnoreCase(type)) {
+            rets = qaDocsService.fulltextSearch(collId, q);
+        } else if ("similarity".equalsIgnoreCase(type)) {
+            rets = qaDocsService.similaritySearch(collId, q);
+        } else if ("hybrid".equalsIgnoreCase(type)) {
+            rets = qaDocsService.hybridSearch(collId, q);
+        } else {
+            return ResponseEntity.badRequest().body(R.data(Map.of("error", "Unsupported search type: " + type)));
+        }
+        return ResponseEntity.ok(R.data(rets));
+    }
+
+    @DeleteMapping("/indexes")
+    public ResponseEntity<R> deleteDocsIndexes(@PathVariable String collId) throws IOException, InterruptedException {
+        int r = qaDocsService.deleteAllIndex(collId);
+        return ResponseEntity.ok(R.data(r));
+    }
+
+    private URI buildDocLocation(GBaseKeyValue created) {
+        if (created == null) {
+            return null;
+        }
+        String id = created.getGid();
+        if (id == null) {
+            id = created.getId();
+        }
+        if (id == null) {
+            Integer intId = created.getIntId();
+            if (intId != null) {
+                id = intId.toString();
+            }
+        }
+        if (id == null) {
+            return null;
+        }
+        return ServletUriComponentsBuilder.fromCurrentRequest()
+                .path("/{id}")
+                .buildAndExpand(id)
+                .toUri();
+    }
+}

+ 84 - 0
server/src/main/java/com/giantan/data/qa/controller/v2/V2ProxySearchController.java

@@ -0,0 +1,84 @@
+package com.giantan.data.qa.controller.v2;
+
+import com.giantan.data.qa.constant.QaConstants;
+import com.giantan.data.qa.repository.QaIndexer;
+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;
+
+@RestController
+@RequestMapping(QaConstants.API_PREFIX_V2 + "/collections/{coll}")
+public class V2ProxySearchController {
+    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
+    QaIndexer qaIndexer;
+
+    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();
+        String query = request.getQueryString();
+
+        int ii = requestUri.indexOf(QaConstants.API_PREFIX_V2);
+
+        String targetPath = requestUri.substring(ii + QaConstants.API_PREFIX_V2.length());
+        targetPath = targetPath.replaceFirst("/collections", "");
+        targetPath = targetPath.replaceFirst("/" + coll, "");
+        targetPath = targetPath.replaceFirst("/indexes", "/documents");
+
+        String collName = qaIndexer.getMappedIndexNameByName(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)));
+
+        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());
+    }
+}

+ 113 - 0
server/src/main/java/com/giantan/data/qa/controller/v2/V2TaskController.java

@@ -0,0 +1,113 @@
+package com.giantan.data.qa.controller.v2;
+
+import com.giantan.ai.common.reponse.R;
+import com.giantan.data.qa.constant.QaConstants;
+import com.giantan.data.qa.service.task.QaTaskManager;
+import com.giantan.data.tasks.TaskContext;
+import com.giantan.data.tasks.TaskType;
+import com.giantan.data.tasks.repository.TaskStatusHistory;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.http.ResponseEntity;
+import org.springframework.web.bind.annotation.*;
+import org.springframework.web.servlet.support.ServletUriComponentsBuilder;
+
+import java.net.URI;
+import java.time.LocalDateTime;
+import java.util.*;
+
+@RestController
+@RequestMapping(QaConstants.API_PREFIX_V2 + "/collections/{collId}/tasks")
+public class V2TaskController {
+
+    @Autowired
+    private QaTaskManager manager;
+
+    @PostMapping
+    public ResponseEntity<R<Map>> submit(@PathVariable String collId, @RequestBody Map<String, Object> payload) {
+        String t = (String) payload.remove("type");
+        TaskType type = TaskType.valueOf(t);
+        Map<String, Object> params = new HashMap<>(payload);
+        String ret = manager.submit(collId, type, params);
+
+        URI location = ServletUriComponentsBuilder.fromCurrentRequest()
+                .path("/{id}")
+                .buildAndExpand(ret)
+                .toUri();
+        return ResponseEntity.accepted().location(location).body(R.data(Map.of("taskId", ret)));
+    }
+
+    @PatchMapping("/{id}")
+    public ResponseEntity<R<Map>> update(@PathVariable String collId,
+                                         @PathVariable String id,
+                                         @RequestBody Map<String, Object> payload) {
+        Object status = payload.get("status");
+        if (status != null && "canceled".equalsIgnoreCase(status.toString())) {
+            boolean ok = manager.cancel(collId, id);
+            return ResponseEntity.ok(R.data(Map.of("canceled", ok)));
+        }
+        return ResponseEntity.badRequest().body(R.data(Map.of("error", "Unsupported status change")));
+    }
+
+    @DeleteMapping("/{id}")
+    public ResponseEntity<R<Map>> delete(@PathVariable String collId, @PathVariable String id) {
+        boolean ok = manager.delete(collId, id);
+        return ResponseEntity.ok(R.data(Map.of("deleted", ok)));
+    }
+
+    @GetMapping("/{id}/status")
+    public ResponseEntity<R<Map>> status(@PathVariable String collId, @PathVariable String id) {
+        TaskContext ctx = manager.getTask(collId, id);
+        Map ret = ctx != null ? ctx.getObjectStatus() : Collections.emptyMap();
+        return ResponseEntity.ok(R.data(ret));
+    }
+
+    @GetMapping("/{id}")
+    public ResponseEntity<R<TaskContext>> getTask(@PathVariable String collId, @PathVariable String id) {
+        TaskContext task = manager.getTask(collId, id);
+        return ResponseEntity.ok(R.data(task));
+    }
+
+    @DeleteMapping
+    public ResponseEntity<R<Map>> cleanup(@PathVariable String collId,
+                                          @RequestParam(value = "cleanup", required = false) Boolean cleanup) {
+        if (cleanup == null || !cleanup) {
+            return ResponseEntity.badRequest().body(R.data(Map.of("error", "cleanup=true is required")));
+        }
+        int r = manager.cleanupNow(collId);
+        return ResponseEntity.ok(R.data(Map.of("deleted", r)));
+    }
+
+    @GetMapping
+    public ResponseEntity<R<Collection<TaskContext>>> listAllTasks(@PathVariable String collId,
+                                                                   @RequestParam(value = "status", required = false) String status) {
+        if (status != null) {
+            return ResponseEntity.ok(R.data(manager.findByStatus(collId, status)));
+        }
+        return ResponseEntity.ok(R.data(manager.allTasks(collId)));
+    }
+
+    @GetMapping("/{id}/history")
+    public ResponseEntity<R<TaskStatusHistory>> getHistoryTask(@PathVariable String collId, @PathVariable String id) {
+        return ResponseEntity.ok(R.data(manager.getHistoryTask(collId, id)));
+    }
+
+    @GetMapping("/history")
+    public ResponseEntity<R<List<TaskStatusHistory>>> getTasks(@PathVariable String collId,
+                                                               @RequestParam(value = "createdAtStart", required = false) String createdAtStart,
+                                                               @RequestParam(value = "createdAtEnd", required = false) String createdAtEnd,
+                                                               @RequestParam(value = "status", required = false) String status) {
+        LocalDateTime startTime = createdAtStart != null ? LocalDateTime.parse(createdAtStart) : null;
+        LocalDateTime endTime = createdAtEnd != null ? LocalDateTime.parse(createdAtEnd) : null;
+        return ResponseEntity.ok(R.data(manager.getHistoryTasks(collId, startTime, endTime, status)));
+    }
+
+    @DeleteMapping("/history")
+    public ResponseEntity<R> deleteHistory(@PathVariable String collId,
+                                           @RequestParam(value = "createdAtStart", required = false) String createdAtStart,
+                                           @RequestParam(value = "createdAtEnd", required = false) String createdAtEnd,
+                                           @RequestParam(value = "status", required = false) String status) {
+        LocalDateTime startTime = createdAtStart != null ? LocalDateTime.parse(createdAtStart) : null;
+        LocalDateTime endTime = createdAtEnd != null ? LocalDateTime.parse(createdAtEnd) : null;
+        return ResponseEntity.ok(R.data(manager.deleteHistoryTasks(collId, startTime, endTime, status)));
+    }
+}

+ 130 - 0
server/src/main/java/com/giantan/data/qa/controller/v2/V2TaxonomyController.java

@@ -0,0 +1,130 @@
+package com.giantan.data.qa.controller.v2;
+
+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.util.List;
+import java.util.Map;
+import java.util.UUID;
+
+@RestController
+@RequestMapping(QaConstants.API_PREFIX_V2 + "/collections/{collName}")
+public class V2TaxonomyController {
+
+    @Autowired
+    private QaTaxonomyService dynamicTaxonomyService;
+
+    @GetMapping("/taxonomy")
+    public ResponseEntity<R> list(@PathVariable String collName,
+                                  @RequestParam(value = "view", required = false) String view,
+                                  @RequestParam(value = "path", required = false) String path) throws Exception {
+        if (path != null) {
+            TaxonomyNode r = dynamicTaxonomyService.findNodeByPath(collName, path);
+            return ResponseEntity.ok(R.data(r));
+        }
+        if ("tree".equalsIgnoreCase(view)) {
+            List<TaxonomyNode> r = dynamicTaxonomyService.listTree(collName);
+            return ResponseEntity.ok(R.data(r));
+        }
+        List<TaxonomyNode> r = dynamicTaxonomyService.listAll(collName);
+        return ResponseEntity.ok(R.data(r));
+    }
+
+    @GetMapping("/taxonomy/{nodeId}/children")
+    public ResponseEntity<R> findChildren(@PathVariable String collName, @PathVariable String nodeId) throws Exception {
+        List<TaxonomyNode> r = dynamicTaxonomyService.findChildren(collName, nodeId);
+        return ResponseEntity.ok(R.data(r));
+    }
+
+    @GetMapping("/taxonomy/{nodeId}/docs")
+    public ResponseEntity<R> findMds(@PathVariable String collName, @PathVariable String nodeId) throws Exception {
+        List<GBaseKeyValue> r = dynamicTaxonomyService.findMds(collName, nodeId);
+        return ResponseEntity.ok(R.data(r));
+    }
+
+    @PostMapping("/taxonomy")
+    public ResponseEntity<R> createNode(@PathVariable String collName, @RequestBody Map<String, Object> data) throws Exception {
+        TaxonomyNode r = dynamicTaxonomyService.createNode(collName, data);
+        return ResponseEntity.status(201).body(R.data(r));
+    }
+
+    @PatchMapping("/taxonomy/{nodeId}")
+    public ResponseEntity<R> updateNode(@PathVariable String collName, @PathVariable String nodeId,
+                                        @RequestBody Map<String, Object> req) throws Exception {
+        String taskId = UUID.randomUUID().toString();
+        if (req.containsKey("newName")) {
+            int ret = dynamicTaxonomyService.renameFolder(collName, nodeId, req, taskId);
+            return ResponseEntity.ok(R.data(ret));
+        }
+        if (req.containsKey("parentId") || req.containsKey("targetId") || req.containsKey("targetPath")) {
+            int ret = dynamicTaxonomyService.moveTo(collName, nodeId, req, taskId);
+            return ResponseEntity.ok(R.data(ret));
+        }
+        return ResponseEntity.badRequest().body(R.data(Map.of("error", "No supported fields to update")));
+    }
+
+    @DeleteMapping("/taxonomy/{nodeId}/docs")
+    public ResponseEntity<R> deleteFolder(@PathVariable String collName, @PathVariable String nodeId) throws Exception {
+        String taskId = UUID.randomUUID().toString();
+        int ret = dynamicTaxonomyService.deleteFolder(collName, nodeId, taskId);
+        return ResponseEntity.ok(R.data(ret));
+    }
+
+    @GetMapping("/taxonomy/{nodeId}/subtree")
+    public ResponseEntity<R> getSubtree(@PathVariable String collName, @PathVariable String nodeId) throws Exception {
+        List<TaxonomyNode> r = dynamicTaxonomyService.findSubtree(collName, nodeId);
+        return ResponseEntity.ok(R.data(r));
+    }
+
+    @GetMapping("/taxonomy/{nodeId}/descendants")
+    public ResponseEntity<R> getDescendants(@PathVariable String collName, @PathVariable String nodeId) throws Exception {
+        List<TaxonomyNode> r = dynamicTaxonomyService.findDescendants(collName, nodeId);
+        return ResponseEntity.ok(R.data(r));
+    }
+
+    @GetMapping("/taxonomy/{nodeId}/ancestors")
+    public ResponseEntity<R> getAncestors(@PathVariable String collName, @PathVariable String nodeId) throws Exception {
+        List<TaxonomyNode> r = dynamicTaxonomyService.findAncestors(collName, nodeId);
+        return ResponseEntity.ok(R.data(r));
+    }
+
+    @PostMapping("/taxonomy/{nodeId}/docs")
+    public ResponseEntity<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 ResponseEntity.status(201).body(R.data(ret));
+    }
+
+    @PostMapping("/docs/by-path")
+    public ResponseEntity<R> createEntityByPath(@PathVariable String collName, @RequestBody Map<String, Object> data)
+            throws Throwable {
+        GBaseKeyValue ret = dynamicTaxonomyService.createEntityByPath(collName, GBaseKeyValue.build(data));
+        return ResponseEntity.status(201).body(R.data(ret));
+    }
+
+    @PostMapping("/taxonomy/by-path")
+    public ResponseEntity<R> createNodeByPath(@PathVariable String collName, @RequestBody Map<String, Object> data)
+            throws Exception {
+        TaxonomyNode r = dynamicTaxonomyService.createNodeByPath(collName, data);
+        return ResponseEntity.status(201).body(R.data(r));
+    }
+
+    @PutMapping("/taxonomy/by-path")
+    public ResponseEntity<R> updateNodeByPath(@PathVariable String collName, @RequestBody Map<String, Object> data)
+            throws Exception {
+        TaxonomyNode r = dynamicTaxonomyService.updateNodeByPath(collName, data);
+        return ResponseEntity.ok(R.data(r));
+    }
+
+    @DeleteMapping("/taxonomy/by-path")
+    public ResponseEntity<R> deleteFolderByPath(@PathVariable String collName, @RequestParam String path) throws Exception {
+        int ret = dynamicTaxonomyService.deleteFolderByPath(collName, path);
+        return ResponseEntity.ok(R.data(ret));
+    }
+}

+ 27 - 8
server/src/main/java/com/giantan/data/qa/service/CollsSearchService.java → server/src/main/java/com/giantan/data/qa/service/QaCollsSearchService.java

@@ -1,25 +1,26 @@
 package com.giantan.data.qa.service;
 
 import cnnlp.util.MultiValueHashMap;
+import com.giantan.data.common.service.GoeSimService;
 import com.giantan.data.index.HybridIndexer;
 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.model.CollsSearchRequest;
+import com.giantan.data.common.model.CollsSearchRequest;
 import com.giantan.data.qa.repository.QaDocRepository;
 import com.giantan.data.qa.repository.QaIndexer;
-import com.giantan.data.tasks.TaskContext;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.stereotype.Service;
 
-import java.io.IOException;
 import java.util.*;
 import java.util.stream.Collectors;
 
 @Service
-public class CollsSearchService {
+public class QaCollsSearchService {
     private static final org.slf4j.Logger log
-            = org.slf4j.LoggerFactory.getLogger(CollsSearchService.class);
+            = org.slf4j.LoggerFactory.getLogger(QaCollsSearchService.class);
+
+    public static final String COLL_NAME = "collectionName";
 
     @Autowired
     QaCollectionService qaCollectionService;
@@ -84,7 +85,7 @@ public class CollsSearchService {
         List<DocSearchResp> resps = hybridSearch.hybridSearch(qasCollName, query, isIcludeCollName);
 
         String q = query.get("query").toString();
-        List<GBaseKeyValue> rets = getEntitiesBySearch(collId, q, resps);
+        List<GBaseKeyValue> rets = getEntitiesBySearch(collId, coll,q, resps);
         return rets;
     }
 
@@ -136,7 +137,20 @@ public class CollsSearchService {
         }
     }
 
-    protected List<GBaseKeyValue> getEntitiesBySearch(String collId, String q, List<DocSearchResp> resps) throws Throwable {
+    private String toCollStr(Object o){
+        if (o instanceof String) {
+            return (String) o;
+        }else if (o instanceof Double) {
+            Double d = (Double) o;
+            String s = String.valueOf(d.longValue());
+            return s;
+        }else if (o instanceof Integer) {
+            return String.valueOf(o);
+        }
+        return String.valueOf(o);
+    }
+
+    protected List<GBaseKeyValue> getEntitiesBySearch(String collId, String collName, String q, List<DocSearchResp> resps) throws Throwable {
         if (resps.isEmpty()) {
             return List.of();
         }
@@ -149,7 +163,8 @@ public class CollsSearchService {
 
             if (metadata != null) {
                 Object o = metadata.get(QaIndexer.COLL_ID);
-                if (o != null && o instanceof String collId1) {
+                if (o != null) {
+                    String collId1 = toCollStr(o);
                     if (collId.equals(collId1)) {
                         o = metadata.get(QaIndexer.DOC_ID);
                         addIntList(ids, o);
@@ -172,6 +187,7 @@ public class CollsSearchService {
                     }
                     ro.put("score", score1);
                     ro.put("matched", r1.left());
+                    ro.put(COLL_NAME,collName);
                     rets.add(ro);
                 }
             }
@@ -279,6 +295,8 @@ public class CollsSearchService {
                     .collect(Collectors.toList());
 
             String collId = e.getKey();
+            String collectionName = qaCollectionService.getCollectionName(toDocId(collId));
+
             List<GBaseKeyValue> rs2 = qaDocRepository.findAllByIds(collId, idList);
             if (rs2 != null) {
                 for (GBaseKeyValue ro : rs2) {
@@ -291,6 +309,7 @@ public class CollsSearchService {
                     }
                     ro.put("score", score1);
                     ro.put("matched", r1.left());
+                    ro.put(COLL_NAME,collectionName);
                     rets.add(ro);
                 }
             }

+ 14 - 1
server/src/main/java/com/giantan/data/qa/service/QaDocsService.java

@@ -501,6 +501,18 @@ public class QaDocsService implements IQaDocsService {
         return r;
     }
 
+    private String toCollStr(Object o){
+        if (o instanceof String) {
+            return (String) o;
+        }else if (o instanceof Double) {
+            Double d = (Double) o;
+            String s = String.valueOf(d.longValue());
+            return s;
+        }else if (o instanceof Integer) {
+            return String.valueOf(o);
+        }
+        return String.valueOf(o);
+    }
 
     protected List<GBaseKeyValue> getEntitiesBySearch(String collId, List<DocSearchResp> resps) throws Throwable {
         if (resps.isEmpty()) {
@@ -511,7 +523,8 @@ public class QaDocsService implements IQaDocsService {
             Map<String, Object> metadata = resp.getMetadata();
             if (metadata != null) {
                 Object o = metadata.get(QaIndexer.COLL_ID);
-                if (o != null && o instanceof String collId1) {
+                if (o != null ) {
+                    String collId1 = toCollStr(o);
                     if (collId.equals(collId1)) {
                         o = metadata.get(QaIndexer.DOC_ID);
                         if (o != null) {

+ 1 - 0
server/src/main/java/com/giantan/data/qa/service/TextSimilarity.java

@@ -3,6 +3,7 @@ package com.giantan.data.qa.service;
 
 import java.util.*;
 
+import com.giantan.data.common.service.GoeSimService;
 import org.cnnlp.service.IOntologyBasedService;
 
 import cnnlp.lexical.CnSegment;