Przeglądaj źródła

增加了QA管理,增加by-path方法

dwp 7 miesięcy temu
rodzic
commit
bd5d910168
26 zmienionych plików z 905 dodań i 95 usunięć
  1. 1 1
      server/pom.xml
  2. 118 0
      server/src/main/java/com/giantan/ai/util/PathUtils.java
  3. 18 0
      server/src/main/java/com/giantan/data/index/HybridSearch.java
  4. 2 0
      server/src/main/java/com/giantan/data/index/IHybridSearch.java
  5. 10 1
      server/src/main/java/com/giantan/data/kvs/kvstore/IGDynamicRepository.java
  6. 49 11
      server/src/main/java/com/giantan/data/kvs/repository/GDynamicRepository.java
  7. 55 10
      server/src/main/java/com/giantan/data/kvs/repository/index/GIndexedRepository.java
  8. 4 3
      server/src/main/java/com/giantan/data/kvs/repository/index/IIndexer.java
  9. 1 1
      server/src/main/java/com/giantan/data/mds/MdsApplication.java
  10. 10 6
      server/src/main/java/com/giantan/data/mds/config/TaskConfiguration.java
  11. 2 0
      server/src/main/java/com/giantan/data/mds/service/impl/MdCollectionsService.java
  12. 1 1
      server/src/main/java/com/giantan/data/mds/service/impl/MdDocsService.java
  13. 78 0
      server/src/main/java/com/giantan/data/qa/controller/QaDocsController.java
  14. 140 0
      server/src/main/java/com/giantan/data/qa/controller/QaTaskController.java
  15. 27 2
      server/src/main/java/com/giantan/data/qa/controller/QaTaxonomyController.java
  16. 33 0
      server/src/main/java/com/giantan/data/qa/repository/QaDocRepository.java
  17. 42 16
      server/src/main/java/com/giantan/data/qa/repository/QaIndexer.java
  18. 1 1
      server/src/main/java/com/giantan/data/qa/service/IQaCollectionService.java
  19. 26 2
      server/src/main/java/com/giantan/data/qa/service/IQaDocsService.java
  20. 1 1
      server/src/main/java/com/giantan/data/qa/service/QaCollectionService.java
  21. 88 11
      server/src/main/java/com/giantan/data/qa/service/QaDocsService.java
  22. 96 7
      server/src/main/java/com/giantan/data/qa/service/QaTaxonomyService.java
  23. 98 0
      server/src/main/java/com/giantan/data/qa/service/task/QasTaskHandler.java
  24. 2 1
      server/src/main/java/com/giantan/data/tasks/TaskType.java
  25. 0 15
      server/src/main/java/com/giantan/data/taxonomy/model/TaxonomyRelation.java
  26. 2 5
      server/src/test/java/com/giantan/data/mds/MdsApplicationTests.java

+ 1 - 1
server/pom.xml

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

+ 118 - 0
server/src/main/java/com/giantan/ai/util/PathUtils.java

@@ -0,0 +1,118 @@
+package com.giantan.ai.util;
+
+public class PathUtils {
+    /**
+     * Normalize a path string:
+     * - Replace '\' with '/'
+     * - Collapse multiple '/' into a single '/'
+     * - Ensure it starts with '/'
+     * - Ensure it does not end with '/' (unless it's root "/")
+     */
+//    public static String normalizePath(String path) {
+//        if (path == null || path.isBlank()) {
+//            return "/";
+//        }
+//        // 替换所有 '\' 为 '/'
+//        String normalized = path.trim().replace('\\', '/');
+//
+//        // 合并多个连续的 '/'
+//        //normalized = normalized.replaceAll("/+", "/");
+//
+//        // 确保以 '/' 开头
+//        if (!normalized.startsWith("/")) {
+//            normalized = "/" + normalized;
+//        }
+//
+//        // 去掉末尾的 '/',但保留根 "/"
+//        while (normalized.length() > 1 && normalized.endsWith("/")) {
+//            normalized = normalized.substring(0, normalized.length() - 1);
+//        }
+//
+//        return normalized;
+//    }
+
+    private static final String SEPARATOR = "/";
+
+    /**
+     * 规范化路径:
+     * - 去掉多余的空格
+     * - 替换重复的 "//"
+     * - 去掉最后的 "/"
+     * - 确保以 "/" 开头
+     */
+    public static String normalize(String path) {
+        if (path == null || path.isBlank()) {
+            return SEPARATOR;
+        }
+        String result = path.trim();
+
+        // 替换所有 '\' 为 '/'
+        String normalized = path.trim().replace('\\', '/');
+
+        // 替换重复的 "//"
+        result = result.replaceAll("/{2,}", SEPARATOR);
+
+        // 去掉末尾 "/"
+        if (result.endsWith(SEPARATOR) && result.length() > 1) {
+            result = result.substring(0, result.length() - 1);
+        }
+
+        // 确保以 "/" 开头
+        if (!result.startsWith(SEPARATOR)) {
+            result = SEPARATOR + result;
+        }
+
+        return result;
+    }
+
+    /**
+     * 拼接路径 (自动规范化)
+     */
+    public static String join(String base, String child) {
+        if (base == null) base = "";
+        if (child == null) child = "";
+        String joined = base + SEPARATOR + child;
+        return normalize(joined);
+    }
+
+    /**
+     * 判断是否是父路径 (严格匹配目录层级)
+     * 例: /a/b 是 /a/b/c 的父路径
+     */
+    public static boolean isParent(String parent, String child) {
+        parent = normalize(parent);
+        child = normalize(child);
+        return child.startsWith(parent + SEPARATOR);
+    }
+
+    /**
+     * 获取父路径
+     * 例: /a/b/c -> /a/b
+     */
+    public static String getParent(String path) {
+        path = normalize(path);
+        int idx = path.lastIndexOf(SEPARATOR);
+        if (idx <= 0) return SEPARATOR;
+        return path.substring(0, idx);
+    }
+
+    /**
+     * 获取最后一个节点名
+     * 例: /a/b/c -> "c"
+     */
+    public static String getName(String path) {
+        path = normalize(path);
+        int idx = path.lastIndexOf(SEPARATOR);
+        if (idx < 0) return path;
+        return path.substring(idx + 1);
+    }
+
+    //public static void main(String[] args) {
+    //    System.out.println(PathUtils.normalize("/a/b/c/")); // /a/b/c
+    //    System.out.println(PathUtils.join("/a/b", "c"));   // /a/b/c
+    //    System.out.println(PathUtils.isParent("/a/b", "/a/b/c/d")); // true
+    //    System.out.println(PathUtils.getParent("/a/b/c")); // /a/b
+    //    System.out.println(PathUtils.getName("/a/b/c"));   // c
+    //}
+
+}

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

@@ -208,6 +208,24 @@ public class HybridSearch implements IHybridSearch {
         return 0;
     }
 
+    @Override
+    public int deleteDocumentsByFilter(String coll, Map<String, Object> query) throws IOException, InterruptedException {
+        String js = JsonUtil.toJson(query);
+        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(js))
+                .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

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

