You've already forked DataMate
feat(knowledge-base): 添加知识库文件全库检索功能
- 新增相对路径字段替代原有的metadata存储方式 - 实现跨知识库文件检索接口searchFiles - 添加前端全库检索页面和相关API调用 - 优化文件路径处理和数据库索引配置 - 统一请求参数类型定义为RequestPayload和RequestParams - 简化RagFile模型中的元数据结构设计
This commit is contained in:
@@ -35,10 +35,10 @@ import org.springframework.transaction.annotation.Transactional;
|
|||||||
import org.springframework.util.StringUtils;
|
import org.springframework.util.StringUtils;
|
||||||
|
|
||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
import java.util.HashMap;
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 知识库服务类
|
* 知识库服务类
|
||||||
@@ -49,7 +49,6 @@ import java.util.Optional;
|
|||||||
@Service
|
@Service
|
||||||
@RequiredArgsConstructor
|
@RequiredArgsConstructor
|
||||||
public class KnowledgeBaseService {
|
public class KnowledgeBaseService {
|
||||||
private static final String RELATIVE_PATH_KEY = "relativePath";
|
|
||||||
private static final String PATH_SEPARATOR = "/";
|
private static final String PATH_SEPARATOR = "/";
|
||||||
private final KnowledgeBaseRepository knowledgeBaseRepository;
|
private final KnowledgeBaseRepository knowledgeBaseRepository;
|
||||||
private final RagFileRepository ragFileRepository;
|
private final RagFileRepository ragFileRepository;
|
||||||
@@ -150,12 +149,7 @@ public class KnowledgeBaseService {
|
|||||||
ragFile.setKnowledgeBaseId(knowledgeBase.getId());
|
ragFile.setKnowledgeBaseId(knowledgeBase.getId());
|
||||||
ragFile.setFileId(fileInfo.id());
|
ragFile.setFileId(fileInfo.id());
|
||||||
ragFile.setFileName(fileInfo.fileName());
|
ragFile.setFileName(fileInfo.fileName());
|
||||||
String relativePath = normalizeRelativePath(fileInfo.relativePath());
|
ragFile.setRelativePath(normalizeRelativePath(fileInfo.relativePath()));
|
||||||
if (StringUtils.hasText(relativePath)) {
|
|
||||||
Map<String, Object> metadata = new HashMap<>();
|
|
||||||
metadata.put(RELATIVE_PATH_KEY, relativePath);
|
|
||||||
ragFile.setMetadata(metadata);
|
|
||||||
}
|
|
||||||
ragFile.setStatus(FileStatus.UNPROCESSED);
|
ragFile.setStatus(FileStatus.UNPROCESSED);
|
||||||
return ragFile;
|
return ragFile;
|
||||||
}).toList();
|
}).toList();
|
||||||
@@ -181,6 +175,41 @@ public class KnowledgeBaseService {
|
|||||||
return PagedResponse.of(page.getRecords(), page.getCurrent(), page.getTotal(), page.getPages());
|
return PagedResponse.of(page.getRecords(), page.getCurrent(), page.getTotal(), page.getPages());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public PagedResponse<KnowledgeBaseFileSearchResp> searchFiles(KnowledgeBaseFileSearchReq request) {
|
||||||
|
IPage<RagFile> page = new Page<>(request.getPage(), request.getSize());
|
||||||
|
page = ragFileRepository.searchPage(page, request);
|
||||||
|
List<RagFile> records = page.getRecords();
|
||||||
|
if (records.isEmpty()) {
|
||||||
|
return PagedResponse.of(Collections.emptyList(), page.getCurrent(), page.getTotal(), page.getPages());
|
||||||
|
}
|
||||||
|
|
||||||
|
List<String> knowledgeBaseIds = records.stream()
|
||||||
|
.map(RagFile::getKnowledgeBaseId)
|
||||||
|
.filter(StringUtils::hasText)
|
||||||
|
.distinct()
|
||||||
|
.toList();
|
||||||
|
Map<String, String> knowledgeBaseNameMap = knowledgeBaseRepository.listByIds(knowledgeBaseIds).stream()
|
||||||
|
.collect(Collectors.toMap(KnowledgeBase::getId, KnowledgeBase::getName));
|
||||||
|
|
||||||
|
List<KnowledgeBaseFileSearchResp> responses = records.stream()
|
||||||
|
.map(file -> {
|
||||||
|
KnowledgeBaseFileSearchResp resp = new KnowledgeBaseFileSearchResp();
|
||||||
|
resp.setId(file.getId());
|
||||||
|
resp.setKnowledgeBaseId(file.getKnowledgeBaseId());
|
||||||
|
resp.setKnowledgeBaseName(knowledgeBaseNameMap.getOrDefault(file.getKnowledgeBaseId(), ""));
|
||||||
|
resp.setFileName(file.getFileName());
|
||||||
|
resp.setRelativePath(file.getRelativePath());
|
||||||
|
resp.setChunkCount(file.getChunkCount());
|
||||||
|
resp.setStatus(file.getStatus());
|
||||||
|
resp.setCreatedAt(file.getCreatedAt());
|
||||||
|
resp.setUpdatedAt(file.getUpdatedAt());
|
||||||
|
return resp;
|
||||||
|
})
|
||||||
|
.toList();
|
||||||
|
|
||||||
|
return PagedResponse.of(responses, page.getCurrent(), page.getTotal(), page.getPages());
|
||||||
|
}
|
||||||
|
|
||||||
@Transactional(rollbackFor = Exception.class)
|
@Transactional(rollbackFor = Exception.class)
|
||||||
public void deleteFiles(String knowledgeBaseId, DeleteFilesReq request) {
|
public void deleteFiles(String knowledgeBaseId, DeleteFilesReq request) {
|
||||||
KnowledgeBase knowledgeBase = Optional.ofNullable(knowledgeBaseRepository.getById(knowledgeBaseId))
|
KnowledgeBase knowledgeBase = Optional.ofNullable(knowledgeBaseRepository.getById(knowledgeBaseId))
|
||||||
|
|||||||
@@ -28,6 +28,10 @@ public class RagFile extends BaseEntity<String> {
|
|||||||
* 文件名
|
* 文件名
|
||||||
*/
|
*/
|
||||||
private String fileName;
|
private String fileName;
|
||||||
|
/**
|
||||||
|
* 相对路径
|
||||||
|
*/
|
||||||
|
private String relativePath;
|
||||||
/**
|
/**
|
||||||
* 文件ID
|
* 文件ID
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ package com.datamate.rag.indexer.domain.repository;
|
|||||||
import com.baomidou.mybatisplus.core.metadata.IPage;
|
import com.baomidou.mybatisplus.core.metadata.IPage;
|
||||||
import com.baomidou.mybatisplus.extension.repository.IRepository;
|
import com.baomidou.mybatisplus.extension.repository.IRepository;
|
||||||
import com.datamate.rag.indexer.domain.model.RagFile;
|
import com.datamate.rag.indexer.domain.model.RagFile;
|
||||||
|
import com.datamate.rag.indexer.interfaces.dto.KnowledgeBaseFileSearchReq;
|
||||||
import com.datamate.rag.indexer.interfaces.dto.RagFileReq;
|
import com.datamate.rag.indexer.interfaces.dto.RagFileReq;
|
||||||
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
@@ -21,4 +22,6 @@ public interface RagFileRepository extends IRepository<RagFile> {
|
|||||||
List<RagFile> findAllByKnowledgeBaseId(String knowledgeBaseId);
|
List<RagFile> findAllByKnowledgeBaseId(String knowledgeBaseId);
|
||||||
|
|
||||||
IPage<RagFile> page(IPage<RagFile> page, RagFileReq request);
|
IPage<RagFile> page(IPage<RagFile> page, RagFileReq request);
|
||||||
|
|
||||||
|
IPage<RagFile> searchPage(IPage<RagFile> page, KnowledgeBaseFileSearchReq request);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import com.datamate.rag.indexer.domain.model.FileStatus;
|
|||||||
import com.datamate.rag.indexer.domain.model.RagFile;
|
import com.datamate.rag.indexer.domain.model.RagFile;
|
||||||
import com.datamate.rag.indexer.domain.repository.RagFileRepository;
|
import com.datamate.rag.indexer.domain.repository.RagFileRepository;
|
||||||
import com.datamate.rag.indexer.infrastructure.persistence.mapper.RagFileMapper;
|
import com.datamate.rag.indexer.infrastructure.persistence.mapper.RagFileMapper;
|
||||||
|
import com.datamate.rag.indexer.interfaces.dto.KnowledgeBaseFileSearchReq;
|
||||||
import com.datamate.rag.indexer.interfaces.dto.RagFileReq;
|
import com.datamate.rag.indexer.interfaces.dto.RagFileReq;
|
||||||
import org.springframework.stereotype.Repository;
|
import org.springframework.stereotype.Repository;
|
||||||
import org.springframework.util.StringUtils;
|
import org.springframework.util.StringUtils;
|
||||||
@@ -20,7 +21,6 @@ import java.util.List;
|
|||||||
*/
|
*/
|
||||||
@Repository
|
@Repository
|
||||||
public class RagFileRepositoryImpl extends CrudRepository<RagFileMapper, RagFile> implements RagFileRepository {
|
public class RagFileRepositoryImpl extends CrudRepository<RagFileMapper, RagFile> implements RagFileRepository {
|
||||||
private static final String RELATIVE_PATH_KEY = "\"relativePath\":\"";
|
|
||||||
private static final String PATH_SEPARATOR = "/";
|
private static final String PATH_SEPARATOR = "/";
|
||||||
@Override
|
@Override
|
||||||
public void removeByKnowledgeBaseId(String knowledgeBaseId) {
|
public void removeByKnowledgeBaseId(String knowledgeBaseId) {
|
||||||
@@ -44,15 +44,23 @@ public class RagFileRepositoryImpl extends CrudRepository<RagFileMapper, RagFile
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public IPage<RagFile> page(IPage<RagFile> page, RagFileReq request) {
|
public IPage<RagFile> page(IPage<RagFile> page, RagFileReq request) {
|
||||||
String relativePathPattern = buildRelativePathPattern(request.getRelativePath());
|
|
||||||
return lambdaQuery()
|
return lambdaQuery()
|
||||||
.eq(RagFile::getKnowledgeBaseId, request.getKnowledgeBaseId())
|
.eq(RagFile::getKnowledgeBaseId, request.getKnowledgeBaseId())
|
||||||
.like(StringUtils.hasText(request.getFileName()), RagFile::getFileName, request.getFileName())
|
.like(StringUtils.hasText(request.getFileName()), RagFile::getFileName, request.getFileName())
|
||||||
.like(StringUtils.hasText(relativePathPattern), RagFile::getMetadata, relativePathPattern)
|
.likeRight(StringUtils.hasText(request.getRelativePath()), RagFile::getRelativePath, normalizeRelativePath(request.getRelativePath()))
|
||||||
.page(page);
|
.page(page);
|
||||||
}
|
}
|
||||||
|
|
||||||
private String buildRelativePathPattern(String relativePath) {
|
@Override
|
||||||
|
public IPage<RagFile> searchPage(IPage<RagFile> page, KnowledgeBaseFileSearchReq request) {
|
||||||
|
return lambdaQuery()
|
||||||
|
.eq(StringUtils.hasText(request.getKnowledgeBaseId()), RagFile::getKnowledgeBaseId, request.getKnowledgeBaseId())
|
||||||
|
.like(StringUtils.hasText(request.getFileName()), RagFile::getFileName, request.getFileName())
|
||||||
|
.likeRight(StringUtils.hasText(request.getRelativePath()), RagFile::getRelativePath, normalizeRelativePath(request.getRelativePath()))
|
||||||
|
.page(page);
|
||||||
|
}
|
||||||
|
|
||||||
|
private String normalizeRelativePath(String relativePath) {
|
||||||
if (!StringUtils.hasText(relativePath)) {
|
if (!StringUtils.hasText(relativePath)) {
|
||||||
return "";
|
return "";
|
||||||
}
|
}
|
||||||
@@ -60,9 +68,6 @@ public class RagFileRepositoryImpl extends CrudRepository<RagFileMapper, RagFile
|
|||||||
while (normalized.startsWith(PATH_SEPARATOR)) {
|
while (normalized.startsWith(PATH_SEPARATOR)) {
|
||||||
normalized = normalized.substring(1);
|
normalized = normalized.substring(1);
|
||||||
}
|
}
|
||||||
if (!StringUtils.hasText(normalized)) {
|
return normalized;
|
||||||
return "";
|
|
||||||
}
|
|
||||||
return RELATIVE_PATH_KEY + normalized;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -105,6 +105,17 @@ public class KnowledgeBaseController {
|
|||||||
return knowledgeBaseService.listFiles(knowledgeBaseId, request);
|
return knowledgeBaseService.listFiles(knowledgeBaseId, request);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 全库检索知识库文件(跨知识库)
|
||||||
|
*
|
||||||
|
* @param request 检索请求
|
||||||
|
* @return 文件列表
|
||||||
|
*/
|
||||||
|
@GetMapping("/files/search")
|
||||||
|
public PagedResponse<KnowledgeBaseFileSearchResp> searchFiles(KnowledgeBaseFileSearchReq request) {
|
||||||
|
return knowledgeBaseService.searchFiles(request);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 删除知识库文件
|
* 删除知识库文件
|
||||||
*
|
*
|
||||||
|
|||||||
@@ -0,0 +1,19 @@
|
|||||||
|
package com.datamate.rag.indexer.interfaces.dto;
|
||||||
|
|
||||||
|
import com.datamate.common.interfaces.PagingQuery;
|
||||||
|
import lombok.Getter;
|
||||||
|
import lombok.Setter;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 知识库文件全库检索请求
|
||||||
|
*
|
||||||
|
* @author dallas
|
||||||
|
* @since 2026-01-30
|
||||||
|
*/
|
||||||
|
@Getter
|
||||||
|
@Setter
|
||||||
|
public class KnowledgeBaseFileSearchReq extends PagingQuery {
|
||||||
|
private String fileName;
|
||||||
|
private String relativePath;
|
||||||
|
private String knowledgeBaseId;
|
||||||
|
}
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
package com.datamate.rag.indexer.interfaces.dto;
|
||||||
|
|
||||||
|
import com.datamate.rag.indexer.domain.model.FileStatus;
|
||||||
|
import lombok.Getter;
|
||||||
|
import lombok.Setter;
|
||||||
|
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 知识库文件全库检索响应
|
||||||
|
*
|
||||||
|
* @author dallas
|
||||||
|
* @since 2026-01-30
|
||||||
|
*/
|
||||||
|
@Getter
|
||||||
|
@Setter
|
||||||
|
public class KnowledgeBaseFileSearchResp {
|
||||||
|
private String id;
|
||||||
|
private String knowledgeBaseId;
|
||||||
|
private String knowledgeBaseName;
|
||||||
|
private String fileName;
|
||||||
|
private String relativePath;
|
||||||
|
private Integer chunkCount;
|
||||||
|
private FileStatus status;
|
||||||
|
private LocalDateTime createdAt;
|
||||||
|
private LocalDateTime updatedAt;
|
||||||
|
}
|
||||||
@@ -17,7 +17,7 @@ import {
|
|||||||
EditOutlined,
|
EditOutlined,
|
||||||
ReloadOutlined,
|
ReloadOutlined,
|
||||||
} from "@ant-design/icons";
|
} from "@ant-design/icons";
|
||||||
import { useNavigate, useParams } from "react-router";
|
import { useNavigate, useParams, useSearchParams } from "react-router";
|
||||||
import DetailHeader from "@/components/DetailHeader";
|
import DetailHeader from "@/components/DetailHeader";
|
||||||
import { SearchControls } from "@/components/SearchControls";
|
import { SearchControls } from "@/components/SearchControls";
|
||||||
import { KBFile, KnowledgeBaseItem } from "../knowledge-base.model";
|
import { KBFile, KnowledgeBaseItem } from "../knowledge-base.model";
|
||||||
@@ -58,7 +58,6 @@ type KBFileRow = KBFile & {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const PATH_SEPARATOR = "/";
|
const PATH_SEPARATOR = "/";
|
||||||
const RELATIVE_PATH_KEY = "relativePath";
|
|
||||||
const normalizePath = (value?: string) =>
|
const normalizePath = (value?: string) =>
|
||||||
(value ?? "").replace(/\\/g, PATH_SEPARATOR);
|
(value ?? "").replace(/\\/g, PATH_SEPARATOR);
|
||||||
|
|
||||||
@@ -81,17 +80,13 @@ const splitRelativePath = (fullPath: string, prefix: string) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const resolveFileRelativePath = (file: KBFile) => {
|
const resolveFileRelativePath = (file: KBFile) => {
|
||||||
const metadata = file?.metadata as Record<string, unknown> | undefined;
|
const rawPath = file.relativePath || file.fileName || file.name || "";
|
||||||
const metadataPath =
|
|
||||||
metadata && typeof metadata[RELATIVE_PATH_KEY] === "string"
|
|
||||||
? String(metadata[RELATIVE_PATH_KEY])
|
|
||||||
: "";
|
|
||||||
const rawPath = metadataPath || file.fileName || file.name || "";
|
|
||||||
return normalizePath(rawPath).replace(/^\/+/, "");
|
return normalizePath(rawPath).replace(/^\/+/, "");
|
||||||
};
|
};
|
||||||
|
|
||||||
const KnowledgeBaseDetailPage: React.FC = () => {
|
const KnowledgeBaseDetailPage: React.FC = () => {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
|
const [searchParams] = useSearchParams();
|
||||||
const { message } = App.useApp();
|
const { message } = App.useApp();
|
||||||
const { id } = useParams<{ id: string }>();
|
const { id } = useParams<{ id: string }>();
|
||||||
const [knowledgeBase, setKnowledgeBase] = useState<KnowledgeBaseItem | undefined>(undefined);
|
const [knowledgeBase, setKnowledgeBase] = useState<KnowledgeBaseItem | undefined>(undefined);
|
||||||
@@ -158,6 +153,16 @@ const KnowledgeBaseDetailPage: React.FC = () => {
|
|||||||
}
|
}
|
||||||
}, [id, fetchKnowledgeBaseDetails]);
|
}, [id, fetchKnowledgeBaseDetails]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!id) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const prefixParam = searchParams.get("prefix");
|
||||||
|
const fileNameParam = searchParams.get("fileName");
|
||||||
|
setFilePrefix(prefixParam ? normalizePrefix(prefixParam) : "");
|
||||||
|
setFileKeyword(fileNameParam ? fileNameParam : "");
|
||||||
|
}, [id, searchParams]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (id) {
|
if (id) {
|
||||||
fetchFiles();
|
fetchFiles();
|
||||||
|
|||||||
@@ -37,7 +37,7 @@ export default function KnowledgeBasePage() {
|
|||||||
await deleteKnowledgeBaseByIdUsingDelete(kb.id);
|
await deleteKnowledgeBaseByIdUsingDelete(kb.id);
|
||||||
message.success("知识库删除成功");
|
message.success("知识库删除成功");
|
||||||
fetchData();
|
fetchData();
|
||||||
} catch (error) {
|
} catch {
|
||||||
message.error("知识库删除失败");
|
message.error("知识库删除失败");
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -47,7 +47,7 @@ export default function KnowledgeBasePage() {
|
|||||||
key: "edit",
|
key: "edit",
|
||||||
label: "编辑",
|
label: "编辑",
|
||||||
icon: <EditOutlined />,
|
icon: <EditOutlined />,
|
||||||
onClick: (item) => {
|
onClick: (item: KnowledgeBaseItem) => {
|
||||||
setIsEdit(true);
|
setIsEdit(true);
|
||||||
setCurrentKB(item);
|
setCurrentKB(item);
|
||||||
},
|
},
|
||||||
@@ -64,7 +64,7 @@ export default function KnowledgeBasePage() {
|
|||||||
okType: "danger",
|
okType: "danger",
|
||||||
cancelText: "取消",
|
cancelText: "取消",
|
||||||
},
|
},
|
||||||
onClick: (item) => handleDeleteKB(item),
|
onClick: (item: KnowledgeBaseItem) => handleDeleteKB(item),
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
@@ -76,7 +76,7 @@ export default function KnowledgeBasePage() {
|
|||||||
fixed: "left" as const,
|
fixed: "left" as const,
|
||||||
width: 200,
|
width: 200,
|
||||||
ellipsis: true,
|
ellipsis: true,
|
||||||
render: (_: any, kb: KnowledgeBaseItem) => (
|
render: (_: unknown, kb: KnowledgeBaseItem) => (
|
||||||
<Button
|
<Button
|
||||||
type="link"
|
type="link"
|
||||||
onClick={() => navigate(`/data/knowledge-base/detail/${kb.id}`)}
|
onClick={() => navigate(`/data/knowledge-base/detail/${kb.id}`)}
|
||||||
@@ -111,7 +111,7 @@ export default function KnowledgeBasePage() {
|
|||||||
key: "actions",
|
key: "actions",
|
||||||
fixed: "right" as const,
|
fixed: "right" as const,
|
||||||
width: 150,
|
width: 150,
|
||||||
render: (_: any, kb: KnowledgeBaseItem) => (
|
render: (_: unknown, kb: KnowledgeBaseItem) => (
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
{operations.map((op) => (
|
{operations.map((op) => (
|
||||||
<Tooltip key={op.key} title={op.label}>
|
<Tooltip key={op.key} title={op.label}>
|
||||||
@@ -132,17 +132,22 @@ export default function KnowledgeBasePage() {
|
|||||||
<div className="h-full flex flex-col gap-4">
|
<div className="h-full flex flex-col gap-4">
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<h1 className="text-xl font-bold">知识生成</h1>
|
<h1 className="text-xl font-bold">知识生成</h1>
|
||||||
<CreateKnowledgeBase
|
<div className="flex items-center gap-2">
|
||||||
isEdit={isEdit}
|
<Button onClick={() => navigate("/data/knowledge-base/search")}>
|
||||||
data={currentKB}
|
全库搜索
|
||||||
onUpdate={() => {
|
</Button>
|
||||||
fetchData();
|
<CreateKnowledgeBase
|
||||||
}}
|
isEdit={isEdit}
|
||||||
onClose={() => {
|
data={currentKB}
|
||||||
setIsEdit(false);
|
onUpdate={() => {
|
||||||
setCurrentKB(null);
|
fetchData();
|
||||||
}}
|
}}
|
||||||
/>
|
onClose={() => {
|
||||||
|
setIsEdit(false);
|
||||||
|
setCurrentKB(null);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<SearchControls
|
<SearchControls
|
||||||
@@ -161,7 +166,9 @@ export default function KnowledgeBasePage() {
|
|||||||
<CardView
|
<CardView
|
||||||
data={tableData}
|
data={tableData}
|
||||||
operations={operations}
|
operations={operations}
|
||||||
onView={(item) => navigate(`/data/knowledge-base/detail/${item.id}`)}
|
onView={(item: KnowledgeBaseItem) =>
|
||||||
|
navigate(`/data/knowledge-base/detail/${item.id}`)
|
||||||
|
}
|
||||||
pagination={pagination}
|
pagination={pagination}
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
|
|||||||
217
frontend/src/pages/KnowledgeBase/Search/KnowledgeBaseSearch.tsx
Normal file
217
frontend/src/pages/KnowledgeBase/Search/KnowledgeBaseSearch.tsx
Normal file
@@ -0,0 +1,217 @@
|
|||||||
|
import { useCallback, useMemo, useState } from "react";
|
||||||
|
import { App, Badge, Breadcrumb, Button, Input, Table } from "antd";
|
||||||
|
import { useNavigate } from "react-router";
|
||||||
|
import {
|
||||||
|
KBFileStatus,
|
||||||
|
KnowledgeBaseFileSearchResult,
|
||||||
|
} from "../knowledge-base.model";
|
||||||
|
import { KBFileStatusMap } from "../knowledge-base.const";
|
||||||
|
import { queryKnowledgeBaseFilesSearchUsingGet } from "../knowledge-base.api";
|
||||||
|
import { formatDateTime } from "@/utils/unit";
|
||||||
|
|
||||||
|
const PATH_SEPARATOR = "/";
|
||||||
|
|
||||||
|
const normalizePath = (value?: string) =>
|
||||||
|
(value ?? "").replace(/\\/g, PATH_SEPARATOR);
|
||||||
|
|
||||||
|
const resolvePrefix = (relativePath?: string) => {
|
||||||
|
const normalized = normalizePath(relativePath);
|
||||||
|
const parts = normalized.split(PATH_SEPARATOR).filter(Boolean);
|
||||||
|
if (parts.length <= 1) {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
parts.pop();
|
||||||
|
return `${parts.join(PATH_SEPARATOR)}${PATH_SEPARATOR}`;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function KnowledgeBaseSearch() {
|
||||||
|
const navigate = useNavigate();
|
||||||
|
const { message } = App.useApp();
|
||||||
|
const [searchTerm, setSearchTerm] = useState("");
|
||||||
|
const [activeKeyword, setActiveKeyword] = useState("");
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
|
const [searched, setSearched] = useState(false);
|
||||||
|
const [results, setResults] = useState<KnowledgeBaseFileSearchResult[]>([]);
|
||||||
|
const [pagination, setPagination] = useState({
|
||||||
|
current: 1,
|
||||||
|
pageSize: 10,
|
||||||
|
total: 0,
|
||||||
|
});
|
||||||
|
|
||||||
|
const fetchResults = useCallback(
|
||||||
|
async (keyword: string, page?: number, pageSize?: number) => {
|
||||||
|
const resolvedPage = page ?? pagination.current;
|
||||||
|
const resolvedPageSize = pageSize ?? pagination.pageSize;
|
||||||
|
if (!keyword) {
|
||||||
|
setResults([]);
|
||||||
|
setPagination((prev) => ({ ...prev, total: 0, current: resolvedPage }));
|
||||||
|
setSearched(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setLoading(true);
|
||||||
|
try {
|
||||||
|
const { data } = await queryKnowledgeBaseFilesSearchUsingGet({
|
||||||
|
fileName: keyword,
|
||||||
|
page: Math.max(resolvedPage - 1, 0),
|
||||||
|
size: resolvedPageSize,
|
||||||
|
});
|
||||||
|
const content = Array.isArray(data?.content) ? data.content : [];
|
||||||
|
setResults(content);
|
||||||
|
setPagination({
|
||||||
|
current: resolvedPage,
|
||||||
|
pageSize: resolvedPageSize,
|
||||||
|
total: data?.totalElements ?? 0,
|
||||||
|
});
|
||||||
|
setSearched(true);
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Failed to search knowledge base files:", error);
|
||||||
|
message.error("检索失败,请稍后重试");
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[message, pagination]
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleSearch = (value?: string) => {
|
||||||
|
const keyword = (value ?? searchTerm).trim();
|
||||||
|
if (!keyword) {
|
||||||
|
message.warning("请输入文件名");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setActiveKeyword(keyword);
|
||||||
|
fetchResults(keyword, 1, pagination.pageSize);
|
||||||
|
};
|
||||||
|
|
||||||
|
const columns = useMemo(
|
||||||
|
() => [
|
||||||
|
{
|
||||||
|
title: "知识库",
|
||||||
|
dataIndex: "knowledgeBaseName",
|
||||||
|
key: "knowledgeBaseName",
|
||||||
|
width: 220,
|
||||||
|
ellipsis: true,
|
||||||
|
render: (text: string) => text || "-",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "文件名",
|
||||||
|
dataIndex: "fileName",
|
||||||
|
key: "fileName",
|
||||||
|
width: 220,
|
||||||
|
ellipsis: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "相对路径",
|
||||||
|
dataIndex: "relativePath",
|
||||||
|
key: "relativePath",
|
||||||
|
ellipsis: true,
|
||||||
|
render: (value: string) => value || "-",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "状态",
|
||||||
|
dataIndex: "status",
|
||||||
|
key: "status",
|
||||||
|
width: 120,
|
||||||
|
render: (status?: KBFileStatus) => {
|
||||||
|
const config = status ? KBFileStatusMap[status] : undefined;
|
||||||
|
if (!config) {
|
||||||
|
return <Badge color="default" text={status || "-"} />;
|
||||||
|
}
|
||||||
|
return <Badge color={config.color} text={config.label} />;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "更新时间",
|
||||||
|
dataIndex: "updatedAt",
|
||||||
|
key: "updatedAt",
|
||||||
|
width: 180,
|
||||||
|
ellipsis: true,
|
||||||
|
render: (value: string) => formatDateTime(value) || "-",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "操作",
|
||||||
|
key: "action",
|
||||||
|
width: 120,
|
||||||
|
align: "right" as const,
|
||||||
|
render: (_: unknown, record: KnowledgeBaseFileSearchResult) => (
|
||||||
|
<Button
|
||||||
|
type="link"
|
||||||
|
onClick={() => {
|
||||||
|
const prefix = resolvePrefix(record.relativePath);
|
||||||
|
const searchParams = new URLSearchParams();
|
||||||
|
if (prefix) {
|
||||||
|
searchParams.set("prefix", prefix);
|
||||||
|
}
|
||||||
|
navigate(
|
||||||
|
`/data/knowledge-base/detail/${record.knowledgeBaseId}?${searchParams.toString()}`
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
定位
|
||||||
|
</Button>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
[navigate]
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="h-full flex flex-col gap-4">
|
||||||
|
<Breadcrumb>
|
||||||
|
<Breadcrumb.Item>
|
||||||
|
<a onClick={() => navigate("/data/knowledge-base")}>知识库</a>
|
||||||
|
</Breadcrumb.Item>
|
||||||
|
<Breadcrumb.Item>全库搜索</Breadcrumb.Item>
|
||||||
|
</Breadcrumb>
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<h1 className="text-xl font-bold">知识库全库检索</h1>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-3">
|
||||||
|
<Input.Search
|
||||||
|
allowClear
|
||||||
|
value={searchTerm}
|
||||||
|
onChange={(event) => setSearchTerm(event.target.value)}
|
||||||
|
onSearch={handleSearch}
|
||||||
|
placeholder="输入文件名,回车或点击搜索"
|
||||||
|
enterButton="搜索"
|
||||||
|
loading={loading}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<Table
|
||||||
|
rowKey="id"
|
||||||
|
loading={loading}
|
||||||
|
columns={columns}
|
||||||
|
dataSource={results}
|
||||||
|
pagination={{
|
||||||
|
current: pagination.current,
|
||||||
|
pageSize: pagination.pageSize,
|
||||||
|
total: pagination.total,
|
||||||
|
showTotal: (total) => `共 ${total} 条`,
|
||||||
|
onChange: (page, pageSize) => {
|
||||||
|
const nextKeyword = activeKeyword.trim();
|
||||||
|
if (!nextKeyword) {
|
||||||
|
message.warning("请输入文件名");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
fetchResults(nextKeyword, page, pageSize || pagination.pageSize);
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
locale={{
|
||||||
|
emptyText: searched ? "暂无匹配文件" : "请输入文件名开始检索",
|
||||||
|
}}
|
||||||
|
onRow={(record) => ({
|
||||||
|
onClick: () => {
|
||||||
|
const prefix = resolvePrefix(record.relativePath);
|
||||||
|
const searchParams = new URLSearchParams();
|
||||||
|
if (prefix) {
|
||||||
|
searchParams.set("prefix", prefix);
|
||||||
|
}
|
||||||
|
navigate(
|
||||||
|
`/data/knowledge-base/detail/${record.knowledgeBaseId}?${searchParams.toString()}`
|
||||||
|
);
|
||||||
|
},
|
||||||
|
})}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,12 +1,15 @@
|
|||||||
import { get, post, put, del } from "@/utils/request";
|
import { get, post, put, del } from "@/utils/request";
|
||||||
|
|
||||||
|
type RequestPayload = Record<string, unknown>;
|
||||||
|
type RequestParams = Record<string, unknown>;
|
||||||
|
|
||||||
// 获取知识库列表
|
// 获取知识库列表
|
||||||
export function queryKnowledgeBasesUsingPost(params: any) {
|
export function queryKnowledgeBasesUsingPost(params: RequestPayload) {
|
||||||
return post("/api/knowledge-base/list", params);
|
return post("/api/knowledge-base/list", params);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 创建知识库
|
// 创建知识库
|
||||||
export function createKnowledgeBaseUsingPost(data: any) {
|
export function createKnowledgeBaseUsingPost(data: RequestPayload) {
|
||||||
return post("/api/knowledge-base/create", data);
|
return post("/api/knowledge-base/create", data);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -16,7 +19,7 @@ export function queryKnowledgeBaseByIdUsingGet(baseId: string) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 更新知识库
|
// 更新知识库
|
||||||
export function updateKnowledgeBaseByIdUsingPut(baseId: string, data: any) {
|
export function updateKnowledgeBaseByIdUsingPut(baseId: string, data: RequestPayload) {
|
||||||
return put(`/api/knowledge-base/${baseId}`, data);
|
return put(`/api/knowledge-base/${baseId}`, data);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -26,17 +29,22 @@ export function deleteKnowledgeBaseByIdUsingDelete(baseId: string) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 获取知识生成文件列表
|
// 获取知识生成文件列表
|
||||||
export function queryKnowledgeBaseFilesUsingGet(baseId: string, data) {
|
export function queryKnowledgeBaseFilesUsingGet(baseId: string, data: RequestParams) {
|
||||||
return get(`/api/knowledge-base/${baseId}/files`, data);
|
return get(`/api/knowledge-base/${baseId}/files`, data);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 全库检索知识库文件
|
||||||
|
export function queryKnowledgeBaseFilesSearchUsingGet(params: RequestParams) {
|
||||||
|
return get("/api/knowledge-base/files/search", params);
|
||||||
|
}
|
||||||
|
|
||||||
// 添加文件到知识库
|
// 添加文件到知识库
|
||||||
export function addKnowledgeBaseFilesUsingPost(baseId: string, data: any) {
|
export function addKnowledgeBaseFilesUsingPost(baseId: string, data: RequestPayload) {
|
||||||
return post(`/api/knowledge-base/${baseId}/files`, data);
|
return post(`/api/knowledge-base/${baseId}/files`, data);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 删除知识生成文件
|
// 删除知识生成文件
|
||||||
export function deleteKnowledgeBaseFileByIdUsingDelete(baseId: string, data: any) {
|
export function deleteKnowledgeBaseFileByIdUsingDelete(baseId: string, data: RequestPayload) {
|
||||||
return del(`/api/knowledge-base/${baseId}/files`, data);
|
return del(`/api/knowledge-base/${baseId}/files`, data);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -29,50 +29,26 @@ export interface KBFile {
|
|||||||
id: string;
|
id: string;
|
||||||
fileName: string;
|
fileName: string;
|
||||||
name?: string;
|
name?: string;
|
||||||
|
relativePath?: string;
|
||||||
createdAt: string;
|
createdAt: string;
|
||||||
updatedAt: string;
|
updatedAt: string;
|
||||||
status: KBFileStatus;
|
status: KBFileStatus;
|
||||||
chunkCount: number;
|
chunkCount: number;
|
||||||
metadata: Record<string, any>;
|
metadata: Record<string, unknown>;
|
||||||
knowledgeBaseId: string;
|
knowledgeBaseId: string;
|
||||||
fileId: string;
|
fileId: string;
|
||||||
updatedBy: string;
|
updatedBy: string;
|
||||||
createdBy: string;
|
createdBy: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface Chunk {
|
export interface KnowledgeBaseFileSearchResult {
|
||||||
id: number;
|
id: string;
|
||||||
content: string;
|
knowledgeBaseId: string;
|
||||||
position: number;
|
knowledgeBaseName: string;
|
||||||
tokens: number;
|
fileName: string;
|
||||||
embedding?: number[];
|
relativePath?: string;
|
||||||
similarity?: string;
|
status?: KBFileStatus;
|
||||||
|
chunkCount?: number;
|
||||||
createdAt?: string;
|
createdAt?: string;
|
||||||
updatedAt?: string;
|
updatedAt?: string;
|
||||||
vectorId?: string;
|
|
||||||
sliceOperator?: string;
|
|
||||||
parentChunkId?: number;
|
|
||||||
metadata?: {
|
|
||||||
source: string;
|
|
||||||
page?: number;
|
|
||||||
section?: string;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
interface VectorizationRecord {
|
|
||||||
id: number;
|
|
||||||
timestamp: string;
|
|
||||||
operation: "create" | "update" | "delete" | "reprocess";
|
|
||||||
fileId: number;
|
|
||||||
fileName: string;
|
|
||||||
chunksProcessed: number;
|
|
||||||
vectorsGenerated: number;
|
|
||||||
status: "success" | "failed" | "partial";
|
|
||||||
duration: string;
|
|
||||||
config: {
|
|
||||||
embeddingModel: string;
|
|
||||||
chunkSize: number;
|
|
||||||
sliceMethod: string;
|
|
||||||
};
|
|
||||||
error?: string;
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -30,6 +30,7 @@ import ManualEvaluatePage from "@/pages/DataEvaluation/Evaluate/ManualEvaluate";
|
|||||||
import KnowledgeBasePage from "@/pages/KnowledgeBase/Home/KnowledgeBasePage";
|
import KnowledgeBasePage from "@/pages/KnowledgeBase/Home/KnowledgeBasePage";
|
||||||
import KnowledgeBaseDetailPage from "@/pages/KnowledgeBase/Detail/KnowledgeBaseDetail";
|
import KnowledgeBaseDetailPage from "@/pages/KnowledgeBase/Detail/KnowledgeBaseDetail";
|
||||||
import KnowledgeBaseFileDetailPage from "@/pages/KnowledgeBase/FileDetail/KnowledgeBaseFileDetail";
|
import KnowledgeBaseFileDetailPage from "@/pages/KnowledgeBase/FileDetail/KnowledgeBaseFileDetail";
|
||||||
|
import KnowledgeBaseSearch from "@/pages/KnowledgeBase/Search/KnowledgeBaseSearch";
|
||||||
|
|
||||||
import OperatorMarketPage from "@/pages/OperatorMarket/Home/OperatorMarket";
|
import OperatorMarketPage from "@/pages/OperatorMarket/Home/OperatorMarket";
|
||||||
import OperatorPluginCreate from "@/pages/OperatorMarket/Create/OperatorPluginCreate";
|
import OperatorPluginCreate from "@/pages/OperatorMarket/Create/OperatorPluginCreate";
|
||||||
@@ -246,6 +247,10 @@ const router = createBrowserRouter([
|
|||||||
index: true,
|
index: true,
|
||||||
Component: KnowledgeBasePage,
|
Component: KnowledgeBasePage,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: "search",
|
||||||
|
Component: KnowledgeBaseSearch,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
path: "detail/:id",
|
path: "detail/:id",
|
||||||
Component: KnowledgeBaseDetailPage,
|
Component: KnowledgeBaseDetailPage,
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ create table if not exists t_rag_file
|
|||||||
id VARCHAR(36) PRIMARY KEY COMMENT 'UUID',
|
id VARCHAR(36) PRIMARY KEY COMMENT 'UUID',
|
||||||
knowledge_base_id VARCHAR(36) NOT NULL COMMENT '知识库ID',
|
knowledge_base_id VARCHAR(36) NOT NULL COMMENT '知识库ID',
|
||||||
file_name VARCHAR(255) NOT NULL COMMENT '文件名',
|
file_name VARCHAR(255) NOT NULL COMMENT '文件名',
|
||||||
|
relative_path VARCHAR(512) NULL COMMENT '相对路径',
|
||||||
file_id VARCHAR(255) NOT NULL COMMENT '文件ID',
|
file_id VARCHAR(255) NOT NULL COMMENT '文件ID',
|
||||||
chunk_count INT COMMENT '切片数',
|
chunk_count INT COMMENT '切片数',
|
||||||
metadata JSON COMMENT '元数据',
|
metadata JSON COMMENT '元数据',
|
||||||
@@ -28,3 +29,6 @@ create table if not exists t_rag_file
|
|||||||
created_by VARCHAR(255) COMMENT '创建者',
|
created_by VARCHAR(255) COMMENT '创建者',
|
||||||
updated_by VARCHAR(255) COMMENT '更新者'
|
updated_by VARCHAR(255) COMMENT '更新者'
|
||||||
) comment '知识库切片表';
|
) comment '知识库切片表';
|
||||||
|
|
||||||
|
create index idx_rag_file_kb_name on t_rag_file (knowledge_base_id, file_name);
|
||||||
|
create index idx_rag_file_kb_path on t_rag_file (knowledge_base_id, relative_path);
|
||||||
|
|||||||
Reference in New Issue
Block a user