فهرست منبع

视频解构向量化多点模式

wangyunpeng 1 هفته پیش
والد
کامیت
b42e368f94

+ 0 - 9
core/src/main/java/com/tzld/videoVector/common/constant/VectorConstants.java

@@ -13,15 +13,6 @@ public interface VectorConstants {
     /** configCode 传 "ALL" 表示搜索所有启用的向量化配置 */
     String ALL_CONFIG_CODE = "ALL";
 
-    // ========================== 多点向量化 ==========================
-
-    /**
-     * 多点向量化复合ID因子
-     * 复合ID = videoId * MULTI_POINT_FACTOR + pointIndex
-     * 支持每个视频最多存储100个向量点
-     */
-    long MULTI_POINT_FACTOR = 100L;
-
     // ========================== Redis Key ==========================
 
     /** 向量存储 Redis Key 前缀 */

+ 23 - 21
core/src/main/java/com/tzld/videoVector/dao/mapper/pgVector/VideoVectorMapper.java

@@ -11,7 +11,7 @@ public interface VideoVectorMapper {
      * This method was generated by MyBatis Generator.
      * This method corresponds to the database table video_vectors
      *
-     * @mbg.generated Wed Apr 29 15:13:43 CST 2026
+     * @mbg.generated Wed Apr 29 19:29:43 CST 2026
      */
     long countByExample(VideoVectorExample example);
 
@@ -19,7 +19,7 @@ public interface VideoVectorMapper {
      * This method was generated by MyBatis Generator.
      * This method corresponds to the database table video_vectors
      *
-     * @mbg.generated Wed Apr 29 15:13:43 CST 2026
+     * @mbg.generated Wed Apr 29 19:29:43 CST 2026
      */
     int deleteByExample(VideoVectorExample example);
 
@@ -27,7 +27,7 @@ public interface VideoVectorMapper {
      * This method was generated by MyBatis Generator.
      * This method corresponds to the database table video_vectors
      *
-     * @mbg.generated Wed Apr 29 15:13:43 CST 2026
+     * @mbg.generated Wed Apr 29 19:29:43 CST 2026
      */
     int deleteByPrimaryKey(Long id);
 
@@ -35,7 +35,7 @@ public interface VideoVectorMapper {
      * This method was generated by MyBatis Generator.
      * This method corresponds to the database table video_vectors
      *
-     * @mbg.generated Wed Apr 29 15:13:43 CST 2026
+     * @mbg.generated Wed Apr 29 19:29:43 CST 2026
      */
     int insert(VideoVector record);
 
@@ -43,7 +43,7 @@ public interface VideoVectorMapper {
      * This method was generated by MyBatis Generator.
      * This method corresponds to the database table video_vectors
      *
-     * @mbg.generated Wed Apr 29 15:13:43 CST 2026
+     * @mbg.generated Wed Apr 29 19:29:43 CST 2026
      */
     int insertSelective(VideoVector record);
 
@@ -51,7 +51,7 @@ public interface VideoVectorMapper {
      * This method was generated by MyBatis Generator.
      * This method corresponds to the database table video_vectors
      *
-     * @mbg.generated Wed Apr 29 15:13:43 CST 2026
+     * @mbg.generated Wed Apr 29 19:29:43 CST 2026
      */
     List<VideoVector> selectByExample(VideoVectorExample example);
 
@@ -59,7 +59,7 @@ public interface VideoVectorMapper {
      * This method was generated by MyBatis Generator.
      * This method corresponds to the database table video_vectors
      *
-     * @mbg.generated Wed Apr 29 15:13:43 CST 2026
+     * @mbg.generated Wed Apr 29 19:29:43 CST 2026
      */
     VideoVector selectByPrimaryKey(Long id);
 
@@ -67,7 +67,7 @@ public interface VideoVectorMapper {
      * This method was generated by MyBatis Generator.
      * This method corresponds to the database table video_vectors
      *
-     * @mbg.generated Wed Apr 29 15:13:43 CST 2026
+     * @mbg.generated Wed Apr 29 19:29:43 CST 2026
      */
     int updateByExampleSelective(@Param("record") VideoVector record, @Param("example") VideoVectorExample example);
 
@@ -75,7 +75,7 @@ public interface VideoVectorMapper {
      * This method was generated by MyBatis Generator.
      * This method corresponds to the database table video_vectors
      *
-     * @mbg.generated Wed Apr 29 15:13:43 CST 2026
+     * @mbg.generated Wed Apr 29 19:29:43 CST 2026
      */
     int updateByExample(@Param("record") VideoVector record, @Param("example") VideoVectorExample example);
 
@@ -83,7 +83,7 @@ public interface VideoVectorMapper {
      * This method was generated by MyBatis Generator.
      * This method corresponds to the database table video_vectors
      *
-     * @mbg.generated Wed Apr 29 15:13:43 CST 2026
+     * @mbg.generated Wed Apr 29 19:29:43 CST 2026
      */
     int updateByPrimaryKeySelective(VideoVector record);
 
@@ -91,36 +91,38 @@ public interface VideoVectorMapper {
      * This method was generated by MyBatis Generator.
      * This method corresponds to the database table video_vectors
      *
-     * @mbg.generated Wed Apr 29 15:13:43 CST 2026
+     * @mbg.generated Wed Apr 29 19:29:43 CST 2026
      */
     int updateByPrimaryKey(VideoVector record);
 
     // ==================== 自定义向量操作方法 ====================
 
     /**
-     * 插入或更新向量(ON CONFLICT 语义)
+     * 插入或更新向量(ON CONFLICT 语义,支持多点模式
      * @param videoId    视频ID
      * @param configCode 配置编码
+     * @param pointIndex 向量点索引(单点模式传0)
      * @param embedding  向量字符串,格式: "[0.1,0.2,...]"
      */
     int upsertVector(@Param("videoId") Long videoId,
                      @Param("configCode") String configCode,
+                     @Param("pointIndex") int pointIndex,
                      @Param("embedding") String embedding);
 
     /**
-     * 判断指定 videoId + configCode 的向量是否存在
+     * 判断指定 videoId + configCode 的向量是否存在(任意 pointIndex)
      */
     int existsByVideoIdAndConfigCode(@Param("videoId") Long videoId,
                                      @Param("configCode") String configCode);
 
     /**
-     * 批量判断 videoIds 在指定 configCode 下是否存在,返回已存在的 videoId 列表
+     * 批量判断 videoIds 在指定 configCode 下是否存在,返回已存在的 videoId 列表(去重)
      */
     List<Long> selectExistingVideoIds(@Param("videoIds") List<Long> videoIds,
                                       @Param("configCode") String configCode);
 
     /**
-     * 获取指定 videoId + configCode 的向量(以字符串形式返回)
+     * 获取指定 videoId + configCode 的向量(以字符串形式返回,取 pointIndex=0
      */
     String selectEmbeddingByVideoIdAndConfigCode(@Param("videoId") Long videoId,
                                                  @Param("configCode") String configCode);
@@ -132,28 +134,28 @@ public interface VideoVectorMapper {
                                              @Param("configCode") String configCode);
 
     /**
-     * 获取指定 configCode 下所有 videoId
+     * 获取指定 configCode 下所有 videoId(去重)
      */
     List<Long> selectAllVideoIdsByConfigCode(@Param("configCode") String configCode);
 
     /**
-     * 删除指定 videoId + configCode 的向量
+     * 删除指定 videoId + configCode 的所有向量
      */
     int deleteByVideoIdAndConfigCode(@Param("videoId") Long videoId,
                                      @Param("configCode") String configCode);
 
     /**
-     * 批量删除指定 videoIds + configCode 的向量
+     * 批量删除指定 videoIds + configCode 的所有向量
      */
     int deleteBatchByVideoIdsAndConfigCode(@Param("videoIds") List<Long> videoIds,
                                            @Param("configCode") String configCode);
 
     /**
      * 余弦相似度搜索 Top-N
-     * @param configCode 配置编码
+     * @param configCode  配置编码
      * @param queryVector 查询向量字符串
-     * @param topN       返回数量
-     * @return VideoVector 列表(包含 videoId 和相似度得分)
+     * @param topN        返回数量
+     * @return VideoVector 列表(包含 videoId、pointIndex 和相似度得分)
      */
     List<VideoVector> searchTopNByCosine(@Param("configCode") String configCode,
                                         @Param("queryVector") String queryVector,

+ 1 - 1
core/src/main/java/com/tzld/videoVector/job/DataMigrationJob.java

@@ -301,7 +301,7 @@ public class DataMigrationJob {
 
                             // Redis 中存储的是归一化后的 JSON 数组 "[0.1,0.2,...]"
                             // pgvector 接受此格式
-                            pgVideoVectorMapper.upsertVector(videoId, configCode, vectorJson);
+                            pgVideoVectorMapper.upsertVector(videoId, configCode, 0, vectorJson);
                             totalSuccess++;
                         } catch (NumberFormatException e) {
                             log.warn("非法 videoId: {}", idStr);

+ 19 - 92
core/src/main/java/com/tzld/videoVector/job/VideoVectorJob.java

@@ -79,7 +79,7 @@ public class VideoVectorJob {
 
             // 2. 分页处理前,每次 Job 执行只做一次审核清理
             for (DeconstructVectorConfig config : configs) {
-                checkAndRemoveNotAuditPassedVideos(config.getConfigCode(), VectorUtils.isMultiPointConfig(config));
+                checkAndRemoveNotAuditPassedVideos(config.getConfigCode());
             }
             log.info("审核清理完成,开始分页向量化处理");
 
@@ -101,21 +101,8 @@ public class VideoVectorJob {
                     String configCode = config.getConfigCode();
 
                     // 3.0 审核清理已移至分页外,此处仅进行向量存在性检查
-                    // 3.1 查询哪些 videoId 在该配置下已有向量
-                    boolean multiPoint = VectorUtils.isMultiPointConfig(config);
-                    Set<Long> existingVideoIds;
-                    if (multiPoint) {
-                        // 多点模式:将 videoId 转为复合基准ID(videoId*100)检查存在性
-                        List<Long> baseIds = videoIds.stream()
-                                .map(id -> encodeMultiPointId(id, 0))
-                                .collect(Collectors.toList());
-                        Set<Long> existingBaseIds = vectorStoreService.existsByIds(configCode, baseIds);
-                        existingVideoIds = existingBaseIds.stream()
-                                .map(VideoVectorJob::decodeVideoId)
-                                .collect(Collectors.toSet());
-                    } else {
-                        existingVideoIds = vectorStoreService.existsByIds(configCode, videoIds);
-                    }
+                    // 3.1 查询哪些 videoId 在该配置下已有向量(数据库层已做 DISTINCT video_id)
+                    Set<Long> existingVideoIds = vectorStoreService.existsByIds(configCode, videoIds);
                     // 3.2 过滤出需要处理的 videoId(排除已有向量的)
                     List<Long> needProcessIds = videoIds.stream()
                             .filter(id -> !existingVideoIds.contains(id))
@@ -334,33 +321,14 @@ public class VideoVectorJob {
     }
 
     /**
-     * 编码多点复合ID
-     * @param videoId 原始视频ID
-     * @param index   点索引(0~99)
-     * @return 复合ID = videoId * 100 + index
-     */
-    private static Long encodeMultiPointId(Long videoId, int index) {
-        return VectorUtils.encodeMultiPointId(videoId, index);
-    }
-
-    /**
-     * 从复合ID解码出原始视频ID
-     * @param compositeId 复合ID
-     * @return 原始视频ID = compositeId / 100
-     */
-    private static Long decodeVideoId(Long compositeId) {
-        return VectorUtils.decodeVideoId(compositeId);
-    }
-
-    /**
-     * 向量化并存储(兼容单点和多点模式)
+     * 向量化并存储视频向量
      * <p>
      * 多点模式(extract_rule 非空):对 texts 中每个有效文本分别向量化,
-     * 以复合ID(videoId * 100 + index)存入同一 configCode 命名空间,
+     * 以真实 videoId + pointIndex 存入同一 configCode 命名空间,
      * 后续搜索时每个点都可被独立匹配。
      * <p>
      * 单点模式(extract_rule 为空):仅取第一段有效文本向量化,
-     * 直接以 videoId 存储(向后兼容)。
+     * 以 pointIndex=0 存储(向后兼容)。
      *
      * @return 成功向量化的数量(单点模式返回0或1)
      */
@@ -371,12 +339,7 @@ public class VideoVectorJob {
 
         if (multiPoint) {
             // ---- 多点模式:每个文本独立向量化存储 ----
-            int maxPoints = (int) VectorConstants.MULTI_POINT_FACTOR;
-            int limit = Math.min(texts.size(), maxPoints);
-            if (texts.size() > maxPoints) {
-                log.warn("videoId={} 配置 {} 文本数量 {} 超过多点模式上限 {},仅处理前 {} 个",
-                        videoId, configCode, texts.size(), maxPoints, maxPoints);
-            }
+            int limit = texts.size();
             int successCount = 0;
             for (int i = 0; i < limit; i++) {
                 String text = texts.get(i);
@@ -391,9 +354,8 @@ public class VideoVectorJob {
                     log.warn("videoId={} 配置 {} 第{}个文本向量化失败", videoId, configCode, i);
                     continue;
                 }
-                Long compositeId = encodeMultiPointId(videoId, i);
-                vectorStoreService.save(configCode, compositeId, vector);
-                log.debug("videoId={} 配置 {} 第{}个点向量化存储成功,compositeId={}", videoId, configCode, i, compositeId);
+                vectorStoreService.save(configCode, videoId, i, vector);
+                log.debug("videoId={} 配置 {} 第{}个点向量化存储成功", videoId, configCode, i);
                 successCount++;
             }
             return successCount;
@@ -504,20 +466,8 @@ public class VideoVectorJob {
             for (DeconstructVectorConfig config : configs) {
                 String configCode = config.getConfigCode();
 
-                // 4.1 查询该配置下已有向量的 videoId,排除已处理过的
-                boolean multiPoint = VectorUtils.isMultiPointConfig(config);
-                Set<Long> existingVideoIds;
-                if (multiPoint) {
-                    List<Long> baseIds = allVideoIds.stream()
-                            .map(id -> encodeMultiPointId(id, 0))
-                            .collect(Collectors.toList());
-                    Set<Long> existingBaseIds = vectorStoreService.existsByIds(configCode, baseIds);
-                    existingVideoIds = existingBaseIds.stream()
-                            .map(VideoVectorJob::decodeVideoId)
-                            .collect(Collectors.toSet());
-                } else {
-                    existingVideoIds = vectorStoreService.existsByIds(configCode, allVideoIds);
-                }
+                // 4.1 查询该配置下已有向量的 videoId,排除已处理过的(数据库层已做 DISTINCT video_id)
+                Set<Long> existingVideoIds = vectorStoreService.existsByIds(configCode, allVideoIds);
                 List<Long> needProcessIds = allVideoIds.stream()
                         .filter(id -> !existingVideoIds.contains(id))
                         .collect(Collectors.toList());
@@ -561,7 +511,7 @@ public class VideoVectorJob {
                             continue;
                         }
 
-                        // 向量化并写入 Redis(多点模式返回成功数>0即为成功)
+                        // 向量化并存储(多点模式返回成功数>0即为成功)
                         int storeCount = vectorizeAndStore(config, videoId, texts);
                         if (storeCount > 0) {
                             totalSuccessCount++;
@@ -717,9 +667,8 @@ public class VideoVectorJob {
      * 检查并移除审核状态不通过的视频向量
      *
      * @param configCode 配置编码
-     * @param multiPoint 是否为多点模式
      */
-    private void checkAndRemoveNotAuditPassedVideos(String configCode, boolean multiPoint) {
+    private void checkAndRemoveNotAuditPassedVideos(String configCode) {
         try {
             // 获取该配置下所有已有的存储ID
             Set<Long> allStoredIds = vectorStoreService.getAllVideoIds(configCode);
@@ -728,21 +677,10 @@ public class VideoVectorJob {
                 return;
             }
 
-            // 多点模式:存储ID是复合ID,需要还原为真实videoId
-            Set<Long> realVideoIds;
-            if (multiPoint) {
-                realVideoIds = allStoredIds.stream()
-                        .map(VideoVectorJob::decodeVideoId)
-                        .collect(Collectors.toSet());
-            } else {
-                realVideoIds = allStoredIds;
-            }
-
-            log.info("配置 {} 开始检查审核状态,共 {} 个视频(存储条目 {} 个)",
-                    configCode, realVideoIds.size(), allStoredIds.size());
+            log.info("配置 {} 开始检查审核状态,共 {} 个视频", configCode, allStoredIds.size());
 
-            // 分批检查审核状态
-            List<Long> videoIdList = new ArrayList<>(realVideoIds);
+            // 分批检查审核状态(数据库层 selectAllVideoIdsByConfigCode 已返回 DISTINCT video_id)
+            List<Long> videoIdList = new ArrayList<>(allStoredIds);
             int totalRemoved = 0;
 
             for (int i = 0; i < videoIdList.size(); i += VectorConstants.AUDIT_CHECK_BATCH_SIZE) {
@@ -753,20 +691,9 @@ public class VideoVectorJob {
                 Set<Long> notPassedIds = videoApiService.getNotAuditPassedVideoIds(batchIds);
 
                 if (!notPassedIds.isEmpty()) {
-                    if (multiPoint) {
-                        // 多点模式:找出所有属于未通过视频的复合ID进行删除
-                        Set<Long> compositeIdsToDelete = allStoredIds.stream()
-                                .filter(storedId -> notPassedIds.contains(decodeVideoId(storedId)))
-                                .collect(Collectors.toSet());
-                        vectorStoreService.deleteBatch(configCode, compositeIdsToDelete);
-                        totalRemoved += compositeIdsToDelete.size();
-                        log.info("配置 {} 移除审核不通过的视频 {} 个(向量条目 {} 个): {}",
-                                configCode, notPassedIds.size(), compositeIdsToDelete.size(), notPassedIds);
-                    } else {
-                        vectorStoreService.deleteBatch(configCode, notPassedIds);
-                        totalRemoved += notPassedIds.size();
-                        log.info("配置 {} 移除审核不通过的视频 {} 个: {}", configCode, notPassedIds.size(), notPassedIds);
-                    }
+                    vectorStoreService.deleteBatch(configCode, notPassedIds);
+                    totalRemoved += notPassedIds.size();
+                    log.info("配置 {} 移除审核不通过的视频 {} 个: {}", configCode, notPassedIds.size(), notPassedIds);
                 }
             }
 

+ 14 - 1
core/src/main/java/com/tzld/videoVector/model/entity/VideoMatch.java

@@ -14,6 +14,9 @@ public class VideoMatch {
     /** 命中的配置编码(用于区分来源) */
     private String configCode;
 
+    /** 向量点索引(多点模式下区分同一视频的不同向量点) */
+    private Integer pointIndex;
+
     public VideoMatch(Long videoId, double score) {
         this.videoId = videoId;
         this.score = score;
@@ -51,6 +54,16 @@ public class VideoMatch {
 
     @Override
     public String toString() {
-        return "VideoMatch{videoId=" + videoId + ", score=" + score + ", configCode='" + configCode + "'}";
+        return "VideoMatch{videoId=" + videoId + ", score=" + score +
+                ", configCode='" + configCode + "'" +
+                ", pointIndex=" + pointIndex + "}";
+    }
+
+    public Integer getPointIndex() {
+        return pointIndex;
+    }
+
+    public void setPointIndex(Integer pointIndex) {
+        this.pointIndex = pointIndex;
     }
 }

+ 64 - 16
core/src/main/java/com/tzld/videoVector/model/po/pgVector/VideoVector.java

@@ -3,56 +3,79 @@ package com.tzld.videoVector.model.po.pgVector;
 import java.util.Date;
 
 /**
+ * Database Table Remarks:
+ *   视频向量存储表:存储视频各维度的embedding向量,支持单点和多点模式
  *
  * This class was generated by MyBatis Generator.
  * This class corresponds to the database table video_vectors
  */
 public class VideoVector {
     /**
+     * Database Column Remarks:
+     *   自增主键
      *
      * This field was generated by MyBatis Generator.
      * This field corresponds to the database column video_vectors.id
      *
-     * @mbg.generated Wed Apr 29 15:13:43 CST 2026
+     * @mbg.generated Wed Apr 29 19:29:43 CST 2026
      */
     private Long id;
 
     /**
+     * Database Column Remarks:
+     *   视频ID(关联业务侧content_id)
      *
      * This field was generated by MyBatis Generator.
      * This field corresponds to the database column video_vectors.video_id
      *
-     * @mbg.generated Wed Apr 29 15:13:43 CST 2026
+     * @mbg.generated Wed Apr 29 19:29:43 CST 2026
      */
     private Long videoId;
 
     /**
+     * Database Column Remarks:
+     *   向量化配置编码(如VIDEO_TOPIC、VIDEO_KEYPOINT等)
      *
      * This field was generated by MyBatis Generator.
      * This field corresponds to the database column video_vectors.config_code
      *
-     * @mbg.generated Wed Apr 29 15:13:43 CST 2026
+     * @mbg.generated Wed Apr 29 19:29:43 CST 2026
      */
     private String configCode;
 
     /**
+     * Database Column Remarks:
+     *   创建时间
      *
      * This field was generated by MyBatis Generator.
      * This field corresponds to the database column video_vectors.created_at
      *
-     * @mbg.generated Wed Apr 29 15:13:43 CST 2026
+     * @mbg.generated Wed Apr 29 19:29:43 CST 2026
      */
     private Date createdAt;
 
     /**
+     * Database Column Remarks:
+     *   更新时间
      *
      * This field was generated by MyBatis Generator.
      * This field corresponds to the database column video_vectors.updated_at
      *
-     * @mbg.generated Wed Apr 29 15:13:43 CST 2026
+     * @mbg.generated Wed Apr 29 19:29:43 CST 2026
      */
     private Date updatedAt;
 
+    /**
+     * Database Column Remarks:
+     *   向量点索引:单点模式=0,多点模式=0,1,2,...
+     *
+     * This field was generated by MyBatis Generator.
+     * This field corresponds to the database column video_vectors.point_index
+     *
+     * @mbg.generated Wed Apr 29 19:29:43 CST 2026
+     */
+    private Integer pointIndex;
+
     /**
      * embedding 向量字段(vector(1024)类型,以字符串形式传递)
      * 格式示例: "[0.1,0.2,...]"
@@ -86,7 +109,7 @@ public class VideoVector {
      *
      * @return the value of video_vectors.id
      *
-     * @mbg.generated Wed Apr 29 15:13:43 CST 2026
+     * @mbg.generated Wed Apr 29 19:29:43 CST 2026
      */
     public Long getId() {
         return id;
@@ -98,7 +121,7 @@ public class VideoVector {
      *
      * @param id the value for video_vectors.id
      *
-     * @mbg.generated Wed Apr 29 15:13:43 CST 2026
+     * @mbg.generated Wed Apr 29 19:29:43 CST 2026
      */
     public void setId(Long id) {
         this.id = id;
@@ -110,7 +133,7 @@ public class VideoVector {
      *
      * @return the value of video_vectors.video_id
      *
-     * @mbg.generated Wed Apr 29 15:13:43 CST 2026
+     * @mbg.generated Wed Apr 29 19:29:43 CST 2026
      */
     public Long getVideoId() {
         return videoId;
@@ -122,7 +145,7 @@ public class VideoVector {
      *
      * @param videoId the value for video_vectors.video_id
      *
-     * @mbg.generated Wed Apr 29 15:13:43 CST 2026
+     * @mbg.generated Wed Apr 29 19:29:43 CST 2026
      */
     public void setVideoId(Long videoId) {
         this.videoId = videoId;
@@ -134,7 +157,7 @@ public class VideoVector {
      *
      * @return the value of video_vectors.config_code
      *
-     * @mbg.generated Wed Apr 29 15:13:43 CST 2026
+     * @mbg.generated Wed Apr 29 19:29:43 CST 2026
      */
     public String getConfigCode() {
         return configCode;
@@ -146,7 +169,7 @@ public class VideoVector {
      *
      * @param configCode the value for video_vectors.config_code
      *
-     * @mbg.generated Wed Apr 29 15:13:43 CST 2026
+     * @mbg.generated Wed Apr 29 19:29:43 CST 2026
      */
     public void setConfigCode(String configCode) {
         this.configCode = configCode;
@@ -158,7 +181,7 @@ public class VideoVector {
      *
      * @return the value of video_vectors.created_at
      *
-     * @mbg.generated Wed Apr 29 15:13:43 CST 2026
+     * @mbg.generated Wed Apr 29 19:29:43 CST 2026
      */
     public Date getCreatedAt() {
         return createdAt;
@@ -170,7 +193,7 @@ public class VideoVector {
      *
      * @param createdAt the value for video_vectors.created_at
      *
-     * @mbg.generated Wed Apr 29 15:13:43 CST 2026
+     * @mbg.generated Wed Apr 29 19:29:43 CST 2026
      */
     public void setCreatedAt(Date createdAt) {
         this.createdAt = createdAt;
@@ -182,7 +205,7 @@ public class VideoVector {
      *
      * @return the value of video_vectors.updated_at
      *
-     * @mbg.generated Wed Apr 29 15:13:43 CST 2026
+     * @mbg.generated Wed Apr 29 19:29:43 CST 2026
      */
     public Date getUpdatedAt() {
         return updatedAt;
@@ -194,17 +217,41 @@ public class VideoVector {
      *
      * @param updatedAt the value for video_vectors.updated_at
      *
-     * @mbg.generated Wed Apr 29 15:13:43 CST 2026
+     * @mbg.generated Wed Apr 29 19:29:43 CST 2026
      */
     public void setUpdatedAt(Date updatedAt) {
         this.updatedAt = updatedAt;
     }
 
+    /**
+     * This method was generated by MyBatis Generator.
+     * This method returns the value of the database column video_vectors.point_index
+     *
+     * @return the value of video_vectors.point_index
+     *
+     * @mbg.generated Wed Apr 29 19:29:43 CST 2026
+     */
+    public Integer getPointIndex() {
+        return pointIndex;
+    }
+
+    /**
+     * This method was generated by MyBatis Generator.
+     * This method sets the value of the database column video_vectors.point_index
+     *
+     * @param pointIndex the value for video_vectors.point_index
+     *
+     * @mbg.generated Wed Apr 29 19:29:43 CST 2026
+     */
+    public void setPointIndex(Integer pointIndex) {
+        this.pointIndex = pointIndex;
+    }
+
     /**
      * This method was generated by MyBatis Generator.
      * This method corresponds to the database table video_vectors
      *
-     * @mbg.generated Wed Apr 29 15:13:43 CST 2026
+     * @mbg.generated Wed Apr 29 19:29:43 CST 2026
      */
     @Override
     public String toString() {
@@ -217,6 +264,7 @@ public class VideoVector {
         sb.append(", configCode=").append(configCode);
         sb.append(", createdAt=").append(createdAt);
         sb.append(", updatedAt=").append(updatedAt);
+        sb.append(", pointIndex=").append(pointIndex);
         sb.append("]");
         return sb.toString();
     }

+ 77 - 17
core/src/main/java/com/tzld/videoVector/model/po/pgVector/VideoVectorExample.java

@@ -9,7 +9,7 @@ public class VideoVectorExample {
      * This field was generated by MyBatis Generator.
      * This field corresponds to the database table video_vectors
      *
-     * @mbg.generated Wed Apr 29 15:13:43 CST 2026
+     * @mbg.generated Wed Apr 29 19:29:43 CST 2026
      */
     protected String orderByClause;
 
@@ -17,7 +17,7 @@ public class VideoVectorExample {
      * This field was generated by MyBatis Generator.
      * This field corresponds to the database table video_vectors
      *
-     * @mbg.generated Wed Apr 29 15:13:43 CST 2026
+     * @mbg.generated Wed Apr 29 19:29:43 CST 2026
      */
     protected boolean distinct;
 
@@ -25,7 +25,7 @@ public class VideoVectorExample {
      * This field was generated by MyBatis Generator.
      * This field corresponds to the database table video_vectors
      *
-     * @mbg.generated Wed Apr 29 15:13:43 CST 2026
+     * @mbg.generated Wed Apr 29 19:29:43 CST 2026
      */
     protected List<Criteria> oredCriteria;
 
@@ -33,7 +33,7 @@ public class VideoVectorExample {
      * This method was generated by MyBatis Generator.
      * This method corresponds to the database table video_vectors
      *
-     * @mbg.generated Wed Apr 29 15:13:43 CST 2026
+     * @mbg.generated Wed Apr 29 19:29:43 CST 2026
      */
     public VideoVectorExample() {
         oredCriteria = new ArrayList<Criteria>();
@@ -43,7 +43,7 @@ public class VideoVectorExample {
      * This method was generated by MyBatis Generator.
      * This method corresponds to the database table video_vectors
      *
-     * @mbg.generated Wed Apr 29 15:13:43 CST 2026
+     * @mbg.generated Wed Apr 29 19:29:43 CST 2026
      */
     public void setOrderByClause(String orderByClause) {
         this.orderByClause = orderByClause;
@@ -53,7 +53,7 @@ public class VideoVectorExample {
      * This method was generated by MyBatis Generator.
      * This method corresponds to the database table video_vectors
      *
-     * @mbg.generated Wed Apr 29 15:13:43 CST 2026
+     * @mbg.generated Wed Apr 29 19:29:43 CST 2026
      */
     public String getOrderByClause() {
         return orderByClause;
@@ -63,7 +63,7 @@ public class VideoVectorExample {
      * This method was generated by MyBatis Generator.
      * This method corresponds to the database table video_vectors
      *
-     * @mbg.generated Wed Apr 29 15:13:43 CST 2026
+     * @mbg.generated Wed Apr 29 19:29:43 CST 2026
      */
     public void setDistinct(boolean distinct) {
         this.distinct = distinct;
@@ -73,7 +73,7 @@ public class VideoVectorExample {
      * This method was generated by MyBatis Generator.
      * This method corresponds to the database table video_vectors
      *
-     * @mbg.generated Wed Apr 29 15:13:43 CST 2026
+     * @mbg.generated Wed Apr 29 19:29:43 CST 2026
      */
     public boolean isDistinct() {
         return distinct;
@@ -83,7 +83,7 @@ public class VideoVectorExample {
      * This method was generated by MyBatis Generator.
      * This method corresponds to the database table video_vectors
      *
-     * @mbg.generated Wed Apr 29 15:13:43 CST 2026
+     * @mbg.generated Wed Apr 29 19:29:43 CST 2026
      */
     public List<Criteria> getOredCriteria() {
         return oredCriteria;
@@ -93,7 +93,7 @@ public class VideoVectorExample {
      * This method was generated by MyBatis Generator.
      * This method corresponds to the database table video_vectors
      *
-     * @mbg.generated Wed Apr 29 15:13:43 CST 2026
+     * @mbg.generated Wed Apr 29 19:29:43 CST 2026
      */
     public void or(Criteria criteria) {
         oredCriteria.add(criteria);
@@ -103,7 +103,7 @@ public class VideoVectorExample {
      * This method was generated by MyBatis Generator.
      * This method corresponds to the database table video_vectors
      *
-     * @mbg.generated Wed Apr 29 15:13:43 CST 2026
+     * @mbg.generated Wed Apr 29 19:29:43 CST 2026
      */
     public Criteria or() {
         Criteria criteria = createCriteriaInternal();
@@ -115,7 +115,7 @@ public class VideoVectorExample {
      * This method was generated by MyBatis Generator.
      * This method corresponds to the database table video_vectors
      *
-     * @mbg.generated Wed Apr 29 15:13:43 CST 2026
+     * @mbg.generated Wed Apr 29 19:29:43 CST 2026
      */
     public Criteria createCriteria() {
         Criteria criteria = createCriteriaInternal();
@@ -129,7 +129,7 @@ public class VideoVectorExample {
      * This method was generated by MyBatis Generator.
      * This method corresponds to the database table video_vectors
      *
-     * @mbg.generated Wed Apr 29 15:13:43 CST 2026
+     * @mbg.generated Wed Apr 29 19:29:43 CST 2026
      */
     protected Criteria createCriteriaInternal() {
         Criteria criteria = new Criteria();
@@ -140,7 +140,7 @@ public class VideoVectorExample {
      * This method was generated by MyBatis Generator.
      * This method corresponds to the database table video_vectors
      *
-     * @mbg.generated Wed Apr 29 15:13:43 CST 2026
+     * @mbg.generated Wed Apr 29 19:29:43 CST 2026
      */
     public void clear() {
         oredCriteria.clear();
@@ -152,7 +152,7 @@ public class VideoVectorExample {
      * This class was generated by MyBatis Generator.
      * This class corresponds to the database table video_vectors
      *
-     * @mbg.generated Wed Apr 29 15:13:43 CST 2026
+     * @mbg.generated Wed Apr 29 19:29:43 CST 2026
      */
     protected abstract static class GeneratedCriteria {
         protected List<Criterion> criteria;
@@ -504,13 +504,73 @@ public class VideoVectorExample {
             addCriterion("updated_at not between", value1, value2, "updatedAt");
             return (Criteria) this;
         }
+
+        public Criteria andPointIndexIsNull() {
+            addCriterion("point_index is null");
+            return (Criteria) this;
+        }
+
+        public Criteria andPointIndexIsNotNull() {
+            addCriterion("point_index is not null");
+            return (Criteria) this;
+        }
+
+        public Criteria andPointIndexEqualTo(Integer value) {
+            addCriterion("point_index =", value, "pointIndex");
+            return (Criteria) this;
+        }
+
+        public Criteria andPointIndexNotEqualTo(Integer value) {
+            addCriterion("point_index <>", value, "pointIndex");
+            return (Criteria) this;
+        }
+
+        public Criteria andPointIndexGreaterThan(Integer value) {
+            addCriterion("point_index >", value, "pointIndex");
+            return (Criteria) this;
+        }
+
+        public Criteria andPointIndexGreaterThanOrEqualTo(Integer value) {
+            addCriterion("point_index >=", value, "pointIndex");
+            return (Criteria) this;
+        }
+
+        public Criteria andPointIndexLessThan(Integer value) {
+            addCriterion("point_index <", value, "pointIndex");
+            return (Criteria) this;
+        }
+
+        public Criteria andPointIndexLessThanOrEqualTo(Integer value) {
+            addCriterion("point_index <=", value, "pointIndex");
+            return (Criteria) this;
+        }
+
+        public Criteria andPointIndexIn(List<Integer> values) {
+            addCriterion("point_index in", values, "pointIndex");
+            return (Criteria) this;
+        }
+
+        public Criteria andPointIndexNotIn(List<Integer> values) {
+            addCriterion("point_index not in", values, "pointIndex");
+            return (Criteria) this;
+        }
+
+        public Criteria andPointIndexBetween(Integer value1, Integer value2) {
+            addCriterion("point_index between", value1, value2, "pointIndex");
+            return (Criteria) this;
+        }
+
+        public Criteria andPointIndexNotBetween(Integer value1, Integer value2) {
+            addCriterion("point_index not between", value1, value2, "pointIndex");
+            return (Criteria) this;
+        }
     }
 
     /**
      * This class was generated by MyBatis Generator.
      * This class corresponds to the database table video_vectors
      *
-     * @mbg.generated do_not_delete_during_merge Wed Apr 29 15:13:43 CST 2026
+     * @mbg.generated do_not_delete_during_merge Wed Apr 29 19:29:43 CST 2026
      */
     public static class Criteria extends GeneratedCriteria {
 
@@ -523,7 +583,7 @@ public class VideoVectorExample {
      * This class was generated by MyBatis Generator.
      * This class corresponds to the database table video_vectors
      *
-     * @mbg.generated Wed Apr 29 15:13:43 CST 2026
+     * @mbg.generated Wed Apr 29 19:29:43 CST 2026
      */
     public static class Criterion {
         private String condition;

+ 10 - 1
core/src/main/java/com/tzld/videoVector/service/VectorStoreService.java

@@ -25,13 +25,22 @@ public interface VectorStoreService {
     void save(Long videoId, List<Float> vector);
 
     /**
-     * 保存视频向量(指定配置)
+     * 保存视频向量(指定配置,单点模式 pointIndex=0
      * @param configCode 配置编码
      * @param videoId    视频ID
      * @param vector     向量数据
      */
     void save(String configCode, Long videoId, List<Float> vector);
 
+    /**
+     * 保存视频向量(指定配置 + 点索引,支持多点模式)
+     * @param configCode 配置编码
+     * @param videoId    视频ID
+     * @param pointIndex 向量点索引(单点模式传0,多点模式传 0,1,2,...)
+     * @param vector     向量数据
+     */
+    void save(String configCode, Long videoId, int pointIndex, List<Float> vector);
+
     /**
      * 判断某个 videoId 的向量是否已存在(默认配置)
      * @param videoId 视频ID

+ 14 - 5
core/src/main/java/com/tzld/videoVector/service/impl/PgVectorStoreServiceImpl.java

@@ -20,8 +20,8 @@ import java.util.stream.Collectors;
  *
  * <p>存储结构:
  * <ul>
- *   <li>表:video_vectors (id, video_id, config_code, embedding vector(1024), created_at, updated_at)</li>
- *   <li>唯一索引:(video_id, config_code)</li>
+ *   <li>表:video_vectors (id, video_id, config_code, point_index, embedding vector(1024), created_at, updated_at)</li>
+ *   <li>唯一索引:(config_code, video_id, point_index)</li>
  *   <li>HNSW 索引:embedding 列,基于余弦距离</li>
  * </ul>
  */
@@ -41,6 +41,11 @@ public class PgVectorStoreServiceImpl implements VectorStoreService {
 
     @Override
     public void save(String configCode, Long videoId, List<Float> vector) {
+        save(configCode, videoId, 0, vector);
+    }
+
+    @Override
+    public void save(String configCode, Long videoId, int pointIndex, List<Float> vector) {
         if (videoId == null || vector == null || vector.isEmpty()) {
             log.warn("save 参数非法,configCode={}, videoId={}", configCode, videoId);
             return;
@@ -50,8 +55,8 @@ public class PgVectorStoreServiceImpl implements VectorStoreService {
         }
 
         String embedding = vectorToString(vector);
-        videoVectorMapper.upsertVector(videoId, configCode, embedding);
-        log.debug("保存向量成功,configCode={}, videoId={}, 维度={}", configCode, videoId, vector.size());
+        videoVectorMapper.upsertVector(videoId, configCode, pointIndex, embedding);
+        log.debug("保存向量成功,configCode={}, videoId={}, pointIndex={}, 维度={}", configCode, videoId, pointIndex, vector.size());
     }
 
     @Override
@@ -221,7 +226,11 @@ public class PgVectorStoreServiceImpl implements VectorStoreService {
         }
 
         List<VideoMatch> matches = results.stream()
-                .map(vv -> new VideoMatch(vv.getVideoId(), vv.getScore() != null ? vv.getScore() : 0.0))
+                .map(vv -> {
+                    VideoMatch m = new VideoMatch(vv.getVideoId(), vv.getScore() != null ? vv.getScore() : 0.0);
+                    m.setPointIndex(vv.getPointIndex());
+                    return m;
+                })
                 .collect(Collectors.toList());
 
         log.info("pgvector搜索完成,configCode={},返回 {} 条结果", configCode, matches.size());

+ 5 - 0
core/src/main/java/com/tzld/videoVector/service/impl/RedisVectorStoreServiceImpl.java

@@ -67,6 +67,11 @@ public class RedisVectorStoreServiceImpl implements VectorStoreService {
 
     @Override
     public void save(String configCode, Long videoId, List<Float> vector) {
+        save(configCode, videoId, 0, vector);
+    }
+
+    @Override
+    public void save(String configCode, Long videoId, int pointIndex, List<Float> vector) {
         if (videoId == null || vector == null || vector.isEmpty()) {
             log.warn("save 参数非法,configCode={}, videoId={}", configCode, videoId);
             return;

+ 9 - 8
core/src/main/java/com/tzld/videoVector/service/impl/VideoSearchServiceImpl.java

@@ -517,10 +517,10 @@ public class VideoSearchServiceImpl implements VideoSearchService {
                     continue;
                 }
 
-                // 多点模式:复合ID解码为真实videoId,同一videoId去重保留最高分
+                // 多点模式:同一videoId去重保留最高分
                 boolean multiPoint = VectorUtils.isMultiPointConfig(config);
                 if (multiPoint) {
-                    matches = decodeAndDeduplicateMultiPointMatches(matches, cfgCode);
+                    matches = deduplicateMultiPointMatches(matches, cfgCode);
                 } else {
                     // 单点模式:直接设置 configCode
                     for (VideoMatch m : matches) {
@@ -625,17 +625,18 @@ public class VideoSearchServiceImpl implements VideoSearchService {
     }
 
     /**
-     * 多点模式下将复合ID解码为真实videoId,并对同一videoId去重(保留最高分)
+     * 多点模式下对同一videoId去重(保留最高分)
+     * 由于 video_vectors 表已有独立的 point_index 列,搜索结果中的 videoId 即为真实值,
+     * 无需再从复合ID解码。
      */
-    private List<VideoMatch> decodeAndDeduplicateMultiPointMatches(List<VideoMatch> matches, String configCode) {
-        // compositeId -> realVideoId,同一videoId保留最高分
+    private List<VideoMatch> deduplicateMultiPointMatches(List<VideoMatch> matches, String configCode) {
         Map<Long, VideoMatch> deduped = new LinkedHashMap<>();
 
         for (VideoMatch match : matches) {
-            Long realVideoId = match.getVideoId() / VectorConstants.MULTI_POINT_FACTOR;
-            VideoMatch existing = deduped.get(realVideoId);
+            Long videoId = match.getVideoId();
+            VideoMatch existing = deduped.get(videoId);
             if (existing == null || match.getScore() > existing.getScore()) {
-                deduped.put(realVideoId, new VideoMatch(realVideoId, match.getScore(), configCode));
+                deduped.put(videoId, new VideoMatch(videoId, match.getScore(), configCode));
             }
         }
 

+ 0 - 35
core/src/main/java/com/tzld/videoVector/util/VectorUtils.java

@@ -4,7 +4,6 @@ import com.alibaba.fastjson.JSON;
 import com.alibaba.fastjson.JSONArray;
 import com.alibaba.fastjson.JSONObject;
 import com.tzld.videoVector.model.po.pgVector.DeconstructVectorConfig;
-import com.tzld.videoVector.common.constant.VectorConstants;
 import org.springframework.util.StringUtils;
 
 import java.util.ArrayList;
@@ -55,40 +54,6 @@ public final class VectorUtils {
         return config != null && StringUtils.hasText(config.getExtractRule());
     }
 
-    // ========================== 多点ID编解码 ==========================
-
-    /**
-     * 编码多点复合ID
-     * 复合ID = videoId * MULTI_POINT_FACTOR + pointIndex
-     *
-     * <p><b>注意:当前方案存在上限约束</b>(每个视频最多 {@code MULTI_POINT_FACTOR - 1} 个向量点),
-     * 若未来单视频向量点数超过此上限,需改为在 video_vectors 表中增加 point_index 列来替代复合ID方案。
-     *
-     * @param videoId   原始视频ID
-     * @param pointIndex 点索引(0 ~ MULTI_POINT_FACTOR - 1)
-     * @return 复合ID
-     * @throws IllegalArgumentException 如果 pointIndex 超出范围
-     */
-    public static long encodeMultiPointId(long videoId, int pointIndex) {
-        if (pointIndex < 0 || pointIndex >= VectorConstants.MULTI_POINT_FACTOR) {
-            throw new IllegalArgumentException(
-                    "pointIndex 超出范围: " + pointIndex + 
-                    ", 允许范围 [0, " + (VectorConstants.MULTI_POINT_FACTOR - 1) + "]");
-        }
-        return videoId * VectorConstants.MULTI_POINT_FACTOR + pointIndex;
-    }
-
-    /**
-     * 从复合ID解码出原始视频ID
-     * 解码公式: realVideoId = compositeId / MULTI_POINT_FACTOR
-     *
-     * @param compositeId 复合ID
-     * @return 原始视频ID
-     */
-    public static long decodeVideoId(long compositeId) {
-        return compositeId / VectorConstants.MULTI_POINT_FACTOR;
-    }
-
     // ========================== JSON 数组项提取 ==========================
 
     /**

+ 12 - 12
core/src/main/resources/generator/mybatis-pgvector-generator-config.xml

@@ -52,12 +52,12 @@
             <property name="enableSubPackages" value="true"/>
         </javaClientGenerator>
 
-        <!-- 视频向量主表(embedding 列类型 MBG 不识别,生成后需手动调整) -->
-<!--        <table tableName="video_vectors" domainObjectName="VideoVector" alias="">-->
-<!--            <generatedKey column="id" sqlStatement="JDBC" identity="true"/>-->
-<!--            &lt;!&ndash; pgvector 的 vector 类型 MBG 无法自动映射,忽略后手动补充 &ndash;&gt;-->
-<!--            <ignoreColumn column="embedding"/>-->
-<!--        </table>-->
+        <!-- 视频向量主表(embedding 列类型 MBG 不识别,生成后需手动补充) -->
+        <table tableName="video_vectors" domainObjectName="VideoVector" alias="">
+            <generatedKey column="id" sqlStatement="JDBC" identity="true"/>
+            <!-- pgvector 的 vector 类型 MBG 无法自动映射,忽略后手动补充 -->
+            <ignoreColumn column="embedding"/>
+        </table>
 
         <!-- 素材向量表(embedding 列类型 MBG 不识别,生成后需手动补充) -->
 <!--        <table tableName="content_vectors" domainObjectName="ContentVector" alias="">-->
@@ -67,14 +67,14 @@
 <!--        </table>-->
 
         <!-- 内容解构主表 -->
-        <table tableName="deconstruct_content" domainObjectName="DeconstructContent" alias="">
-            <generatedKey column="id" sqlStatement="JDBC" identity="true"/>
-        </table>
+<!--        <table tableName="deconstruct_content" domainObjectName="DeconstructContent" alias="">-->
+<!--            <generatedKey column="id" sqlStatement="JDBC" identity="true"/>-->
+<!--        </table>-->
 
         <!-- 向量化字段配置表 -->
-        <table tableName="deconstruct_vector_config" domainObjectName="DeconstructVectorConfig" alias="">
-            <generatedKey column="id" sqlStatement="JDBC" identity="true"/>
-        </table>
+<!--        <table tableName="deconstruct_vector_config" domainObjectName="DeconstructVectorConfig" alias="">-->
+<!--            <generatedKey column="id" sqlStatement="JDBC" identity="true"/>-->
+<!--        </table>-->
     </context>
 
 </generatorConfiguration>

+ 52 - 35
core/src/main/resources/mapper/pgVector/VideoVectorMapper.xml

@@ -5,19 +5,20 @@
     <!--
       WARNING - @mbg.generated
       This element is automatically generated by MyBatis Generator, do not modify.
-      This element was generated on Wed Apr 29 15:13:43 CST 2026.
+      This element was generated on Wed Apr 29 19:29:43 CST 2026.
     -->
     <id column="id" jdbcType="BIGINT" property="id" />
     <result column="video_id" jdbcType="BIGINT" property="videoId" />
     <result column="config_code" jdbcType="VARCHAR" property="configCode" />
     <result column="created_at" jdbcType="TIMESTAMP" property="createdAt" />
     <result column="updated_at" jdbcType="TIMESTAMP" property="updatedAt" />
+    <result column="point_index" jdbcType="INTEGER" property="pointIndex" />
   </resultMap>
   <sql id="Example_Where_Clause">
     <!--
       WARNING - @mbg.generated
       This element is automatically generated by MyBatis Generator, do not modify.
-      This element was generated on Wed Apr 29 15:13:43 CST 2026.
+      This element was generated on Wed Apr 29 19:29:43 CST 2026.
     -->
     <where>
       <foreach collection="oredCriteria" item="criteria" separator="or">
@@ -51,7 +52,7 @@
     <!--
       WARNING - @mbg.generated
       This element is automatically generated by MyBatis Generator, do not modify.
-      This element was generated on Wed Apr 29 15:13:43 CST 2026.
+      This element was generated on Wed Apr 29 19:29:43 CST 2026.
     -->
     <where>
       <foreach collection="example.oredCriteria" item="criteria" separator="or">
@@ -85,15 +86,15 @@
     <!--
       WARNING - @mbg.generated
       This element is automatically generated by MyBatis Generator, do not modify.
-      This element was generated on Wed Apr 29 15:13:43 CST 2026.
+      This element was generated on Wed Apr 29 19:29:43 CST 2026.
     -->
-    id, video_id, config_code, created_at, updated_at
+    id, video_id, config_code, created_at, updated_at, point_index
   </sql>
   <select id="selectByExample" parameterType="com.tzld.videoVector.model.po.pgVector.VideoVectorExample" resultMap="BaseResultMap">
     <!--
       WARNING - @mbg.generated
       This element is automatically generated by MyBatis Generator, do not modify.
-      This element was generated on Wed Apr 29 15:13:43 CST 2026.
+      This element was generated on Wed Apr 29 19:29:43 CST 2026.
     -->
     select
     <if test="distinct">
@@ -112,7 +113,7 @@
     <!--
       WARNING - @mbg.generated
       This element is automatically generated by MyBatis Generator, do not modify.
-      This element was generated on Wed Apr 29 15:13:43 CST 2026.
+      This element was generated on Wed Apr 29 19:29:43 CST 2026.
     -->
     select 
     <include refid="Base_Column_List" />
@@ -123,7 +124,7 @@
     <!--
       WARNING - @mbg.generated
       This element is automatically generated by MyBatis Generator, do not modify.
-      This element was generated on Wed Apr 29 15:13:43 CST 2026.
+      This element was generated on Wed Apr 29 19:29:43 CST 2026.
     -->
     delete from video_vectors
     where id = #{id,jdbcType=BIGINT}
@@ -132,7 +133,7 @@
     <!--
       WARNING - @mbg.generated
       This element is automatically generated by MyBatis Generator, do not modify.
-      This element was generated on Wed Apr 29 15:13:43 CST 2026.
+      This element was generated on Wed Apr 29 19:29:43 CST 2026.
     -->
     delete from video_vectors
     <if test="_parameter != null">
@@ -143,18 +144,18 @@
     <!--
       WARNING - @mbg.generated
       This element is automatically generated by MyBatis Generator, do not modify.
-      This element was generated on Wed Apr 29 15:13:43 CST 2026.
+      This element was generated on Wed Apr 29 19:29:43 CST 2026.
     -->
     insert into video_vectors (video_id, config_code, created_at, 
-      updated_at)
+      updated_at, point_index)
     values (#{videoId,jdbcType=BIGINT}, #{configCode,jdbcType=VARCHAR}, #{createdAt,jdbcType=TIMESTAMP}, 
-      #{updatedAt,jdbcType=TIMESTAMP})
+      #{updatedAt,jdbcType=TIMESTAMP}, #{pointIndex,jdbcType=INTEGER})
   </insert>
   <insert id="insertSelective" keyColumn="id" keyProperty="id" parameterType="com.tzld.videoVector.model.po.pgVector.VideoVector" useGeneratedKeys="true">
     <!--
       WARNING - @mbg.generated
       This element is automatically generated by MyBatis Generator, do not modify.
-      This element was generated on Wed Apr 29 15:13:43 CST 2026.
+      This element was generated on Wed Apr 29 19:29:43 CST 2026.
     -->
     insert into video_vectors
     <trim prefix="(" suffix=")" suffixOverrides=",">
@@ -170,6 +171,9 @@
       <if test="updatedAt != null">
         updated_at,
       </if>
+      <if test="pointIndex != null">
+        point_index,
+      </if>
     </trim>
     <trim prefix="values (" suffix=")" suffixOverrides=",">
       <if test="videoId != null">
@@ -184,13 +188,16 @@
       <if test="updatedAt != null">
         #{updatedAt,jdbcType=TIMESTAMP},
       </if>
+      <if test="pointIndex != null">
+        #{pointIndex,jdbcType=INTEGER},
+      </if>
     </trim>
   </insert>
   <select id="countByExample" parameterType="com.tzld.videoVector.model.po.pgVector.VideoVectorExample" resultType="java.lang.Long">
     <!--
       WARNING - @mbg.generated
       This element is automatically generated by MyBatis Generator, do not modify.
-      This element was generated on Wed Apr 29 15:13:43 CST 2026.
+      This element was generated on Wed Apr 29 19:29:43 CST 2026.
     -->
     select count(*) from video_vectors
     <if test="_parameter != null">
@@ -201,7 +208,7 @@
     <!--
       WARNING - @mbg.generated
       This element is automatically generated by MyBatis Generator, do not modify.
-      This element was generated on Wed Apr 29 15:13:43 CST 2026.
+      This element was generated on Wed Apr 29 19:29:43 CST 2026.
     -->
     update video_vectors
     <set>
@@ -220,6 +227,9 @@
       <if test="record.updatedAt != null">
         updated_at = #{record.updatedAt,jdbcType=TIMESTAMP},
       </if>
+      <if test="record.pointIndex != null">
+        point_index = #{record.pointIndex,jdbcType=INTEGER},
+      </if>
     </set>
     <if test="_parameter != null">
       <include refid="Update_By_Example_Where_Clause" />
@@ -229,14 +239,15 @@
     <!--
       WARNING - @mbg.generated
       This element is automatically generated by MyBatis Generator, do not modify.
-      This element was generated on Wed Apr 29 15:13:43 CST 2026.
+      This element was generated on Wed Apr 29 19:29:43 CST 2026.
     -->
     update video_vectors
     set id = #{record.id,jdbcType=BIGINT},
       video_id = #{record.videoId,jdbcType=BIGINT},
       config_code = #{record.configCode,jdbcType=VARCHAR},
       created_at = #{record.createdAt,jdbcType=TIMESTAMP},
-      updated_at = #{record.updatedAt,jdbcType=TIMESTAMP}
+      updated_at = #{record.updatedAt,jdbcType=TIMESTAMP},
+      point_index = #{record.pointIndex,jdbcType=INTEGER}
     <if test="_parameter != null">
       <include refid="Update_By_Example_Where_Clause" />
     </if>
@@ -245,7 +256,7 @@
     <!--
       WARNING - @mbg.generated
       This element is automatically generated by MyBatis Generator, do not modify.
-      This element was generated on Wed Apr 29 15:13:43 CST 2026.
+      This element was generated on Wed Apr 29 19:29:43 CST 2026.
     -->
     update video_vectors
     <set>
@@ -261,6 +272,9 @@
       <if test="updatedAt != null">
         updated_at = #{updatedAt,jdbcType=TIMESTAMP},
       </if>
+      <if test="pointIndex != null">
+        point_index = #{pointIndex,jdbcType=INTEGER},
+      </if>
     </set>
     where id = #{id,jdbcType=BIGINT}
   </update>
@@ -268,35 +282,36 @@
     <!--
       WARNING - @mbg.generated
       This element is automatically generated by MyBatis Generator, do not modify.
-      This element was generated on Wed Apr 29 15:13:43 CST 2026.
+      This element was generated on Wed Apr 29 19:29:43 CST 2026.
     -->
     update video_vectors
     set video_id = #{videoId,jdbcType=BIGINT},
       config_code = #{configCode,jdbcType=VARCHAR},
       created_at = #{createdAt,jdbcType=TIMESTAMP},
-      updated_at = #{updatedAt,jdbcType=TIMESTAMP}
+      updated_at = #{updatedAt,jdbcType=TIMESTAMP},
+      point_index = #{pointIndex,jdbcType=INTEGER}
     where id = #{id,jdbcType=BIGINT}
   </update>
 
   <!-- ==================== 自定义向量操作 SQL ==================== -->
 
-  <!-- Upsert: 插入或更新向量 -->
+  <!-- Upsert: 插入或更新向量(支持多点模式) -->
   <insert id="upsertVector">
-    INSERT INTO video_vectors (video_id, config_code, embedding, created_at, updated_at)
-    VALUES (#{videoId}, #{configCode}, #{embedding}::vector, NOW(), NOW())
-    ON CONFLICT (video_id, config_code)
+    INSERT INTO video_vectors (video_id, config_code, point_index, embedding, created_at, updated_at)
+    VALUES (#{videoId}, #{configCode}, #{pointIndex}, #{embedding}::vector, NOW(), NOW())
+    ON CONFLICT (config_code, video_id, point_index)
     DO UPDATE SET embedding = EXCLUDED.embedding, updated_at = NOW()
   </insert>
 
-  <!-- 判断是否存在 -->
+  <!-- 判断是否存在(任意 pointIndex) -->
   <select id="existsByVideoIdAndConfigCode" resultType="int">
     SELECT COUNT(1) FROM video_vectors
     WHERE video_id = #{videoId} AND config_code = #{configCode}
   </select>
 
-  <!-- 批量查询已存在的 videoId -->
+  <!-- 批量查询已存在的 videoId(去重) -->
   <select id="selectExistingVideoIds" resultType="java.lang.Long">
-    SELECT video_id FROM video_vectors
+    SELECT DISTINCT video_id FROM video_vectors
     WHERE config_code = #{configCode}
     AND video_id IN
     <foreach collection="videoIds" item="vid" open="(" separator="," close=")">
@@ -304,10 +319,10 @@
     </foreach>
   </select>
 
-  <!-- 获取单个向量 -->
+  <!-- 获取单个向量(取 pointIndex=0) -->
   <select id="selectEmbeddingByVideoIdAndConfigCode" resultType="java.lang.String">
     SELECT embedding::text FROM video_vectors
-    WHERE video_id = #{videoId} AND config_code = #{configCode}
+    WHERE video_id = #{videoId} AND config_code = #{configCode} AND point_index = 0
   </select>
 
   <!-- 批量获取向量 -->
@@ -315,13 +330,14 @@
     <id column="id" jdbcType="BIGINT" property="id" />
     <result column="video_id" jdbcType="BIGINT" property="videoId" />
     <result column="config_code" jdbcType="VARCHAR" property="configCode" />
+    <result column="point_index" jdbcType="INTEGER" property="pointIndex" />
     <result column="embedding" jdbcType="VARCHAR" property="embedding" />
     <result column="created_at" jdbcType="TIMESTAMP" property="createdAt" />
     <result column="updated_at" jdbcType="TIMESTAMP" property="updatedAt" />
   </resultMap>
 
   <select id="selectVectorsByVideoIds" resultMap="VectorWithEmbeddingResultMap">
-    SELECT id, video_id, config_code, embedding::text as embedding, created_at, updated_at
+    SELECT id, video_id, config_code, point_index, embedding::text as embedding, created_at, updated_at
     FROM video_vectors
     WHERE config_code = #{configCode}
     AND video_id IN
@@ -330,13 +346,13 @@
     </foreach>
   </select>
 
-  <!-- 获取指定 configCode 下所有 videoId -->
+  <!-- 获取指定 configCode 下所有 videoId(去重) -->
   <select id="selectAllVideoIdsByConfigCode" resultType="java.lang.Long">
-    SELECT video_id FROM video_vectors
+    SELECT DISTINCT video_id FROM video_vectors
     WHERE config_code = #{configCode}
   </select>
 
-  <!-- 删除单个向量 -->
+  <!-- 删除单个视频的所有向量 -->
   <delete id="deleteByVideoIdAndConfigCode">
     DELETE FROM video_vectors
     WHERE video_id = #{videoId} AND config_code = #{configCode}
@@ -352,14 +368,15 @@
     </foreach>
   </delete>
 
-  <!-- 余弦相似度搜索 Top-N -->
+  <!-- 余弦相似度搜索 Top-N(返回 videoId + pointIndex + score) -->
   <resultMap id="SearchResultMap" type="com.tzld.videoVector.model.po.pgVector.VideoVector">
     <result column="video_id" jdbcType="BIGINT" property="videoId" />
+    <result column="point_index" jdbcType="INTEGER" property="pointIndex" />
     <result column="score" jdbcType="DOUBLE" property="score" />
   </resultMap>
 
   <select id="searchTopNByCosine" resultMap="SearchResultMap">
-    SELECT video_id, 1 - (embedding &lt;=&gt; #{queryVector}::vector) AS score
+    SELECT video_id, point_index, 1 - (embedding &lt;=&gt; #{queryVector}::vector) AS score
     FROM video_vectors
     WHERE config_code = #{configCode}
     ORDER BY embedding &lt;=&gt; #{queryVector}::vector