@@ -29,6 +29,8 @@ public interface IHybridSearch {
 
     int deleteDocumentsByIdFilter(String coll, String filter) throws IOException, InterruptedException;
 
+    int deleteDocumentsByFilter(String coll, Map<String, Object> query) 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;

+ 10 - 1
server/src/main/java/com/giantan/data/kvs/kvstore/IGDynamicRepository.java

@@ -60,5 +60,14 @@ public interface IGDynamicRepository {
 
     int deleteByPath(String collId, String path);
 
-    int deleteByPathPrefix(String collId, String path);
+    int deleteByPrefix(String collId, String path);
+
+    //DELETE FROM %s.%s
+    //WHERE (path LIKE ? OR path = '/ab')
+    //RETURNING *;
+    // 上述的deleteByPrefix 只要是前缀都删除  例如 删 /ab  那么 /abc 也删了
+    // 而该方法 不会
+    int deletePathAndDescendants(String collId, String path);
+
+    List<GBaseKeyValue> getChildren(String collId, String parent);
 }

+ 49 - 11
server/src/main/java/com/giantan/data/kvs/repository/GDynamicRepository.java

@@ -4,6 +4,7 @@ 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.PathUtils;
 import com.giantan.data.kvs.kvstore.GBaseKeyValue;
 import com.giantan.data.kvs.kvstore.IGDynamicRepository;
 import com.giantan.ai.util.id.IdGenerator;
@@ -272,8 +273,8 @@ public class GDynamicRepository implements IGDynamicRepository {
                     ps.setString(7, entity.getPath());
                     ps.setString(8, toJson(entity.getAttributes())); // 必须是合法 JSON 字符串
 
-                    JdbcUtils.setStringArray(ps,3,entity.getAltlabels());
-                    JdbcUtils.setStringArray(ps,6,entity.getTags());
+                    JdbcUtils.setStringArray(ps, 3, entity.getAltlabels());
+                    JdbcUtils.setStringArray(ps, 6, entity.getTags());
                 });
 
         List<Integer> rets = new ArrayList<>();
@@ -372,10 +373,11 @@ public class GDynamicRepository implements IGDynamicRepository {
 
     @Override
     public List<GBaseKeyValue> findByPathPrefix(String collId, String prefix) {
+        String like = prefix + "%";
         String sql = String.format("SELECT * FROM %s.%s WHERE path LIKE ?", schema, tableName(collId));
         List<GEntity> query = jdbc.query(
                 sql,
-                new Object[]{prefix},
+                new Object[]{like},
                 (rs, rowNum) -> toGEntry(rs)
         );
 
@@ -514,16 +516,13 @@ public class GDynamicRepository implements IGDynamicRepository {
 
             return ps;
         }, (rs, rowNum) -> toGEntry(rs));
-        if (rets!= null && !rets.isEmpty()) {
+        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) {
@@ -728,8 +727,6 @@ public class GDynamicRepository implements IGDynamicRepository {
     }
 
 
-
-
 //    @Override
 //    public List<String> setArrayField1(String collId, Integer id, String field, List<String> values) {
 //        if (field == null || values == null) {
@@ -784,7 +781,6 @@ public class GDynamicRepository implements IGDynamicRepository {
     }
 
 
-
     @Override
     public List<String> removeArrayField(String collId, Integer id, String field, List<String> values) {
         if (field == null || values == null || values.size() == 0) {
@@ -890,9 +886,51 @@ public class GDynamicRepository implements IGDynamicRepository {
     }
 
     @Override
-    public int deleteByPathPrefix(String collId, String prefix) {
+    public int deleteByPrefix(String collId, String prefix) {
         String like = prefix + "%";
         String sql = String.format("DELETE FROM %s.%s WHERE path LIKE ?", schema, tableName(collId));
         return jdbc.update(sql, like);
     }
+
+    //DELETE FROM %s.%s
+    //WHERE (path LIKE ? OR path = '/a')
+    //RETURNING *;
+    // 上述的deleteByPathPrefix 只要是前缀都删除  例如 删 /ab  那么 /abc 也删了
+    // 而该方法 不会
+    @Override
+    public int deletePathAndDescendants(String collId, String path) {
+        String p1 = PathUtils.normalize(path);
+        String like = p1 + "/" + "%";
+        String sql = String.format("DELETE FROM %s.%s WHERE (path = ? OR path LIKE ?) RETURNING *", schema, tableName(collId));
+        //return jdbc.update(sql, like);
+        List<GEntity> rets = jdbc.query(sql, new Object[]{p1, like}, (rs, rowNum) -> toGEntry(rs));
+        if (rets == null || rets.isEmpty()) {
+            return 0;
+        }
+        //int r = indexer.onDelete(collId, rets);
+        return rets.size();
+    }
+
+    @Override
+    ///QQQ 这个 getChildren 仅适用于 path 类似 "/a/b" 不以 "/"结尾的path
+    public List<GBaseKeyValue> getChildren(String collId, String parent) {
+        //String parent = PathUtils.normalizePath(parentPath);
+        // 父路径深度
+        int parentDepth = parent.equals("/") ? 1 : parent.split("/").length;
+
+        String sql = String.format(
+                "SELECT * FROM %s.%s " +
+                        "WHERE path LIKE ? " +
+                        "AND path <> ? " +
+                        "AND (length(path) - length(replace(path, '/', ''))+1) = ?",
+                schema,
+                tableName(collId)
+        );
+
+        // 直接子目录:path 以 parent + '/' 开头,深度 = parentDepth + 1
+        String prefix = parent.equals("/") ? "/%" : parent + "/%";
+        List<GEntity> rows = jdbc.query(sql, new Object[]{prefix, parent, parentDepth + 1}, (rs, rowNum) -> toGEntry(rs));
+        List<GBaseKeyValue> rets = GConverter.fromEntity(rows);
+        return rets;
+    }
 }

+ 55 - 10
server/src/main/java/com/giantan/data/kvs/repository/index/GIndexedRepository.java

@@ -5,6 +5,7 @@ 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.PathUtils;
 import com.giantan.ai.util.id.IdGenerator;
 import com.giantan.ai.util.id.UlidGenerator;
 import com.giantan.data.kvs.kvstore.GBaseKeyValue;
@@ -173,7 +174,7 @@ public class GIndexedRepository implements IGDynamicRepository {
     }
 
     // 这个比用  new GRowMapper() 高效
-    private GEntity toGEntry(ResultSet rs) throws SQLException {
+    protected GEntity toGEntry(ResultSet rs) throws SQLException {
         GEntity entity = new GEntity();
         entity.setId(rs.getLong(GEntityConfig.ID));
         entity.setGid(rs.getString(GEntityConfig.GID));
@@ -353,10 +354,11 @@ public class GIndexedRepository implements IGDynamicRepository {
 
     @Override
     public List<GBaseKeyValue> findByPathPrefix(String collId, String prefix) {
+        String like = prefix + "%";
         String sql = String.format("SELECT * FROM %s.%s WHERE path LIKE ?", schema, tableName(collId));
         List<GEntity> query = jdbc.query(
                 sql,
-                new Object[]{prefix},
+                new Object[]{like},
                 (rs, rowNum) -> toGEntry(rs)
         );
 
@@ -686,7 +688,6 @@ public class GIndexedRepository implements IGDynamicRepository {
                 schema,
                 tableName(collId),
                 field,
-                field,
                 field
         );
 
@@ -725,9 +726,8 @@ public class GIndexedRepository implements IGDynamicRepository {
         }
 
         String sql = String.format(
-                "UPDATE %s.%s SET %s = ? WHERE id = ? RETURNING %s",
-                schema, tableName(collId), field, field
-        );
+                "UPDATE %s.%s SET %s = ? WHERE id = ? RETURNING *",
+                schema, tableName(collId), field);
 
         List<GEntity> rets = jdbc.query(con -> {
             PreparedStatement ps = con.prepareStatement(sql);
@@ -764,7 +764,8 @@ public class GIndexedRepository implements IGDynamicRepository {
             bs1.append(", ?)");
         }
 
-        bs.append(bs1).append(" WHERE id = ? RETURNING ").append(field);
+        //bs.append(bs1).append(" WHERE id = ? RETURNING ").append(field);
+        bs.append(bs1).append(" WHERE id = ? RETURNING *");
 
         // 参数列表
         Object[] args = new Object[values.size() + 1];
@@ -877,7 +878,7 @@ public class GIndexedRepository implements IGDynamicRepository {
     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));
+        List<GEntity> rets = jdbc.query(sql, new Object[]{path}, (rs, rowNum) -> toGEntry(rs));
         if (rets == null || rets.isEmpty()) {
             return 0;
         }
@@ -886,15 +887,59 @@ public class GIndexedRepository implements IGDynamicRepository {
     }
 
     @Override
-    public int deleteByPathPrefix(String collId, String prefix) {
+    public int deleteByPrefix(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));
+        List<GEntity> rets = jdbc.query(sql, new Object[]{like}, (rs, rowNum) -> toGEntry(rs));
+        if (rets == null || rets.isEmpty()) {
+            return 0;
+        }
+        int r = indexer.onDelete(collId, rets);
+        return rets.size();
+    }
+
+    //DELETE FROM %s.%s
+    //WHERE (path LIKE ? OR path = '/a')
+    //RETURNING *;
+    // 上述的deleteByPathPrefix 只要是前缀都删除  例如 删 /ab  那么 /abc 也删了
+    // 而该方法 不会
+    @Override
+    public int deletePathAndDescendants(String collId, String path) {
+        String p1 = PathUtils.normalize(path);
+        String like = p1 + "/" + "%";
+        String sql = String.format("DELETE FROM %s.%s WHERE (path = ? OR path LIKE ?) RETURNING *", schema, tableName(collId));
+        //return jdbc.update(sql, like);
+        List<GEntity> rets = jdbc.query(sql, new Object[]{p1, like}, (rs, rowNum) -> toGEntry(rs));
         if (rets == null || rets.isEmpty()) {
             return 0;
         }
         int r = indexer.onDelete(collId, rets);
         return rets.size();
     }
+
+    @Override
+    ///QQQ 这个 getChildren 仅适用于 path 类似 "/a/b" 不以 "/"结尾的path
+    public List<GBaseKeyValue> getChildren(String collId, String parentPath) {
+        String parent = PathUtils.normalize(parentPath);
+
+        // 父路径深度
+        int parentDepth = parent.equals("/") ? 1 : parent.split("/").length;
+
+        String sql = String.format(
+                "SELECT * FROM %s.%s " +
+                        "WHERE path LIKE ? " +
+                        "AND path <> ? " +
+                        "AND (length(path) - length(replace(path, '/', ''))+1) = ?",
+                schema,
+                tableName(collId)
+        );
+
+        // 直接子目录:path 以 parent + '/' 开头,深度 = parentDepth + 1
+        String prefix = parent.equals("/") ? "/%" : parent + "/%";
+        List<GEntity> rows = jdbc.query(sql, new Object[]{prefix, parent, parentDepth + 1}, (rs, rowNum) -> toGEntry(rs));
+        List<GBaseKeyValue> rets = GConverter.fromEntity(rows);
+        return rets;
+    }
+
 }

+ 4 - 3
server/src/main/java/com/giantan/data/kvs/repository/index/IIndexer.java

@@ -1,8 +1,7 @@
 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 com.giantan.data.qa.service.IQaCollectionService;
 
 import java.io.IOException;
 import java.util.List;
@@ -31,5 +30,7 @@ public interface IIndexer {
 
 
     // 用于获取collection的attributes 信息
-    void init(ICollectionService collectionService);
+    void init(IQaCollectionService collectionService);
+
+    int clearCollection(String collection) throws IOException, InterruptedException;
 }

+ 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 2.1.1");
+        log.info("Mds server started. Version 2.1.2");
     }
 
 }

+ 10 - 6
server/src/main/java/com/giantan/data/mds/config/TaskConfiguration.java

@@ -2,11 +2,12 @@ package com.giantan.data.mds.config;
 
 import com.giantan.data.mds.bot.GChatClient;
 import com.giantan.data.index.IHybridSearch;
-import com.giantan.data.mds.repository.MdDynamicTaskRepository;
 import com.giantan.data.mds.service.IMdChunksService;
 import com.giantan.data.mds.service.IMdFilesService;
 import com.giantan.data.mds.service.IVectorization;
 import com.giantan.data.mds.task.impl.*;
+import com.giantan.data.qa.service.IQaDocsService;
+import com.giantan.data.qa.service.task.QasTaskHandler;
 import com.giantan.data.tasks.*;
 import com.google.common.eventbus.AsyncEventBus;
 import com.google.common.eventbus.EventBus;
@@ -21,11 +22,6 @@ import java.util.concurrent.Executor;
 @Configuration
 class TaskConfiguration {
 
-//    @Bean
-//    public EventBus eventBus() {
-//        return new EventBus();
-//    }
-
     @Autowired
     IMdFilesService mdFilesService;
 
@@ -44,6 +40,9 @@ class TaskConfiguration {
     @Autowired
     IPersistentTaskService persistentTaskService;
 
+    @Autowired
+    IQaDocsService qaDocsService;
+
     @Bean
     public Executor taskExecutor() {
         //return Executors.newFixedThreadPool(10);
@@ -88,4 +87,9 @@ class TaskConfiguration {
     public ChunksTaskHandler chunkTaskHandler() {
         return new ChunksTaskHandler(mdChunksService,vectorizationService,hybridSearch,gChatClient);
     }
+
+    @Bean
+    public QasTaskHandler qasTaskHandler() {
+        return new QasTaskHandler(qaDocsService);
+    }
 }

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

@@ -91,6 +91,8 @@ public class MdCollectionsService {   //extends KvCollectionService
                     instance = CollectionInstance.build(kv.getIntId(), kv.getGid(), kv.getName(),kv.get(GEntityConfig.ATTRIBUTES));
                     mdCores.put(name, instance);
                     return instance.getId();
+                }else{
+                    log.warn("Collection with name '" + name + "' not exist.");
                 }
             } catch (Throwable e) {
 

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

@@ -201,7 +201,7 @@ public class MdDocsService implements IMdDocsService {
         if (kvs == null || kvs.size()<=0){
             return 0;
         }
-        int count = mdDynamicRepository.deleteByPathPrefix(collId, prefix);
+        int count = mdDynamicRepository.deleteByPrefix(collId, prefix);
         int deleted = gkbStorer.delete(coll, prefix);
         //System.out.println("deleted="+deleted);
         log.info("{} 已删除", prefix);

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

@@ -8,6 +8,7 @@ import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.http.ResponseEntity;
 import org.springframework.web.bind.annotation.*;
 
+import java.io.IOException;
 import java.lang.invoke.MethodHandles;
 import java.util.List;
 import java.util.Map;
@@ -62,6 +63,13 @@ public class QaDocsController {
         return ResponseEntity.ok(R.data(ret));
     }
 
+    @GetMapping("/docs/{id}")
+    public R<?> getById(@PathVariable String collId, @PathVariable String id
+    ) throws Throwable {
+        GBaseKeyValue ret = qaDocsService.findByIdOrGid(collId, id);
+        return R.data(ret);
+    }
+
     @DeleteMapping("/docs/{gid}")
     public R<?> delete(@PathVariable String collId, @PathVariable String gid
     ) throws Throwable {
@@ -229,6 +237,31 @@ public class QaDocsController {
 //        return ResponseEntity.ok(R.data(entities));
 //    }
 
+    ////////////////////
+    //GET /collections/demo/paths/get?path=/a/b/c
+    //DELETE /collections/demo/paths/delete?path=/a/b/c
+    //GET /collections/demo/paths/list?prefix=/a
+    //GET /collections/demo/paths/children?path=/a
+    //GET /collections/demo/paths/descendants?path=/a
+
+    @GetMapping("/docs/by-path")
+    public R getDocsBypPath(@PathVariable String collId, @RequestParam String path) throws Throwable {
+        List<GBaseKeyValue> rets = qaDocsService.findByPath(collId, path);
+        return R.data(rets);
+    }
+
+    @GetMapping("/docs/by-prefix")
+    public R getDocsBypPrefix(@PathVariable String collId, @RequestParam String prefix) throws Throwable {
+        List<GBaseKeyValue> rets = qaDocsService.findByPrefix(collId, prefix);
+        return R.data(rets);
+    }
+
+    @DeleteMapping("/docs/by-path")
+    public R deletePathAndDescendants(@PathVariable String collId, @RequestParam String path) throws Throwable {
+        int rets = qaDocsService.deletePathAndDescendants(collId, path);
+        return R.data(rets);
+    }
+
     // 获取记录数
     @GetMapping("/docs/count")
     public R getCount(@PathVariable String collId) {
@@ -289,4 +322,49 @@ public class QaDocsController {
         List<GBaseKeyValue> rets = qaDocsService.hybridSearch(collId, query);
         return ResponseEntity.ok(R.data(rets));
     }
+
+    //////////////
+    @GetMapping("/docs")
+    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 = null;
+        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("/docs")
+    public ResponseEntity<R> deleteDocs(
+            @PathVariable String collId,
+            @RequestParam(required = false) String path,
+            @RequestParam(required = false) String prefix,
+            @RequestParam(required = false) String subtree) throws Throwable {
+        int ret = 0;
+        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 {
+            throw new IllegalArgumentException("Must provide path or prefix to delete");
+        }
+        return ResponseEntity.ok(R.data(ret));
+    }
+
+    @DeleteMapping("/docs/indexes")
+    public ResponseEntity<R> deleteDocsIndexes(@PathVariable String collId) throws IOException, InterruptedException {
+        int r = qaDocsService.deleteAllIndex(collId);
+        return ResponseEntity.ok(R.data(r));
+    }
 }

+ 140 - 0
server/src/main/java/com/giantan/data/qa/controller/QaTaskController.java

@@ -0,0 +1,140 @@
+package com.giantan.data.qa.controller;
+
+import com.giantan.data.qa.constant.QaConstants;
+import com.giantan.data.tasks.TaskContext;
+import com.giantan.data.tasks.TaskManager;
+import com.giantan.data.tasks.TaskOperationsStatus;
+import com.giantan.data.tasks.TaskType;
+import com.giantan.data.tasks.repository.TaskStatusHistory;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.web.bind.annotation.*;
+
+import java.time.LocalDateTime;
+import java.util.*;
+
+@RestController
+@RequestMapping(QaConstants.API_PREFIX + "/collections/{collId}/tasks")
+public class QaTaskController {
+
+    @Autowired
+    private TaskManager manager;
+
+    /*
+    {
+	"type": "SLICE",
+	"objectIds": [2],  //或者 fromId/toId
+	"mdType": "simple",  // faq/tagged
+	"chunkMetadata": {
+		"k1": "v1"
+	    },
+	"chunkSize": 512,
+    "chunkOverlap": 64
+    }
+     */
+
+    @PostMapping("/submit")
+    public Map submit(@PathVariable String collId, @RequestBody Map<String, Object> payload) {
+        String t = (String) payload.remove("type");
+
+        TaskType type = TaskType.valueOf(t);
+
+//        List<Object> objects = null;
+//
+//        if (payload.containsKey("objectIds")) {
+//            objects = (List<Object>) payload.remove("objectIds");
+//        } else if (payload.containsKey("fromId") && payload.containsKey("toId")) {
+//            int from = (int) payload.remove("fromId");
+//            int to = (int) payload.remove("toId");
+//            objects = new ArrayList<>();
+//            for (int i = from; i <= to; i++) {
+//                objects.add(i);
+//            }
+//        } else {
+//            throw new IllegalArgumentException("必须提供 objectIds 或 fromId/toId");
+//        }
+
+        //Map<String, Object> params = (Map<String, Object>) payload.getOrDefault("params", new HashMap<>());
+        Map<String, Object> params = new HashMap<>(payload);
+
+        String ret = manager.submit(collId, type, params);
+        return Map.of("taskId", ret);
+    }
+
+    @PostMapping("/{id}/cancel")
+    public Map cancel(@PathVariable String collId, @PathVariable String id) {
+        boolean ok = manager.cancel(collId, id);
+        return Map.of("canceled", ok);
+    }
+
+    @DeleteMapping("/{id}")
+    public Map delete(@PathVariable String collId, @PathVariable String id) {
+        boolean ok = manager.delete(collId, id);
+        return Map.of("deleted", ok);
+    }
+
+    @GetMapping("/{id}/status")
+    public Map<String, TaskOperationsStatus> status(@PathVariable String collId, @PathVariable String id) {
+        TaskContext ctx = manager.getTask(collId, id);
+        return ctx != null ? ctx.getObjectStatus() : Collections.emptyMap();
+    }
+
+    @GetMapping("/{id}")
+    public TaskContext getTask(@PathVariable String collId, @PathVariable String id) {
+        return manager.getTask(collId, id);
+    }
+
+    @DeleteMapping("/cleanup")
+    public Map cleanup(@PathVariable String collId) {
+        int r = manager.cleanupNow(collId);
+        //return ResponseEntity.ok("Task cleanup triggered.");
+        return Map.of("deleted", r);
+    }
+
+    @GetMapping
+    public Collection<TaskContext> listAllTasks(@PathVariable String collId) {
+        return manager.allTasks(collId);
+    }
+
+    @GetMapping("/status/{status}")
+    public Collection<TaskContext> listTasksByStatus(@PathVariable String collId, @PathVariable String status) {
+        //TaskStatus statusEnum = TaskStatus.valueOf(status.toUpperCase());
+        return manager.findByStatus(collId, status);
+    }
+
+    @GetMapping("/history")
+    public 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 = null;
+        if (createdAtStart != null) {
+            startTime = LocalDateTime.parse(createdAtStart);
+        }
+
+        LocalDateTime endTime = null;
+        if (createdAtEnd != null) {
+            endTime = LocalDateTime.parse(createdAtEnd);
+        }
+        return manager.getHistoryTasks(collId,startTime, endTime, status);
+    }
+
+    @DeleteMapping("/history/cleanup")
+    public int 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 = null;
+        if (createdAtStart != null) {
+            startTime = LocalDateTime.parse(createdAtStart);
+        }
+
+        LocalDateTime endTime = null;
+        if (createdAtEnd != null) {
+            endTime = LocalDateTime.parse(createdAtEnd);
+        }
+        return manager.deleteHistoryTasks(collId,startTime, endTime, status);
+    }
+
+}

+ 27 - 2
server/src/main/java/com/giantan/data/qa/controller/QaTaxonomyController.java

@@ -137,8 +137,33 @@ public class QaTaxonomyController {
     ////////////
 
     @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));
+    public R createEntityByPath(@PathVariable String collName, @RequestBody Map<String, Object> data) throws Throwable {
+        GBaseKeyValue ret = dynamicTaxonomyService.createEntityByPath(collName, GBaseKeyValue.build(data));
         return R.data(ret);
     }
+
+    @PostMapping("/taxonomy/by-path")
+    public R createNodeByPath(@PathVariable String collName, @RequestBody Map<String, Object> data) throws Exception {
+        TaxonomyNode r = dynamicTaxonomyService.createNodeByPath(collName, data);
+        return R.data(r);
+    }
+
+    @PutMapping("/taxonomy/by-path")
+    public R updateNodeByPath(@PathVariable String collName, @RequestBody Map<String, Object> data) throws Exception {
+        TaxonomyNode r = dynamicTaxonomyService.updateNodeByPath(collName, data);
+        return R.data(r);
+    }
+
+    @GetMapping("/taxonomy/by-path")
+    public R findNodeByPath(@PathVariable String collName, @RequestParam String path) throws Exception {
+        TaxonomyNode r = dynamicTaxonomyService.findNodeByPath(collName, path);
+        return R.data(r);
+    }
+
+    @DeleteMapping("/taxonomy/by-path")
+    public R deleteFolderByPath(@PathVariable String collName, @RequestParam String path) throws Exception {
+        int ret = dynamicTaxonomyService.deleteFolderByPath(collName, path);
+        return R.data(ret);
+    }
+
 }

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

@@ -1,5 +1,8 @@
 package com.giantan.data.qa.repository;
 
+import com.giantan.data.kvs.kvstore.GBaseKeyValue;
+import com.giantan.data.kvs.repository.GConverter;
+import com.giantan.data.kvs.repository.GEntity;
 import com.giantan.data.kvs.repository.index.GIndexedRepository;
 
 import jakarta.annotation.PostConstruct;
@@ -7,6 +10,9 @@ import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.jdbc.core.JdbcTemplate;
 import org.springframework.stereotype.Repository;
 
+import java.io.IOException;
+import java.util.List;
+
 @Repository
 public class QaDocRepository extends GIndexedRepository {
     private static final org.slf4j.Logger log
@@ -38,4 +44,31 @@ public class QaDocRepository extends GIndexedRepository {
     public String getMappingCollection(String collId) {
         return qaIndexer.getMappedIndexName(collId);
     }
+
+    public int fullIndexing(String collId) throws IOException, InterruptedException {
+        int batchSize = 100;
+        String sql = String.format("SELECT * FROM %s.%s  WHERE id > ? ORDER BY id LIMIT ?", schema, tableName(collId));
+        List<GEntity> query = null;
+        int count = 0;
+        boolean notEmpty = false;
+        do {
+            int lastId = 0;
+            Integer[] ids = new Integer[2];
+            ids[0] = lastId;
+            ids[1] = batchSize;
+            query = jdbc.query(
+                    sql,
+                    ids,
+                    (rs, rowNum) -> toGEntry(rs)
+            );
+
+            notEmpty = query != null && !query.isEmpty();
+            if (notEmpty) {
+                qaIndexer.onAdd(collId, query);
+                count = count + query.size();
+            }
+        } while (notEmpty);
+        return count;
+    }
+
 }

+ 42 - 16
server/src/main/java/com/giantan/data/qa/repository/QaIndexer.java

@@ -7,7 +7,7 @@ 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 com.giantan.data.qa.service.IQaCollectionService;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.expression.Expression;
 import org.springframework.expression.ExpressionParser;
@@ -33,6 +33,8 @@ public class QaIndexer implements IIndexer {
     String defaultIndexPrefix = "qas";
     String defaultChunkMode = "single";
 
+    String cellectionPrefix = "qas_";
+
     String splitter = "\n";
 
     @Autowired
@@ -40,23 +42,23 @@ public class QaIndexer implements IIndexer {
 
 
     //    ICollectionMapper collMapper;
-    ICollectionService collectionService;
+    IQaCollectionService collectionService;
 
     public QaIndexer() {
         //collMapper = new DefaultCollectionMapper();
     }
 
     @Override
-    public void init(ICollectionService collectionService) {
+    public void init(IQaCollectionService collectionService) {
         // 根据 params的attributes
         setCollectionService(collectionService);
     }
 
-    public ICollectionService getCollectionService() {
+    public IQaCollectionService getCollectionService() {
         return collectionService;
     }
 
-    public void setCollectionService(ICollectionService collectionService) {
+    public void setCollectionService(IQaCollectionService collectionService) {
         this.collectionService = collectionService;
     }
 
@@ -243,11 +245,23 @@ public class QaIndexer implements IIndexer {
     //        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());
+        if (entity.getAttributes() != null) {
+            variables.put(GEntityConfig.ATTRIBUTES, entity.getAttributes());
+        } else {
+            variables.put(GEntityConfig.ATTRIBUTES, new HashMap<>());
+        }
 
+        if (entity.getAltlabels() != null) {
+            variables.put(GEntityConfig.ALTLABELS, entity.getAltlabels());
+        } else {
+            variables.put(GEntityConfig.ALTLABELS, new ArrayList<String>());
+        }
+        if (entity.getDescription() != null) {
+            variables.put(GEntityConfig.DESCRIPTION, entity.getDescription());
+        } else {
+            variables.put(GEntityConfig.DESCRIPTION, "");
+        }
         StandardEvaluationContext context = new StandardEvaluationContext();
         context.setVariables(variables);
         return context;
@@ -273,7 +287,7 @@ public class QaIndexer implements IIndexer {
             dr1.setText(chunk);
             buildMetadata(coll, entity, dr1);
             lst.add(dr1);
-        }else {
+        } else {
             for (int i = 0; i < templates.size(); i++) {
                 Expression expression = parser.parseExpression(templates.get(i));
                 String chunk = expression.getValue(context, String.class);
@@ -290,14 +304,14 @@ public class QaIndexer implements IIndexer {
 
     @Override
     public int onAdd(String collection, GEntity entity) throws IOException, InterruptedException {
-        log.info("{} onAdd: {}", collection, entity.getName());
+        log.info("{}{} onAdd: {}", cellectionPrefix, 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());
+        log.info("{}{} onAdd: size = {}", cellectionPrefix, collection, entities.size());
         List<DocReq> ls = new ArrayList<>();
         for (GEntity e : entities) {
             List<DocReq> docReqs = toDocReq(collection, e);
@@ -309,7 +323,7 @@ public class QaIndexer implements IIndexer {
 
     @Override
     public int onDelete(String collection, GEntity entity) {
-        log.info("{} onDelete: {}", collection, entity.getName());
+        log.info("{}{} onDelete: {}", cellectionPrefix, collection, entity.getName());
         String gid = entity.getGid();
         int i = 0;
         try {
@@ -322,7 +336,7 @@ public class QaIndexer implements IIndexer {
 
     @Override
     public int onDelete(String collection, List<GEntity> entities) {
-        log.info("{} onDelete: size = {}", collection, entities.size());
+        log.info("{}{} onDelete: size = {}", cellectionPrefix, collection, entities.size());
         int count = 0;
         try {
             for (GEntity e : entities) {
@@ -337,7 +351,7 @@ public class QaIndexer implements IIndexer {
 
     @Override
     public int onUpdate(String collection, GEntity entity) throws IOException, InterruptedException {
-        log.info("{} onUpdate: {}", collection, entity.getName());
+        log.info("{}{} onUpdate: {}", cellectionPrefix, collection, entity.getName());
 
         int i = hybridSearch.deleteDocumentsByIdFilter(getMappedIndexName(collection), entity.getGid());
         List<DocResp> rs = hybridSearch.add(getMappedIndexName(collection), toDocReq(collection, entity));
@@ -346,7 +360,7 @@ public class QaIndexer implements IIndexer {
 
     @Override
     public int onUpdateField(String collection, GEntity entity, String field) {
-        log.info("{} onUpdateField: ret = {}, field = {}", collection, entity.getGid(), field);
+        log.info("{}{} onUpdateField: ret = {}, field = {}", cellectionPrefix, collection, entity.getGid(), field);
         int count = 0;
         try {
             int i = hybridSearch.deleteDocumentsByIdFilter(getMappedIndexName(collection), entity.getGid());
@@ -361,7 +375,7 @@ public class QaIndexer implements IIndexer {
 
     @Override
     public int onUpdateField(String collection, List<GEntity> entities, String field) {
-        log.info("{} onUpdateField: size = {}, field = {}", collection, entities.size(), field);
+        log.info("{}{} onUpdateField: size = {}, field = {}", cellectionPrefix, collection, entities.size(), field);
         int count = 0;
         try {
             for (GEntity e : entities) {
@@ -390,6 +404,9 @@ public class QaIndexer implements IIndexer {
             } else {
                 //TODO 特殊处理
                 //  "filterExpression": "metadata[\"__cid\"] == \"qas_2\""
+                Map<String, Object> filterMap = Map.of("filterExpression", "metadata[\"__cid\"] == \"" + collection + "\"");
+                int r = hybridSearch.deleteDocumentsByFilter(getMappedIndexName(collection), filterMap);
+                return r;
             }
         } catch (Exception e) {
             throw new RuntimeException(e);
@@ -406,6 +423,9 @@ public class QaIndexer implements IIndexer {
             } else {
                 //TODO 特殊处理
                 //  "filterExpression": "metadata[\"__cid\"] == \"qas_2\""
+                Map<String, Object> filterMap = Map.of("filterExpression", "metadata[\"__cid\"] == \"" + collection + "\"");
+                int r = hybridSearch.deleteDocumentsByFilter(getMappedIndexName(collection), filterMap);
+                return r;
             }
         } catch (Exception e) {
             throw new RuntimeException(e);
@@ -413,5 +433,11 @@ public class QaIndexer implements IIndexer {
         return b ? 0 : 1;
     }
 
+    @Override
+    public int clearCollection(String collection) throws IOException, InterruptedException {
+        Map<String, Object> filterMap = Map.of("filterExpression", "metadata[\"__cid\"] == \"" + collection + "\"");
+        int r = hybridSearch.deleteDocumentsByFilter(getMappedIndexName(collection), filterMap);
+        return r;
+    }
 
 }

+ 1 - 1
server/src/main/java/com/giantan/data/qa/service/ICollectionService.java → server/src/main/java/com/giantan/data/qa/service/IQaCollectionService.java

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

+ 26 - 2
server/src/main/java/com/giantan/data/qa/service/IDocsService.java → server/src/main/java/com/giantan/data/qa/service/IQaDocsService.java

@@ -6,13 +6,25 @@ import java.io.IOException;
 import java.util.List;
 import java.util.Map;
 
-public interface IDocsService {
+public interface IQaDocsService {
     long deleteAll(String coll) throws Exception;
 
     List<GBaseKeyValue> findAll(String coll) throws Throwable;
 
+    //
+
+
     GBaseKeyValue findByGid(String coll, String gid) throws Throwable;
 
+    //    String collId = getStrOfCollId(coll);
+//        int intId = getIntId(coll, nodeId);
+//        String path = getPath(collId, intId);
+//        List<GBaseKeyValue> ret = qaDynamicRepository.findByPath(collId, path);
+//        return ret;
+    List<GBaseKeyValue> findByPath(String coll, String path) throws Throwable;
+
+    List<GBaseKeyValue> findByPrefix(String coll, String prefix) throws Throwable;
+
     GBaseKeyValue findByMdid(String coll, String mdId) throws Throwable;
 
     int delete(String coll, String gid) throws Throwable;
@@ -21,7 +33,11 @@ public interface IDocsService {
 
     int deleteByName(String coll, String name) throws Throwable;
 
-    int deleteByPathPrefix(String coll, String prefix) throws Throwable;
+    int deleteByPath(String collId, String path);
+    // 严格按 目录 删除
+    int deletePathAndDescendants(String coll, String path) throws Throwable;
+
+    int deleteByPrefix(String coll, String prefix) throws Throwable;
 
     //Object getMetadataByKey(String coll, String mdId, String key) throws Throwable;
 
@@ -64,4 +80,12 @@ public interface IDocsService {
     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;
+
+    GBaseKeyValue findByIdOrGid(String coll, String id) throws Throwable;
+
+    List<GBaseKeyValue> getChildren(String coll, String parent);
+
+    int deleteAllIndex(String coll) throws IOException, InterruptedException;
+
+    int fullIndexing(String coll) throws IOException, InterruptedException;
 }

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

@@ -19,7 +19,7 @@ import java.util.List;
 import java.util.Map;
 
 @Service
-public class QaCollectionService implements ICollectionService {
+public class QaCollectionService implements IQaCollectionService {
     private static final org.slf4j.Logger log
             = org.slf4j.LoggerFactory.getLogger(QaCollectionService.class);
 

+ 88 - 11
server/src/main/java/com/giantan/data/qa/service/QaDocsService.java

@@ -15,7 +15,7 @@ import java.util.List;
 import java.util.Map;
 
 @Service
-public class QaDocsService implements IDocsService {
+public class QaDocsService implements IQaDocsService {
     private static final org.slf4j.Logger log
             = org.slf4j.LoggerFactory.getLogger(QaDocsService.class);
 
@@ -115,6 +115,25 @@ public class QaDocsService implements IDocsService {
         return null;
     }
 
+    //    String collId = getStrOfCollId(coll);
+//        int intId = getIntId(coll, nodeId);
+//        String path = getPath(collId, intId);
+//        List<GBaseKeyValue> ret = qaDynamicRepository.findByPath(collId, path);
+//        return ret;
+    @Override
+    public List<GBaseKeyValue> findByPath(String coll, String path) throws Throwable {
+        String collId = getStrOfCollId(coll);
+        List<GBaseKeyValue> rets = qaDocRepository.findByPath(collId, path);
+        return rets;
+    }
+
+    @Override
+    public List<GBaseKeyValue> findByPrefix(String coll, String prefix) throws Throwable {
+        String collId = getStrOfCollId(coll);
+        List<GBaseKeyValue> rets = qaDocRepository.findByPathPrefix(collId, prefix);
+        return rets;
+    }
+
 
     @Override
     public GBaseKeyValue findByMdid(String coll, String mdId) throws Throwable {
@@ -163,19 +182,40 @@ public class QaDocsService implements IDocsService {
     }
 
     @Override
-    public int deleteByPathPrefix(String coll, String prefix) throws Throwable {
+    public int deleteByPath(String coll, String path) {
         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 count = qaDocRepository.deleteByPath(collId, path);
+        //int deleted = gkbStorer.delete(coll, prefix);
+        //System.out.println("deleted="+deleted);
+        log.info("{} 已删除", path);
+        return count;
+    }
+
+    @Override
+    public int deleteByPrefix(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.deleteByPrefix(collId, prefix);
         //int deleted = gkbStorer.delete(coll, prefix);
         //System.out.println("deleted="+deleted);
         log.info("{} 已删除", prefix);
         return count;
     }
 
+    @Override
+    public int deletePathAndDescendants(String coll, String path) throws Throwable {
+        String collId = getStrOfCollId(coll);
+        int count = qaDocRepository.deletePathAndDescendants(collId, path);
+        //int deleted = gkbStorer.delete(coll, prefix);
+        //System.out.println("deleted="+deleted);
+        log.info("{} 已删除", path);
+        return count;
+    }
+
+
     public int doDelete(String coll, GBaseKeyValue kv) throws Throwable {
         String collId = getStrOfCollId(coll);
         long d1 = qaDocRepository.delete(collId, kv.getIntId());
@@ -423,6 +463,42 @@ public class QaDocsService implements IDocsService {
         return rets;
     }
 
+    @Override
+    public GBaseKeyValue findByIdOrGid(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 List<GBaseKeyValue> getChildren(String coll, String parent) {
+        String collId = getStrOfCollId(coll);
+        return qaDocRepository.getChildren(collId, parent);
+    }
+
+    @Override
+    public int deleteAllIndex(String coll) throws IOException, InterruptedException {
+        String collId = getStrOfCollId(coll);
+        Map<String, Object> filterMap = Map.of("filterExpression", "metadata[\"__cid\"] == \"" + collId + "\"");
+        String indexName = qaDocRepository.getMappingCollection(collId);
+        int r = hybridSearch.deleteDocumentsByFilter(indexName, filterMap);
+        return r;
+    }
+
+    @Override
+    public int fullIndexing(String coll) throws IOException, InterruptedException {
+        String collId = getStrOfCollId(coll);
+        int r = qaDocRepository.fullIndexing(collId);
+        return r;
+    }
+
+
     protected List<GBaseKeyValue> getEntitiesBySearch(String collId, List<DocSearchResp> resps) throws Throwable {
         if (resps.isEmpty()) {
             return List.of();
@@ -432,12 +508,12 @@ public class QaDocsService implements IDocsService {
             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 && 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());
+                            r1.put("score", resp.getScore());
                             rets.add(r1);
                         }
                     }
@@ -453,10 +529,11 @@ public class QaDocsService implements IDocsService {
 
     private int toDocId(Object o) {
         if (o instanceof Integer) {
-            return ((Integer)o).intValue();
-        }else {
+            return ((Integer) o).intValue();
+        } else {
             return Integer.parseInt(o.toString());
         }
     }
 
+
 }

+ 96 - 7
server/src/main/java/com/giantan/data/qa/service/QaTaxonomyService.java

@@ -132,11 +132,12 @@ public class QaTaxonomyService {
             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);
+//            List<GBaseKeyValue> kvs = qaDynamicRepository.findByPath(collId, path);
+//            if (kvs == null || kvs.size() <= 0) {
+//                return 0;
+//            }
+            //int count = qaDynamicRepository.deleteByPathPrefix(collId, path);
+            int count = qaDynamicRepository.deletePathAndDescendants(collId, path);
             qaTaxonomyRepository.deleteSubtree(collId, intId);
             return count;
         } catch (Throwable e) {
@@ -259,6 +260,21 @@ public class QaTaxonomyService {
         return node;
     }
 
+    protected TaxonomyNode getPath(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) {
+                parentId = node.getId();
+            }else{
+                break;
+            }
+        }
+        return node;
+    }
+
     // 根据 目录id 建立qa
     public GBaseKeyValue createQa(String coll, String nodeId, GBaseKeyValue entity) throws Throwable {
         String collId = getStrOfCollId(coll);
@@ -269,8 +285,10 @@ public class QaTaxonomyService {
         return r;
     }
 
+    ///////////////////////////
+
     // 根据 entity中的path字段建taxonomy的节点,并新增entity
-    public GBaseKeyValue createEntryByPath(String coll, GBaseKeyValue entity) throws Throwable {
+    public GBaseKeyValue createEntityByPath(String coll, GBaseKeyValue entity) throws Throwable {
         String collId = getStrOfCollId(coll);
         Object o = entity.get(GEntityConfig.PATH);
         if (o != null && o instanceof String s) {
@@ -280,11 +298,82 @@ public class QaTaxonomyService {
             entity.put(GEntityConfig.PATH, path);
             GBaseKeyValue ret = qaDynamicRepository.save(collId, entity);
             return ret;
-        }else{
+        } else {
             GBaseKeyValue ret = qaDynamicRepository.save(collId, entity);
             return ret;
         }
     }
+
+    public TaxonomyNode createNodeByPath(String coll, Map<String, Object> data) throws Exception {
+        String collId = getStrOfCollId(coll);
+        Object o = data.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);
+            int parentId = qaTaxonomyRepository.getRoot();
+            if (node != null) {
+                parentId = node.getId();
+            }
+            Map<String, Object> kv = new HashMap<String, Object>();
+            kv.put(GEntityConfig.NAME, data.get(GEntityConfig.NAME));
+            kv.put("parentId", parentId);
+            Object o1 = data.get(GEntityConfig.DESCRIPTION);
+            if (o1 != null) {
+                kv.put(GEntityConfig.DESCRIPTION, o1);
+            }
+            o1 = data.get(GEntityConfig.ATTRIBUTES);
+            if (o1 != null) {
+                kv.put(GEntityConfig.ATTRIBUTES, o1);
+            }
+            node = qaTaxonomyRepository.createNode(collId, kv);
+            return node;
+        }
+        return null;
+    }
+
+    public TaxonomyNode updateNodeByPath(String coll, Map<String, Object> data) throws Exception {
+        String collId = getStrOfCollId(coll);
+        Object o = data.remove(GEntityConfig.PATH);
+        if (o != null && o instanceof String s) {
+            List<String> ps = toPathList(s);
+            TaxonomyNode node = getPath(collId, ps);
+            if (node != null) {
+                //Map<String, Object> attr = new HashMap<>();
+                //attr.put(GEntityConfig.ATTRIBUTES, data);
+                TaxonomyNode ret = qaTaxonomyRepository.updateFields(collId, node.getId(), data);
+                return ret;
+            }
+        }
+        return null;
+    }
+
+    public TaxonomyNode findNodeByPath(String coll, String path) throws Exception {
+        String collId = getStrOfCollId(coll);
+        if (path != null) {
+            List<String> ps = toPathList(path);
+            TaxonomyNode node = getPath(collId, ps);
+            return node;
+        }
+        return null;
+    }
+
+    public int deleteFolderByPath(String coll, String path) throws Exception {
+        String collId = getStrOfCollId(coll);
+        if (path != null) {
+            List<String> ps = toPathList(path);
+            TaxonomyNode node = getPath(collId, ps);
+            if (node != null) {
+                //int count = qaDynamicRepository.deleteByPathPrefix(collId, path);
+                int count = qaDynamicRepository.deletePathAndDescendants(collId, path);
+                qaTaxonomyRepository.deleteSubtree(collId, node.getId());
+                return count;
+            }
+        }
+        return 0;
+    }
+
 }
 
 

+ 98 - 0
server/src/main/java/com/giantan/data/qa/service/task/QasTaskHandler.java

@@ -0,0 +1,98 @@
+package com.giantan.data.qa.service.task;
+
+import com.giantan.data.mds.task.impl.BaseTaskHandler;
+import com.giantan.data.qa.service.IQaDocsService;
+import com.giantan.data.tasks.*;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+
+public class QasTaskHandler extends BaseTaskHandler {
+    private static final org.slf4j.Logger log
+            = org.slf4j.LoggerFactory.getLogger(QasTaskHandler.class);
+
+    public static final String TYPE = "type";
+    public static final String ACTION = "action";
+
+    public static final String MD_IDS = "qaIds";
+    public static final String MD_START_ID = "qaStartId";
+    public static final String MD_END_ID = "qaEndId";
+
+    //IHybridSearch hybridSearch;
+
+    IQaDocsService qaDocsService;
+
+    public QasTaskHandler(IQaDocsService qaDocsService) {
+        this.qaDocsService = qaDocsService;
+    }
+
+    // 主要是 对 objectIds 进行处理
+    protected void preProcess(final TaskContext taskContext) {
+        List<Object> objects = new ArrayList<>();
+        taskContext.setObjectIds(objects);
+        Map<String, Object> payload = taskContext.getParams();
+
+    }
+
+    @Override
+    public TaskType getType() {
+        return TaskType.QA;
+    }
+
+    @Override
+    public void doing(TaskContext context, Object objectId) {
+        List<String> operations = context.getOperations();
+        for (String operation : operations) {
+            doing(context, objectId, operation);
+        }
+    }
+
+    public void doing(TaskContext context, Object objectId, String operation) {
+        if (operation.equalsIgnoreCase("indexCreate")) {
+            indexCreate(context, objectId, "indexCreate");
+        } else if (operation.equalsIgnoreCase("indexDelete")) {
+            indexDelete(context, objectId, "indexDelete");
+        }
+    }
+
+    private boolean isAll(Object objectId) {
+        if (objectId != null && objectId.equals("*")) {
+            return true;
+        }
+        return false;
+    }
+
+    private void indexDelete(TaskContext context, Object objectId, String indexDelete) {
+        String coll = context.getCollection();
+        Map<String, Object> params = context.getParams();
+
+        //String mdId = objectId.toString();
+        if (isAll(objectId)) {
+            // deleteAll
+            try {
+                int r = qaDocsService.deleteAllIndex(coll);
+            } catch (IOException e) {
+                throw new RuntimeException(e);
+            } catch (InterruptedException e) {
+                throw new RuntimeException(e);
+            }
+        }
+    }
+
+    private void indexCreate(TaskContext context, Object objectId, String indexCreate) {
+        String coll = context.getCollection();
+        if (isAll(objectId)) {
+            // allIndex
+            try {
+                int r = qaDocsService.fullIndexing(coll);
+            } catch (IOException e) {
+                throw new RuntimeException(e);
+            } catch (InterruptedException e) {
+                throw new RuntimeException(e);
+            }
+        }
+    }
+
+}

+ 2 - 1
server/src/main/java/com/giantan/data/tasks/TaskType.java

@@ -1,5 +1,6 @@
 package com.giantan.data.tasks;
 
 public enum TaskType {
-    UPLOAD, SLICE,EXTRACT_KEYWORDS, EMBEDDING, INDEX, MD,CHUNK
+    UPLOAD, MD, CHUNK, QA,
+    SLICE, EXTRACT_KEYWORDS, EMBEDDING, INDEX
 }

+ 0 - 15
server/src/main/java/com/giantan/data/taxonomy/model/TaxonomyRelation.java

@@ -1,15 +0,0 @@
-package com.giantan.data.taxonomy.model;
-
-import lombok.Data;
-
-//@Data
-public class TaxonomyRelation {
-    private Integer taxonomyId;
-    private String objectType;
-    private String objectId;
-
-    public TaxonomyRelation(){
-
-    }
-
-}

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

@@ -1,14 +1,11 @@
 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 com.giantan.data.qa.service.IQaDocsService;
 import org.junit.jupiter.api.Test;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.boot.test.context.SpringBootTest;
@@ -33,7 +30,7 @@ class MdsApplicationTests {
 	HybridSearch hybridSearch;
 
 	@Autowired
-	IDocsService qaDocsService;
+	IQaDocsService qaDocsService;
 
 	private void testDeepseek(){
 		String s = """